Ammo.js Physics Loop with requestAnimationFrame
This article explains how to set up and optimize the standard physics
update loop when integrating the Ammo.js physics engine with
JavaScript’s requestAnimationFrame. You will learn how to
calculate frame delta times, step the physics simulation accurately, and
synchronize physical bodies with your visual 3D meshes for smooth
rendering.
The Core Concept: stepSimulation
At the heart of any Ammo.js project is the
dynamicsWorld.stepSimulation method. Unlike graphics, which
can render at variable frame rates, physics simulations require a fixed
time step to remain stable and predictable.
The stepSimulation function handles this by taking three
main arguments:
dynamicsWorld.stepSimulation(timeStep, maxSubSteps, fixedTimeStep);timeStep: The amount of time (in seconds) that has passed since the last step.maxSubSteps: The maximum number of substeps Ammo.js is allowed to take to catch up to the real-time elapsed. Usually set to10.fixedTimeStep: The size of the internal physics step. Usually set to1 / 60(representing 60Hz), though1 / 120can be used for high-precision simulations.
Implementing the Loop
To run the simulation smoothly, you must calculate the exact elapsed
time (delta time) between each call of
requestAnimationFrame using a high-resolution timer like
performance.now().
Here is the standard implementation of the update loop:
let physicsWorld; // Your Ammo.btDiscreteDynamicsWorld instance
let lastTime = performance.now();
function animate(currentTime) {
// Schedule the next frame
requestAnimationFrame(animate);
// Calculate delta time in seconds
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// Step the physics simulation
if (physicsWorld) {
physicsWorld.stepSimulation(deltaTime, 10, 1 / 60);
}
// Update your 3D rendering engine (e.g., Three.js meshes)
updatePhysicsBodies();
// Render the scene
renderer.render(scene, camera);
}
// Start the loop
requestAnimationFrame(animate);Synchronizing Graphics and Physics
Once the physics world steps forward, the positions and rotations of your rigid bodies will change. You must copy these updated transformations to your visual meshes (such as Three.js meshes) in every frame.
This is typically done by iterating over an array of paired objects (physics body and graphical mesh) and updating the mesh’s position and quaternion:
const rigidBodies = []; // Array containing pairs of { mesh, body }
function updatePhysicsBodies() {
const transformAux = new Ammo.btTransform();
for (let i = 0; i < rigidBodies.length; i++) {
const objThree = rigidBodies[i].mesh;
const objAmmo = rigidBodies[i].body;
const motionState = objAmmo.getMotionState();
if (motionState) {
// Get the transform from the physics body
motionState.getWorldTransform(transformAux);
const origin = transformAux.getOrigin();
const rotation = transformAux.getRotation();
// Apply to the Three.js mesh
objThree.position.set(origin.x(), origin.y(), origin.z());
objThree.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
}
// Free memory used by the temporary transform helper
Ammo.destroy(transformAux);
}By ensuring that stepSimulation is supplied with the
correct delta time, and by properly transferring the physics transforms
to your visual scene inside the requestAnimationFrame
callback, your simulation will remain smooth, robust, and visually
accurate across different hardware and frame rates.