Inverse kinematics

Entity-target IK goals with joint limits, FABRIK chains, and two-joint limb solvers.

meep’s IK lives in two places:

  • engine/physics/inverse_kinematics/ - the solver algorithms, written as plain functions against Transform64 / Vector3 / Quaternion.
  • engine/ecs/ik/ - the ECS layer that binds those solvers to entities, to the transform hierarchy, and to entity targets.

The algorithms are pure maths. In the ECS layer a bone is an entity, not an object inside a renderer’s skeleton. The goal system evaluates world poses from the hierarchy and live animation clips, then writes local corrections back.

Entity-target goals

IKGoal places an effector bone at a target entity’s world pose through the TransformAttachment hierarchy. Add the component to the effector and register IKGoalSystem; the chain root rotates but stays in place. Physics Joint components on the chain supply joint limits. This path does not require a terrain.

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

await em.addSystem(new IKGoalSystem());
ecd.addComponentToEntity(effectorEntity, IKGoal.from({
    target: targetEntity,
    chain_root: rootBoneEntity,
    position_weight: 1,
    rotation_weight: 0,
}));

All four fields passed to IKGoal.from are required. Both weights range from 0 to 1: zero position weight restores the pose without the goal, while rotation weight blends the effector’s chain rotation towards the target’s rotation. IKGoalSystem depends on [IKGoal, TransformAttachment]; max_iterations defaults to 16 and tolerance to 1e-6 world units.

Target and chain-root entity references stay fixed while the component is attached. Remove and re-add it to change them. Destroying the target or a bone in the chain removes the goal. IKGoal.serializable is false; recreate goals after spawning the model’s bones.

Physics contact targets

ContactProbe and ContactProbeSystem under src/engine/physics/ecs/ place a target entity through its TransformAttachment. A probe casts from a reference entity’s world pose plus a world-axis offset. radius: 0 uses a ray; a positive radius uses a sphere cast. Its unit-length direction, positive distance and non-negative radius define the query. A hit places the target on the surface and aligns its up axis with the contact normal; a miss places it at the reference pose. The hit field reports the last result.

Register ContactProbeSystem before IKGoalSystem so contact targets are placed before IK consumes them. A probe reference can be an animated bone, but must not be a bone held by a CPU writer such as IK or cloth: that would feed the previous correction back into the target. Probes skip the character’s own colliders by following ParentEntity lifetime roots. ContactProbe.serializable is false.

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. Use this low-level helper for chains you drive yourself; IKGoalSystem owns its own goal-solving path.

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

fabrik_solve(
    joints,             // Transform64[] - 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 (default)
);

Parameters

ParameterTypeNotes
jointsTransform64[]Chain joints, root first. Translations, rotations and matrices are written - the announcement is not: call t64_announce_change(ecd, entity) per joint afterwards, or the meshes built on those transforms stay where they were
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 - the solver exits early once the 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 - the distance from origin to target exceeds the summed link lengths - the primitive stretches the chain in a straight line toward the target instead of iterating.

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


Two-bone IK

The low-level two-joint helper turns a three-joint limb towards a target.

Algorithm

two_joint_ik in engine/physics/inverse_kinematics/two_joint_ik.js is based on the analytic two-joint solution described in “Simple Two-Joint IK”, which the source cites.

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

two_joint_ik(
    a, b, c,        // Vector3 world positions of root, mid, effector
    t,              // Vector3 target position
    new Vector3(0, 0, 1), // unit hinge axis in the mid joint's local frame
    a_gr, b_gr,     // Quaternion global rotations of root and mid bone
    a_lr, b_lr      // Quaternion local rotations - updated in place
);

The hinge axis must be normalized. The solver transforms it by the mid joint’s global rotation, preserves each bone’s component along that hinge, clamps the target to the resulting reachable interval, and writes local rotation deltas.

  • Skeletons & skinning - how a bone is addressed, and the GPU skinning path IK writes into
  • Animation graphs - clip-driven animation that IK post-processes
  • Hierarchy - TransformAttachment, and why a solver writes local transforms
  • Physics queries - casts used to produce contact targets
  • Source: engine/physics/inverse_kinematics/, engine/ecs/ik/