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
Animation blending combines multiple animation clips to create smooth transitions and complex behaviors. The engine provides blend trees, crossfading, additive blending, and layered animation with bone masking.
Blend trees interpolate between multiple animation clips based on runtime parameters. The engine supports 1D and 2D blend trees with zero-allocation evaluation.
1D blend trees blend along a single parameter axis (e.g., speed, turn angle). Perfect for locomotion blending: idle → walk → run → sprint.
import { BlendTreeEvaluator } from '@web-engine-dev/core/animation';import type { AnimationBlendTree1DNode } from '@web-engine-dev/core/animation';// Define 1D blend tree for locomotionconst locomotionBlendTree: AnimationBlendTree1DNode = {id: 'locomotion',name: 'Locomotion',type: 'blendTree1D',parameterName: 'speed', // Parameter to blend onthresholds: [{ clipName: 'Idle', threshold: 0.0, speed: 1.0 },{ clipName: 'Walk', threshold: 1.5, speed: 1.0 },{ clipName: 'Run', threshold: 4.0, speed: 1.0 },{ clipName: 'Sprint', threshold: 7.0, speed: 1.2 },],transitions: [],events: [],};// Evaluate blend treeconst evaluator = new BlendTreeEvaluator();const parameters = new Map([['speed', 2.5]]); // Current speed value// Speed = 2.5 blends between Walk (1.5) and Run (4.0)// Weight = (2.5 - 1.5) / (4.0 - 1.5) = 0.4// Result: Walk @ 60% + Run @ 40%evaluator.evaluate1D(locomotionBlendTree,actions, // Map<string, AnimationAction>parameters,1.0 // Master weight);
For parameter value P between thresholds T₁ and T₂:
t = (P - T₁) / (T₂ - T₁)weight₁ = 1 - tweight₂ = t
2D blend trees blend in 2D parameter space (e.g., X/Z velocity for strafing). Uses barycentric interpolation for directional accuracy or inverse-distance weighting for flexibility.
import type { AnimationBlendTree2DNode } from '@web-engine-dev/core/animation';// Define 2D blend tree for directional movementconst directionalBlendTree: AnimationBlendTree2DNode = {id: 'directional',name: 'Directional Movement',type: 'blendTree2D',parameterX: 'velocityX', // Left/rightparameterY: 'velocityZ', // Forward/backthresholds: [// Center{ clipName: 'Idle', position: [0, 0] },// Cardinal directions{ clipName: 'WalkForward', position: [0, 1] },{ clipName: 'WalkBackward', position: [0, -1] },{ clipName: 'StrafeLeft', position: [-1, 0] },{ clipName: 'StrafeRight', position: [1, 0] },// Diagonals{ clipName: 'WalkFL', position: [-0.707, 0.707] }, // Forward-left{ clipName: 'WalkFR', position: [0.707, 0.707] }, // Forward-right{ clipName: 'WalkBL', position: [-0.707, -0.707] }, // Back-left{ clipName: 'WalkBR', position: [0.707, -0.707] }, // Back-right],transitions: [],events: [],};// Evaluate 2D blendconst parameters = new Map([['velocityX', 0.5], // Slight right['velocityZ', 0.8], // Mostly forward]);evaluator.evaluate2D(directionalBlendTree,actions,parameters,1.0);// Result: Blends WalkForward + WalkFR based on distance to each threshold
Finds the triangle containing the parameter point and uses barycentric coordinates. Provides accurate directional blending with only 3 active clips:
Fallback when point is outside all triangles. Blends all clips based on distance:
Optimization Tip
Keep 2D blend trees under 16 thresholds for optimal cache usage. Pre-allocate weight buffers per node to avoid per-frame allocations.
Crossfading smoothly transitions between two animations by blending their weights over time. The AnimationStateMachine handles crossfading automatically during state transitions.
Simple linear interpolation between source and target:
sourceWeight = 1 - ttargetWeight = t(where t = elapsed / duration)
The engine supports multiple blend curves for natural transitions:
| Curve | Formula | Use Case |
|---|---|---|
| Linear | t | Constant speed, mechanical |
| SmoothStep | 3t² - 2t³ | Default, natural ease in/out |
| EaseIn | t² | Slow start, fast end |
| EaseOut | 1 - (1-t)² | Fast start, slow end |
| SmootherStep | 6t⁵ - 15t⁴ + 10t³ | Smoothest, cinematics |
import {AnimationStateMachine,BlendCurve} from '@web-engine-dev/core/animation';// Configure state machine with blend curveconst config = {maxQueueSize: 16,maxRequestAgeFrames: 10,blendCurve: BlendCurve.SmoothStep, // Default smooth blendentityId: entity,};const stateMachine = AnimationStateMachine.acquire(config);// Queue transition with custom durationstateMachine.queueTransition('Run', // Target state0.3, // 300ms crossfadeTransitionPriority.Normal);
Additive blending adds animation deltas on top of a base pose. Perfect for layering procedural adjustments (aim offset, recoil, breathing) on top of locomotion.
Compute additive delta relative to a reference pose (usually first frame):
additivePose = currentPose - referencePose
Apply additive layer with weight:
finalPose = basePose + (additivePose × weight)
import {AnimationLayerManager,createAnimationLayerState} from '@web-engine-dev/core/animation';import type { AnimationLayerBlendMode } from '@web-engine-dev/core/animation';// Create base locomotion layerconst layerManager = new AnimationLayerManager(world);const baseLayer = createAnimationLayerState('locomotion','Override' as AnimationLayerBlendMode, // Replace previous pose1.0 // Full weight);// Create additive aim layerconst aimLayer = createAnimationLayerState('aim_offset','Additive' as AnimationLayerBlendMode, // Add to base pose0.8 // 80% blend);// Configure bone mask (only affect upper body)aimLayer.mask = {includedBones: new Set(['Spine', 'Spine1', 'Spine2', 'LeftArm', 'RightArm']),recursive: true, // Include all children};// Additive layer adds aim offset to upper body only// Lower body continues playing locomotion animation
Animation layers allow multiple animation graphs to blend together with bone masking. Each layer can use Override or Additive blending.
Layers are evaluated bottom-to-top, with each layer blending onto the previous:
// Layer 0: Base locomotion (full body)const locomotionLayer = {name: 'locomotion',blendMode: 'Override',weight: 1.0,mask: null, // Affects all bones};// Layer 1: Upper body actions (shooting, reloading)const upperBodyLayer = {name: 'upper_body',blendMode: 'Override',weight: 1.0,mask: {includedBones: new Set(['Spine', 'LeftArm', 'RightArm']),recursive: true,},};// Layer 2: Additive aim offsetconst aimLayer = {name: 'aim',blendMode: 'Additive',weight: 0.8,mask: {includedBones: new Set(['Spine1', 'Spine2', 'LeftArm', 'RightArm']),recursive: true,},};// Final pose:// - Legs: locomotion layer (walk/run)// - Upper body: shooting animation + aim offset// - Head: follows aim target (from aim layer)
Bone masks control which bones are affected by each layer:
import {HUMANOID_BONE_GROUPS,PRESET_AVATAR_MASKS,expandHumanoidMask} from '@web-engine-dev/core/animation';// Use preset masks for common casesconst upperBodyMask = PRESET_AVATAR_MASKS.upperBody;const lowerBodyMask = PRESET_AVATAR_MASKS.lowerBody;const leftArmMask = PRESET_AVATAR_MASKS.leftArm;const rightArmMask = PRESET_AVATAR_MASKS.rightArm;// Or create custom mask from humanoid groupsconst torsoMask = expandHumanoidMask({head: false,leftArm: false,rightArm: false,leftLeg: false,rightLeg: false,spine: true, // Only spine});// Or manually specify bone namesconst customMask = {includedBones: new Set(['Hips', 'Spine', 'Spine1', 'Spine2','LeftShoulder', 'LeftArm', 'LeftForeArm', 'LeftHand',]),recursive: true, // Include all children (fingers, etc.)};
Sync multiple layers to the same normalized time for coordinated animation:
import type { AnimationSyncGroup } from '@web-engine-dev/core/animation';// Create sync group for coordinated layersconst walkSyncGroup: AnimationSyncGroup = {id: 'walk_sync',name: 'Walk Synchronization',leaderLayer: 'locomotion', // This layer controls timefollowerLayers: ['upper_body', 'aim'], // These match leader time};// All synced layers advance at the same normalized time// Prevents upper/lower body desync during transitions
Frame Budget
Blend tree evaluation: < 0.02ms per node. Layer blending: < 0.05ms per layer. Total animation blending budget: < 0.2ms per entity at 60 FPS.