Three.js From Zero · Article s2-03

Joints, Constraints, Ragdoll

← threejs-from-zero S2 · Article 03 Season 2
Article S2-03 · Three.js From Zero

Joints, Constraints, Ragdoll

In S2-01 and S2-02 bodies were independent — they bumped into each other but had no real attachment. Joints change that: they glue bodies together with specific constraints on how they can move relative to each other. A hinge can only rotate around one axis. A slider can only translate along one line. A ball joint can rotate freely but can't separate. A rope can pull but not push.

Put those constraints together in the right skeleton and you get a ragdoll. The demo is one — a humanoid built from 10 rigid bodies connected by 9 joints, standing upright until you knock it over. Plus a hinge pendulum swinging in the corner and a hanging chain so you can see all three joint types in one view.

loading Rapier…
drag to orbit

What a joint actually is

In every physics engine, a joint is a constraint equation that the solver enforces on a pair of bodies each tick. "These two points on these two bodies must coincide." "This axis on body A must stay parallel to this axis on body B." "The distance between these two points must be ≤ 2 meters."

The solver iterates — adjusting positions and velocities — until all constraints in the system are simultaneously satisfied (or close enough). Joints are soft by default: they allow tiny violations each frame that get corrected over subsequent steps, which prevents explosions when the solver disagrees with itself.

Impulse joints vs multibody joints

Rapier has two joint solver flavors:

Impulse jointsMultibody joints
How they solveIterative constraint impulsesReduced-coordinate ("featherstone") articulated body
StabilityCan jitter at high stiffness / long chainsRock-solid chains & articulated structures
CostCheap per jointHigher per chain, but converges faster
Use forGeneral scene joints, breakables, ragdollsRobot arms, long rigid chains that must not flex

99% of use cases: impulse joints. They're simpler and the default in this article and the demo. Reach for multibody only when an impulse-joint chain visibly sags or vibrates.

The five joint types

1. Fixed — bodies glued together

const joint = world.createImpulseJoint(
  RAPIER.JointData.fixed(
    { x: 0, y: 0, z: 0 },     // anchor in body1's local space
    { w: 1, x: 0, y: 0, z: 0 }, // frame rotation in body1
    { x: 0, y: 0.5, z: 0 },   // anchor in body2's local space
    { w: 1, x: 0, y: 0, z: 0 },
  ),
  body1, body2, true,
);

Zero degrees of freedom. Bodies move as if welded. Useful for connecting compound parts that need independent collision shapes but shared motion (rare — usually a compound collider on a single body is better).

2. Revolute — a hinge

// Pendulum: the bob hangs from an anchor in space, free to swing
const hinge = world.createImpulseJoint(
  RAPIER.JointData.revolute(
    { x: 0, y: 2, z: 0 },   // anchor1 (world-fixed-ish, on "anchor body")
    { x: 0, y: 2, z: 0 },   // anchor2 (on the bob — at its top)
    { x: 0, y: 0, z: 1 },   // axis — rotate around Z
  ),
  anchorBody, bobBody, true,
);

1 degree of freedom: rotation around a single axis. Doors, wheels, elbows, pendulums. Limits pin the rotation to a range:

hinge.setLimits(-Math.PI / 2, Math.PI / 2);   // ±90°

3. Prismatic — a slider

const slider = world.createImpulseJoint(
  RAPIER.JointData.prismatic(
    { x: 0, y: 0, z: 0 },
    { x: 0, y: 0, z: 0 },
    { x: 1, y: 0, z: 0 },   // axis — translate along X
  ),
  railBody, carBody, true,
);
slider.setLimits(-2, 2);   // can slide up to 2 units either way

1 DOF: translation along a single axis. Elevators, pistons, sliding doors.

4. Spherical — ball-and-socket

const shoulder = world.createImpulseJoint(
  RAPIER.JointData.spherical(
    { x: 0, y: 0.4, z: 0 },    // anchor on torso (at shoulder pocket)
    { x: 0, y: 0.3, z: 0 },    // anchor on upper arm (at its top)
  ),
  torso, upperArm, true,
);

3 DOF: free rotation, no translation. The joint used in every shoulder, hip, and neck in a ragdoll. Spherical joints don't have Rapier limits in 0.14's impulse-joint API — if you want a cone limit, layer in angular damping or use a generic joint.

5. Rope / Spring / Generic

// A distance limit between two anchor points (rope / tether)
const rope = world.createImpulseJoint(
  RAPIER.JointData.rope(2.0, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }),
  anchorBody, payloadBody, true,
);

"Rope" means the two anchors can be anywhere from 0 to 2m apart but not more. Springs are a variation (target distance + stiffness). For anything beyond these presets there's JointData.generic() — you pass bitflags for which axes are locked.

Joint limits

Limits pin a joint's DOF to a range. For a revolute joint, the range is angular:

elbow.setLimits(0, Math.PI * 0.9);   // elbow bends 0° to ~160°, can't hyperextend

Without limits, a ragdoll's elbows rotate freely through the body and look horrifying. With limits, you get realistic posing. Flip the joint limits checkbox in the demo to see the difference — without limits, the ragdoll puddles.

Motors — powered joints

Every joint DOF can have a motor that drives it toward a target velocity or position:

