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);

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.