Documentation

Everything you need to know about using EasyRig to add professional-quality rigged, animated characters to your web games. Full support for twist bones, stretchy limbs, facial rigs, muscle systems, and game-ready export.

Installation

Add EasyRig to your project with a single script tag. The library is served from our CDN and requires Three.js as a peer dependency.

<!-- Load Three.js first -->
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.160.0/examples/js/loaders/GLTFLoader.js"></script>

<!-- Then load EasyRig -->
<script src="https://easy-rig.vercel.app/api/easyrig"></script>

If you're using ES modules:

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

// EasyRig attaches to window.EasyRig
await import('https://easy-rig.vercel.app/api/easyrig')

// Wait for ready event
document.addEventListener('easyrig:ready', (e) => {
  console.log('EasyRig v' + e.detail.version + ' loaded')
})

Quick Start

Get a fully-featured character up and running in under a minute. This example loads a model, sets up advanced systems, and starts playing animations.

// Initialize EasyRig with full features
EasyRig.init({ 
  debug: true,
  config: {
    enableTwistBones: true,
    enableStretchyLimbs: true,
    enableFootRoll: true,
    enableFingerCurl: true,
    enableFacialRig: true,
    enableMuscleSystem: true
  }
})

// Load your character model
const player = await EasyRig.load('player', '/models/character.glb')

// Apply animations
await player.applyAnimation('/animations/idle.glb', 'idle')
await player.applyAnimation('/animations/run.glb', 'run')

// Add to scene
scene.add(player.getModel())

// Play with smooth blending
player.play('idle')

// Set up finger poses
player.fingerCurl.setPose('left', 'open')
player.fingerCurl.setPose('right', 'fist')

// Set facial expression
player.facial.setExpression('smile', 0.8)

// Start auto-blink
player.facial.startAutoBlink()

// In your render loop
function animate() {
  requestAnimationFrame(animate)
  
  const delta = clock.getDelta()
  EasyRig.update(delta) // Updates all characters
  
  renderer.render(scene, camera)
}

animate()

Tip: Enable debug mode during development to see detailed logs about skeleton generation, bone mapping, and all advanced systems initialization.

Configuration

EasyRig offers extensive configuration options. Pass them during initialization to customize behavior.

Core Settings

defaultBoneSize: 0.05

Default size for generated bones

weightingAlgorithm: 'heatmap'

Algorithm for vertex weights

maxBonesPerVertex: 4

Maximum bone influences per vertex

autoNormalize: true

Auto-normalize vertex weights

ikIterations: 20

IK solver iteration count

ikTolerance: 0.0001

IK solver convergence tolerance

Twist Bone Settings

enableTwistBones: true

Enable automatic twist bones

twistBoneCount: 3

Number of twist bones per segment

twistDistribution: 'smooth'

Options: 'smooth', 'linear', 'exponential'

Stretchy Limb Settings

enableStretchyLimbs: true

Enable stretchy IK limbs

stretchLimit: 1.5

Maximum stretch ratio (150%)

squashLimit: 0.5

Minimum squash ratio (50%)

volumePreservation: true

Maintain volume when stretching

Foot & Hand Settings

enableFootRoll: true

Enable foot roll system

enableToePivot: true

Enable toe pivot control

enableFingerCurl: true

Enable finger curl system

fingerJoints: 3

Joints per finger

Advanced Settings

enableSpaceSwitching: true

Enable parent space switching

enableRibbonSpine: true

Enable ribbon spine deformation

enableFacialRig: true

Enable facial rig system

enableMuscleSystem: true

Enable muscle deformation

enableAdvancedShoulder: true

Auto clavicle rotation

spineSegments: 5

Ribbon spine segment count

Game Export Settings

gameExportOptimization: true

Enable export optimizer

maxExportBones: 75

Max bones for game export

bakeTwistBones: false

Bake twist into weights

removeHelperBones: true

Remove control bones on export

Requirements

Loading Models

EasyRig accepts models in several ways. The most common is loading from a URL, but you can also pass existing Three.js objects.

From URL

const character = await EasyRig.load('hero', '/models/hero.glb')

