How to Sync Three.js Meshes with Ammo.js Rigid Bodies

Synchronizing Three.js meshes with Ammo.js rigid bodies is essential for creating realistic 3D physics simulations in the browser. This article explains how to link your visual 3D models with their physical representations and update their positions and rotations inside the animation loop. You will learn how to set up the association between graphics and physics, retrieve motion states from Ammo.js, and apply those transformations directly to Three.js meshes on every frame.

The Core Concept of Synchronization

In a WebGL physics application, Three.js manages what the user sees (the graphics), while Ammo.js calculates where objects should be based on forces like gravity and collisions (the physics). To sync them, you must let Ammo.js calculate the movements first, extract the updated position and rotation data from the physical “rigid body,” and then apply that data to the corresponding Three.js “mesh.”

Step 1: Pair Your Meshes and Rigid Bodies

To update your meshes, you need a way to loop through them. The most common approach is to create an array that holds pairs of Three.js meshes and Ammo.js rigid bodies.

// Array to hold physics/graphics pairs
const syncList = [];

function createBox(width, height, depth, mass, position) {
    // 1. Create Three.js Mesh (Graphics)
    const geometry = new THREE.BoxGeometry(width, height, depth);
    const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.copy(position);
    scene.add(mesh);

    // 2. Create Ammo.js Rigid Body (Physics)
    const transform = new Ammo.btTransform();
    transform.setIdentity();
    transform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z));
    
    const motionState = new Ammo.btDefaultMotionState(transform);
    const colShape = new Ammo.btBoxShape(new Ammo.btVector3(width * 0.5, height * 0.5, depth * 0.5));
    
    const localInertia = new Ammo.btVector3(0, 0, 0);
    if (mass > 0) colShape.calculateLocalInertia(mass, localInertia);

    const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, colShape, localInertia);
    const body = new Ammo.btRigidBody(rbInfo);

    physicsWorld.addRigidBody(body);

    // 3. Pair them together and add to the tracking list
    syncList.push({
        mesh: mesh,
        rigidBody: body
    });
}

Step 2: Set Up Temporary Math Objects

Instantiating new objects inside your render loop causes garbage collection overhead, which leads to performance stuttering. To prevent this, define temporary Ammo.js variables outside your render loop to reuse them during updates.

// Create a reusable transform object once
const tempTransform = new Ammo.btTransform();

Step 3: Update the Physics World and Sync Meshes

Inside your requestAnimationFrame render loop, you must advance the physics simulation and loop through your paired objects to copy the physical coordinates to the visual meshes.

const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);

    const deltaTime = clock.getDelta();

    // Step the physics simulation
    if (physicsWorld) {
        physicsWorld.stepSimulation(deltaTime, 10);
    }

    // Synchronize graphics with physics
    for (let i = 0; i < syncList.length; i++) {
        const obj = syncList[i];
        const mesh = obj.mesh;
        const rigidBody = obj.rigidBody;
        const motionState = rigidBody.getMotionState();

        if (motionState) {
            // Get the updated transform from the rigid body
            motionState.getWorldTransform(tempTransform);

            // Extract position
            const origin = tempTransform.getOrigin();
            mesh.position.set(origin.x(), origin.y(), origin.z());

            // Extract rotation (Quaternion)
            const rotation = tempTransform.getRotation();
            mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
        }
    }

    renderer.render(scene, camera);
}

Important Considerations