Ammo.js Performance: Thousands of Active Motion States

Using ammo.js—the WebAssembly port of the Bullet physics engine—with thousands of active motion states can severely degrade application performance. This article examines how active motion states impact CPU overhead, memory usage, and rendering frame rates in JavaScript environments, while outlining concrete strategies to mitigate these performance bottlenecks.

The WebAssembly-JavaScript Bridge Bottleneck

In ammo.js, a motion state acts as the bridge that synchronizes the transform (position and rotation) of a physics rigid body in WebAssembly (Wasm) memory with a graphical representation in JavaScript (such as a Three.js mesh).

When thousands of motion states are active, the application must repeatedly cross the boundary between JS and Wasm every frame. Each active body triggers a callback to update its transform. This constant serialization and deserialization of float arrays across the Wasm boundary generates massive CPU overhead, quickly choking the main thread.

Garbage Collection and Memory overhead

Every time an active motion state updates, data must be extracted from the physics world. If your implementation instantiates new JavaScript objects (like vectors or quaternions) during this synchronization loop, it will trigger frequent Garbage Collection (GC) spikes.

In JavaScript, GC pauses stop code execution. Having thousands of active motion states creating temporary objects will result in noticeable frame drops (micro-stuttering), ruining the user experience.

Collision Pipeline Saturation

An “active” motion state implies that the corresponding rigid body is awake and participating in the simulation. When thousands of bodies are active simultaneously: * Broadphase Collision Detection: The engine must calculate bounding box overlaps for thousands of pairs, which scales quadratically in worst-case scenarios. * Narrowphase Collision Detection: The engine performs precise, mathematically expensive collision manifold calculations for all overlapping bodies. * Constraint Solver: Resolving contacts, friction, and joints for thousands of active bodies requires massive mathematical computations that can easily exceed the 16.6ms frame budget required for 60 FPS.

How to Optimize Ammo.js Performance

To maintain high frame rates while handling large-scale physics simulations, implement the following optimization techniques:

1. Enable Rigid Body Sleeping (Deactivation)

By default, Bullet puts rigid bodies to “sleep” when they stop moving. Ensure your bodies are allowed to sleep by configuring their activation states:

// Allow the body to sleep when inactive
body.setActivationState(DISABLE_DEACTIVATION = 4); // Turn this off to allow sleeping (default is active)
body.setActivationState(1); // 1 = Island awake, 2 = Island sleep

Sleeping bodies do not trigger motion state updates, drastically reducing CPU usage.

2. Manual Transform Synchronization

Instead of relying on individual btMotionState callbacks for every object, disable motion states entirely for non-essential objects. Instead, query the rigid body transforms in a single batched loop only for the objects currently visible in the camera frustum.

3. Use Web Workers

Offload the entire ammo.js physics simulation to a Web Worker. By running the physics loop on a separate CPU thread, the main thread remains free to handle rendering, user input, and UI updates. You only need to send serialized transform arrays back to the main thread via Transferable Objects, which incurs minimal overhead.

4. Object Pooling

Never instantiate new vectors, matrices, or quaternions inside the simulation or synchronization loop. Pre-allocate a pool of temporary math objects and reuse them to eliminate Garbage Collection overhead.