Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
The Animation component controls basic skeletal animation playback with support for blending, speed control, and cross-fading between clips.
| 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 |
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;
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).
// 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% visible
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).
| Property | Type | Default | Description |
|---|---|---|---|
| graphId | ui32 | 0 | Hash of the animation graph asset ID |
| state | ui8 | 0 | Graph execution state (0 = Running, 1 = Paused) |
Animation graphs consist of nodes, transitions, and parameters:
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 motiontransitions: [{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};
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 jumpruntime.parameters.set('jump', true);// Set grounded stateruntime.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.
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%
Blends animations in 2D parameter space (e.g., strafe movement):
{id: 'strafe',name: 'Strafe Movement',type: 'blendTree2D',parameterX: 'moveX', // Left/RightparameterY: 'moveY', // Forward/Backthresholds: [{ 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
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 animationpayload: { 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);}});
The AnimationLOD component optimizes animation performance by reducing update frequency and quality for distant characters. Critical for large crowds or battle scenes.
| 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 |
| 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 |
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 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.
// 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;
// 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 startsif (CharacterController.verticalVelocity[player] > 0 && grounded) {runtime.parameters.set('jump', true);}}
{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 reloadconditions: []}]}