Smoothing ammo.js Physics Rendering via Interpolation
This article explains how to utilize the interpolation transform to eliminate visual jitter and achieve smooth rendering of physics objects in ammo.js. By decoupling the fixed-timestep physics simulation from the variable frame rate of the browser’s render loop, you can use interpolation to render physics bodies smoothly even on high-refresh-rate monitors or during frame rate drops.
The Cause of Visual Jitter
In WebGL applications using ammo.js (a port of the Bullet physics
engine), physics simulations run at a fixed timestep—typically 60Hz
(0.01666 seconds per step). Browsers, however, render frames using
requestAnimationFrame, which syncs with the monitor’s
refresh rate (e.g., 60Hz, 90Hz, 144Hz, or higher).
Because the rendering loop and the physics loop run at different, varying intervals, rendering the physics bodies at their exact physical state at the end of each render frame causes temporal aliasing. This manifests as micro-stuttering or jitter, especially noticeable when objects move quickly or when the camera tracks a physics object.
Leveraging Bullet’s Built-in Interpolation
The most efficient way to smooth out this jitter in ammo.js is to
utilize Bullet’s built-in interpolation via the
btMotionState.
When you step the physics world using
dynamicsWorld.stepSimulation(timeElapsed, maxSubSteps, fixedTimeStep),
ammo.js automatically calculates an interpolation factor. This factor
represents how far the rendering time is between the last physics step
and the next physics step. Ammo.js then interpolates the transform of
the rigid bodies and writes this smooth, interpolated transform to the
body’s associated btMotionState.
Step 1: Use btMotionState for Rigid Bodies
Instead of reading the transform directly from the rigid body’s
center of mass using getCenterOfMassTransform(), you must
instantiate and assign a btMotionState to each rigid
body.
// Create a transform helper
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(x, y, z));
// Create the motion state
const motionState = new Ammo.btDefaultMotionState(transform);
// Create the rigid body construction info using the motion state
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, collisionShape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);
dynamicsWorld.addRigidBody(body);Step 2: Configure stepSimulation Correctly
For interpolation to work, stepSimulation must be called
with the elapsed clock time. The engine will handle executing the fixed
steps and calculating the remainder time internally.
// In your render/update loop:
const deltaTime = clock.getDelta(); // Time elapsed since last frame
// stepSimulation(dT, maxSubSteps, fixedTimeStep)
// Bullet will interpolate the state if dT does not perfectly match fixedTimeStep
dynamicsWorld.stepSimulation(deltaTime, 10, 1 / 60);Step 3: Extract the Interpolated Transform
During your render loop, retrieve the transform from the
btMotionState rather than the rigid body itself. The motion
state contains the interpolated transform calculated by ammo.js during
stepSimulation.
function updateVisuals() {
const graphicsTransform = new Ammo.btTransform();
// Iterate through your mesh/physics body pairs
for (let i = 0; i < physicsObjects.length; i++) {
const obj = physicsObjects[i];
const motionState = obj.physicsBody.getMotionState();
if (motionState) {
// This retrieves the smoothly interpolated transform
motionState.getWorldTransform(graphicsTransform);
const origin = graphicsTransform.getOrigin();
const rotation = graphicsTransform.getRotation();
// Apply to your WebGL/Three.js mesh
obj.mesh.position.set(origin.x(), origin.y(), origin.z());
obj.mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
}
}Manual Interpolation (Alternative Approach)
If you require precise control over the interpolation process or are
not using btMotionState, you can perform manual
interpolation by tracking the “previous” and “current” states of your
physics bodies.
- Store States: Save the position and rotation of the rigid body at the end of the previous physics tick.
- Track Accumulator Time: Maintain a time accumulator of the remaining time that wasn’t simulated during the fixed step.
- Calculate Alpha: Divide the remaining accumulator
time by your fixed timestep size to get an alpha value between
0.0and1.0. - Lerp and Slerp: Linear interpolate (Lerp) the positions and spherical linear interpolate (Slerp) the rotations of your visual meshes between the previous state and the current state using the alpha value:
\[\text{RenderPosition} = \text{Lerp}(\text{PrevPosition}, \text{CurrentPosition}, \alpha)\]