Fix Ammo.js Physics Scaling in Large Game Worlds

Developing large-scale 3D games with ammo.js—the JavaScript port of the Bullet physics engine—often introduces severe physics jitter, tunneling, and collision detection failures. These issues are primarily caused by floating-point precision loss as objects move far away from the world origin (0, 0, 0). This article explains why these scaling issues occur and provides practical techniques to resolve them, including floating origin systems, optimized physics stepping, and dynamic world partitioning.

The Cause: Floating-Point Precision Loss

Ammo.js relies on single-precision 32-bit floating-point numbers (float) for its calculations. A 32-bit float has 24 bits of precision, which equates to roughly 7 decimal digits of accuracy.

When your game world extends tens of thousands of units away from the origin, the distance values require larger numbers, leaving fewer decimal places to represent tiny movements. This results in: * Jittering: Objects shake or stutter because the engine cannot resolve micro-movements accurately. * Collision Failures (Tunneling): Fast-moving objects pass through walls because collision detection calculations lose precision. * Inconsistent Gravity and Forces: Applied forces produce unpredictable behavior at extreme coordinates.


Solution 1: Implement a Floating Origin (Origin Shifting)

The most effective way to solve precision loss in a massive world is to implement a Floating Origin (also known as camera relative rendering or origin shifting).

Instead of letting the player travel infinitely far into the coordinate space, you keep the player at or near the origin (0, 0, 0). When the player travels beyond a set threshold (e.g., 5,000 units from the origin), you shift the entire world—including all active physics bodies and the visual scene—in the opposite direction.

How to implement origin shifting in Ammo.js:

  1. Define a distance threshold (e.g., THRESHOLD = 5000).
  2. Track the player’s distance from (0, 0, 0).
  3. If the threshold is exceeded, calculate the offset vector: offset = -player.position.
  4. Iterate through all active rigid bodies in the Ammo.js world.
  5. Retrieve each body’s world transform, apply the offset to its origin, and set the new transform:
let transform = new Ammo.btTransform();
for (let i = 0; i < rigidBodies.length; i++) {
    let body = rigidBodies[i];
    let motionState = body.getMotionState();
    if (motionState) {
        motionState.getWorldTransform(transform);
        let origin = transform.getOrigin();
        
        // Apply the shift offset
        origin.setValue(origin.x() + offsetX, origin.y() + offsetY, origin.z() + offsetZ);
        transform.setOrigin(origin);
        
        body.setWorldTransform(transform);
        motionState.setWorldTransform(transform);
        
        // Clear forces and update interpolation to prevent physics glitches
        body.activate(true);
    }
}

Solution 2: Maintain a 1 Unit = 1 Meter Scale

Physics engines are optimized for real-world scales. Bullet Physics (and therefore Ammo.js) is tuned for objects between 0.2 meters and 5.0 meters in size.


Solution 3: Dynamic World Partitioning (Active Zones)

In an exceptionally large world, you cannot simulate the physics of the entire map simultaneously. This will cripple CPU performance. You must partition your world into grid cells or chunks.

  1. Broadphase Selection: Ensure you are using btDbvtBroadphase (Dynamic AABB Tree Broadphase) instead of btAxisSweep3. The sweep-and-prune broadphase requires fixed world bounds, whereas btDbvtBroadphase dynamically adapts to an expanding world size without performance degradation.
  2. Active Physics Radius: Only add rigid bodies to the active btDiscreteDynamicsWorld if they are within a certain radius of the player.
  3. Deactivation and Pooling: When an object exits the active radius, remove its rigid body from the physics world and store its state in a data structure. Re-instantiate or re-add it to the world only when the player approaches again.

Solution 4: Optimize Physics Stepping and Substepping

Large worlds with fast-moving entities require precise integration steps to prevent tunneling.

When calling stepSimulation, configure it to allow substepping:

// world.stepSimulation( timeStep, maxSubSteps, fixedTimeStep );
physicsWorld.stepSimulation( deltaTime, 10, 1 / 60 );