// Listen to load progress
character.on('loadProgress', (data) => {
  console.log('Loading:', data.percent + '%')
})

From Three.js Object

// If you already have a loaded mesh
const character = await EasyRig.load('hero', existingMesh)

From Geometry

// From raw geometry
const geometry = new THREE.BoxGeometry(1, 2, 1)
const character = await EasyRig.load('box', geometry)

Skeleton Generation

When you load a model without an existing skeleton, EasyRig analyzes the mesh geometry to generate an appropriate bone structure.

Model Type Detection

EasyRig automatically detects the type of model based on its proportions:

Type Detection Criteria Skeleton Structure
humanoid Height/width ratio 1.5 - 4.0 Full human skeleton with fingers
quadruped Depth > width × 1.5 Four-legged skeleton with tail
creature Ratio 0.5 - 1.5 Simplified central skeleton
vehicle Height/width < 0.5 Wheel and axle skeleton
custom Other proportions Adaptive point-based skeleton

Twist Bones New

Twist bones prevent the "candy wrapper" effect on forearms and upper arms by distributing rotation across multiple bones.

// Twist bones are auto-created for arms when enabled
// Access the twist system directly for custom control

// Create custom twist chain
player.twistSystem.create(
  player.getBone('LeftUpperArm'),
  player.getBone('LeftForeArm'),
  'left_upper_arm'
)

// Twist distribution options:
// 'smooth' - cubic smoothstep (default, best for skin)
// 'linear' - even distribution
// 'exponential' - more twist near end

Stretchy Limbs New

Enable cartoon-style stretchy limbs that maintain volume while extending beyond their rest length.

// Create stretchy limb
player.stretchyLimbs.create(
  [shoulder, elbow, wrist],
  'left_arm',
  {
    stretchLimit: 1.5,    // 150% max stretch
    squashLimit: 0.5,     // 50% min squash
    volumePreservation: true
  }
)

// Set target position (limb stretches to reach)
player.stretchyLimbs.setTarget('left_arm', targetPosition)

// Reset to rest length
player.stretchyLimbs.reset('left_arm')

Volume Preservation: When enabled, limbs will get thinner as they stretch and thicker as they compress, maintaining visual volume.

Foot Roll New

Professional foot roll system with heel, ball, and toe pivots for natural walking and running animations.

// Foot roll is auto-created when enabled
// Control foot roll angle (-30 to 90 degrees)
player.footRoll.setRoll('left', 45)  // heel up, ball pivot
player.footRoll.setRoll('left', -20) // heel plant, toe up

// Bank (side-to-side tilt)
player.footRoll.setBank('left', 15)  // tilt outward

// Toe wiggle
player.footRoll.setToeWiggle('left', 10)

// Roll thresholds (customizable)
// heelRollStart: -30 (when heel lifts)
// ballRollStart: 0 (when ball pivots)
// toeRollStart: 45 (when toe pivots)

Finger Curl New

Complete finger control system with individual curl, spread, and preset poses.

// Set curl on individual fingers (0-1)
player.fingerCurl.setCurl('left', 'index', 0.5)
player.fingerCurl.setCurl('left', 'thumb', 0.3)

// Set finger spread
player.fingerCurl.setSpread('left', 'index', 0.5)

// Make a fist (0-1)
player.fingerCurl.setFist('left', 1)

// Use preset poses
player.fingerCurl.setPose('left', 'open')
player.fingerCurl.setPose('left', 'fist')
player.fingerCurl.setPose('left', 'point')
player.fingerCurl.setPose('left', 'thumbsUp')
player.fingerCurl.setPose('left', 'peace')
player.fingerCurl.setPose('left', 'grab')
player.fingerCurl.setPose('left', 'pinch')

Space Switching New

Switch constraint spaces for IK targets with smooth blending between world, root, chest, head, or hand spaces.

// Register custom space
player.spaceSwitching.registerSpace('weapon', weaponBone)

// Create control on a bone
player.spaceSwitching.createControl('LeftHand', [
  'world', 'root', 'chest', 'weapon'
])

// Switch space with smooth blend
player.spaceSwitching.switchSpace('LeftHand', 'chest', true, 0.3)

