Animation Components

Skeletal animation with Animation, AnimationGraphComponent, and AnimationLOD for complex character animation systems.

Animation components enable skeletal animation for characters and objects. Web Engine uses Three.js AnimationMixer for playback with custom state machines, blend trees, and LOD systems for performance optimization.

Animation#

The Animation component controls basic skeletal animation playback with support for blending, speed control, and cross-fading between clips.

Properties#

PropertyTypeDefaultDescription
clipIndexui80Index of the primary animation clip in the model's animation array
speedf321.0Playback speed multiplier (1.0 = normal, 2.0 = double speed)
playingui80Playback state (0 = paused, 1 = playing)
weightf321.0Blend weight for the primary clip (0.0 to 1.0)
loopui81Loop mode (0 = play once, 1 = loop)
crossFadeDurationf320.3Duration in seconds for cross-fading to secondary clip
secondaryClipIndexui8255Index of secondary clip for blending (255 = none)
secondaryWeightf320.0Blend weight for the secondary clip (0.0 to 1.0)
secondaryMaskBoneui320Hash of bone name for masking secondary clip to specific bones

Basic Usage#

import { addComponent, Animation } from '@web-engine/core';
// Create animated character
const character = world.addEntity();
addComponent(world, Transform, character);
addComponent(world, MeshRenderer, character);
addComponent(world, Animation, character);
// Load model with animations
MeshRenderer.assetId[character] = characterModelId; // Model must have animations
// Play idle animation (clip 0)
Animation.clipIndex[character] = 0;
Animation.speed[character] = 1.0;
Animation.playing[character] = 1;
Animation.loop[character] = 1;
Animation.weight[character] = 1.0;
// Play walk animation (clip 1)
Animation.clipIndex[character] = 1;
Animation.playing[character] = 1;

Animation Blending#

Use cross-fading to smoothly transition between animations, or blend two animations simultaneously for additive effects:

// Cross-fade from idle (clip 0) to walk (clip 1)
Animation.clipIndex[character] = 0; // Current clip
Animation.secondaryClipIndex[character] = 1; // Target clip
Animation.crossFadeDuration[character] = 0.5; // 0.5 second fade
Animation.secondaryWeight[character] = 1.0; // Fade to 100% weight
// Blend two animations simultaneously (e.g., walk + wave)
Animation.clipIndex[character] = 1; // Walk animation
Animation.weight[character] = 1.0;
Animation.secondaryClipIndex[character] = 4; // Wave animation
Animation.secondaryWeight[character] = 0.5; // 50% blend
// Mask secondary animation to upper body only
const shoulderBoneHash = hashString("mixamorig:Spine1");
Animation.secondaryMaskBone[character] = shoulderBoneHash;

Animation Blending

Use secondaryClipIndex for layered animations like walking while waving. Use crossFadeDuration for smooth transitions between states. Use secondaryMaskBone to limit blend to specific bone hierarchies (e.g., upper body only).

Runtime Control#

// Pause animation
Animation.playing[character] = 0;
// Resume animation
Animation.playing[character] = 1;
// Play in reverse (useful for doors, drawers)
Animation.speed[character] = -1.0;
// Fast forward
Animation.speed[character] = 2.0;
// Play once (disable looping)
Animation.loop[character] = 0;
// Fade out animation
Animation.weight[character] = 0.5; // 50% visible

AnimationGraphComponent#

The AnimationGraphComponent enables complex state machine-based animation with transitions, blend trees, and parameters. Ideal for character controllers with multiple states (idle, walk, run, jump).

Properties#

PropertyTypeDefaultDescription
graphIdui320Hash of the animation graph asset ID
stateui80Graph execution state (0 = Running, 1 = Paused)

Graph Structure#

Animation graphs consist of nodes, transitions, and parameters:

  • Nodes: Animation clips, 1D blend trees, or 2D blend trees
  • Transitions: Conditions for moving between nodes with blend duration
  • Parameters: Float, bool, or trigger values used in transition conditions
  • Blend Trees: Smoothly blend between animations based on parameter values

Creating Animation Graphs#

