Ammo.js Contact Manifold Caching Best Practices
In high-performance WebGL applications, managing memory is crucial to preventing frame rate stutter caused by garbage collection. This article outlines the best practices for caching and reusing contact manifolds in ammo.js to minimize JavaScript object allocation and ensure smooth 60 FPS physics simulations.
The Impact of WebAssembly Wrappers on Garbage Collection
Ammo.js is a WebAssembly port of the Bullet physics engine. Because of this architecture, every time you query collision data or retrieve contact manifolds, JavaScript creates temporary wrapper objects to reference the underlying C++ memory. Creating and destroying thousands of these wrappers inside the physics loop every frame triggers frequent garbage collection spikes, resulting in noticeable micro-stutters during gameplay.
Pre-Allocate Scratchpad Objects
Never instantiate helper objects like Ammo.btVector3
inside your collision loops. Instead, pre-allocate a global or scoped
set of scratchpad objects when your application initializes.
// Pre-allocated scratchpad objects
const tempVector3_A = new Ammo.btVector3();
const tempVector3_B = new Ammo.btVector3();Use these scratchpads to extract positional and normal data from contact points, and then immediately copy the scalar values into native JavaScript variables or typed arrays.
Efficiently Iterate the Dispatcher
To analyze active collisions without generating garbage, iterate
directly over the physics dispatcher’s manifold list using a standard
for loop. Avoid functional array methods like
forEach or map, which allocate closures and
new array references.
const dispatcher = dynamicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();
for (let i = 0; i < numManifolds; i++) {
const manifold = dispatcher.getManifoldByIndexInternal(i);
const numContacts = manifold.getNumContacts();
if (numContacts === 0) continue;
const body0 = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody);
const body1 = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody);
for (let j = 0; j < numContacts; j++) {
const contactPoint = manifold.getContactPoint(j);
// Extract data using pre-allocated scratchpad
contactPoint.getPositionWorldOnA(tempVector3_A);
const posX = tempVector3_A.x();
const posY = tempVector3_A.y();
const posZ = tempVector3_A.z();
// Process data using native primitive values
}
}Store Manifold Data in Typed Arrays
If you need to pass manifold data (such as contact positions,
normals, and impulses) to other subsystems like visual effects or audio
engines, cache this data in pre-allocated Float32Array
objects.
Using typed arrays allows you to write coordinates directly into a fixed block of memory. This completely bypasses the need to create custom JavaScript objects for each collision point, allowing your updates to remain entirely garbage-free.
Avoid Custom Collision Callbacks
Ammo.js allows you to register custom contact added or processed callbacks. While convenient, these callbacks cross the WebAssembly-to-JavaScript boundary repeatedly, creating a massive amount of overhead and generating temporary objects for every single contact point.
Instead, perform post-simulation checks. Query the dispatcher directly once per physics step. This batch-processing approach keeps the interface communication minimal and highly optimized.