// Instant switch (no blend)
player.spaceSwitching.switchSpace('LeftHand', 'world', false)

// Default spaces available:
// 'world', 'root', 'chest', 'head', 'hand_l', 'hand_r'

Ribbon Spine New

Smooth spline-based spine deformation for fluid body movement.

// Ribbon spine is auto-created when enabled

// Bend the spine
player.ribbonSpine.setBend('spine', 
  0.3,  // x bend (forward/back)
  0.1   // z bend (side to side)
)

// Add twist along the spine
player.ribbonSpine.setTwist('spine', 0.2)

// Access control points for advanced manipulation
const ribbon = player.ribbonSpine.ribbons.get('spine')
ribbon.controlPoints[1].add(new THREE.Vector3(0, 0.1, 0))

Facial Rig New

Complete facial animation system supporting both bone-based and blendshape-based rigs.

Blendshapes

// Set individual blendshape values (0-1)
player.facial.setBlendshape('mouthSmile_L', 0.8)
player.facial.setBlendshape('mouthSmile_R', 0.8)
player.facial.setBlendshape('browUp_L', 0.5)

// Get current value
const value = player.facial.getBlendshape('mouthOpen')

Expressions

// Use preset expressions
player.facial.setExpression('smile', 1.0)
player.facial.setExpression('frown', 0.5)
player.facial.setExpression('surprise', 0.8)
player.facial.setExpression('angry', 1.0)
player.facial.setExpression('sad', 0.7)

// Register custom expression
player.facial.registerExpression('smirk', {
  'mouthSmile_L': 0.8,
  'mouthSmile_R': 0.2,
  'browUp_L': 0.3
})

Eye Controls

// Make eyes look at target
player.facial.lookAt(targetPosition)

// Manual blink
player.facial.blink(0.15) // duration in seconds

// Wink
player.facial.setExpression('wink_l', 1)

// Start automatic blinking
player.facial.startAutoBlink(
  4000,  // base interval (ms)
  2000   // random variance (ms)
)

Muscle System New

Simulate muscle bulging and jiggle physics for more realistic character deformation.

// Create muscle between two bones
player.muscles.create(
  player.getBone('LeftUpperArm'),
  player.getBone('LeftForeArm'),
  'left_bicep',
  {
    influence: 0.3,
    jiggleDecay: 0.9
  }
)

// Bind muscle to mesh vertices
player.muscles.bindToMesh('left_bicep', characterMesh, [
  123, 124, 125, 126  // vertex indices
])

// Add jiggle on impact
player.muscles.addJiggle('left_bicep', 0.05)

// Muscle automatically bulges when bone contracts
// and flattens when bone stretches

Advanced Shoulders New

Automatic clavicle rotation when arms are raised or moved forward.

// Advanced shoulders are auto-created when enabled

// Configuration options
EasyRig.init({
  config: {
    enableAdvancedShoulder: true,
    clavicleAutoRotation: true,
    shoulderRaise: 0.3  // how much clavicle follows arm
  }
})

// Access shoulder data
const shoulder = player.advancedShoulder.shoulders.get('left')
shoulder.raiseAmount = 0.4  // increase clavicle motion
shoulder.armRaiseThreshold = 30  // degrees before clavicle activates

FK/IK Matching New

Seamlessly switch between forward kinematics and inverse kinematics with automatic pose matching.

// Create FK/IK chain
player.fkikMatching.create(
  [shoulder, elbow, wrist],
  'left_arm',
  {
    defaultWeight: 1,  // start in IK mode
    snapThreshold: 0.01
  }
)

// Switch between modes
player.fkikMatching.setMode('left_arm', 'fk')  // pose automatically matches
player.fkikMatching.setMode('left_arm', 'ik')

// Blend between FK and IK (0 = pure FK, 1 = pure IK)
player.fkikMatching.setBlend('left_arm', 0.5)

// Snap FK rotations to current IK pose
player.fkikMatching.snapFK('left_arm')

// Snap IK target to current FK pose
player.fkikMatching.snapIK('left_arm')

Game Export New

Optimize and export your rigged character for game engines with reduced bone counts and baked deformations.

