Assign Custom User Data to Ammo.js Rigid Body

When integrating the Ammo.js physics engine with a 3D graphics library like Three.js, you often need to identify which graphical mesh corresponds to a specific rigid body during collision detection. Because Ammo.js is a WebAssembly/Emscripten port of Bullet Physics, standard JavaScript object properties attached directly to Ammo objects can be lost when wrappers are recreated. This article explains how to reliably use the native C++ setUserPointer and getUserPointer methods to assign and retrieve custom user data for quick identification.


The Challenge with WebAssembly Wrappers

In Ammo.js, objects like btRigidBody are JavaScript wrappers pointing to raw memory in the WebAssembly heap. If you attempt to assign a custom property directly to the object (e.g., rigidBody.myMesh = mesh), this reference may disappear. When Ammo.js returns collision objects during a collision manifold check, it often instantiates new JavaScript wrappers for the same underlying C++ pointers, meaning your custom property will be undefined.

To prevent this, you must use the native memory-safe methods provided by the Bullet API.

Step 1: Establish a Lookup Map

Because the native setUserPointer method only accepts an integer (representing a memory address or index) rather than a complex JavaScript object, the most reliable approach is to map unique integer IDs to your JavaScript objects.

Three.js meshes, for example, have a built-in auto-incrementing integer id. Create a global or scoped map to hold these associations:

const physicsLookupMap = new Map();

Step 2: Assign the User Pointer

When creating your rigid body, register your 3D mesh (or custom data object) in your map using its unique ID. Then, pass that ID to the rigid body using setUserPointer:

// 1. Create your visual mesh (e.g., Three.js Mesh)
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 2. Create your Ammo.js rigid body
const transform = new Ammo.btTransform();
// ... setup transform, mass, localInertia, motionState, and shape ...
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);

// 3. Store the mesh in your lookup map using its unique integer ID
physicsLookupMap.set(mesh.id, mesh);

// 4. Assign the integer ID to the Ammo.js rigid body
body.setUserPointer(mesh.id);

// 5. Add the body to the physics world
physicsWorld.addRigidBody(body);

Step 3: Retrieve the User Pointer During Collisions

When processing collisions inside your physics loop, you can extract the user pointer from the colliding bodies and look up the corresponding JavaScript objects instantly.

const dispatcher = physicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();

for (let i = 0; i < numManifolds; i++) {
    const manifold = dispatcher.getManifoldByIndexInternal(i);
    const numContacts = manifold.getNumContacts();
    
    if (numContacts > 0) {
        // Cast the generic collision objects to btCollisionObject
        const objA = Ammo.castObject(manifold.getBody0(), Ammo.btCollisionObject);
        const objB = Ammo.castObject(manifold.getBody1(), Ammo.btCollisionObject);

        // Retrieve the stored integer IDs
        const idA = objA.getUserPointer();
        const idB = objB.getUserPointer();

        // Fetch the original Three.js meshes from your map
        const meshA = physicsLookupMap.get(idA);
        const meshB = physicsLookupMap.get(idB);

        if (meshA && meshB) {
            console.log(`Collision detected between Mesh ${meshA.name} and Mesh ${meshB.name}`);
        }
    }
}

Cleaning Up Memory

To avoid memory leaks, ensure that you delete entries from your lookup map whenever a physics body and its associated mesh are removed from your scene:

function removeEntity(body, mesh) {
    physicsWorld.removeRigidBody(body);
    physicsLookupMap.delete(mesh.id);
    
    // Clean up Ammo memory
    Ammo.destroy(body);
}