# Skeletal animation

Reference for skinned characters: how skeletons and clips are cooked, how animation graphs evaluate, and what game code can drive and read back. For a step-by-step first character, start with [Tutorial: first skeletal animation](/docs/tutorials/first-skeletal-animation.md). For non-skeletal value animation (UI, springs, transitions), see [Motion Kit](/docs/systems/motion-kit.md).

## Pipeline

```text
glTF / USD / FBX ──Shinra──► .shinmodel (mesh + skeleton + clips)
.animgraph.json   ──Shinra──► .shinanimgraph (compiled graph)
                                   │
            Animation component ───┤ per frame, in parallel character jobs
                                   ▼
        graph evaluation → layers → foot IK → joint palette → vertex-shader skinning
```

One evaluation module serves the game runtime and the editor's preview, so what Asset Detail shows is what Play runs.

## Cooked data

- **Axes.** Engine and glTF are right-handed, Y-up, −Z forward; skin data is never axis-remapped. See [Coordinate space](/docs/systems/coordinate-space.md).
- **One skin per model.** A model with several skins fails the cook. Multi-part models (`_p0`, `_p1`, …) carry the same skeleton in every weighted part; clips live on `_p0`.
- **Joints.** The palette is the skin's joints plus their ancestors up to a root, at most **256** joints (bone indices are 8-bit). Joint names are required and are the identity used for sockets, masks and IK.
- **Rest pose** is the authored local transform (glTF default pose, UsdSkel `restTransforms`, FBX node transform). It may differ from the bind pose.
- **Weights.** Up to 4 influences per vertex, normalised. Skinned vertices are 40 bytes, rigid vertices 32 bytes.
- **Bounds** are cooked once: the rest bounds grown by the largest displacement any clip produces. They are not recomputed per frame, so culling and shadows never miss a limb.
- **Collision** uses the bind pose or an authored capsule. Animated colliders are not supported.
- **Clips** are uncompressed. A cooked clip flagged as compressed is rejected at load.

## Animation component

The builtin `animation` component sits beside Render in the inspector.

