---
title: "Animation"
description: "Animated models in Zero carry named clips (idle, walk, run, …). A clip plays on a skinned body through the AnimGraph — it retargets the clip from its source rig onto the body's rig and drives the…"
section: "Topics"
slug: "topics-animation"
canonical: "https://origozero.ai/docs/topics-animation"
updated: "2026-08-17T20:23:51.291749783+00:00"
tags: ["documentation", "guide"]
---

# Animation

## Playing a clip

The durable way is the **ClipPlayer** component — attach it to the skinned body and
it plays one clip through the AnimGraph:

```lua
entity.find("hero").component.add("ClipPlayer", {
    clip = asset.resolve("A_Walk_F_Masc", "animation"),
    looping = true,
})
```

`clip` is an `AssetRef<animation>` (survives renames/moves); `looping`, `speed`, and
`playing` control playback. ClipPlayer waits for the body's skeleton to hydrate, so
it works on a character spawned from a bundle where the rigged mesh loads a few
frames later.

## Testing a clip quickly

To eyeball a clip without authoring a component, use the **animation** toolbox:

```lua
animation.play("A_Walk_F_Masc", "hero")                       -- play once, auto-cleanup
animation.play("Idle", "hero", { offset = "5s" })             -- wait 5s, then play
animation.play("Wave", "hero", { loop = true, duration = 8 }) -- loop for 8s
```

`animation.play` resolves the clip by identity or name and the entity by id or name,
finds the SkinnedModel on it (or under it — the imported-character layout puts the
rigged mesh on a child), plays through the AnimGraph, and tears everything down when
the window ends. On a body that already animates it overrides for the window and the
body's own animation resumes untouched. Playback runs in play mode, like any
component-driven animation.

## Where animation comes from

Animated content is assets: rigged models with embedded clips, and standalone
animation clips you can retarget. Survey what exists with `asset.list("animation")`,
`asset.list("bundle")`, and the shared library, or generate humanoid animations with
the animation service (the generating-assets-and-content guide). `asset.inspect` on a rigged
model shows its clip names.

## How a joint blends

A skinned vertex is placed by combining the joints that influence it, and there
are two ways to combine them. The default averages the joint matrices — cheap,
and what every rig gets unless it says otherwise. Where a joint twists far (a
wrist rolling over, a shoulder, a spine), that average stops being a rotation
and the limb pinches toward its own axis: the candy-wrapper. Switching that body
to dual-quaternion skinning averages the joints' rigid motions instead and
renormalises, so the cross-section survives the twist:

```lua
entity.find("hero").component.get("SkinnedModel").skinning = "dualQuaternion"
```

It is per body, so a character that twists hard can carry it while the rest of
the cast stays on the cheaper default, and a rig at rest looks the same either
way.

## Checking what is animating, and why a body is not

`animation.observe()` reports every body the engine holds animation state for.
It reads the pose the engine measured landing on each armature, so it answers
whether a body is moving rather than whether something asked it to:

```lua
for _, body in animation.observe().bodies do
    print(body.entity, body.animating, body.reason, body.matched .. "/" .. body.total)
end
```

`animating` is the engine's verdict. A `ClipPlayer` whose `playing` field is
`true` on a body whose bones never move reads `animating = false` here, with
`reason` naming the cause from a closed set: `deactivated`, `noRiggedSkeleton`,
`noGraph`, `clipUnreadable`, `noOutputNode`, `retargetMatchedNoRoles`,
`stopped`, `finished`, `paused`, `poseNotApplied`, `poseUnchanged`.

An editor sits paused until you press play, and a paused engine steps no
animator. Every body reads `animating = false` there, and `reason` separates the
ones that will move on resume — `paused` — from the ones that will not: a clip
that reaches none of the rig's bones still reports `retargetMatchedNoRoles`
while paused, because that is what is still wrong when the world starts again.
The clips, playheads and coverage a body was last driven with stay readable
across the pause.

### Starting from one character

`animation.whyStill` is the one-line form, and it accepts a character root —
the rig lives on a skinned child, and the call resolves down to it:

```lua
local why, detail = animation.whyStill("hero")
if why then print("hero is not animating:", why, detail) end
```

### When more than one animator is bound

`body.driver` is the animator the engine measured posing the body. A body can
carry two — a script animator and an `AnimGraph` component — and then
`body.otherDrivers` names the ones the report is not built from:

```lua
local body = animation.body("hero")
print(body.driver, table.concat(body.otherDrivers, ", "))
```

Each animator's poses are measured on their own, so the one whose pose the
engine saw changing is the one the body's clips, playheads and coverage
describe.

### Is the clip reaching this rig

A clip is retargeted onto the body's rig when it binds, and the coverage that
bake produced is the number that separates a working clip from a dead one:

```lua
local matched, total = animation.coverage("hero")   -- 41, 50
for _, clip in animation.clips("hero") do
    print(clip.name, clip.time, clip.matched .. "/" .. clip.total, #clip.unmatched)
end
```

A humanoid clip on a humanoid rig reaches most of the bones. A clip from an
unrelated rig reaches none — `0/50` — and the body stands in its rest pose while
the playhead advances exactly as it does on a working body. `clip.unmatched`
names the bones the clip leaves at rest.

### Measure the pose, not the playhead

`body.pose` is what the engine wrote onto the bones:

```lua
local pose = animation.body("hero").pose
print(pose.bonesWritten .. "/" .. pose.bonesRequested, pose.revision, pose.sinceChangedMs)
```

`applies` counts poses that reached the armature; `revision` counts the ones
whose values differed from the pose before. A body being animated advances both.
A body held in one pose advances `applies` alone, and `sinceChangedMs` grows.
`pose.refusal` names why a pose never landed at all — `deactivated`,
`unsupportedStride`, `targetMissing`, `noSkeleton`, `noArmatureRoot`,
`armatureDrivenElsewhere`.

`armatureDrivenElsewhere` is the one to know when two animators share a body: an
`AnimGraph` component and a legacy animation player each rebuild the joint
matrices the renderer draws from their own graph, so a pose another animator
writes onto the same armature's bones is replaced before a frame is drawn from
it. The body then reads the animator that owns its skinning as its `driver`, and
names the other in `otherDrivers`.

### From the shell, and on screen

`tools.use("animation", "observe")` condenses the same report to one line per
body, and `/zero/runtime/animation` serves it as a file:

```lua
vfs.read("/zero/runtime/animation")                    -- the whole observation
vfs.read("/zero/runtime/animation/bodies/" .. body.id) -- one body
```

The `AnimDebug` component in the character-controller package renders the same
report as an on-screen HUD.

## Going further

For blending, crossfades, and state machines, build on the AnimGraph directly
(`@builtin::systems.anim.AnimGraph` — `layoutForEntity`, `Clip`, `Mixer`,
`BlendSpace2D`, `Layer`). For full character movement — locomotion blending, turning,
jumps — the built-in character controller wires clips to input and physics; inspect it
rather than rebuilding that from clips.
