Problem: skeletal animation was one hardcoded three-state machine. Done in #1694: packages/core/src/anim/animGraph.ts (AnimGraph, createAnimGraphRuntime), packages/core/src/anim/locomotionGraph.ts, ModelAnimationConfig.graph, the graph path in packages/shell/src/render/useModelAnimation.ts, and the animation.event event. Read those before starting.
Tasks, one PR each, in order:
-
Graph becomes the only path. In useModelAnimation.ts, when animation.states is set and animation.graph is not, build the graph with locomotionGraph({ idle, walk, run, walkSpeed, runSpeed, fadeSec, oneShots }) (for string[] one-shot variants pick with resolveOneShotClip at trigger time and set the state clip through a per-variant state) and delete the old stateActionsRef machine and one-shot code. Keep the single-clip and paused/time paths. Verify with bun run shoot claudecraft --mode play before and after (same rig, same pose) plus a bun run pr-video clip of walk to run to attack. CHANGELOG Changed.
-
IK solver. New packages/core/src/anim/ikSolver.ts: solveTwoBone({ root, mid, tip, target, pole }, out), solveFabrik(chain, target, { iterations, tolerance }), lookAt({ from, target, up, maxYaw, maxPitch }); pure vector math over [x,y,z] tuples, no three. Tests against known geometry. bun run gen.
-
Foot IK in the shell. New packages/shell/src/render/useFootIk.ts: after mixer.update, for bones named by ModelConfig.ik?: { feet: { root: string; mid: string; tip: string }[] ; lookAt?: { bone: string } }, raycast down from each foot with ctx.scene.raycast, run solveTwoBone, write bone quaternions, and blend by a weight that fades out while airborne. Wire it from SceneModels.tsx. Clip on a KayKit character standing on a terrain() slope.
-
Root motion. On load, read each clip's root-bone position track into AnimGraphClipInfo (widen it to { duration: number; rootTrack?: { times: Float32Array; values: Float32Array } }; keep plain numbers accepted for compatibility). Add rootMotion?: boolean to AnimState; when set, advance returns rootDelta: [x, y, z] for the time slice, and the shell zeroes the root bone translation and applies rootDelta via ctx.scene.entity.setPose. Test in animGraph.test.ts with a synthetic root track.
-
Instanced skinned crowds. New packages/shell/src/render/SkinnedInstances.tsx: bake each clip of a rig into a vertex-animation texture (bone matrices per frame) and render N instances with a custom ShaderMaterial sampling it (per-instance clip index, time offset). Prove with a scene of 200 animated humanoids in a clip and a frame-time number in the PR.
-
Editor Animation workspace. Flip supported: false in packages/editor/src/shell/WorkspaceRail.tsx; new packages/editor/src/AnimationPanel.tsx listing layers, states, and transitions of the selected marker's model graph (stored through packages/editor/src/modelAnimationAuthoring.ts meta), a scrub slider driving runtime.advance, and a trigger button per transition. Shots of the panel. Closes #1683.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1683; bun run gen after export changes (JSDoc on every export); CHANGELOG bullet; check-types and test on core, shell, editor as touched plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; merge origin/main before pushing; shots via bun run games:clone, bun run shoot daemon start, bun run shoot <game> --mode play --size half, clips via bun run pr-video, publish with bun run pr-shots; squash auto-merge; fix CI on the same branch.
Problem: skeletal animation was one hardcoded three-state machine. Done in #1694:
packages/core/src/anim/animGraph.ts(AnimGraph,createAnimGraphRuntime),packages/core/src/anim/locomotionGraph.ts,ModelAnimationConfig.graph, the graph path inpackages/shell/src/render/useModelAnimation.ts, and theanimation.eventevent. Read those before starting.Tasks, one PR each, in order:
Graph becomes the only path. In
useModelAnimation.ts, whenanimation.statesis set andanimation.graphis not, build the graph withlocomotionGraph({ idle, walk, run, walkSpeed, runSpeed, fadeSec, oneShots })(forstring[]one-shot variants pick withresolveOneShotClipat trigger time and set the state clip through a per-variant state) and delete the oldstateActionsRefmachine and one-shot code. Keep the single-clip and paused/time paths. Verify withbun run shoot claudecraft --mode playbefore and after (same rig, same pose) plus abun run pr-videoclip of walk to run to attack. CHANGELOG Changed.IK solver. New
packages/core/src/anim/ikSolver.ts:solveTwoBone({ root, mid, tip, target, pole }, out),solveFabrik(chain, target, { iterations, tolerance }),lookAt({ from, target, up, maxYaw, maxPitch }); pure vector math over[x,y,z]tuples, no three. Tests against known geometry.bun run gen.Foot IK in the shell. New
packages/shell/src/render/useFootIk.ts: aftermixer.update, for bones named byModelConfig.ik?: { feet: { root: string; mid: string; tip: string }[] ; lookAt?: { bone: string } }, raycast down from each foot withctx.scene.raycast, runsolveTwoBone, write bone quaternions, and blend by a weight that fades out while airborne. Wire it fromSceneModels.tsx. Clip on a KayKit character standing on aterrain()slope.Root motion. On load, read each clip's root-bone position track into
AnimGraphClipInfo(widen it to{ duration: number; rootTrack?: { times: Float32Array; values: Float32Array } }; keep plain numbers accepted for compatibility). AddrootMotion?: booleantoAnimState; when set,advancereturnsrootDelta: [x, y, z]for the time slice, and the shell zeroes the root bone translation and appliesrootDeltaviactx.scene.entity.setPose. Test inanimGraph.test.tswith a synthetic root track.Instanced skinned crowds. New
packages/shell/src/render/SkinnedInstances.tsx: bake each clip of a rig into a vertex-animation texture (bone matrices per frame) and render N instances with a customShaderMaterialsampling it (per-instance clip index, time offset). Prove with a scene of 200 animated humanoids in a clip and a frame-time number in the PR.Editor Animation workspace. Flip
supported: falseinpackages/editor/src/shell/WorkspaceRail.tsx; newpackages/editor/src/AnimationPanel.tsxlisting layers, states, and transitions of the selected marker's model graph (stored throughpackages/editor/src/modelAnimationAuthoring.tsmeta), a scrub slider drivingruntime.advance, and a trigger button per transition. Shots of the panel.Closes #1683.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1683;bun run genafter export changes (JSDoc on every export); CHANGELOG bullet;check-typesandteston core, shell, editor as touched pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet; mergeorigin/mainbefore pushing; shots viabun run games:clone,bun run shoot daemon start,bun run shoot <game> --mode play --size half, clips viabun run pr-video, publish withbun run pr-shots; squash auto-merge; fix CI on the same branch.