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 sleepSleeping 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.