Three.js From Zero · Article s2-03
Joints, Constraints, Ragdoll
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.
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 joints | Multibody joints | |
|---|---|---|
| How they solve | Iterative constraint impulses | Reduced-coordinate ("featherstone") articulated body |
| Stability | Can jitter at high stiffness / long chains | Rock-solid chains & articulated structures |
| Cost | Cheap per joint | Higher per chain, but converges faster |
| Use for | General scene joints, breakables, ragdolls | Robot 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:
- Create each body with its dynamic rigid-body descriptor + a capsule/ball collider
- Position all bodies in T-pose — this determines joint anchor points
- Create joints: for each pair, the anchors are the midpoint between the two body centers, expressed in each body's local space
- For revolute joints (elbows, knees), add limits so they only bend one way
- 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
- 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. - 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.
- 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.