Three.js From Zero · Article s3-04
Inverse Kinematics — CCDIK, FABRIK, Two-Bone
Inverse Kinematics — CCDIK, FABRIK, Two-Bone
Forward kinematics: set each joint angle, the end effector lands wherever. Rotate the shoulder, the hand moves by consequence.
Inverse kinematics: start from the desired end-effector position, solve for the joint angles that get you there. The character wants to grab a door handle at (1.2, 1.5, 0.3) — what should the shoulder, elbow, wrist angles be? That's IK.
The demo has a 5-segment arm that chases the cursor in 3D space. Drag the target point around and watch the arm curl to reach it. Toggle between algorithms to see their different behaviors.
Three algorithms, one job
| Solver | How it works | Best for |
|---|---|---|
| CCDIK | From end to root, rotate each bone to point at target. Iterate. | Tentacles, tails, any chain. Easy to implement. |
| FABRIK | Forward pass: move each joint toward target while preserving segment length. Backward pass: pull root back. Repeat. | Long chains, cloth-like behavior. |
| Two-bone analytic | Closed-form formula for 2 segments (upper arm + forearm). No iteration. | Arms, legs. Fast, stable. |
Two-bone IK — the analytic solution
The most important IK solver you'll implement. Exact. Zero iterations. Used for every arm and leg in every game.
Given three joints (shoulder, elbow, wrist), their rest lengths (upper = L1, forearm = L2), and a target position T: find the elbow angle and shoulder rotation so the wrist reaches T.
Step 1: Law of cosines for the elbow angle.
const d = target.distanceTo(shoulder);
const d_clamped = Math.min(d, L1 + L2 - 0.001); // can't stretch beyond reach
// Interior angle at elbow (triangle with sides L1, L2, d)
const cosInterior = (L1*L1 + L2*L2 - d_clamped*d_clamped) / (2 * L1 * L2);
const elbowInteriorAngle = Math.acos(cosInterior);
// The bend angle we apply to the elbow bone (π - interior)
const elbowBend = Math.PI - elbowInteriorAngle;
Step 2: Aim the upper bone along the line shoulder → target.
const toTarget = target.clone().sub(shoulder).normalize();
const restDir = new THREE.Vector3(0, -1, 0); // whatever your rig's down-axis is
const q = new THREE.Quaternion().setFromUnitVectors(restDir, toTarget);
shoulderBone.quaternion.copy(q);
// Apply the bend to the elbow around its local bending axis (X or Z)
elbowBone.rotation.x = elbowBend;
Step 3 (optional but realistic): use a pole vector to resolve the ambiguity of which way the elbow points. Without it, the elbow can flip. Common fix: aim the elbow toward a "pole" point that's usually offset outward from the chain.
CCDIK — the easy iterative solver
Cyclic Coordinate Descent. Works on any chain length. Simple, stable, sometimes snaky. Here's the full algorithm:
function solveCCDIK(chain, target, iterations = 8) {
// chain = [root, ..., endEffector], each an Object3D
for (let iter = 0; iter < iterations; iter++) {
// Loop from last-but-one bone back to root
for (let i = chain.length - 2; i >= 0; i--) {
const bone = chain[i];
const end = chain[chain.length - 1];
const bonePos = bone.getWorldPosition(new THREE.Vector3());
const endPos = end.getWorldPosition(new THREE.Vector3());
const toEnd = endPos.clone().sub(bonePos).normalize();
const toTarget = target.clone().sub(bonePos).normalize();
// The rotation that moves the end effector toward the target
const q = new THREE.Quaternion().setFromUnitVectors(toEnd, toTarget);
// Convert world-space rotation to bone's local frame
const parentQ = bone.parent.getWorldQuaternion(new THREE.Quaternion());
const localQ = parentQ.invert().multiply(q).multiply(bone.getWorldQuaternion(new THREE.Quaternion()));
bone.quaternion.copy(localQ);
}
}
}
8 iterations usually converges. 16 if you need precision. Each iteration walks from end toward root; over several passes, the whole chain accommodates the target.
Adding joint limits to CCDIK
Without limits, CCDIK bends joints any direction. Add constraints inline:
// After computing localQ, clamp the Euler angles
const e = new THREE.Euler().setFromQuaternion(localQ, 'XYZ');
e.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, e.x));
e.y = Math.max(-0.2, Math.min(0.2, e.y)); // tight twist limit
e.z = Math.max(0, Math.min(Math.PI * 0.9, e.z));
bone.quaternion.setFromEuler(e);
FABRIK — Forward And Backward Reaching IK
Each iteration does two passes over the chain, operating on bone POSITIONS directly (rotations computed after):
function solveFABRIK(positions, lengths, target, iters = 8) {
const root = positions[0].clone();
const totalReach = lengths.reduce((s, l) => s + l, 0);
const distToTarget = positions[0].distanceTo(target);
if (distToTarget > totalReach) {
// Fully stretched — point every segment at target
for (let i = 1; i < positions.length; i++) {
const dir = target.clone().sub(positions[i-1]).normalize();
positions[i].copy(positions[i-1]).addScaledVector(dir, lengths[i-1]);
}
return;
}
for (let iter = 0; iter < iters; iter++) {
// Forward: move end to target, walk back
positions[positions.length - 1].copy(target);
for (let i = positions.length - 2; i >= 0; i--) {
const dir = positions[i].clone().sub(positions[i+1]).normalize();
positions[i].copy(positions[i+1]).addScaledVector(dir, lengths[i]);
}
// Backward: pin root, walk forward
positions[0].copy(root);
for (let i = 1; i < positions.length; i++) {
const dir = positions[i].clone().sub(positions[i-1]).normalize();
positions[i].copy(positions[i-1]).addScaledVector(dir, lengths[i-1]);
}
}
}
Converges faster than CCDIK in most cases. Feels more "cloth-like" — the whole chain adjusts together rather than joint-by-joint.
To drive bones from positions, compute the rotation needed to align each bone's rest-forward with the vector to the next position, and apply.
The pole vector — which way does the elbow bend?
If your arm can bend in any plane, IK leaves the elbow ambiguous. Pole vectors fix it.
A pole is a position in space. The plane containing (shoulder, wrist, pole) is the plane in which the elbow must stay. For an arm, the pole is typically behind the shoulder — the elbow points backward. For a leg, below/behind the hip — the knee points forward.
// After computing the bend angle (two-bone), rotate the arm around the shoulder→wrist axis
// so the elbow points toward the pole
const axis = target.clone().sub(shoulder).normalize();
const elbowProject = projectPointOnLine(elbow, shoulder, target);
const elbowToPlane = elbow.clone().sub(elbowProject);
const poleToPlane = pole.clone().sub(projectPointOnLine(pole, shoulder, target));
const q = new THREE.Quaternion().setFromUnitVectors(
elbowToPlane.normalize(), poleToPlane.normalize(),
);
upperArmBone.applyQuaternion(q);
Foot IK — for locomotion
Game characters walking on uneven terrain. The clip was authored for flat ground — on a slope, one foot hovers or sinks. Fix with foot IK:
- Each frame, cast a ray from above each foot, downward.
- If the ray hits the ground at a different height than the foot's current height, adjust.
- Solve two-bone IK for the leg so the foot lands on the ray hit point.
- Rotate the foot to align with the surface normal.
- Optionally lower the hip so both feet can reach.
Unreal Engine's "ControlRig" does this. So does Unity's "Rig Builder". In Three.js you write it yourself (or use drei-vanilla's IK helpers).
Look-at IK — head / eyes
Not a solver — just a rotation to make a bone face a target. Three.js has
Object3D.lookAt but that sets world orientation, which doesn't play with
child-bone parents. For bones, use:
function lookAtIK(bone, target) {
const bonePos = bone.getWorldPosition(new THREE.Vector3());
const toTarget = target.clone().sub(bonePos).normalize();
const restDir = new THREE.Vector3(0, 0, 1); // forward in bone's local
const worldQ = new THREE.Quaternion().setFromUnitVectors(restDir, toTarget);
const parentInvQ = bone.parent.getWorldQuaternion(new THREE.Quaternion()).invert();
bone.quaternion.copy(parentInvQ.multiply(worldQ));
}
Apply to head/neck/eye bones. Add soft limits and damping so it doesn't snap 180° instantly.
Three.js's built-in CCDIK helper
For glTF characters, drei-vanilla's CCDIKSolver plays nicely:
import { CCDIKSolver } from 'three/addons/animation/CCDIKSolver.js';
const iks = [{
target: 22, // bone index of the IK target
effector: 8, // bone index of the end effector
links: [{ index: 7 }, { index: 6 }], // chain, effector's parents
iteration: 10,
minAngle: 0.0, maxAngle: 1.0,
}];
const ikSolver = new CCDIKSolver(skinnedMesh, iks);
// In the animation loop:
ikSolver.update();
Common first-time pitfalls
- End effector jitters. Too few iterations, or your target is unreachable. Increase iterations, or clamp target to chain's reach.
- Chain flips when target passes through root. Classic CCDIK behavior. Use FABRIK or add hysteresis.
- Elbow points wrong direction. Missing pole vector. Add one at a sensible location.
- Bone twists oddly. Your rest-forward axis mismatch. The bone's "forward" in local space must match the
restDiryou use in setFromUnitVectors. - IK fights with keyframed animation. IK runs AFTER the mixer updates. Either layer IK on top (additive correction) or replace the affected bones entirely.
- Target snaps to zero on first frame. Target was never set before solve. Initialize target to effector's world position at start.
Exercises
- Tentacle: 15-segment chain chasing the cursor smoothly with FABRIK.
- Arm with pole: implement a two-bone arm + pole vector. Drag the pole separately from the target.
- Foot IK on a glTF character: raycast from each foot down to a heightmap, solve two-bone for each leg.
What's next
S3-05 — Procedural Animation. Secondary motion that makes characters feel alive without keyframed animation: breathing, hair sway, tail wags, head-bob, follow-through on hand gestures.