hinge.configureMotorVelocity(
  5.0,    // target angular velocity (rad/s)
  1.0,    // damping factor (higher = more resistance)
);

// Or drive to a target angle:
hinge.configureMotorPosition(
  Math.PI / 2,  // target
  100,          // stiffness
  5,            // damping
);

Motors turn joints into actuators. Wheels that drive. Doors that open automatically. Robot arms. Rocket gimbals. Set a velocity motor on a revolute joint and your wheel rotates under power.

Building a ragdoll — the demo's skeleton

The ragdoll in the demo has 10 bodies + 9 joints. Drawing the skeleton is half the work:

                 head        (ball shape)
                  │
                  · neck (spherical joint)
                  │
             ┌────┴────┐
       shoulder·   ·shoulder   (spherical)
             │         │
       upper_arm   upper_arm   (capsules)
             │         │
         elbow·         ·elbow (revolute, limited)
             │         │
       lower_arm   lower_arm
             torso (capsule)
             │
        ┌────┴────┐
      hip·         ·hip        (spherical)
        │           │
   upper_leg   upper_leg       (capsules)
        │           │
    knee·           ·knee      (revolute, limited)
        │           │
   lower_leg   lower_leg

Order of operations, in code:

  1. Create each body with its dynamic rigid-body descriptor + a capsule/ball collider
  2. Position all bodies in T-pose — this determines joint anchor points
  3. Create joints: for each pair, the anchors are the midpoint between the two body centers, expressed in each body's local space
  4. For revolute joints (elbows, knees), add limits so they only bend one way
  5. Disable self-collision — body parts of the same ragdoll shouldn't collide with each other (use collision groups from S2-02)
// A shoulder in detail
const shoulderX = 0.18;       // distance from torso center to shoulder
const shoulderY = 0.55;       // torso top

const jd = RAPIER.JointData.spherical(
  { x: shoulderX, y: shoulderY, z: 0 },   // anchor relative to torso center
  { x: 0, y: 0.3, z: 0 },                  // anchor at top of upper-arm (it's 0.6 tall)
);
world.createImpulseJoint(jd, torso, leftUpperArm, true);

Disabling self-collision

Adjacent bodies of the ragdoll overlap slightly at joints. If they collide, the solver fights the joint → jitter. Fix: put all ragdoll parts in one collision group that doesn't collide with itself.

const RAGDOLL_GROUP = 0x0002;

// Ragdoll body parts: member of RAGDOLL_GROUP, collide with everything EXCEPT RAGDOLL_GROUP
colliderDesc.setCollisionGroups(
  (RAGDOLL_GROUP << 16) | ~RAGDOLL_GROUP & 0xffff
);

The ragdoll still collides with the ground, props, other ragdolls (different group) — just not itself.

Applying forces to a ragdoll

To knock over the demo's ragdoll, we apply an impulse to the torso:

torso.applyImpulseAtPoint(
  { x: 8, y: 2, z: 0 },         // impulse vector (strong sideways + up)
  torso.translation(),          // world-space point of application
  true,                         // wake body
);

Applying at a point (rather than at the center of mass) imparts torque as well as force. Hit the ragdoll's head to spin it, hit its torso center to translate it cleanly.

Breakable joints

const joint = world.createImpulseJoint(...);
joint.setContactsEnabled(true);    // let the body parts collide

// Check force each frame and break if exceeded
if (joint.impulseMagnitude() > THRESHOLD) {
  world.removeImpulseJoint(joint, true);
}

This is how you build destructible structures (chains that snap, armor that tears). The threshold is trial-and-error; typical ragdoll joints might survive 500–1000 N·s before tearing in a car crash simulation.

Common first-time pitfalls

  • Ragdoll explodes on spawn. Joint anchors don't match the bodies' actual positions. Joints try to pull them into alignment → giant impulses. Position bodies first, compute anchors from their final positions.
  • Ragdoll jitters at rest. Self-collision is on. Disable via collision groups.
  • Limbs pass through the body. Missing limits on revolute joints, OR the limits are wrong sign. Elbows bend 0° to +π, knees bend in the other direction — signs matter.
  • Chain sags or stretches. Impulse joints under high load need more solver iterations: world.integrationParameters.numSolverIterations = 8. Or upgrade to multibody joints.
  • Motor doesn't move the joint. You set target velocity but the body's inertia is too high for the motor's stiffness. Increase stiffness or lower the body's mass.
  • Joint anchor visualization doesn't match body position. Anchors are in LOCAL space. To visualize in world: body.localToWorld(anchor).

Exercises

  1. Add a breakable rope: a rope joint between two balls, track joint.impulseMagnitude(), and remove the joint when a threshold is exceeded. Swing the rope fast to snap it.
  2. Build a powered wheel: a cylinder on a revolute joint with a velocity motor. Stick it on a kinematic chassis. You've got a driven wheel, the core of any vehicle.
  3. Add cone limits to the neck: the spherical joint version doesn't support them natively, so use a generic joint with angular limits on the X and Z axes (leaving Y free for head turning).

What's next

Article S2-04 — Character Controller. We've made a body that flops; next we make one that walks. Capsule controller, WASD input, ground detection via shapecast, slope handling, jumping with coyote time and jump buffering.