# Animation Authoring

Blacksite's first production animation path supports skinned glTF 2.0 models (`.gltf` and `.glb`).
It normalizes imported rig and clip metadata into generated manifests, keeps authored scene data
independent of Bevy's runtime graph/player components, and uses the same controller contract in
editor preview, PIE, and standalone builds.

## Import And Placement

Place animated sources under `assets/` and refresh or reimport them from the Asset Browser. The
registry writes `assets/animations/generated/<asset-id>.animation.ron` beside the existing static
mesh artifact. Expanding a model row shows:

- skeleton entries with joint count and signature summary;
- animation clips with duration and event count;
- mesh, material, and texture entries from the static import contract.

Model Import Settings also exposes **Default animation**. Choosing a clip stores its stable
sub-asset ID and makes normal renderable-model placement create a one-state controller for that
exact clip. The editor samples the clip at its authored range start and pauses it as the edit-mode
rest presentation. **Imported rest pose** creates no controller and preserves the source node pose;
Blacksite never guesses the first clip. Generic Scene Instance placement remains animation-free.

Skeleton rows are inspect-only. Dragging an animation clip into the viewport creates one
`SkinnedMesh` actor with a stable `SkinnedMeshRenderer` and `AnimationControllerDesc` and selects it for
immediate inspection. When a skinned-mesh actor is selected, the same drag assigns the clip as a
new controller state instead. Assignment is one undoable command and requires an exact
skeleton-signature match.

Skinned primitive rows place their owning model through `SkinnedMeshRenderer` because their vertex
data requires the source skin hierarchy. Their thumbnails render the complete source scene; no
drag, details action, or context-menu action can create an invalid detached static-mesh slot.
Unrigged primitives continue to place independently through `StaticMeshRenderer`.

## Controller Inspector

The Animation Controller component exposes the selected skeleton, default crossfade, named states,
default state, clip, loop, speed, and optional playback range. State IDs are gameplay keys and must
be non-empty and unique; labels are presentation only. Component add, remove, reset, copy, paste,
and property changes use typed editor history and participate in scene dirty tracking.

The Preview section selects a state, plays or pauses it, stops playback, and scrubs within the
authored range. Preview requests use the game runtime adapter and do not change the controller,
history, or scene dirty state. Entering PIE clears any paused edit-mode scrub so the authored
default state can run; leaving PIE clears transient playback while retaining authored data. The
runtime captures imported descendant transforms and morph weights before evaluating animation.
Stop, controller removal, invalid graph rebuild, and PIE exit restore that baseline before the
configured edit-rest state is sampled again, so a preview cannot strand the actor in its last pose.

## Runtime API

Gameplay changes state by sending a validated request through `game::animation`:

```rust
game::animation::request_animation_state(
    world,
    game::animation::AnimationStateRequest::new(actor, "run").with_crossfade(0.15),
)?;
```

An omitted crossfade uses the controller default. Invalid actor or state requests return an error
and preserve current playback. If edited controller data cannot rebuild its runtime graph, the old
graph is stopped and cleared instead of continuing stale playback. `AnimationGraph`,
`AnimationPlayer`, transitions, preview state, and hydrated skinned-mesh roots are runtime-only and never
serialize into authored scenes.

## Stable Identity And Moves

`SkinnedMeshRenderer`, skeleton references, and clip references carry the model registry UUID. Generated
artifacts are keyed by that UUID; source paths are cached resolution and repair hints. When one
model is moved or renamed on disk, catalog refresh reconciles it by the registry's shared
content-addressed source fingerprint, preserves the UUID, and regenerates both manifests with the
new source path. The same registry policy covers textures and audio clips. An identical copy is not
treated as a move while the original still exists. Ambiguous content matches intentionally receive
a new identity rather than silently binding the wrong asset.

Scene schema v3 migrates legacy animated actors that paired `ModelRef` with
`AnimationControllerDesc` to the dedicated skinned renderer. Generic `ImportedModel + ModelRef`
scene instances remain readable and intentionally do not become animation actors.

Skeleton compatibility hashes the exact Bevy 0.19 glTF node-name segments and bind poses, including
whitespace and slash characters. The readable joint paths shown by the editor are deliberately not
used as identity. Animation-manifest schema v1 artifacts must be reimported to generate schema v2
signatures; schema v3 adds the explicit default-clip ID. Schema v2 deserializes as Imported rest
pose and never receives an inferred default.

Source identity uses exact byte length plus BLAKE3 and never checkout mtime. Existing animation
schema-v3 files may retain a legacy `modified_unix_secs` field in their exact bytes; the current
reader ignores it, and an equivalent refresh preserves the whole file unchanged. Static-mesh schema
v4 uses the same content fingerprint. See
[ADR 0043](../adr/0043-content-addressed-import-fingerprints.md).

## Validation And Format Boundary

Interactive diagnostics and `cargo validate-levels` resolve registry UUIDs, generated manifests,
stable subasset IDs, source dependencies, default/state IDs, playback ranges, speeds, crossfades,
runtime format support, explicit default-clip resolution, exact source-content fingerprints, and
exact rig compatibility. Findings
identify the owning actor and controller property and include a repair action. Packaging includes
the same source files and generated artifacts checked by validation.

FBX remains supported for static model import only. An FBX containing skin deformers or animation
stacks emits a blocking diagnostic directing the author to convert it to glTF. Animation v1 does
not support one glTF asset containing animation channels under multiple top-level roots, because
Bevy creates a separate `AnimationPlayer` for each root. Split those rigs into separate assets or
export them beneath one common root. Animation v1 also does not provide implicit retargeting, blend
trees, a visual state graph, root motion, IK, or motion matching.

See [ADR 0031](../adr/0031-animation-authoring-runtime-contract.md) for the controller/runtime
boundary, [ADR 0033](../adr/0033-dedicated-skinned-mesh-renderer.md) for renderer ownership, and
[the working implementation plan](../../.cursor/plans/animation_authoring_2026-07-11.plan.md)
for acceptance gates.