// Optimize for game export
const exportData = player.gameExporter.optimize({
  maxBones: 75,           // max bones in final rig
  bakeTwistBones: true,    // bake twist into weights
  removeHelperBones: true, // remove IK/control bones
  simplifyWeights: true,   // reduce weight influences
  maxInfluences: 4         // max bones per vertex
})

console.log(exportData)
// { boneCount: 65, meshCount: 1, animationCount: 5, optimized: true }

// Export to GLTF/GLB
const glb = await player.gameExporter.exportToGLTF({
  binary: true  // false for .gltf
})

// Download the file
const blob = new Blob([glb], { type: 'model/gltf-binary' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'character.glb'
a.click()

Unity/Unreal Ready: Exported GLB files are compatible with Unity (via glTFast) and Unreal Engine (via glTF Runtime).

Animations

Load animations from external files and apply them to your character. EasyRig handles bone name mapping automatically.

// Load and name an animation
await player.applyAnimation('/anims/walk.glb', 'walk')

// Play with options
player.play('walk', {
  loop: true,
  fadeTime: 0.3,
  speed: 1.2
})

// Stop animation
player.stop()

Animation Retargeting

When applying animations, EasyRig maps bones from the animation file to your character's skeleton. It handles different naming conventions automatically.

Supported naming conventions:

Ragdoll Physics

Enable physics-based ragdoll with a single call. Requires a physics world from Cannon.js, Ammo.js, or Rapier.

import * as CANNON from 'cannon-es'

// Create physics world
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)

// Enable ragdoll on character
player.enableRagdoll(world)

// Activate on impact
function onHit(impactVelocity) {
  player.activateRagdoll(impactVelocity)
}

// Deactivate and return to animation
function recover() {
  player.deactivateRagdoll()
  player.play('getup')
}

// Apply forces to specific bones
player.applyForce('Head', { x: 100, y: 50, z: 0 })

Inverse Kinematics

Built-in IK system for procedural animation, foot placement, and look-at targets.

// Enable IK
player.enableIK()

// Set IK target for left hand
player.setIKTarget('leftArm', [1, 1.5, 0.5])

// Foot placement on terrain
const groundY = terrain.getHeightAt(player.position)
player.setIKTarget('leftLeg', [player.position.x - 0.1, groundY, player.position.z])
player.setIKTarget('rightLeg', [player.position.x + 0.1, groundY, player.position.z])

Events

Listen to character events for gameplay integration.

// Load progress
player.on('loadProgress', (data) => {
  console.log('Loading:', data.percent + '%')
})

// Animation events
player.on('animationStart', (data) => {
  console.log('Started:', data.name, 'duration:', data.duration)
})

player.on('animationStop', () => {
  console.log('Animation stopped')
})

// Ragdoll events
player.on('ragdollActivate', () => {
  playSound('impact')
})

player.on('ragdollDeactivate', () => {
  resetGameplay()
})

EasyRig API

EasyRig.init(options)

Initialize the EasyRig library. Must be called before using other methods.

options.debug
Boolean. Enable detailed logging. Default: false
options.config
Object. Override default configuration settings. See Configuration section for all options.
EasyRig.load(id, source, options) → Promise<Character>

Load a 3D model and create a character. Returns a Character instance.

id
String. Unique identifier for this character.
source
String | Object3D | Geometry. URL to model file, Three.js object, or geometry.
options
Object. Optional configuration for loading.
EasyRig.get(id) → Character | undefined

Retrieve a previously loaded character by ID.

EasyRig.remove(id)

Remove a character and dispose of its resources.

EasyRig.update(delta)

Update all managed characters. Alternative to calling update on each character individually.

EasyRig.clearCache()

Clear all cached skeletons, animations, geometries, and blendshapes.

Character API

Core Methods

character.applyAnimation(source, name) → Promise

Load and apply an animation to the character. Handles retargeting automatically.

source
String | AnimationClip | Array. URL to animation file or Three.js clip(s).
name
String. Optional name for the animation. Defaults to filename or clip name.
character.play(name, options) → Character

Play an animation with optional blending and speed control.

name
String. Name of the animation to play.
options.loop
Boolean. Whether to loop the animation. Default: true
options.fadeTime
Number. Crossfade duration in seconds. Default: 0.3
options.speed
Number. Playback speed multiplier. Default: 1.0
character.stop() → Character

Stop current animation with fadeout.

character.update(delta) → Character

Update character animations and all systems. Call every frame.

character.getBone(name) → Bone | null

Get a bone by name (case-insensitive).

character.getModel() → Object3D

Get the Three.js model for adding to scene.

character.getSkeleton() → Skeleton

Get the character's skeleton.

character.on(event, callback) → Character

Listen to character events.

character.dispose()

Clean up and remove all resources.

System Access

Access advanced systems through character properties:

Property Type Description
character.twistSystem TwistBoneSystem Twist bone controls
character.stretchyLimbs StretchyLimbSystem Stretchy limb controls
character.footRoll FootRollSystem Foot roll controls
character.fingerCurl FingerCurlSystem Finger pose controls
character.spaceSwitching SpaceSwitchingSystem Space switching controls
character.ribbonSpine RibbonSpineSystem Spine deformation controls
character.facial FacialRigSystem Facial animation controls
character.muscles MuscleSystem Muscle deformation controls
character.advancedShoulder AdvancedShoulderSystem Auto shoulder controls
character.fkikMatching FKIKMatchingSystem FK/IK blend controls
character.gameExporter GameExportOptimizer Export optimization

Model Preparation

While EasyRig works with any model, following these guidelines gives the best results:

Recommended

Avoid

Using Mixamo

Mixamo is a free service that provides thousands of animations. Here's how to use them with EasyRig:

  1. Go to mixamo.com and select an animation
  2. Download as FBX for Unity (yes, even for web)
  3. Convert to GLB using online tools or Blender
  4. Apply to your character with applyAnimation()

Note: You don't need to upload your model to Mixamo. Just download the animation with Mixamo's default character and EasyRig will retarget it.

Physics Engines

EasyRig's ragdoll system works with these physics libraries:

Cannon.js (Recommended)

import * as CANNON from 'cannon-es'

const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
world.broadphase = new CANNON.SAPBroadphase(world)

player.enableRagdoll(world)

Rapier

import RAPIER from '@dimforge/rapier3d'

await RAPIER.init()
const world = new RAPIER.World({ x: 0, y: -9.82, z: 0 })

player.enableRagdoll(world)

Performance

Tips for getting the best performance from EasyRig:

Model Optimization

Animation Optimization

Advanced Systems Optimization

Physics Optimization

Example: For a game with 50 characters, keep 10 fully animated with all systems, 30 with simple idle animations and basic systems, and 10 completely paused. Only 2-3 ragdolls active at once.

Performance Monitoring

// Access performance stats
console.log(player.performance)
// { lastUpdate: 1.23, avgUpdateTime: 0.45, updateCount: 1000 }

// Disable systems dynamically based on distance
function updateCharacterLOD(character, distanceToCamera) {
  if (distanceToCamera > 50) {
    // Disable expensive systems for far characters
    character.twistSystem = null
    character.muscles = null
    character.facial = null
  } else if (distanceToCamera > 20) {
    // Disable some systems for medium distance
    character.muscles = null
  }
}

Troubleshooting

Common Issues

Model doesn't animate

Skeleton not generated

Animations look wrong

Facial rig not working

Performance issues

Debug Mode

// Enable comprehensive logging
EasyRig.init({ debug: true })

// Logs include:
// - Model loading progress
// - Skeleton generation details
// - Bone mapping results
// - Animation retargeting info
// - System initialization status
// - Performance warnings

Complete Examples

Basic Character Setup

// Complete setup with all features
async function setupCharacter() {
  // Initialize
  EasyRig.init({
    debug: true,
    config: {
      enableTwistBones: true,
      enableStretchyLimbs: true,
      enableFootRoll: true,
      enableFingerCurl: true,
      enableFacialRig: true,
      enableMuscleSystem: true,
      enableAdvancedShoulder: true,
      enableRibbonSpine: true,
      enableSpaceSwitching: true
    }
  })
  
  // Load character
  const player = await EasyRig.load('player', '/models/hero.glb')
  
  // Load animations
  await player.applyAnimation('/anims/idle.glb', 'idle')
  await player.applyAnimation('/anims/walk.glb', 'walk')
  await player.applyAnimation('/anims/run.glb', 'run')
  await player.applyAnimation('/anims/jump.glb', 'jump')
  
  // Add to scene
  scene.add(player.getModel())
  
  // Setup facial
  player.facial.startAutoBlink()
  player.facial.setExpression('smile', 0.3)
  
  // Setup hands
  player.fingerCurl.setPose('left', 'open')
  player.fingerCurl.setPose('right', 'open')
  
  // Start idle
  player.play('idle')
  
  // Setup events
  player.on('animationStart', (data) => {
    console.log('Playing:', data.name)
  })
  
  return player
}

// Game loop
const clock = new THREE.Clock()
let player

setupCharacter().then(p => player = p)

function animate() {
  requestAnimationFrame(animate)
  
  const delta = clock.getDelta()
  
  if (player) {
    player.update(delta)
    
    // Make character look at mouse
    player.facial.lookAt(mouseWorldPosition)
  }
  
  renderer.render(scene, camera)
}

animate()

Character Controller with State Machine

class CharacterController {
  constructor(player) {
    this.player = player
    this.state = 'idle'
    this.velocity = new THREE.Vector3()
    this.grounded = true
  }
  
  setState(newState) {
    if (this.state === newState) return
    
    const oldState = this.state
    this.state = newState
    
    // Handle state transitions
    switch (newState) {
      case 'idle':
        this.player.play('idle', { fadeTime: 0.3 })
        this.player.facial.setExpression('smile', 0.3)
        break
        
      case 'walk':
        this.player.play('walk', { fadeTime: 0.2 })
        break
        
      case 'run':
        this.player.play('run', { fadeTime: 0.2 })
        this.player.facial.setExpression('surprise', 0.2)
        break
        
      case 'jump':
        this.player.play('jump', { loop: false, fadeTime: 0.1 })
        this.player.facial.setExpression('surprise', 0.5)
        break
        
      case 'punch':
        this.player.play('punch', { loop: false, fadeTime: 0.1 })
        this.player.fingerCurl.setPose('right', 'fist')
        this.player.facial.setExpression('angry', 0.8)
        break
    }
  }
  
  update(input, delta) {
    const speed = input.shift ? 10 : 5
    
    // Movement
    if (input.forward || input.back || input.left || input.right) {
      this.setState(input.shift ? 'run' : 'walk')
    } else if (this.grounded) {
      this.setState('idle')
    }
    
    // Jump
    if (input.jump && this.grounded) {
      this.setState('jump')
      this.velocity.y = 8
      this.grounded = false
    }
    
    // Attack
    if (input.attack) {
      this.setState('punch')
    }
    
    // Foot placement on terrain
    if (this.player.footRoll) {
      const leftY = terrain.getHeightAt(this.leftFootPos)
      const rightY = terrain.getHeightAt(this.rightFootPos)
      
      // Adjust foot roll based on terrain slope
      const slopeDiff = leftY - rightY
      this.player.footRoll.setBank('left', slopeDiff * 10)
      this.player.footRoll.setBank('right', -slopeDiff * 10)
    }
  }
}

Facial Animation System

class FacialAnimator {
  constructor(player) {
    this.player = player
    this.facial = player.facial
    this.currentMood = 'neutral'
    this.talkingWeight = 0
    this.lookTarget = null
  }
  
  setMood(mood, intensity = 1) {
    this.currentMood = mood
    
    // Reset previous expressions
    this.facial.setExpression('smile', 0)
    this.facial.setExpression('frown', 0)
    this.facial.setExpression('angry', 0)
    this.facial.setExpression('sad', 0)
    this.facial.setExpression('surprise', 0)
    
    // Apply new mood
    switch (mood) {
      case 'happy':
        this.facial.setExpression('smile', intensity)
        break
      case 'sad':
        this.facial.setExpression('sad', intensity)
        break
      case 'angry':
        this.facial.setExpression('angry', intensity)
        break
      case 'surprised':
        this.facial.setExpression('surprise', intensity)
        break
    }
  }
  
  startTalking() {
    this.talking = true
  }
  
  stopTalking() {
    this.talking = false
  }
  
  update(delta) {
    // Talking animation
    if (this.talking) {
      // Procedural mouth movement
      const mouthOpen = (Math.sin(Date.now() * 0.01) + 1) * 0.3
      this.facial.setBlendshape('mouthOpen', mouthOpen)
      
      // Slight jaw movement
      const jawMove = Math.sin(Date.now() * 0.008) * 0.2
      this.facial.setBlendshape('jawOpen', jawMove + 0.2)
    } else {
      this.facial.setBlendshape('mouthOpen', 0)
      this.facial.setBlendshape('jawOpen', 0)
    }
    
    // Look at target
    if (this.lookTarget) {
      this.facial.lookAt(this.lookTarget)
    }
  }
}

// Usage
const facialAnimator = new FacialAnimator(player)
facialAnimator.setMood('happy', 0.8)
facialAnimator.lookTarget = camera.position

// In dialogue
facialAnimator.startTalking()
setTimeout(() => facialAnimator.stopTalking(), 3000)

Ragdoll on Death

class RagdollController {
  constructor(player, physicsWorld) {
    this.player = player
    this.world = physicsWorld
    this.isRagdoll = false
    this.recoveryTime = 0
    
    // Enable ragdoll system
    player.enableRagdoll(physicsWorld)
    
    // Listen for ragdoll events
    player.on('ragdollActivate', () => {
      this.isRagdoll = true
      this.playImpactSound()
    })
    
    player.on('ragdollDeactivate', () => {
      this.isRagdoll = false
    })
  }
  
  takeDamage(amount, impactPoint, impactForce) {
    if (amount > 50) {
      // Big hit - go ragdoll
      this.player.activateRagdoll()
      
      // Apply impact force
      this.player.applyForce('Chest', impactForce)
      
      // Schedule recovery
      this.recoveryTime = 3
    }
  }
  
  die(impactForce) {
    this.player.stop()
    this.player.activateRagdoll()
    
    if (impactForce) {
      this.player.applyForce('Chest', impactForce)
    }
    
    // No recovery - permanent ragdoll
    this.recoveryTime = -1
  }
  
  update(delta) {
    if (this.recoveryTime > 0) {
      this.recoveryTime -= delta
      
      if (this.recoveryTime <= 0) {
        this.recover()
      }
    }
  }
  
  recover() {
    this.player.deactivateRagdoll()
    this.player.play('getup', { loop: false })
  }
  
  playImpactSound() {
    // Play sound effect
    audio.play('impact')
  }
}

Export for Unity/Unreal

async function exportForGameEngine(player, engineType) {
  const exporter = player.gameExporter
  
  // Optimize based on target engine
  const options = engineType === 'unity' ? {
    maxBones: 75,
    bakeTwistBones: true,
    removeHelperBones: true,
    maxInfluences: 4
  } : {
    maxBones: 256,  // Unreal supports more
    bakeTwistBones: false,
    removeHelperBones: true,
    maxInfluences: 8
  }
  
  // Optimize
  const stats = exporter.optimize(options)
  console.log('Optimized:', stats)
  
  // Export
  const glbData = await exporter.exportToGLTF({ binary: true })
  
  // Create download
  const blob = new Blob([glbData], { type: 'model/gltf-binary' })
  const url = URL.createObjectURL(blob)
  
  const link = document.createElement('a')
  link.href = url
  link.download = 'character_' + engineType + '.glb'
  link.click()
  
  URL.revokeObjectURL(url)
  
  return stats
}

// Export buttons
document.getElementById('exportUnity').onclick = () => {
  exportForGameEngine(player, 'unity')
}

document.getElementById('exportUnreal').onclick = () => {
  exportForGameEngine(player, 'unreal')
}

Changelog

Version 1.0.0

Support

Need help? Here are your options:

Pro Tip: Enable debug mode and check the console first. Most issues are clearly logged with suggestions for fixes.