Create Ragdoll Physics with Ammo.js Rigid Bodies

This article provides a practical guide on implementing a robust, stable ragdoll character using Ammo.js, the JavaScript port of the Bullet physics engine. You will learn how to define the character’s skeletal structure using rigid bodies, connect those bodies using specialized physics constraints, prevent unnatural joint configurations, and optimize the simulation to avoid common glitches like clipping or physics “explosions.”

1. Understanding the Ragdoll Architecture

A ragdoll in Ammo.js consists of a hierarchical chain of rigid bodies connected by constraints (joints). The rigid bodies represent the physical mass of the bones (such as the torso, head, thighs, and forearms), while the constraints restrict their relative motion to mimic human anatomy.

To prevent the character from collapsing into itself or jittering violently, you must configure two key components correctly: collision filtering and constraint limits.

2. Creating the Rigid Bodies

For a humanoid ragdoll, use simplified collision shapes to represent body segments. Capsule shapes (btCapsuleShapeX/Y/Z) are ideal for limbs because they lack sharp edges, reducing the likelihood of getting stuck on environment geometry. Use a sphere (btSphereShape) for the head and boxes (btBoxShape) or capsules for the torso segments.

When instantiating each rigid body: * Mass: Assign realistic relative masses (e.g., torso has the highest mass, while hands and feet have the lowest). * Damping: Set linear and angular damping on each rigid body (using body.setDamping(0.05, 0.85)) to simulate joint resistance and prevent the ragdoll from flailing infinitely.

3. Interconnecting Bodies with Constraints

To link the rigid bodies together, use Ammo.js constraints. The choice of constraint depends on the biological joint you are simulating:

Setting Up a Cone-Twist Joint

To connect two limbs (Body A and Body B), you must define local transformation frames (btTransform) for where the joint attaches relative to the center of mass of each body.

// Define local frames of reference for the joint
const frameInA = new Ammo.btTransform();
frameInA.setIdentity();
frameInA.setOrigin(new Ammo.btVector3(0, -lowerOffset, 0)); // Bottom of Body A

const frameInB = new Ammo.btTransform();
frameInB.setIdentity();
frameInB.setOrigin(new Ammo.btVector3(0, upperOffset, 0)); // Top of Body B

// Create the Cone-Twist Constraint
const coneTwist = new Ammo.btConeTwistConstraint(bodyA, bodyB, frameInA, frameInB);

// Set limits (swing1, swing2, twist in radians)
coneTwist.setLimit(Math.PI / 4, Math.PI / 4, Math.PI / 6);

// Add constraint to the physics world
physicsWorld.addConstraint(coneTwist, true); 

The second argument in addConstraint(constraint, disableCollisionsBetweenLinkedBodies) is crucial. Setting it to true disables collisions between the two connected bodies, which prevents joint fighting and physics explosions.

4. Resolving Collision and Joint Stability Issues

Ragdolls are notorious for collapsing, clipping, or vibrating. Implement these three techniques to ensure robust stability:

Custom Collision Filtering

Adjacent limbs must not collide with each other, but they still need to collide with the environment. Set up custom collision groups and masks when adding rigid bodies to the world:

const RAGDOLL_GROUP = 1 << 1;
const ENVIRONMENT_GROUP = 1 << 2;

// Collides with everything except other parts of the same ragdoll if manually excluded,
// or use Ammo's built-in group/mask bitwise operations.
physicsWorld.addRigidBody(body, RAGDOLL_GROUP, ENVIRONMENT_GROUP);

Tuning Solver Iterations

By default, the physics solver might not resolve joint constraints accurately under heavy loads. Increase the solver precision in your physics world initialization:

const info = physicsWorld.getSolverInfo();
info.set_m_numIterations(15); // Increase from default (usually 10) to 15 or 20 for stiffer joints

Restricting Joint Angles

Always set strict lower and upper limits on hinges and cone-twist joints. Without boundaries, limbs will bend backward, breaking realism and causing the physics engine to calculate extreme corrective forces that destabilize the simulation.

5. Synchronizing Physics with the Visual Mesh

Once the physical ragdoll is simulated, you must map the position and rotation of the Ammo.js rigid bodies back to your 3D graphics skeleton (such as a Three.js SkinnedMesh).

During the physics update loop: 1. Iterate through each physical body segment. 2. Retrieve its world transform using body.getMotionState().getWorldTransform(tempTransform). 3. Extract the position and quaternion from tempTransform. 4. Apply these coordinates directly to the corresponding visual bone. Ensure you account for any initial rotational offsets between your physics shapes and the original skeleton’s bind pose.