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
| Solver | For | Why |
|---|---|---|
twoBone | arms, legs | Exact, single pass. The law of cosines answers outright. |
ccd | short chains with joint limits | One bone rotates per step, so a limit is enforced there and then. |
fabrik | tentacles, ropes, long spines | Solves in positions rather than rotations; converges far faster on long runs. |
aim | weapons, turrets | Points an axis. An orientation, not a position. |
lookAt | heads, spines | Splits 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.