Command Palette

Search for a command to run...

Hectal
Course outline
Topic 9.3Intermediate ~22 min · step 3 of 6

The Animation System

In one line

A clip stores keyframes, a mixer plays them on a model, and an action is one playing copy you can pause, loop and crossfade. You drive a real animated fox and learn the nine reasons an action never plays.

0/6 · 0%

Think of it like this

A film reel holds the pictures, the projector plays them, and the projectionist can pause, rewind or fade from one reel to the next. In three.js the clip is the reel, the mixer is the projector for one model, and an action is one reel loaded in it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

keyframe
A value at one moment in time. The values between two keyframes are worked out by interpolation.
AnimationClip
A named, timed bundle of tracks: one whole motion such as Walk.
AnimationMixer
The player for one model. It owns the clock and blends actions. Call update(delta) every frame.
AnimationAction
One playing (or waiting) copy of a clip, with its own time, speed, weight and loop mode.
crossfade
Handing over smoothly from one action to another by moving their weights.
weight
How strongly an action counts, from 0 to 1. Weights of running actions are blended.

Step by step

01Build a clip by hand

The first example makes a clip with one track: the node named crank, its z rotation, from 0 at 0 s to -2π at 2 s. It prints the track's name, times and values.

02Play it with a fixed step

mixer.update(1 / 60) 120 times is exactly 2 s. After 30 updates the crank has turned -0.25 turns, after 60 -0.5, after 120 -1. One more update loops: time 0.017.

03Nine attempts

The second example tries nine ways to turn the crank for half a second. Only two turn it: the correct one and reset().play(). See why each of the others does not.

04A real model

The third example loads the Fox, plays Walk, then crossfades to Run. The weights are read after the same fixed steps.

Try it yourself

  1. 1

    A slower crossfade

    In the Fox example change crossFadeTo(run, 0.5, false) to crossFadeTo(run, 2, false). The weights after 15 and 30 updates become walk 0.88 and run 0.13, then walk 0.75 and run 0.25: a fade over 2 s is only a quarter done after half a second.

  2. 2

    Play Survey

    Replace "Walk" with "Survey". The clip is 3.417 s long, so the first log line after 30 updates shows time 0.500 of 3.417.

Code & diagrams

A clip, a mixer, an action New tab

No canvas. Everything is driven with a fixed step of 1/60 s, so the output is the same every time.

idle

Console

Run the scene to see its console.

Expected console output

clip: pedal 2 s, 1 track: crank.rotation[z] | times [ 0, 2 ] values [ '0.000', '-6.283' ]
after 30 updates of 1/60 s: time 0.500, crank turned -0.250 turns
after 60 updates of 1/60 s: time 1.000, crank turned -0.500 turns
after 120 updates of 1/60 s: time 2.000, crank turned -1.000 turns
one more update (loops by default): time 0.017 turns -0.008
Nine ways an action never plays New tab

The misspelled node name is the only one that prints a warning. The rest are silent.

idle

Console

Run the scene to see its console.

Expected console output

play() and update()                    turns:  -0.250
forgot play()                          turns:   0.000
forgot mixer.update()                  turns:   0.000
THREE.PropertyBinding: No target node found for track: cranck.rotation[z].
track names a node that is not there   turns:   0.000
setEffectiveWeight(0)                  turns:   0.000
paused = true                          turns:   0.000
timeScale = 0                          turns:   0.000
LoopOnce, finished, then play() again  turns:   0.000
LoopOnce, finished, reset().play()     turns:  -0.250
The Fox: Walk, then a crossfade to Run New tab

Fox by PixelMannen (CC0), rigging and animation by tomkranis (CC BY 4.0), glTF conversion by @AsoboStudio and @scurest (CC BY 4.0). Open the scene tree: one Group, a root bone chain and one SkinnedMesh.

idle

Console

Run the scene to see its console.

Expected console output

clips: Survey (3.417 s), Walk (0.708 s), Run (1.158 s)
Walk after 30 updates: time 0.500 of 0.708 | weight 1
crossfade starts: walk 1.00, run 1.00
after 15 updates (0.25 s): walk 0.50, run 0.50
after 30 updates (0.5 s): walk 0.00, run 1.00

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A misspelled node

In the first example change "crank.rotation[z]" to "cranck.rotation[z]".

terminal
$ Run
── what you'll see ──
THREE.PropertyBinding: No target node found for track: cranck.rotation[z].
A warning, not an error. The crank stays at 0 turns.

Break #2

A misspelled clip name

In the Fox example change "Run" to "run" (names are case-sensitive).

terminal
$ Run
── what you'll see ──
[error] Uncaught TypeError: Cannot read properties of null (reading 'reset')
`findByName` returns `null` and `clipAction(null)` returns `null` without a warning, so the first error is two lines later, at `run.reset()`. A model with no animations at all has `gltf.animations` equal to `[]` and fails the same way.

Break #3

No update

In the second example, add { skipUpdate: true } as a third argument to the first row, attempt("play() and update()", (a) => a.play()).

terminal
$ Run
── what you'll see ──
play() and update() turns: 0.000
No warning at all.

Myth vs fact

Myth

Playing an action animates the model.

Fact

Only mixer.update(delta) does. play() marks the action as running.

Myth

play() on a finished action plays it again.

Fact

A finished LoopOnce action is switched off. Call reset().play(), as the last row of the table shows.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Each model needs its own mixer. A mixer's cost grows with the number of tracks it drives, so a crowd of 200 foxes updates 200 × 21 tracks every frame on the CPU.

  • ▸

    clampWhenFinished = true keeps the last pose with paused = true instead of enabled = false: use it for a door that stays open. The mixer fires finished and loop events on itself, with the action in the event.

  • ▸

    A weight of 0 does not stop the action's clock, it just stops it affecting the pose. paused stops the clock. crossFadeTo(..., true) (warp) also stretches the clips to match their durations so footsteps stay in step.

Remember this

  1. 1

    The problem this solves: writing every motion by hand in the loop (Topic 9.1) works for a wheel. It does not work for a fox running: 24 bones, 21 of them with their own curve, made by an artist in Blender. You need a way to store, play and blend that data.

  2. 2

    **A KeyframeTrack** says: node crank, property rotation[z], takes these values at these times. The name format is nodeName.property. Classes by type: NumberKeyframeTrack, VectorKeyframeTrack (positions, scales), QuaternionKeyframeTrack (rotations), ColorKeyframeTrack, BooleanKeyframeTrack. Between keyframes the values are interpolated (linear by default, smooth or stepped if you ask; quaternions use slerp from Topic 2.6).

  3. 3

    **An AnimationClip** is a name, a duration and a list of tracks. The Fox model has three: Survey (3.417 s), Walk (0.708 s) and Run (1.158 s), each with 21 tracks. A loader gives them to you as gltf.animations. Find one with THREE.AnimationClip.findByName(gltf.animations, "Run").

  4. 4

    **An AnimationMixer** is made for one root object: new THREE.AnimationMixer(fox). It finds each track's node by name under that root, and holds the clocks. You must call mixer.update(delta) once per frame: no update, no motion. **mixer.clipAction(clip)** returns an AnimationAction.

  5. 5

    An action is a playing copy of a clip. play() starts it, stop() ends it, reset() rewinds it. Settings: paused, timeScale (2 is double speed, negative plays backwards), weight (0 to 1: how much it counts when several actions run), setLoop(THREE.LoopOnce | LoopRepeat | LoopPingPong, repetitions) and clampWhenFinished (hold the last pose). The default is LoopRepeat, forever.

  6. 6

    Crossfade. walk.crossFadeTo(run, 0.5, false) lowers Walk's weight and raises Run's over 0.5 seconds. The action you fade to must already be playing (run.reset().play() first). The weights change inside mixer.update, which is why the example prints 1.00 and 1.00 right after the call, then 0.50 and 0.50 after a quarter second.

  7. 7

    Credits. The Fox is by PixelMannen (CC0). Its rigging and animation are by tomkranis and its glTF conversion by @AsoboStudio and @scurest, both CC BY 4.0, via the Khronos glTF Sample Assets. CC BY means you must credit them wherever you ship it.

Explain it without notes

01

Name the four objects between an animation file and a moving model, and say what each does.

02

A walk cycle plays on the Fox but the Run action does nothing after crossFadeTo. Give two likely causes.

Practice

01

Play the Fox's Survey clip once and hold the last pose.

02

Write a function switchTo(next) that crossfades the current action to next over 0.3 s, and call it for Walk and Run.

Trade-offs

  • ↔

    Clip-based animation is best for authored motion. A one-line formula (the wheel) is simpler and cheaper than a clip.

Done when you can

  • Name the four animation objects and what each does

  • Build a clip with one track and play it with a fixed step

  • Crossfade two actions on a real model

  • Find the reason an action does not play