Log inGet started

Inverse kinematics

An animation clip stores joint angles. Play it and the character does the same thing every time, which is exactly what you want from a performance and exactly what fails the moment the world has a…

Inverse kinematics is the correction. Forward kinematics goes from joint angles to where the hand ends up; inverse kinematics goes the other way — you say where the hand should end up, and the solver works out the angles. In practice IK almost never replaces animation. It takes the pose a clip produced and adjusts the parts of it the world constrains.

The shape of it

You give a solver a chain (a run of bones from a root to a tip), a target, and a weight. It rewrites the chain's rotations so the tip reaches the target, blended against the pose that came in.

hero.component.add("IKLimb", { role = "rightarm", target = doorknobRef })

That is the whole idea. Everything else is about which chain, which solver, and how much.

Two ways to name a chain

By bone name. Works on any rig — humanoid, quadruped, crane, tentacle, desk lamp. Nothing assumes a skeleton shape.

crane.component.add("IKChain", {
    rootBone = "boom_base", tipBone = "boom_tip",
    target = hookRef, solver = "ccd",
})

By canonical role. A humanoid rig carries a role map, so "rightarm" resolves to whatever that particular mesh calls its upper arm. The same configuration then works on every humanoid.

hero.component.add("IKLimb", { role = "rightarm", target = doorknobRef })

The role names are the engine's own vocabulary and are not always the obvious word: an upper arm is leftarm and the forearm is leftforearm, while an upper leg is leftupleg and the shin is leftleg. IKLimb accepts leftarm, rightarm, leftleg, and rightleg, and tells you so if you name something else.

A rig with no role map — most non-humanoids — fills none of them. That is not a failure; it is what the bone-name form is for.

Picking a solver

SolverForWhy
twoBonearms, legsExact, single pass. The law of cosines answers outright.
ccdshort chains with joint limitsOne bone rotates per step, so a limit is enforced there and then.
fabriktentacles, ropes, long spinesSolves in positions rather than rotations; converges far faster on long runs.
aimweapons, turretsPoints an axis. An orientation, not a position.
lookAtheads, spinesSplits one clamped turn between body and head.

Weight is how IK gets used

weight blends the solved pose against the animated one. Zero is an exact pass-through and costs nothing — the solve is skipped. One is the full correction.

Ramping it is the normal pattern. A hand that snaps onto a doorknob reads as a glitch; a hand that reaches over a quarter second reads as intent. Fade in as the character commits, fade out as they leave.

Poles, and why a knee bends the wrong way

Two bones reaching a target have two solutions — the joint can bend either way. A pole target picks one: put an entity behind the elbow or in front of the knee and the joint bends toward it.

hero.component.add("IKLimb", {
    role = "rightarm", target = doorknobRef, pole = elbowHintRef,
})

Without a pole the bend plane the incoming animation produced is preserved. That is stable and usually right, and it is why IK does not flip a joint that was already posed sensibly. Reach for a pole when the animated bend is not the one you want, or when the chain starts straight and has no plane to preserve.

Feet on real ground

IKRig.grounding casts a ray under each foot, moves that foot's target to the surface, tilts it to the normal within groundingMaxSlope, and drops the pelvis far enough that neither leg has to over-reach.

hero.component.add("IKRig", { grounding = true, groundingMaxSlope = 45 })

The pelvis drop is the part people forget. Put a character on a slope and correct only the feet, and the downhill leg stretches straight and still comes up short. Lowering the hips is what gives it the room.

An explicit foot target wins over the grounder, which still counts that foot toward the pelvis drop. That is how "plant this foot on the ledge, let the other follow the terrain" stays coherent.

Reach is finite

A limb reaches as far as its bones are long, and no further. A target beyond that has no solution: the chain extends straight toward it and stops. This is correct, and it is also the single most common reason a solve looks broken when nothing is wrong.

Before debugging anything else, measure how far the target is from the chain's root and compare that against reach:

local s = hero.component.get("IKLimb"):limbState()
-- s.reach            how far this chain can extend
-- s.distanceToTarget how far the tip still is FROM the target after solving

