How to Use and Scale btMultiSphereShape in Ammo.js

This article provides a direct guide on how to instantiate and scale a btMultiSphereShape within an Ammo.js physics simulation. You will learn how to define multiple spheres with distinct positions and radii to create complex composite collision shapes, manage WebAssembly memory allocations for these shapes, and dynamically apply scaling factors to them during runtime.

Instantiating a btMultiSphereShape

The btMultiSphereShape represents a collision volume defined by an array of spheres, each with its own local position and radius. This shape is ideal for representing non-uniform, organic volumes like capsules with varying thicknesses, dumbbells, or simplified character physics hulls.

Because Ammo.js is a WebAssembly port of the C++ Bullet Physics engine, you cannot pass native JavaScript arrays directly to its constructors. Instead, you must allocate memory on the Emscripten heap for both the positions (btVector3 array) and the radii (float array).

Here is how to instantiate a btMultiSphereShape containing two spheres:

// Define the spheres' local positions and radii
const spheres = [
    { x: 0, y: -1, z: 0, radius: 1.0 },
    { x: 0, y: 1, z: 0, radius: 1.5 }
];

const numSpheres = spheres.length;

// 1. Allocate memory for btVector3 positions array (16 bytes per btVector3 alignment)
const vec3Size = 16; // Ammo.btVector3 size on the heap
const posBuffer = Ammo._malloc(numSpheres * vec3Size);

// 2. Allocate memory for float (btScalar) radii array (4 bytes per float)
const scalarSize = 4;
const radBuffer = Ammo._malloc(numSpheres * scalarSize);

// 3. Populate the allocated memory buffers
for (let i = 0; i < numSpheres; i++) {
    const s = spheres[i];
    
    // Create temporary Ammo vector and copy it into the heap buffer
    const tempVec = new Ammo.btVector3(s.x, s.y, s.z);
    
    // Calculate the heap offset for the current vector
    const vecPtr = posBuffer + (i * vec3Size);
    
    // Write x, y, z values directly to the WebAssembly memory
    Ammo.HEAPF32[vecPtr >> 2] = tempVec.x();
    Ammo.HEAPF32[(vecPtr + 4) >> 2] = tempVec.y();
    Ammo.HEAPF32[(vecPtr + 8) >> 2] = tempVec.z();
    
    // Write radius to the radii buffer
    Ammo.HEAPF32[(radBuffer + (i * scalarSize)) >> 2] = s.radius;
    
    // Clean up temporary vector
    Ammo.destroy(tempVec);
}

// 4. Instantiate the shape using the heap pointers
const multiSphereShape = new Ammo.btMultiSphereShape(posBuffer, radBuffer, numSpheres);

// 5. Free the allocated heap memory now that the shape is created
Ammo._free(posBuffer);
Ammo._free(radBuffer);

Scaling a btMultiSphereShape

To scale a btMultiSphereShape dynamically within your simulation, use the setLocalScaling method inherited from the base btCollisionShape class. This method accepts an Ammo.btVector3 representing the scaling factors along the X, Y, and Z axes.

// Define the scale vector (e.g., scale x and z by 1.5, and y by 2.0)
const scaleVector = new Ammo.btVector3(1.5, 2.0, 1.5);

// Apply the scale to the multi-sphere shape
multiSphereShape.setLocalScaling(scaleVector);

// Clean up the scale vector from memory
Ammo.destroy(scaleVector);

Updating the Physics World After Scaling

If the btMultiSphereShape is already attached to an active btRigidBody in your physics world, applying a new scale will not automatically update the bounding volume or the rigid body’s inertia tensor. You must manually force the physics world to recalculate these values:

// Assuming 'rigidBody' is the body using your multiSphereShape
// and 'physicsWorld' is your Ammo.btDiscreteDynamicsWorld instance

// 1. Recalculate local inertia for the scaled shape
const localInertia = new Ammo.btVector3(0, 0, 0);
const mass = rigidBody.getInvMass() > 0 ? 1.0 / rigidBody.getInvMass() : 0;
if (mass > 0) {
    multiSphereShape.calculateLocalInertia(mass, localInertia);
    rigidBody.setMassProps(mass, localInertia);
}

// 2. Update the broadphase collision AABB (Axis-Aligned Bounding Box)
physicsWorld.updateSingleAABB(rigidBody);

// 3. Clean up
Ammo.destroy(localInertia);