| Field | Meaning |
|---|---|
| Graph | The `.animgraph.json` asset that drives the character. Without one, Clip plays directly |
| Playing / Looping | Playback state |
| Speed | Global rate; negative plays backwards |
| Time | Seek. Zero re-enters every layer's initial state |
| Root motion | `Move actor`, `Report only` or `Off` (JSON `"transform"`, `"script"`, `"off"`) — see [Root motion](/docs/systems/animation.md#root-motion) |
| Important | Exempt from the evaluation budget (not from distance culling) |

A skinned model without an animation component still draws in its rest pose.

## Animation graphs

Graphs are authored in Asset Detail (`.animgraph.json`; read the [document schema](/docs/systems/document-schemas.md) before writing one by hand). A new graph has no states and is valid; it simply holds the rest pose.

### States and parameters

- A state plays one **clip**, a **1D blend** along one float parameter, or a **2D blend** over two float parameters.
- **2D blends** use gradient-band interpolation over freeform points (up to 32). The strongest four weights are kept and renormalised. Coincident points are refused. A 2D blend needs two float parameters to exist.
- Parameters are floats, bools and triggers. They are shared by every layer of the graph.

### Transitions

- A transition has a **condition** on parameters, an optional **exit time**, a duration and a priority. Several transitions may connect the same pair of states.
- **Exit time** is a fraction of the source state's dominant clip, combined with the condition. Looping states re-arm it every lap; one-shot states keep it open once reached. `always` with exit time `1` means "return when done".
- **Interruption.** A transition can be interrupted by another one while it blends. The outgoing mix is frozen and blended toward the new destination; each fading source keeps its own clock. Set `interruptible: false` to block selection until the blend lands.
- **Any State.** Each layer has one Any State. Its transitions compete with the current state's own transitions by priority; on a tie the current state wins. An Any State transition into the current state only fires for a trigger condition.

### Sync groups

States in the same sync group share a phase clock: every clip samples at `phase × its own length`, so a Walk ↔ Run blend keeps feet planted. Transitions within a group carry the phase across; leaving the group restarts from zero. The engine aligns phase, not content: clips must be authored with matching foot contacts.

### Layers

A graph is one **base** layer plus up to four ordered **overlays** (an upper-body aim, a wave). Each overlay has its own state machine, a weight (static, optionally multiplied by a float parameter) and a joint mask (empty means the whole skeleton).

| Blend | Result |
|---|---|
| `override` | Lerps toward the layer's pose by its weight |
| `additive` | Adds the layer's difference from the **rest pose**, scaled by weight |

Because the additive reference is the rest pose, DCC-style "rest plus offset" clips need no extra metadata. Overlays never take part in the base layer's sync groups, and only the base layer produces root motion. A graph without overlays costs nothing extra.

### Clip events

Markers such as footsteps or hit frames belong to a **clip**, placed in clip seconds, so re-timing a state does not move them. Each marker has a name (up to 64 bytes) and one float `value`.

- **One clip speaks per state.** In a blend, only the dominant clip fires its markers. A 50/50 Walk/Run blend does not put down two footsteps per stride.
- **During a transition both states fire**, so the destination's first-frame markers are not delayed.
- A marker fires once per crossing, including when a frame skips several loop laps. One-shot clips fire their end marker once.
- Markers fire even when pose evaluation is skipped by LOD or budget. A distance-culled character's clock is frozen and fires nothing.
- At most 8 markers per character per step; overflow is flagged.

Delivery happens on the game thread after animation finishes: Zig `onAnimationEvent`, polling `hi.animation().events(id)` / `fired(id, name)`, or Kawa `on_animation_event(name, value, time)`. Markers are edited on single-clip state cards in the graph editor.

### Foot IK

Foot IK is an optional pass that runs after the pose (layers included) and before skinning. It is configured on the graph in the **Foot IK** panel: one leg chain (hip → knee → foot) per leg, ray lengths, sole offset and ankle tilt limits. Ranges are fractions of the rig's size.

- One ray per leg is cast down from where the foot landed last frame, ignoring the character's own body.
- The pelvis drops by the largest downward reach a **planted** foot needs (capped and smoothed); a foot that needs to come up bends its leg instead.
- A leg fades out of IK when the ground is far below a swinging foot or out of the leg's reach.
- Feet align to the ground normal within the ankle limits.
- An incomplete leg chain is skipped (and reported in the editor), so preview keeps working while you edit.

## Root motion

The **graph** decides what is extracted: the motion joint and which axes (translation X/Y/Z, yaw). The **component** decides who applies it:

| Target | Behaviour |
|---|---|
| `transform` (Move actor) | The engine moves the actor's local transform each frame |
| `script` (Report only) | Extracted and published only; read `hi.animation().rootMotion` and move a character controller yourself |
| `off` | Not extracted |

Extracted motion is removed from the pose so the character does not move twice. Crossfades contribute each state's motion at its weight, loops add whole cycles, and root motion survives LOD pose skipping. A distance-culled character produces none. Use `hi.animation().setRootMotionTarget` to switch at runtime.

## Game API

`hi.animation()` covers everything the inspector authors, plus readback.

| Area | Calls |
|---|---|
| Graph | `setGraph` / `clearGraph`, `setGraphFloat` / `setGraphBool` / `triggerGraph`, `graphFloat` / `graphBool` |
| Playback | `play` / `playGraph`, `pause` / `resumePlayback` / `stop`, `setLooping`, `setSpeed` / `speed`, `setTime`, `isPlaying` |
| Clips | `clipCount` / `clipName` / `hasClip` |
| State readback | `state` / `layerState`, `stateName` / `layerStateName`, `isInState` |
| Events | `events`, `fired`, `eventCount` / `eventAt` |
| Root motion | `rootMotion`, `setRootMotionTarget` / `rootMotionTarget` |
| Joints | `jointCount`, `jointName`, `findJoint`, `joint(actor, index, .model \| .world)`, `jointNamed` |
| Cost | `setImportant`, `setGlobalCullDistance`, `setGlobalEvaluationBudget`, `setGlobalLodBands` |

- **State readback** returns the state's name hash, unbounded time, `progress` (looping states fold to `[0, 1)`), clip length, blend weight and the previous state. Cooked graphs carry state names, so games never hash names themselves.
- **Joint readback** is null until the rig has posed once.
- A graph that has not finished cooking is remembered and bound when it arrives.

Kawa mirrors the same surface as `Animation.*`; see [Scripting with Kawa](/docs/guides/scripting-kawa.md).

### Joint sockets

An actor can hang from a joint of its parent's rig (a sword in `hand_r`): scene `parent_joint`, inspector *Attach to joint*, `hi.world().attachToJoint` / `setParentJoint`, Kawa `Actor.attach_to_joint`. The joint **name** is the identity, so a model swap that keeps joint names keeps the socket. Before the rig binds, the child hangs from the parent's origin; reparenting drops the socket. See [Scenes and gameplay](/docs/systems/scenes-and-gameplay.md).

Joint-palette allocation failure logs once per heap and uses the reserved two-matrix identity span only when that storage is resident. If even identity storage cannot be allocated, allocation is empty and GPU skinning is omitted; callers must not bind an empty span. Editor previews can retain joint overlays without a full GPU palette. Failed preview pose-buffer allocations release partial ownership and warn before leaving geometry static.

## Performance

- Characters evaluate in parallel jobs; the frame waits for them before uploading joint palettes.
- **Distance LOD** comes from the project's animation bands (`setGlobalLodBands`) and per-actor distance. Distant characters update less often, and beyond the cull distance their clock freezes.
- **Evaluation budget.** `setGlobalEvaluationBudget` caps per-frame evaluation; skipped characters still advance their clock, events and root motion. `Important` characters are exempt.
- No heap allocation per frame after a graph binds.
- Raster skinning runs in the vertex shader (4 influences, linear blend). With ray tracing on, a compute pass also writes skinned vertices for the acceleration structure.

## Current limits

- Transparent and forward-lit skinned materials render unskinned. Skinned meshes are supported in the opaque G-buffer and shadow passes.
- Swarms render rigid instances only; skinned crowds use one actor per character.
- No clip compression, retargeting, nested state machines or polar 2D blends.
- Markers carry one float value and are authored through a graph's clip state.
- Collision does not follow the animated pose.
