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);
}