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 deforms meshes by transforming bones in a hierarchy. Each vertex is influenced by multiple bones with weighted blending, enabling realistic character animation with minimal data.
A skeleton is a tree of bones (joints) representing a character's structure. Each bone has:
The BoneRegistry provides fast lookup and hierarchy traversal for bone masking:
import { BoneRegistry } from '@web-engine-dev/core/animation';// Register skeleton when entity spawnsconst rootBone = skinnedMesh.skeleton.bones[0];BoneRegistry.registerSkeleton(entity.uuid, rootBone);// Lookup bone by nameconst spineBone = BoneRegistry.getByName('Spine');// Get all bones in hierarchy (for bone masking)const upperBodyBones = BoneRegistry.getBoneHierarchy('Spine');console.log(`Upper body: ${upperBodyBones.length} bones`);// Find bones by pattern (case-insensitive)const fingerBones = BoneRegistry.findBonesByPattern('finger');// IMPORTANT: Unregister when entity despawnsBoneRegistry.unregisterSkeleton(entity.uuid);
Memory Leak Prevention
Always call BoneRegistry.unregisterSkeleton() when entities are destroyed or scenes are unloaded. Failure to do so will cause bone references to accumulate in memory.
Consistent bone names enable retargeting between different skeletons:
| Bone | Common Names | Purpose |
|---|---|---|
| Hips | Hips, Pelvis, Root | Root bone for locomotion |
| Spine | Spine, Spine1, Spine2 | Torso bending |
| Head | Head, Neck | Head rotation, look-at |
| Arms | LeftArm, RightArm, LeftForeArm | Arm movement, IK targets |
| Legs | LeftUpLeg, RightLeg, LeftFoot | Locomotion, foot IK |
Skinned meshes (SkinnedMesh in Three.js) are deformed by bone transformations. Each vertex stores bone indices and weights:
Vertex positions are computed as a weighted sum of bone transforms:
// Pseudocode for GPU skinning (vertex shader)vec4 skinnedPosition = vec4(0.0);for (int i = 0; i < 4; i++) {int boneIndex = int(skinIndex[i]);float boneWeight = skinWeight[i];mat4 boneMatrix = boneMatrices[boneIndex];skinnedPosition += boneWeight * (boneMatrix * vec4(position, 1.0));}gl_Position = projectionMatrix * viewMatrix * skinnedPosition;
Performance Tip
Limit bone influences to 2 per vertex on mobile devices. 4 influences require 4x texture fetches and significantly increase vertex shader cost.
The bind pose (T-pose or A-pose) is the skeleton's initial state when skinning weights are authored. The bind matrix transforms from bone space to mesh space:
// Get skeleton bind matricesconst skeleton = skinnedMesh.skeleton;const bindMatrices = skeleton.boneInverses; // Array of inverse bind matrices// Bone matrix for skinning = currentWorldMatrix * boneInverse// This transforms vertices from bind pose to current pose
Animation clips store keyframe data for bone transforms over time. Each clip contains:
Each track targets a specific bone property with time and value arrays:
import * as THREE from 'three';// Manually create animation clip (usually loaded from GLTF)const times = [0, 0.5, 1.0]; // Keyframe times in secondsconst values = [0, 0, 0, 0, 1, 0, 0, 0, 0]; // Y position: 0 → 1 → 0 (jump)const positionTrack = new THREE.VectorKeyframeTrack('Hips.position', // Bone name + propertytimes,values,THREE.InterpolateLinear // or InterpolateSmooth, InterpolateDiscrete);const rotationTrack = new THREE.QuaternionKeyframeTrack('Head.quaternion',[0, 1.0],[0, 0, 0, 1, 0.707, 0, 0, 0.707], // Rotate 90° over 1 second);const clip = new THREE.AnimationClip('Jump', 1.0, [positionTrack, rotationTrack]);
The AnimationRegistry provides centralized clip management with validation:
import { AnimationRegistry } from '@web-engine-dev/core/animation';// Register clips from loaded modelconst model = await loadGLTF('character.glb');const records = AnimationRegistry.registerClips(model.uuid, // Asset UUID (stable across reloads)model.assetId, // Numeric ID (optional)model.animations // Array of THREE.AnimationClip);console.log(`Registered ${records.length} clips`);// Retrieve clip by nameconst walkClip = AnimationRegistry.findClip(model.uuid, 'Walk');// Get all clips for an assetconst allClips = AnimationRegistry.getClipsForAssetUuid(model.uuid);// Get clip metadataconst walkRecord = AnimationRegistry.get(records[0].id);console.log(`Duration: ${walkRecord.duration}s, Tracks: ${walkRecord.trackCount}`);// Validate clipconst issues = AnimationRegistry.validateClip(walkClip);if (issues.some(i => i.severity === 'error')) {console.error('Invalid clip:', issues);}
The registry automatically validates clips during registration:
Validation Errors
If clips fail validation, an AnimationValidationError is thrown with all blocking issues. Fix errors in your 3D authoring tool (Blender, Maya, etc.) before exporting.
Retargeting applies animations from one skeleton to another with different bone names or proportions. The engine uses name-based matching with fallback strategies:
Simplest approach: match bones by name between source and target skeletons.
// Retarget animation from source to target skeletonfunction retargetAnimation(sourceClip: THREE.AnimationClip,sourceSkeleton: THREE.Skeleton,targetSkeleton: THREE.Skeleton): THREE.AnimationClip {const retargetedTracks: THREE.KeyframeTrack[] = [];for (const track of sourceClip.tracks) {// Extract bone name from track name (e.g., "Hips.position" → "Hips")const [boneName, property] = track.name.split('.');// Find matching bone in target skeletonconst targetBone = targetSkeleton.bones.find(b => b.name === boneName);if (!targetBone) continue; // Skip if bone doesn't exist in target// Clone track with new targetconst retargetedTrack = track.clone();retargetedTrack.name = `${targetBone.name}.${property}`;retargetedTracks.push(retargetedTrack);}return new THREE.AnimationClip(sourceClip.name,sourceClip.duration,retargetedTracks);}
For skeletons with different proportions, use bone mapping with scale correction:
Humanoid Retargeting
For humanoid characters, the engine provides preset bone mappings for common skeleton rigs (Mixamo, UE4, Unity). Use HumanoidGraph preset for automatic retargeting.
The engine uses WebAssembly SIMD for 4x faster bone matrix updates:
import {updateSkeletonMatricesWASM,isSkeletonWASMAvailable} from '@web-engine-dev/core/animation';// Check SIMD supportconst hasSIMD = await isSkeletonWASMAvailable();console.log(`WASM SIMD: ${hasSIMD ? 'enabled' : 'fallback to JS'}`);// Automatic SIMD accelerationfunction updateSkeletons(world, delta) {const query = defineQuery([SkinnedMesh]);const entities = query(world);for (const eid of entities) {const skeleton = SkinnedMesh.skeleton[eid];// Uses SIMD if available, JS fallback otherwiseupdateSkeletonMatricesWASM(skeleton);}}// Benchmark SIMD vs JSimport { benchmarkSkeletonMatrices } from '@web-engine-dev/core/animation';const results = await benchmarkSkeletonMatrices(skeleton, 1000);console.log(`SIMD: ${results.simd.toFixed(2)}ms, JS: ${results.js.toFixed(2)}ms`);console.log(`Speedup: ${(results.js / results.simd).toFixed(1)}x`);
Reduce bone count for distant characters to save CPU/GPU:
import { setSkeletonLOD } from '@web-engine-dev/core/animation';// LOD 0: Full skeleton (100 bones)// LOD 1: Simplified (50 bones, merge finger/toe bones)// LOD 2: Minimal (20 bones, spine becomes single bone)const distanceToCamera = entity.position.distanceTo(camera.position);if (distanceToCamera > 50) {setSkeletonLOD(skeleton, 2); // Minimal LOD} else if (distanceToCamera > 20) {setSkeletonLOD(skeleton, 1); // Medium LOD} else {setSkeletonLOD(skeleton, 0); // Full quality}