The two are different measurements and mixing them up is its own dead end. distanceToTarget is the residual error, not the distance you compare against reach — a small distanceToTarget means the solve arrived, and reading it as "the target is close, so there is plenty of headroom" gets the diagnosis exactly backwards. To compare against reach, measure root-to-target yourself from the joint positions.

reach is in the rig's model space, which is frequently not world units — the standard humanoid here reports a reach of about 61 for an arm that is 0.61 m long. The factor usually lives on a node inside the imported body rather than on the entity you set localScale on, so an entity at scale 1 can still report model units 100× world. Convert with bind.modelToWorld, or simply compare model-space quantities against each other.

Checking a solve

A screenshot cannot tell a hand that is tracking a target from a hand that happens to be near one. Every component answers that directly:

hero.component.get("IKLimb"):limbState()
-- { ready, status, boneCount, reach, distanceToTarget, hasPole, weight, error }

status is the field to read first when nothing seems to be happening: it says in words what the component is doing. A body spawned from a bundle takes a few frames to arrive, so ready is briefly false while status explains it is still waiting — and if the engine is paused it says the wait will not clear on its own. That is a different situation from a chain that genuinely could not resolve, and error is set only in that second case. Checking error alone reads a nil and concludes everything is fine.

The equivalents are chainState(), lookState(), aimState(), and rigState().

lookState().clamped deserves attention: it distinguishes "watching the target" from "turned as far as allowed and still facing away", which look identical in a capture.

How it fits with animation

IK layers onto whatever animation graph is driving a body, so a clip or a locomotion blend keeps running underneath and IK corrects the parts it owns. A body nothing animates gets a graph built around its rest pose, so IK reaches it the same way — and if an animator publishes its own graph later, IK moves onto it without being told.

Because solving happens on the pose buffer inside that graph, the corrected pose travels the same path the animation already took: skinning, bone attachments, colliders, debug drawing. IK adds no second route to the skeleton.

Try it

demos/ik_showcase has four scenes. ik_reach fades a hand onto a moving sphere. ik_grounded walks a character over a staircase and a ramp with the feet planting. ik_lookat tracks an orbiting target into the clamp and back. ik_chain runs a FABRIK tail and a clamped look-at on a fox — 25 bones, no humanoid roles at all.

Those scenes live inside the builtin library, so their require lines resolve against it: a demo writes require("modules.api.engine.lighting"). Copied verbatim into a scene in your own world that does not resolve, because the search starts at /source/. Prefix it — require("@builtin::modules.api.engine.lighting") — and it works. The same applies to any builtin path you lift out of a demo.

Measure in play mode, not edit mode

Edit mode is paused, so a ClipPlayer never advances and the body sits in its rest pose rather than the pose the clip would put it in. Any measurement taken there describes a character standing differently from the one that will exist at runtime.

The numbers stay internally consistent, which is what makes this costly: the same setup can report reachable = false at 0.66 m in edit mode and reachable = true at 0.48 m in play, purely because a crouch clip moves the shoulder. Enter play before measuring anything, and re-measure there after any change to the clip or the pose.

This is also why an animated pose is worth measuring against rather than reasoning about. A crouch clip can carry the hips and shoulders a long way from the entity's own origin — far enough that a target placed sensibly relative to the entity sits outside the arm's reach. ik.bones gives the real shoulder position; place targets relative to that.

Checking a rig, and a solve, from outside

The ik toolbox answers the questions IK debugging actually asks:

tools.use("ik", "bones", "hero", "ankle")   -- bone names + world positions + role map
tools.use("ik", "reach", "hero", "rightarm", "doorknob")  -- reachable band, in world units
tools.use("ik", "state", "hero")            -- every IK component and what it is doing

ik.bones is the one to reach for first on an unfamiliar rig: it returns the canonical role map alongside the bones, so you can stop guessing what this particular mesh calls things. ik.reach reports in world units, which is the comparison reach on a component does not give you.