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.