IK and feet
Corrections the engine makes to a character's animated pose: IK chains on bones (IK.TwoBone
puts a hand or foot on a target, IK.LookAt turns a head or turret toward one) and the feet of
every Animator, anim.feet (foot lock, ground IK, footstep events). To turn a whole
node toward something it is the scenario aspect LookAt, not a bone chain.
- put a hand on a weapon grip, a door handle or a ledge
- make the head look at the camera or a target
- keep the feet planted without sliding
- put the feet on stairs and slopes
- play a footstep sound when a foot lands
- keep both hands on a gun while it moves and reloads
Platforms: every host. Ground IK (anim.feet.ik) follows the real ground only where there is a
physics world — desktop, the web player, the le.codes viewer app and the full engine variant on iOS /
Android; on the web (Web Lite), in headless tests and in the 3d variant each foot's ground is the
node's own plane.
At a glance
const main = async () => {
const scene = new Scene()
const hero = await Model.load(asset('./hero.glb'))
scene.add(hero)
scene.open()
hero.anim.playLoop('Idle') // IK corrects a pose: something must play
const grip = new Node() // a node: the engine reads it every frame
grip.position = [0.3, 1.2, 0.4]
scene.add(grip)
const hand = hero.bone('LeftHand')!.aspect(IK.TwoBone, { target: grip, pole: [0.6, 1.2, -1] }) // LeftArm ▸ LeftForeArm ▸ LeftHand
hero.bone('Head')!.aspect(IK.LookAt, { target: scene.camera, axis: [0, 0, 1], limit: 60 })
hand.ik.weight = 0.5 // blend in / out
console.log(`hand off its target: ${hand.ik.error.toFixed(3)} m`)
hero.anim.feet.set({ lock: true, ik: true }) // planted feet stay put; feet on the ground
hero.anim.feet.onStep((side, at) => console.log(`${side} foot down at ${at.x.toFixed(2)}, ${at.z.toFixed(2)}`))
}
main()on a loaded Model whose Animator has not used any clip yet (nothing played or
measured), attaching a chain collapses the whole skeleton onto the model's origin — even at
weight: 0. Once a clip has been bound (played — stopped since or not — or read with
anim.time() / anim.clipInfo()), chains work. Start a loop first. A bare node tree is not
affected.
When the IK solves in a frame: moving targets, reading bones {#frame-order}
Nothing runs in JS per frame — the aspects describe, the engine solves. The order: updateBefore() →
physics → the animators (clips, root motion, the warp, the feet)
→ update() → the IK chains → the dynamic bones → skinning. A target the
game moves in update() is reached the same frame; a chain's bone read in update() still shows the
pose before the solve (read it in updateBefore(), which sees the previous frame's final pose).
Put a hand or foot on a target: two-bone IK, IK.TwoBone {#two-bone-ik--iktwobone}
bone.aspect(IK.TwoBone, { target?, pole?, rotation?, rotationWeight?, weight?, enabled?, anchor?, anchorTime?, anchorPin? })
bone.ik // the aspect — every option is a live property
bone.ik.error // after the last solve: metres the end bone is still off its target (0 = reached)| Option | Default | Meaning |
|---|---|---|
target |
— | Where the end bone's origin goes: a Node (read by the engine every frame) or a world point |
pole |
the animated bend | Bend hint — the middle joint is pulled toward it: a Node or a world point |
rotation |
the animated one | World rotation the end bone takes: a Node (its world rotation) or a Quat — a hand on a grip, a foot flat on a slope |
rotationWeight |
1 |
Share of rotation, on top of weight |
weight |
1 |
0–1 blend of the whole solve |
enabled |
true |
false = no solve |
anchor, anchorTime, anchorPin |
'', 0.05, 0 |
Ride a socket instead of a target — below |
Attach to the END bone: the chain is it, its parent and its grandparent (upper arm ▸ forearm ▸ hand).
The grandparent and parent rotate so the end's origin reaches target; a target out of reach
stretches the limb toward it and error says how far it stayed. The chain belongs to the bone's rig —
the nearest ancestor carrying an Animator (every Model); a bare node tree gets an Animator on its
topmost node. Detaching the aspect removes the chain.
Make a head look at a target: IK.LookAt {#look-at--iklookat}
bone.aspect(IK.LookAt, { target?, axis?, limit?, weight?, enabled? })
bone.lookAt // the aspect (weight / enabled / error, as above)| Option | Default | Meaning |
|---|---|---|
target |
— | A Node (read every frame) or a world point |
axis |
[0, 0, -1] |
The bone's LOCAL direction that should point at the target — rigs differ (many Mixamo heads look along +Z) |
limit |
80 |
Max deflection from the animated direction, degrees |
weight |
1 |
0–1 blend |
enabled |
true |
false = no solve |
Attach to the bone itself. A target beyond limit turns the bone limit degrees toward it and no
further.
the accessor is lookAt, so on that bone it replaces the node's own lookAt() method.
The scenario aspect LookAt (accessor facing) is a different thing:
it turns a whole node, in JS.
Keep the hands on a weapon: sockets and anchored chains {#sockets-and-anchored-chains}
anim.sockets(bone: Node | string, { [name]: Node | { position?, quaternion? } }, { set? }): Animator
anim.removeSocket(name, set?)
hand.aspect(IK.TwoBone, { anchor: 'grip_l', anchorTime?, anchorPin? })
clip.anchors(bone: string, { donor?, spans?: [from, to, socket, rotation?, pin?][] }): AnimationClip // seconds
clip.clearAnchors(bone)
hand.ik.anchorDelta // { distance (m), angle (rad) } — how far the live socket moved the hand off the takeFor hands on a weapon. A socket is a named frame on a bone of the rig: a Node (an empty on the
gun — read every frame, so a socket on a moving part follows it) or a fixed { position, quaternion }
in the bone's space. The default set is the LIVE one (the gun in the hands); { set: 'tr15' } names a
DONOR set — the same sockets on the gun the takes were authored on. An anchored chain
(anchor: 'grip_l' — target, pole and rotation are then ignored) keeps the animated hand's
pose relative to its socket on the donor and replays it relative to the live one: shifted and turned
by the two sockets' difference, never re-authored. rotationWeight is the share of the socket's turn
it takes.
A clip's anchor spans move the hand between sockets while it plays: [from, to, socket] in
seconds of the clip ('' = free: the hand rides its own joint), rotation 0–1 (default 1), pin 0–1
(default 0 — a hand on its way to a magazine keeps the take's own motion). Between spans the hand is on
the chain's own socket, held there by anchorPin (0–1: 1 = exactly on the socket, so nothing in the
pose below can slide it). The hand moves between sockets on a spring whose halflife is anchorTime
seconds. A clip without anchors takes no part; AnimationClip.from carries the spans, re-timed
(without their pin, which falls back to 0), and a mirrored clip keeps them on the same bone.
arms.anim.sockets('ik_hand_gun', { grip_r: gun.gripR, grip_l: gun.gripL, mag: gun.mag })
arms.anim.sockets('ik_hand_gun', { grip_l: { position: [0.03, -0.12, -0.05] } }, { set: 'tr15' })
arms.bone('hand_l')!.aspect(IK.TwoBone, { anchor: 'grip_l' })
arms.anim.clip('Reload')!.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag']] })Planted feet and feet on the ground: anim.feet {#the-feet--animfeet}
anim.feet.set({ lock?, ik?, left?, right?, pelvis?, … }) // only the keys given change — chainable
anim.feet.lock = true / anim.feet.ik = true / … // every option is a property too
anim.feet.options // what was set
anim.feet.state('left') // FootState | undefined — { locked, weight, anchor: Vec3, target: Vec3 }, world
anim.feet.kneeAxis('left') // the calibrated knee hinge axis (thigh-local) + its spread (rad); undefined = no leg chain
anim.feet.onStep((side, position) => …) / anim.feet.offStep(cb) // a foot planted: 'left' | 'right', world Vec3| Option | Default | Meaning |
|---|---|---|
lock |
false |
FOOT LOCK: a foot the shown clip calls planted stays where it landed — heel to ball, rolling as the clip rolls — the leg re-solved while the body moves on |
ik |
false |
GROUND IK: each foot on the ground probed under it (stairs, a slope, a kerb), aligned to its normal, the pelvis lowered so the leg can reach. Probes the physics world; without one the ground is the node's own plane |
left, right |
classified | The contact bones per side — 'LeftFoot', or ['LeftFoot', 'LeftToeBase']. Default: found by name (Mixamo / Unity / Blender / UE). Setting them re-bakes every clip's contacts |
pelvis |
0.35 |
Metres the pelvis may drop for the ground |
unlockDistance |
0.10 |
Metres of difference a locked foot absorbs at most — beyond it the anchor follows the animation instead of fighting it |
lockIn, lockOut |
0.08, 0.12 |
The lock's ease in / out, seconds |
align |
1 |
0–1: how much the foot tilts onto the ground normal |
probe |
0.6 |
The probe ray's reach above and below the ankle, metres |
plantSpeed |
0.2 |
The lock plants only once the animated ankle is slower than this, m/s |
state() is undefined before anything has played and on hosts without the feet stage. A
Locomotion sets the feet itself (the lock on by default).
Footstep sounds and events: anim.feet.onStep {#footsteps}
anim.feet.onStep((side, position) => …) fires when a foot lands in what the BASE layer shows —
'left' or 'right' and the world point — at each contact of the clip: the contact marks the asset
pipeline writes into the GLB (lecodes assets marks), or contacts detected from the motion when
there are none; a clip without contacts fires nothing. offStep(cb) removes the listener.
const main = async () => {
const step = await Sound.load(asset('./sfx/step.wav'))
const hero = await Model.load(asset('./hero.glb'))
const scene = new Scene()
scene.add(hero)
scene.open()
hero.anim.playLoop('Walk')
hero.anim.feet.onStep((side, at) => audio.playAt(step, at, { volume: side === 'left' ? 0.5 : 0.6 }))
}
main()See also
- Animator — clips, loops, one-shots, layers
- Locomotion — character movement, stride and orientation warping
- DynamicBone — secondary motion, solved after the chains
- Aspects —
update/updateBefore, ordering