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

TypeScript
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()
Note

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}

TypeScript
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}

TypeScript
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.

Note

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}

TypeScript
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 take

For 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.

TypeScript
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}

TypeScript
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.

TypeScript
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