Animation
This file covers the skeletal-animation stack: the asset formats and import lanes, the deterministic animator tick, the per-frame evaluation and pose-modifier stack, and the editor windows that author all of it.
1. Skeletal animation (assets, animator, blend trees, layers, IK, retargeting — phases A0–A9)
The skeletal-animation stack (full plan in docs/design/ANIMATION_SYSTEM.md) is a
Unity-Mecanim-parity character system split across three domains — an asset domain that cooks
skeletons, clips, and controllers; a deterministic simulation domain that advances animator state
at fixed tick; and a per-frame evaluation domain that samples, blends, and skins. Phase A0 is the
foundation the other domains stand on: the math and the skeleton asset, with no evaluator, ECS
columns, or renderer coupling yet.
The math seam additions
(engine/foundation/core/include/SushiEngine/core/types.hpp, see
the value-type seam). Sampling, blending, and IK are all
interpolation and matrix algebra the seam did not previously expose. A0 adds, behind the same one
file, lerp (vector and scalar), nlerp/slerp (both neighbourhood-corrected: they flip a
rotation to the near hemisphere so a blend takes the short arc, and slerp degrades to nlerp
when the inputs are nearly parallel), quaternion_from_matrix, affine_inverse (a true inverse
through the adjugate, so it is correct under non-uniform scale — this is what builds inverse-bind
matrices, where the rotation-only transpose short cut would be wrong), and decompose_transform
(the TRS decompose that inverts compose_transform). The interpolators are element-parametric
(Vector3T<T>/QuaternionT<T>) so the evaluator can run float while the boundary stays double;
the matrix ops stay at boundary precision because they run at import, not per frame.
The skeleton asset (engine/domain/animation/include/SushiEngine/animation/). A skeleton is a
flat, immutable, relocatable blob, never a pointer-linked tree. SkeletonView
(engine/domain/animation/include/SushiEngine/animation/skeleton.hpp) is a non-owning
structure-of-arrays view: a topologically sorted parent array with the invariant parent[i] < i (so
composing model space is a forward scan, never a pointer chase — this is what retires the engine’s
old host-side, non-topological hierarchy), bind-pose local TRS, object-space inverse-bind matrices
(JointMatrix, 16 floats, GLSL column-major — the palette layout, distinct from the double
Matrix4 because object-space joint data never needs the range that forces double), FNV-1a 64 joint
name hashes (engine/domain/animation/include/SushiEngine/animation/hash.hpp) for mask / IK /
attachment lookup, and a bone-LOD table.
The .sushiskel format (engine/domain/animation/include/SushiEngine/animation/skeleton_blob.hpp)
is versioned, little-endian, and position-independent (byte offsets, no pointers): the cook
build_skeleton_blob sorts joints by depth so the invariant holds, remaps every parent reference,
derives inverse-bind matrices from the bind pose when the source did not supply them (glTF does),
and lays the SoA sections out at aligned offsets; the load load_skeleton_blob validates the header
and returns a SkeletonView that aliases the bytes with zero copy or parse.
The glTF import lane (engine/asset/gltf/include/SushiEngine/gltf/skeleton_import.hpp). The
mesh importer bakes each primitive into its node’s world transform and drops the node graph, skins,
and inverse-bind matrices — the exact data a rig is. import_gltf_skeleton is the parallel lane
that keeps them: it reads one skin, turns each joint node’s local transform into bind-pose TRS,
copies the inverse-bind matrices through (or lets the cook derive them), and produces a .sushiskel
blob. The declaration lives on the engine’s animation surface; the implementation lives in the cgltf
lane (engine/asset/gltf/source/skeleton_importer.cpp) — the one place cgltf is linked — and adds
no new dependency, since animation is header-only over
engine/foundation/core/include/SushiEngine/core/types.hpp.
The database seam
(engine/domain/animation/include/SushiEngine/animation/animation_database.hpp).
IAnimationDatabase is the dependency-inversion boundary the simulation and evaluator will sit
behind — they request a SkeletonView by AssetId and never see the importer, the file format, or
the byte buffers. AnimationDatabase is the in-memory owner: each blob lives in its own heap buffer
so the buffers do not move as the database grows and the views it hands out stay valid for its
lifetime. Clip and controller views extend this same interface in later phases; the id space is
shared across all three asset kinds.
The editor overlay (applications/editor/source/animation/skeleton_debug_draw.*).
SkeletonPreview imports a rigged glTF and caches its bind-pose model-space joint positions;
draw_skeleton_overlay projects those through the Scene panel’s camera (the gizmo’s exact
view-projection) and paints bones, joint octahedra, and names over the rendered image. The
viewport panel takes it as one optional, defaulted parameter, so no other call site changes.
Nothing in the editor constructs one today: ViewportFrameInputs::skeleton is left null, so the
overlay is built and linked but never drawn. The readable names come from the blob’s debug name
table (SkeletonView::joint_name); the runtime path still addresses joints by hash.
samples/animation/skeleton_demo.cpp validates the whole data foundation headlessly (TRS
round-trips, the affine inverse, slerp/nlerp behaviour, an out-of-order chain that cooks and
loads to satisfy parent[i] < i, name-hash lookup, name-string round-trip, and an identity skin
matrix at the bind pose), and import_gltf_skeleton is exercised by
assets/models/rigged_chain.gltf. With this, A0 is complete; A1 adds single-clip playback
and GPU skinning (a skinned character looping an animation with zero TAA ghosting).
Of the two rigged fixtures, only assets/models/rigged_arm_anim.gltf carries an animation;
assets/models/rigged_chain.gltf is a skeleton and skin with no clip. A test that needs a
second animated controller cannot build one from repository assets and has to add a fixture.
The evaluation + animator stack (phases A1–A5, CPU cores shipped). Above the A0 assets sits the
two-halves split of the design doc’s §5. The deterministic tick
(engine/domain/animation/include/SushiEngine/animation/animator_step.hpp) is the interpreter over
the compiled .sushictrl controller
(engine/domain/animation/include/SushiEngine/animation/animator_controller.hpp): per layer it
advances normalized time, runs the state machine (Any-State then current, crossfades, triggers,
events), and accumulates root motion — all over trivially-copyable ECS columns
(engine/domain/animation/include/SushiEngine/animation/animator_components.hpp), so it is
byte-snapshottable and rollback-exact (A3).
The derived frame pose
(engine/domain/animation/include/SushiEngine/animation/animator_evaluator.hpp) is recomputed each
frame and never snapshotted: it resolves each layer’s active state to weighted clip contributions,
samples and weight-blends them, folds the layers, composes model space, and builds the object-space
palette. A state’s motion is a single clip or a blend tree
(engine/domain/animation/include/SushiEngine/animation/blend_tree.hpp, A4) — five node kinds (1D,
2D simple/freeform-directional, freeform-cartesian, direct) over a flat node/child array with a
gradient-band pair table baked at compile; the controller blob is v2 to carry it.
Layers fold gated by an avatar mask
(engine/domain/animation/include/SushiEngine/animation/avatar_mask.hpp, A5) — a name-hash-keyed
.sushimask resolved to the rig — either override (nlerp) or additive (FMA of a delta baked at
import by engine/domain/animation/include/SushiEngine/animation/additive.hpp); a layer’s weight
can be driven by a parameter through animator_step. Clip sampling and compression (clip*.hpp,
A1/A2) and the batched crowd evaluator with bone-LOD and update-rate throttling
(engine/domain/animation/include/SushiEngine/animation/batch_evaluator.hpp, A2) sit behind the
same views.
After the layers fold, an ordered pose-modifier stack
(engine/domain/animation/include/SushiEngine/animation/pose_modifier.hpp, A6) runs in model space
between compose and palette — the IPoseModifier seam, Unity’s pass ordering, so IK corrects the
final blended pose. Seven modifiers derive from it: analytic pole-controlled two-bone
(engine/domain/animation/include/SushiEngine/animation/ik_two_bone.hpp), weight-distributed
cone-clamped look-at (engine/domain/animation/include/SushiEngine/animation/ik_look_at.hpp),
iteration-capped FABRIK (engine/domain/animation/include/SushiEngine/animation/ik_chain.hpp),
composite foot placement
(engine/domain/animation/include/SushiEngine/animation/ik_foot_placement.hpp) that rays to the
ground through the IPoseTaskContext seam, multi-effector CCD
(engine/domain/animation/include/SushiEngine/animation/ik_full_body.hpp), spring-damper
secondary motion (engine/domain/animation/include/SushiEngine/animation/jiggle_bone.hpp), and
the physics-to-pose blend
(engine/domain/animation/include/SushiEngine/animation/ragdoll_blend.hpp), which converts a
body’s object-space transform into a joint-local pose before blending.
Beyond posing joints, a clip carries morph-weight and generic float tracks (.anim v2, A7):
engine/domain/animation/include/SushiEngine/animation/morph.hpp maps a clip’s morph tracks onto a
mesh’s target order — by target name, so one clip drives any mesh sharing the naming — and gives
the CPU reference of the skin-pass morph blend. The importer fills those tracks from glTF weights
animation channels, naming each after the target it drives
(engine/asset/gltf/source/animation_importer.cpp; GLTFAnimationImport::morph_target_names
reports the mesh’s own target order, the order the render mesh’s delta buffer is uploaded in), and
engine/domain/animation/include/SushiEngine/animation/generic_track.hpp routes generic tracks to
an IFloatSink binding registry (material/UI/script hooks).
Retargeting (A8) lets one clip library drive many rigs:
engine/domain/animation/include/SushiEngine/animation/humanoid.hpp maps a skeleton’s joints to
canonical HumanBones (by an alias heuristic or an explicit table), and
engine/domain/animation/include/SushiEngine/animation/retarget.hpp transfers a clip’s bind-pose
deltas onto another rig (retarget_clip) or mirrors them left-to-right (mirror_clip).
Authoring (A9) persists a controller as JSON
(engine/domain/animation/include/SushiEngine/animation/animator_controller_json.hpp, the only
animation header that pulls in nlohmann/json — the editor’s save/load and undo/redo, round-tripping
to a byte-identical blob) and previews states off the loop
(engine/domain/animation/include/SushiEngine/animation/edit_preview.hpp’s scrub_to_state).
Under the dense runtime clip sits the sparse keyframe authoring model
(engine/domain/animation/include/SushiEngine/animation/keyframe.hpp): ScalarCurve /
QuaternionCurve (the dope-sheet / curve-editor primitives, constant/linear/cubic), a
ClipAuthoring bundle that bakes to the dense ClipDescription, and a PoseRecorder that
captures a live pose into keys (the “record” workflow). The editor’s Animation window
(applications/editor/source/animation/animation_panel.*) is a GUI over it, Unity’s
Animation-window shape: it targets the Hierarchy-selected entity, and with Record armed keys its
transform as it is moved, or with Record off drives the object live in the Scene view from its
keys as the timeline is scrubbed — Bake writes a dense .anim.
The Animator Graph window
(applications/editor/source/animation/animator_graph_panel.*) is the Mecanim state-machine graph
editor over a ControllerDescription — a grid canvas of draggable state nodes, transition arrows
made by right-click ▸ Make Transition To (a "Exit" target compiles to the layer’s entry),
Entry/Exit/Any-State nodes, a parameter panel, and JSON save/load through
engine/domain/animation/include/SushiEngine/animation/animator_controller_json.hpp.
Guarded by eleven CTest suites, 138 cases — Unit_AnimationClip (the asset formats and the
compressed error bound), Unit_AnimatorStep (the state machine, root motion, and the byte-exact
determinism/rollback contract), Unit_AnimatorComponent (the ECS column’s attach/read/write
contract), Unit_AnimationBlendTree, Unit_AnimationLayers, Unit_AnimationIk (seven pose
modifiers including foot placement), Unit_AnimationMorphImport, Unit_AnimationRetarget,
Unit_AnimationControllerJSON, Unit_AnimationKeyframe, and Unit_AnimationAuthoringTail (the
design doc’s §12.4 motion matching, dual-quaternion algebra, ARKit-52 mapping and sequencer
timeline). The 21 headless demos under samples/animation/ remain as runnable examples rather
than as the verification of record.
One piece is still untested and is named rather than counted: DeviceBatchEvaluator’s
host/device agreement, the only part of the stack that needs SushiRuntime. What remains besides
it is a visual pass on the GPU side:
engine/presentation/render/source/passes/skinning_pass.*,
engine/presentation/render/source/scene/skinning_system.* and
engine/presentation/render/shaders/skinning.comp are built, linked and driven by a live
producer (Editor::AnimatedMeshPreview,
applications/editor/source/animation/animated_mesh_preview.*, for the authored subject,
RenderScene::skinned_instances for animator entities), but the compute pre-skin, the morph blend
and the opt-in dual-quaternion path have never been looked at on a display. The editor windows
the A9 phase named all shipped (Animation, Animator Graph, Animator).

