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:
- Define a distance threshold (e.g.,
THRESHOLD = 5000). - Track the player’s distance from
(0, 0, 0). - If the threshold is exceeded, calculate the offset vector:
offset = -player.position. - Iterate through all active rigid bodies in the Ammo.js world.
- 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.
- Avoid micro-scales: Do not make a human character 0.01 units tall.
- Avoid macro-scales: Do not make a spaceship 10,000 units long.
- Normalize your scale: Keep 1 unit in Ammo.js strictly equal to 1 meter. If your world requires vast distances, scale down the visual assets and physics representations to fit within this optimal range, or use a separate visual-only scaling system.
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.
- Broadphase Selection: Ensure you are using
btDbvtBroadphase(Dynamic AABB Tree Broadphase) instead ofbtAxisSweep3. The sweep-and-prune broadphase requires fixed world bounds, whereasbtDbvtBroadphasedynamically adapts to an expanding world size without performance degradation. - Active Physics Radius: Only add rigid bodies to the
active
btDiscreteDynamicsWorldif they are within a certain radius of the player. - 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 );fixedTimeStep: Keep this consistent (typically1/60or1/120for high-precision scenarios).maxSubSteps: Increase this value (e.g., to10or12) if you have high-velocity objects. This ensures that even if the frame rate drops, the engine performs enough sub-ticks to resolve fast collisions accurately without letting objects pass through geometry.