import { AnimationGraphDef } from '@web-engine/core';
const characterGraph: AnimationGraphDef = {
id: 'character-locomotion',
name: 'Character Locomotion',
parameters: [
{ name: 'speed', type: 'float', defaultValue: 0.0 },
{ name: 'grounded', type: 'bool', defaultValue: true },
{ name: 'jump', type: 'trigger', defaultValue: false },
],
nodes: [
// Idle clip
{
id: 'idle',
name: 'Idle',
type: 'clip',
clipName: 'idle',
loop: true,
transitions: [
{
id: 'idle-to-locomotion',
targetNodeId: 'locomotion',
duration: 0.2,
conditions: [
{ parameterName: 'speed', operator: '>', value: 0.1 }
],
hasExitTime: false,
},
{
id: 'idle-to-jump',
targetNodeId: 'jump',
duration: 0.1,
conditions: [
{ parameterName: 'jump', operator: '==', value: true }
],
hasExitTime: false,
}
],
},
// Locomotion blend tree (walk to run)
{
id: 'locomotion',
name: 'Locomotion',
type: 'blendTree1D',
parameterName: 'speed',
thresholds: [
{ clipName: 'walk', threshold: 1.0 },
{ clipName: 'run', threshold: 5.0 },
],
transitions: [
{
id: 'locomotion-to-idle',
targetNodeId: 'idle',
duration: 0.2,
conditions: [
{ parameterName: 'speed', operator: '<', value: 0.1 }
],
hasExitTime: false,
}
],
},
// Jump clip
{
id: 'jump',
name: 'Jump',
type: 'clip',
clipName: 'jump',
loop: false,
rootMotion: true,
rootMotionY: true, // Include vertical motion
transitions: [
{
id: 'jump-to-idle',
targetNodeId: 'idle',
duration: 0.2,
conditions: [
{ parameterName: 'grounded', operator: '==', value: true }
],
hasExitTime: true,
exitTime: 0.9, // Exit near end of jump animation
}
],
}
],
entryNodeId: 'idle', // Start in idle state
};

Using Animation Graphs#

import { addComponent, AnimationGraphComponent, AnimationGraphMap,
AnimationGraphRuntimeMap } from '@web-engine/core';
// Register the graph
import { AnimationGraphRegistry } from '@web-engine/core';
AnimationGraphRegistry.registerGraph(characterGraph);
// Create entity with animation graph
const character = world.addEntity();
addComponent(world, Transform, character);
addComponent(world, MeshRenderer, character);
addComponent(world, AnimationGraphComponent, character);
// Assign graph to entity
const graphHash = hashString('character-locomotion');
AnimationGraphComponent.graphId[character] = graphHash;
AnimationGraphComponent.state[character] = 0; // Running
// Store graph ID in map
AnimationGraphMap.set(character, 'character-locomotion');
// Set parameters to control animation
const runtime = AnimationGraphRuntimeMap.get(character);
if (runtime) {
// Control movement speed (0 = idle, 1 = walk, 5 = run)
runtime.parameters.set('speed', 3.0); // Blend between walk and run
// Trigger jump
runtime.parameters.set('jump', true);
// Set grounded state
runtime.parameters.set('grounded', false);
}

State Machine Execution

The AnimationSystem evaluates transition conditions every frame and automatically blends between nodes. Parameters can be updated from gameplay systems (character controller, input, AI) to drive animation state changes.

Blend Trees#

1D Blend Tree#

Blends animations along a single parameter axis (e.g., speed, turning):

{
id: 'movement',
name: 'Movement Blend',
type: 'blendTree1D',
parameterName: 'speed',
thresholds: [
{ clipName: 'idle', threshold: 0.0 },
{ clipName: 'walk', threshold: 2.0 },
{ clipName: 'jog', threshold: 4.0 },
{ clipName: 'run', threshold: 6.0 },
{ clipName: 'sprint', threshold: 10.0 },
],
transitions: [],
}
// Setting speed = 3.0 will blend between walk (2.0) and jog (4.0)
// Weight calculation: walk = 50%, jog = 50%

2D Blend Tree#

Blends animations in 2D parameter space (e.g., strafe movement):

{
id: 'strafe',
name: 'Strafe Movement',
type: 'blendTree2D',
parameterX: 'moveX', // Left/Right
parameterY: 'moveY', // Forward/Back
thresholds: [
{ clipName: 'idle', position: [0, 0] },
{ clipName: 'walk-forward', position: [0, 1] },
{ clipName: 'walk-back', position: [0, -1] },
{ clipName: 'strafe-left', position: [-1, 0] },
{ clipName: 'strafe-right', position: [1, 0] },
{ clipName: 'walk-forward-left', position: [-0.7, 0.7] },
{ clipName: 'walk-forward-right', position: [0.7, 0.7] },
{ clipName: 'walk-back-left', position: [-0.7, -0.7] },
{ clipName: 'walk-back-right', position: [0.7, -0.7] },
],
transitions: [],
}
// Setting moveX = 0.5, moveY = 0.5 will blend between:
// idle, walk-forward, strafe-right, walk-forward-right
// based on distance-weighted interpolation

Animation Events#

Trigger gameplay events at specific times during animation playback:

{
id: 'attack',
name: 'Sword Attack',
type: 'clip',
clipName: 'sword-slash',
loop: false,
events: [
{
name: 'playSwingSound',
time: 0.2, // 20% through animation
payload: { soundId: 'sword-whoosh' }
},
{
name: 'dealDamage',
time: 0.5, // 50% through animation (impact frame)
payload: { damage: 25, radius: 2.0 }
},
{
name: 'playHitSound',
time: 0.52,
payload: { soundId: 'sword-hit' }
}
],
transitions: [],
}
// Listen for animation events
world.events.on('animation:event', (event) => {
if (event.eventName === 'dealDamage') {
applyDamageInRadius(event.entity, event.payload.damage, event.payload.radius);
}
});

AnimationLOD#

The AnimationLOD component optimizes animation performance by reducing update frequency and quality for distant characters. Critical for large crowds or battle scenes.

