Animation

Inverse kinematics

Meep provides three IK solvers — FABRIK for arbitrary chains, two-bone IK for limbs with a pole target, and one-bone surface alignment for foot placement on terrain.

Meep’s IK systems live in two places:

  • engine/physics/inverse_kinematics/ — the solver algorithms, written as plain functions against Transform / Vector3 / Quaternion.
  • engine/ecs/ik/ — the ECS layer that binds those solvers to entities, skeletons, and terrain raycasts.

ECS components and system

An entity that needs IK adds an InverseKinematics component. The component holds an array of IKConstraint objects — one per limb or surface-aligned bone. InverseKinematicsSystem runs every frame after rendering, reads the skeleton from the co-located Mesh component, and dispatches each constraint to the appropriate solver.

import { InverseKinematics } from
    "@woosh/meep-engine/src/engine/ecs/ik/InverseKinematics.js";
import { InverseKinematicsSystem } from
    "@woosh/meep-engine/src/engine/ecs/ik/InverseKinematicsSystem.js";

em.addSystem(new InverseKinematicsSystem());

const ik = new InverseKinematics();
ik.add({
    effector: "foot-left",          // HumanoidBoneType string
    solver:   "2BIK",               // "2BIK" or "1BSA"
    offset:   0.05,                 // metres above contact point
    distanceMin: 0,
    distanceMax: 0.3,               // influence fade-out range
    strength: 1,
    limit:    Math.PI * 0.9         // max rotation delta in radians
});

IKConstraint fields

FieldTypeMeaning
effectorstringHumanoidBoneType value identifying the end bone
solverstringSolver key: "2BIK" or "1BSA"
offsetnumberDistance offset along the contact normal (positive = above surface)
distanceNumericInterval[distanceMin, distanceMax] — effector distances mapped to [full, no] influence
strengthnumberOverall influence multiplier [0, 1]
limitnumberMaximum rotation angle (radians) before the constraint is clamped or skipped

InverseKinematics.add({…}) is the factory method; it creates an IKConstraint and pushes it onto constraints.

How the system dispatches

Each frame InverseKinematicsSystem.update(dt):

  1. Retrieves the terrain from the scene.
  2. Traverses all entities with both InverseKinematics and Mesh.
  3. Skips entities whose mesh has MeshFlags.InView cleared (not visible).
  4. Creates an IKProblem (pooled) per constraint and queues it under the solver key.
  5. After traversal, calls solver.solve(problem) for every queued problem.

The two registered solvers are keyed "2BIK" (TwoBoneInverseKinematicsSolver) and "1BSA" (OneBoneSurfaceAlignmentSolver).


FABRIK — arbitrary chains

fabrik_solve in engine/physics/inverse_kinematics/fabrik/fabrik_solve.js implements FABRIK (Forward And Backward Reaching Inverse Kinematics) for chains of arbitrary length.

import { fabrik_solve } from
    "@woosh/meep-engine/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.js";

fabrik_solve(
    joints,             // Transform[] — updated in place
    lengths,            // number[] — distance to the next joint
    origin,             // Vector3 — where the root must stay
    target,             // Vector3 — where the tip should reach
    4,                  // max_iterations (default)
    1e-7                // distance_tolerance squared (default)
);

Parameters

ParameterTypeNotes
jointsTransform[]Chain joints, root first. Positions and rotations are written.
lengthsnumber[]lengths[i] is the distance from joint i to joint i+1
originVector3Root anchor — the first joint is pinned here
targetVector3Goal position for the tip joint
max_iterationsnumberDefault 4 — solver exits early if tolerance is met
distance_tolerancenumberSquared distance threshold for early exit; default 1e-7

The underlying primitive fabrik3d_solve_primitive works entirely on a flat Float32Array of packed XYZ positions to avoid allocation in the inner loop. Chains up to 64 joints use a pre-allocated scratch buffer; longer chains allocate a temporary array.

When the target is unreachable (distance to target exceeds total chain length), the primitive stretches the chain in a straight line toward the target rather than iterating.

After the position pass, fabrik_solve computes the rotation delta for each joint by comparing the before-and-after bone direction using Quaternion.fromUnitVectors and applies it with multiplyQuaternions.


Two-bone IK

TwoBoneInverseKinematicsSolver (solver key "2BIK") handles limbs — the canonical three-bone chain: upper-arm/thigh (A), forearm/shin (B), hand/foot (C). It uses a terrain raycast to find the contact point, then calls two_joint_ik to compute the local-space rotation deltas.

Algorithm

two_joint_ik in engine/physics/inverse_kinematics/two_joint_ik.js is based on the analytic two-joint solution from Danny Green’s “Simple Two-Joint IK”.

import { two_joint_ik } from
    "@woosh/meep-engine/src/engine/physics/inverse_kinematics/two_joint_ik.js";

two_joint_ik(
    a, b, c,        // Vector3 world positions of root, mid, effector
    t,              // Vector3 target position
    0.01,           // epsilon for rounding compensation
    a_gr, b_gr,     // Quaternion global rotations of root and mid bone
    a_lr, b_lr      // Quaternion local rotations — updated in place
);

It computes the required interior angles using the cosine rule, then applies angular deltas via axis-angle rotations constructed from the cross product of current and target bone directions.

Terrain contact and influence

The ECS solver raycasts the terrain twice:

  1. From boneA toward boneC to find the surface intersection.
  2. From boneC back along the contact normal to refine the contact point on uneven geometry.

If the effector is already above the surface by more than constraint.distance.max (normalised against limb length), influence is 0 and no rotation is applied. If the effector is below the surface (penetration), influence is 1. Between those extremes, influence fades linearly with the hover distance. The final rotations are lerped between the current pose and the IK target pose by influence × strength.


One-bone surface alignment

OneBoneSurfaceAlignmentSolver (solver key "1BSA") aligns a single bone’s up-axis to the terrain normal — the primary use case is a foot bone conforming to a sloped surface.

How it works

  1. Raycasts the terrain along the parent-to-child bone direction to find the contact point and normal.
  2. Raycasts again from the bone back along the normal to get the precise surface point.
  3. Computes an axis-angle rotation that rotates the bone’s local forward vector to align with the contact normal.
  4. Clamps the rotation against constraint.limit (angle to the parent’s quaternion). If the required rotation exceeds the limit, influence is reduced to the limit boundary.
  5. Lerps the bone’s current rotation toward the target by strength × influence and writes it back.

The offset field displaces the target position along the contact normal before alignment, letting you keep the bone’s pivot a fixed distance above the surface (e.g. a foot pivoting above the ground plane).


  • Skeletons & skinningHumanoidBoneType, BoneMapping, and findSkeletonBoneByType which IK solvers depend on
  • Animation graphs — clip-driven animation that IK post-processes
  • Source: engine/physics/inverse_kinematics/, engine/ecs/ik/