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#
| Property | Type | Default | Description |
|---|---|---|---|
| clipIndex | ui8 | 0 | Index of the primary animation clip in the model's animation array |
| speed | f32 | 1.0 | Playback speed multiplier (1.0 = normal, 2.0 = double speed) |
| playing | ui8 | 0 | Playback state (0 = paused, 1 = playing) |
| weight | f32 | 1.0 | Blend weight for the primary clip (0.0 to 1.0) |
| loop | ui8 | 1 | Loop mode (0 = play once, 1 = loop) |
| crossFadeDuration | f32 | 0.3 | Duration in seconds for cross-fading to secondary clip |
| secondaryClipIndex | ui8 | 255 | Index of secondary clip for blending (255 = none) |
| secondaryWeight | f32 | 0.0 | Blend weight for the secondary clip (0.0 to 1.0) |
| secondaryMaskBone | ui32 | 0 | Hash of bone name for masking secondary clip to specific bones |
Basic Usage#
import { addComponent, Animation } from '@web-engine/core'; // Create animated characterconst character = world.addEntity();addComponent(world, Transform, character);addComponent(world, MeshRenderer, character);addComponent(world, Animation, character); // Load model with animationsMeshRenderer.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 clipAnimation.secondaryClipIndex[character] = 1; // Target clipAnimation.crossFadeDuration[character] = 0.5; // 0.5 second fadeAnimation.secondaryWeight[character] = 1.0; // Fade to 100% weight // Blend two animations simultaneously (e.g., walk + wave)Animation.clipIndex[character] = 1; // Walk animationAnimation.weight[character] = 1.0;Animation.secondaryClipIndex[character] = 4; // Wave animationAnimation.secondaryWeight[character] = 0.5; // 50% blend // Mask secondary animation to upper body onlyconst 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 animationAnimation.playing[character] = 0; // Resume animationAnimation.playing[character] = 1; // Play in reverse (useful for doors, drawers)Animation.speed[character] = -1.0; // Fast forwardAnimation.speed[character] = 2.0; // Play once (disable looping)Animation.loop[character] = 0; // Fade out animationAnimation.weight[character] = 0.5; // 50% visibleAnimationGraphComponent#
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#
| Property | Type | Default | Description |
|---|---|---|---|
| graphId | ui32 | 0 | Hash of the animation graph asset ID |
| state | ui8 | 0 | Graph 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 graphimport { AnimationGraphRegistry } from '@web-engine/core';AnimationGraphRegistry.registerGraph(characterGraph); // Create entity with animation graphconst character = world.addEntity();addComponent(world, Transform, character);addComponent(world, MeshRenderer, character);addComponent(world, AnimationGraphComponent, character); // Assign graph to entityconst graphHash = hashString('character-locomotion');AnimationGraphComponent.graphId[character] = graphHash;AnimationGraphComponent.state[character] = 0; // Running // Store graph ID in mapAnimationGraphMap.set(character, 'character-locomotion'); // Set parameters to control animationconst 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 interpolationAnimation 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 eventsworld.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#
| Property | Type | Default | Description |
|---|---|---|---|
| enabled | ui8 | 0 | Whether animation LOD is enabled (0 = disabled, 1 = enabled) |
| distance | f32 | 0.0 | Current distance to camera (meters, updated by AnimationSystem) |
| lodLevel | ui8 | 0 | Current LOD level (0-3): 0=full, 1=reduced, 2=minimal, 3=paused |
| updateInterval | f32 | 1.0 | Update interval multiplier (1.0 = every frame, 2.0 = every other frame) |
| skipBlending | ui8 | 0 | Skip blend tree evaluation for distant entities (0 = false, 1 = true) |
| nearDistance | f32 | 10.0 | Distance threshold for LOD 0 (full quality) in meters |
| midDistance | f32 | 30.0 | Distance threshold for LOD 1 (reduced frequency) in meters |
| farDistance | f32 | 60.0 | Distance threshold for LOD 2 (minimal) in meters |
| cullDistance | f32 | 100.0 | Distance threshold for LOD 3 (paused) in meters |
LOD Levels#
| Level | Distance | Update Rate | Description |
|---|---|---|---|
| 0 | 0 - near | Every frame | Full quality - all blend trees, events, and IK |
| 1 | near - mid | Every 2-3 frames | Reduced frequency - skip some blend evaluations |
| 2 | mid - far | Every 4-6 frames | Minimal - simplified pose updates only |
| 3 | far - cull | Paused | Culled - no animation updates, freeze last pose |
Usage#
import { addComponent, AnimationLOD } from '@web-engine/core'; // Add LOD to animated characterconst npc = world.addEntity();addComponent(world, Transform, npc);addComponent(world, Animation, npc);addComponent(world, AnimationLOD, npc); // Configure LOD thresholdsAnimationLOD.enabled[npc] = 1;AnimationLOD.nearDistance[npc] = 15.0; // Full quality within 15mAnimationLOD.midDistance[npc] = 40.0; // Reduced quality 15-40mAnimationLOD.farDistance[npc] = 80.0; // Minimal quality 40-80mAnimationLOD.cullDistance[npc] = 120.0; // Pause beyond 120m // System automatically updates distance and lodLevel each frame// Based on distance to active cameraPerformance 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 NPCsAnimationLOD.nearDistance[backgroundNpc] = 8.0;AnimationLOD.midDistance[backgroundNpc] = 20.0;AnimationLOD.farDistance[backgroundNpc] = 40.0;AnimationLOD.cullDistance[backgroundNpc] = 60.0; // Conservative LOD for important charactersAnimationLOD.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 controllerconst 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: [] } ]}