Safe Variable Time Step Main Loop in Ammo.js

Implementing physics in web applications using ammo.js (a WebAssembly port of the Bullet Physics engine) requires a robust simulation loop. While fixed time steps are ideal for physics stability, real-world browser frame rates fluctuate due to rendering loads and hardware differences. This article explains how to safely implement a variable time step in your ammo.js main loop by configuring the stepSimulation parameters correctly to prevent physics jitter, slow-downs, and object tunneling.

Understanding stepSimulation

The core of safe time stepping in ammo.js lies in the dynamicsWorld.stepSimulation method. This method takes three parameters:

dynamicsWorld.stepSimulation(timeStep, maxSubSteps, fixedTimeStep);
  1. timeStep: The actual elapsed time (in seconds) since the last frame. This is your variable delta time.
  2. maxSubSteps: The maximum number of internal fixed steps ammo.js is allowed to take in a single frame to catch up with the elapsed real time.
  3. fixedTimeStep: The duration of each internal simulation step. By default, this is 1 / 60 (about 16.67 milliseconds).

The Mechanics of Safe Stepping

Ammo.js resolves the conflict between variable frame rates and physics stability by running a fixed-step simulation inside a variable-step game loop.

When you pass the variable timeStep to stepSimulation, ammo.js accumulates this time. It then runs as many internal steps of size fixedTimeStep as necessary to match the accumulated time. If there is leftover time that does not fit into a full fixedTimeStep, ammo.js automatically interpolates the positions of the physics bodies for smooth rendering.

Implementation Guide

To implement this safely, use the following pattern in your application loop:

let lastTime = performance.now();
const fixedTimeStep = 1 / 60; // 60Hz physics step
const maxSubSteps = 10;       // Prevents the "spiral of death"

function tick() {
    requestAnimationFrame(tick);

    const currentTime = performance.now();
    // Calculate delta time in seconds
    let deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    // 1. Cap the maximum delta time
    // If the tab loses focus or lags severely, this prevents 
    // the physics engine from attempting to simulate hundreds of steps at once.
    if (deltaTime > 0.25) {
        deltaTime = 0.25;
    }

    // 2. Step the simulation
    // Ammo.js will run up to 10 substeps of 1/60s to cover the deltaTime.
    dynamicsWorld.stepSimulation(deltaTime, maxSubSteps, fixedTimeStep);

    // 3. Update your graphics/render engine
    updateGraphicsTransforms();
    renderer.render(scene, camera);
}

requestAnimationFrame(tick);

Critical Rules for Stability

To ensure the simulation remains stable and performant, adhere to the following rules: