Skip to content

The animation graph

Three.js gives you an AnimationMixer that plays and crossfades clips. vgai adds the layer it doesn’t have: a state machine with parameter-driven transitions and blend trees — the AnimGraph (packages/engine/src/animation/anim-graph.ts).

AnimGraph wraps a THREE.AnimationMixer. The mixer still does clip playback and blending; the graph decides which clips play and at what weight:

idle, walk, run states with parameter-driven transitions
A one-layer state machine driven by a `speed` parameter.
  • Parameters — typed values you set from gameplay: float, int, bool, trigger.
  • Layers — independent state machines blended together, each with a blendMode of override or additive and an optional boneMask.
  • States — each plays a single clip (with loop / speed) or a blend tree.
  • Transitions — move between states when conditions hold, crossfading over a duration.
graph.setParameter('speed', 5.0); // float | boolean
graph.getParameter('speed');
graph.trigger('jump'); // fire a trigger (auto-resets after it's consumed)
graph.getCurrentState(layerIndex); // current state name (default layer 0)
graph.update(dt); // call each frame

A transition has from (a state, or '*' for “any state”), to, an optional list of conditions ({ param, op, value } with ops > < >= <= == != trigger), a crossfade duration (seconds), and an optional exitTime (0–1 normalized clip time before which it won’t fire).

A graph is authored as { version, parameters, layers } (packages/engine/src/animation/anim-graph-types.ts):

  • parameters: Record<string, { type, default }>
  • layers: [{ name, blendMode?, weight?, boneMask?, defaultState, states, transitions }]
  • states: Record<string, { clip?, loop?, speed?, blendTree? }>

AnimGraph is a class instance (it wraps a mixer), so it lives in a Map<Object3D, AnimGraph> rather than a flat store. Register one system in the animation phase:

systems.add('animation', (dt) => animationSystem(animGraphs, dt));

animationSystem (packages/engine/src/animation/anim-system.ts) simply calls graph.update(dt) on every registered graph. GLTF-loaded scenes and scene-defined animations register their graphs here automatically.