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
Procedural animation generates motion algorithmically instead of using keyframe data. The engine provides IK solvers, look-at constraints, foot IK for terrain adaptation, and ragdoll physics for dynamic character death.
IK solves bone rotations to position an end effector (hand, foot) at a target location. The engine provides multiple IK solvers optimized for different use cases:
Analytic solver for 2-segment chains (arms, legs). Extremely fast and stable:
import {TwoBoneIKSolver,setupLegIK,setupArmIK} from '@web-engine-dev/core/animation';import type { TwoBoneIKState } from '@web-engine-dev/core/animation';// Setup leg IK (e.g., for foot placement)const legIK: TwoBoneIKState = {rootBone: null, // Will be set by setupLegIKmiddleBone: null, // KneeendBone: null, // Foottarget: [0, 0, 0],poleTarget: [0, 0, 1], // Knee directionweight: 1.0,lengths: { upper: 0, lower: 0 }, // Auto-calculated};// Initialize from skeletonsetupLegIK(legIK, skeleton, 'left');// Set target position (e.g., ground contact point)legIK.target = [0.5, 0.1, 1.0];legIK.weight = 1.0;// Solve IK (updates bone rotations)TwoBoneIKSolver.solve(legIK, legIK, delta);
Uses law of cosines for analytic solution:
// 1. Calculate distance to targetdistance = length(target - root)// 2. Clamp to reachable rangemaxReach = upperLength + lowerLengthminReach = abs(upperLength - lowerLength)distance = clamp(distance, minReach, maxReach)// 3. Calculate angles using law of cosines// a² = b² + c² - 2bc·cos(A)angleUpper = acos((upper² + dist² - lower²) / (2·upper·dist))angleMid = acos((upper² + lower² - dist²) / (2·upper·lower))// 4. Orient bones to target with pole vector constraint// ... (detailed quaternion math)
Forward And Backward Reaching IK for longer chains (spine, tail, tentacles). Iterative solver that converges to solution:
import {FABRIKSolver,Chain} from '@web-engine-dev/core/animation';// Create IK chain from root to end effectorconst spine = skinnedMesh.skeleton.getBoneByName('Spine');const head = skinnedMesh.skeleton.getBoneByName('Head');const chain = new Chain(spine, head);// Configure solverconst solver = new FABRIKSolver(0.001, // Tolerance (convergence threshold)10 // Max iterations);// Set target position (e.g., look-at point)const target = new THREE.Vector3(0, 2, 5);// Solve IK chainsolver.solve(chain, target);// Chain bones now orient towards target
Iterates forward (tip to root) and backward (root to tip):
// Forward pass (tip to root)positions[n] = targetfor i = n-1 to 0:direction = normalize(positions[i] - positions[i+1])positions[i] = positions[i+1] + direction * length[i]// Backward pass (root to tip)positions[0] = rootPositionfor i = 0 to n-1:direction = normalize(positions[i+1] - positions[i])positions[i+1] = positions[i] + direction * length[i]// Repeat until converged or max iterations
Cyclic Coordinate Descent for simple chains. Fast but less stable than FABRIK:
import { CCDSolver } from '@web-engine-dev/core/animation';const solver = new CCDSolver(10, // Max iterations0.001 // Tolerance);// Solve chainsolver.solve(chain, target);
Orient bones (head, eyes, turret) to track targets with angle limits:
import {LookAtIKSolver,setupLookAtIK} from '@web-engine-dev/core/animation';import type { LookAtIKState } from '@web-engine-dev/core/animation';// Create look-at stateconst lookAt: LookAtIKState = {headBone: null,neckBone: null,spineBones: [],target: [0, 1.6, 5], // Eye level, 5m forwardweight: 1.0,config: {headBone: 'Head',neckBone: 'Neck',spineBones: ['Spine1', 'Spine2'],maxHorizontalAngle: Math.PI / 2, // 90° left/rightmaxVerticalAngle: Math.PI / 4, // 45° up/downweightDistribution: [0.6, 0.25, 0.1, 0.05], // Head, neck, spine},};// Initialize from skeletonsetupLookAtIK(lookAt, skeleton);// Update target each framelookAt.target = [enemy.x, enemy.y + 1.6, enemy.z];lookAt.weight = 0.8; // 80% blend// Solve look-atLookAtIKSolver.solve(lookAt, delta);
Look-at rotation is distributed across multiple bones for natural movement:
Natural Look-At
Distribute rotation across multiple bones instead of rotating only the head. This prevents unnatural "owl neck" and creates believable character awareness.
Foot IK adapts character feet to terrain slope and height for grounded locomotion:
import {setupFootIK,setFootIKGroundObjects,FootIK} from '@web-engine-dev/core/animation';import type { FootIKState } from '@web-engine-dev/core/animation';// Create foot IK stateconst footIK: FootIKState = {leftLegIK: {rootBone: null,middleBone: null,endBone: null,target: [0, 0, 0],poleTarget: [0, 0, 1],weight: 1.0,lengths: { upper: 0, lower: 0 },},rightLegIK: {rootBone: null,middleBone: null,endBone: null,target: [0, 0, 0],poleTarget: [0, 0, 1],weight: 1.0,lengths: { upper: 0, lower: 0 },},hipAdjustment: [0, 0, 0],weight: 1.0,config: {raycastDistance: 0.5, // Max distance to groundfootOffset: 0.05, // Foot above groundhipAdjustmentSpeed: 5, // Hip lowering smoothnessminHipAdjustment: -0.2, // Max hip dropmaxHipAdjustment: 0.1, // Max hip raise},};// Initialize from skeletonsetupFootIK(footIK, skeleton);// Set ground objects for raycastingsetFootIKGroundObjects([terrainMesh, floorMesh]);// Update foot IK each frameFootIK.solve(footIK, character.position, delta);// Feet now align to terrain slope and height
Performance Warning
Foot IK requires raycasting each frame. Limit to player and nearby NPCs. Disable for distant characters or use animation LOD to skip IK updates.
Ragdolls simulate character death/unconsciousness with physics-driven bone animation:
import { createRagdoll } from '@web-engine-dev/core/animation';import * as RAPIER from '@dimforge/rapier3d';// Create ragdoll from skeletonconst ragdoll = createRagdoll(skeleton, world, {// Physics propertiesdensity: 1.0,friction: 0.5,restitution: 0.1,// Joint limitsenableJointLimits: true,jointStiffness: 0.8,jointDamping: 0.5,// Collision groupscollisionGroups: 0x0002,collisionMask: 0xFFFF,});// Activate ragdoll on deathfunction onCharacterDeath(entity) {// Disable animationAnimator.enabled[entity] = false;// Enable ragdollragdoll.activate();// Apply death impulseconst forceDirection = getHitDirection(entity);ragdoll.applyImpulse('Spine',forceDirection.multiplyScalar(5.0),new THREE.Vector3(0, 1, 0) // Apply above center of mass);}// Update ragdoll (reads physics, writes bone transforms)function updateRagdoll(delta) {if (ragdoll.active) {ragdoll.syncFromPhysics();}}
| Joint | Type | Limits |
|---|---|---|
| Spine | Ball-Socket | ±45° twist, ±30° bend |
| Neck | Ball-Socket | ±60° twist, ±45° bend |
| Shoulder | Ball-Socket | ±90° all axes |
| Elbow | Hinge | 0° to 140° bend |
| Hip | Ball-Socket | ±90° twist, ±120° bend |
| Knee | Hinge | 0° to 135° bend |
Procedurally adjust upper body to aim at targets while moving:
function updateAimOffset(entity, target) {const aimDirection = target.clone().sub(entity.position).normalize();// Calculate pitch and yaw relative to character forwardconst yaw = Math.atan2(aimDirection.x, aimDirection.z);const pitch = Math.asin(aimDirection.y);// Clamp angles to reasonable limitsconst clampedPitch = Math.max(-Math.PI/4, Math.min(Math.PI/4, pitch));const clampedYaw = Math.max(-Math.PI/3, Math.min(Math.PI/3, yaw));// Set aim parameters for blend treestateMachine.setParameter('aim_pitch', clampedPitch);stateMachine.setParameter('aim_yaw', clampedYaw);}
Lean character based on velocity for dynamic locomotion:
function updateProceduralLean(entity, velocity) {const speed = velocity.length();const direction = velocity.clone().normalize();// Calculate lean angle (proportional to speed)const maxLeanAngle = Math.PI / 12; // 15°const leanAngle = Math.min(speed / 10.0, 1.0) * maxLeanAngle;// Apply lean in direction of movementconst spineBone = skeleton.getBoneByName('Spine1');const leanQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(-direction.z, 0, direction.x),leanAngle);// Blend with current rotationspineBone.quaternion.slerp(leanQuat, 0.1 * delta);}
Add subtle breathing to idle poses:
function updateBreathing(entity, time) {// Sine wave for breathing cycleconst breathCycle = Math.sin(time * 0.5) * 0.5 + 0.5; // 0-1// Apply to chest boneconst chestBone = skeleton.getBoneByName('Spine2');const breathScale = 1.0 + breathCycle * 0.05; // 0-5% scalechestBone.scale.set(breathScale, breathScale, breathScale);}
| Solver | Complexity | Cost/Frame |
|---|---|---|
| Two-Bone IK | O(1) | < 0.01ms |
| FABRIK (4 bones) | O(n·i) | < 0.05ms |
| CCD (4 bones) | O(n·i) | < 0.03ms |
| Look-At | O(b) | < 0.02ms |
| Foot IK (both legs) | O(1) | < 0.05ms |
IK Budget
Budget 0.1ms per character for all IK (2 legs + 2 arms + look-at). Allows 10-15 characters with full IK at 60 FPS.