Skip to content

fix(physics): keep interpolation in sync on teleport for high refresh rates#8915

Merged
willeastcott merged 4 commits into
mainfrom
fix-teleport-interpolation
Jun 17, 2026
Merged

fix(physics): keep interpolation in sync on teleport for high refresh rates#8915
willeastcott merged 4 commits into
mainfrom
fix-teleport-interpolation

Conversation

@willeastcott

Copy link
Copy Markdown
Contributor

Description

Fixes the unreliable teleport() behaviour on high refresh-rate displays.

On a dynamic rigid body, teleport() (via syncEntityToBody) only set the body's world transform. On frames that run zero fixed sub-steps — dt < fixedTimeStep, i.e. high refresh-rate displays — Bullet fills the motion state by extrapolating from the body's interpolation world transform, which setWorldTransform doesn't touch. _updateDynamic then reads that stale transform back into the entity, so the teleport appears not to take and entity.getPosition() returns the pre-teleport pose. Being frame-rate dependent is why it surfaced as "breaks at 120 Hz".

For dynamic bodies, this also resets the interpolation world transform and interpolation linear/angular velocities, guarded by body.setInterpolationWorldTransform feature-detection so projects on an older ammo build are unaffected (teleport behaves exactly as before).

This needs the btCollisionObject interpolation accessors added upstream in kripken/ammo.js#446, so the bundled ammo build under examples/assets/wasm/ammo/ is updated to that build. The bulk of the diff is the regenerated (minified) ammo glue + .wasm — the actual engine change is the 7-line branch in component.js.

Verification — headless: teleport a dynamic body, then step one dt = 1/240 frame (zero sub-steps). Before: motion-state read-back is stale (x ≈ 0.4, the pre-teleport pose). After: read-back is the teleport target (x = 10.0).

Fixes #4277
Fixes #7822

Checklist

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

… rates

A dynamic body's teleport (syncEntityToBody) only set its world transform. On frames
that run zero fixed sub-steps (dt < fixedTimeStep, e.g. high refresh-rate displays),
Bullet fills the motion state by extrapolating from the stale interpolation transform,
so the entity read back its pre-teleport pose (#4277, #7822).

Reset the interpolation transform and linear/angular velocity for dynamic bodies,
feature-detected so older ammo builds (lacking the bindings) no-op. Bundled ammo
updated to the build exposing the btCollisionObject interpolation accessors
(kripken/ammo.js#446).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LeXXik

LeXXik commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Hmm, this doesn't seem correct. I mean, we are changing the body transforms and linear angular velocities to correspond to some fraction of a fixed timestep. I don't see that fraction being used anywhere. On the other hand, in most cases that will be a very small delta, so I guess it is ok? I think Bullet is using something like this utility to get the correct transform and velocities for a given timestep:
https://github.com/bulletphysics/bullet3/blob/63c4d67e337017f9d8b298c900e9aabdb69296e7/src/LinearMath/btTransformUtil.h#L115

…exact

Per review: copying the body's velocity into the interpolation state left the
read-back within velocity*fixedTimeStep of the target (Bullet extrapolates from
the interpolation transform by the leftover sub-step fraction). Zeroing the
interpolation velocities makes getPosition() return exactly the teleport target
on every frame, including for a moving body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willeastcott

Copy link
Copy Markdown
Contributor Author

Thanks — you're right to push on this. To the "where's the fraction used" point: it's applied by Bullet, not us. synchronizeMotionStates() feeds our interpolation transform + velocities into the exact integrateTransform you linked, with m_localTime as the time, so we're only supplying the inputs.

But that's exactly why the velocity matters, and you're right about it. Measured on a body teleported to x=10 carrying vx=6, stepping zero-substep (dt=1/240) frames:

read-back x
setWorldTransform only (the bug) 0.0 (stale)
interp vel = body velocity 9.925 / 9.950 / 9.975
interp vel = 0 10.000 (exact)

(this build has m_latencyMotionStateInterpolation on, so the extrapolation time is localTime - fixedTimeStep; the offset is bounded by velocity * fixedTimeStep.)

So copying the body's velocity left the read-back within ~v/60 of the target but not exactly it; zeroing the interpolation velocity makes getPosition() return exactly the teleport target on every frame, and it stays smooth once the sim resumes (no jump). Pushed that in 6410fa6. Thanks for the catch.

@willeastcott willeastcott self-assigned this Jun 17, 2026
@willeastcott willeastcott added the area: physics Physics related issue label Jun 17, 2026
@willeastcott willeastcott merged commit 4471111 into main Jun 17, 2026
9 checks passed
@willeastcott willeastcott deleted the fix-teleport-interpolation branch June 17, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: physics Physics related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entity.getPosition is sometimes wrong after teleport Physics simulation has issues on high refresh rate monitors

2 participants