Properties#

PropertyTypeDefaultDescription
enabledui80Whether animation LOD is enabled (0 = disabled, 1 = enabled)
distancef320.0Current distance to camera (meters, updated by AnimationSystem)
lodLevelui80Current LOD level (0-3): 0=full, 1=reduced, 2=minimal, 3=paused
updateIntervalf321.0Update interval multiplier (1.0 = every frame, 2.0 = every other frame)
skipBlendingui80Skip blend tree evaluation for distant entities (0 = false, 1 = true)
nearDistancef3210.0Distance threshold for LOD 0 (full quality) in meters
midDistancef3230.0Distance threshold for LOD 1 (reduced frequency) in meters
farDistancef3260.0Distance threshold for LOD 2 (minimal) in meters
cullDistancef32100.0Distance threshold for LOD 3 (paused) in meters

LOD Levels#

LevelDistanceUpdate RateDescription
00 - nearEvery frameFull quality - all blend trees, events, and IK
1near - midEvery 2-3 framesReduced frequency - skip some blend evaluations
2mid - farEvery 4-6 framesMinimal - simplified pose updates only
3far - cullPausedCulled - no animation updates, freeze last pose

Usage#

import { addComponent, AnimationLOD } from '@web-engine/core';
// Add LOD to animated character
const npc = world.addEntity();
addComponent(world, Transform, npc);
addComponent(world, Animation, npc);
addComponent(world, AnimationLOD, npc);
// Configure LOD thresholds
AnimationLOD.enabled[npc] = 1;
AnimationLOD.nearDistance[npc] = 15.0; // Full quality within 15m
AnimationLOD.midDistance[npc] = 40.0; // Reduced quality 15-40m
AnimationLOD.farDistance[npc] = 80.0; // Minimal quality 40-80m
AnimationLOD.cullDistance[npc] = 120.0; // Pause beyond 120m
// System automatically updates distance and lodLevel each frame
// Based on distance to active camera

Performance Gains

AnimationLOD can reduce animation CPU cost by 60-80% for large crowds. For a scene with 100 characters, typically only 5-10 will be at LOD 0, with the rest at reduced update rates or culled entirely.

Performance Tuning#

// Aggressive LOD for background NPCs
AnimationLOD.nearDistance[backgroundNpc] = 8.0;
AnimationLOD.midDistance[backgroundNpc] = 20.0;
AnimationLOD.farDistance[backgroundNpc] = 40.0;
AnimationLOD.cullDistance[backgroundNpc] = 60.0;
// Conservative LOD for important characters
AnimationLOD.nearDistance[mainCharacter] = 30.0;
AnimationLOD.midDistance[mainCharacter] = 60.0;
AnimationLOD.farDistance[mainCharacter] = 100.0;
AnimationLOD.cullDistance[mainCharacter] = 150.0;
// Disable LOD for player character (always full quality)
AnimationLOD.enabled[player] = 0;

Best Practices#

  • Use Animation component for simple playback (NPCs, props, doors)
  • Use AnimationGraphComponent for complex state machines (player characters, AI)
  • Enable AnimationLOD for all non-player characters in large scenes
  • Use 1D blend trees for speed-based blending (walk to run)
  • Use 2D blend trees for directional movement (strafe, turn in place)
  • Keep transition durations short (0.1-0.3s) for responsive gameplay
  • Use exitTime for animations that must complete (attacks, reloads)
  • Use triggers for one-shot actions (jump, shoot)
  • Use booleans for persistent states (grounded, aiming)
  • Use floats for continuous values (speed, turning)
  • Add animation events for footsteps, weapon impacts, and VFX timing
  • Set rootMotion for physically-grounded character movement

Common Animation Patterns#

Character Controller Integration#

// Update animation parameters from character controller
const velocity = CharacterController.movement[player];
const speed = Math.sqrt(velocity[0]**2 + velocity[2]**2);
const grounded = CharacterController.grounded[player] === 1;
const runtime = AnimationGraphRuntimeMap.get(player);
if (runtime) {
runtime.parameters.set('speed', speed);
runtime.parameters.set('grounded', grounded);
// Trigger jump when jump starts
if (CharacterController.verticalVelocity[player] > 0 && grounded) {
runtime.parameters.set('jump', true);
}
}

Weapon Reload Animation#

{
id: 'reload',
name: 'Reload Weapon',
type: 'clip',
clipName: 'rifle-reload',
loop: false,
events: [
{
name: 'ejectMagazine',
time: 0.3,
payload: { magazineSlot: 'hand-left' }
},
{
name: 'insertMagazine',
time: 0.6,
payload: { magazineSlot: 'weapon' }
},
{
name: 'reloadComplete',
time: 0.95,
payload: { ammoCount: 30 }
}
],
transitions: [
{
id: 'reload-to-idle',
targetNodeId: 'idle',
duration: 0.15,
hasExitTime: true,
exitTime: 1.0, // Must complete entire reload
conditions: []
}
]
}
Components | Web Engine Docs | Web Engine Docs