How to Crossfade Animations in Three.js
Transitioning smoothly between 3D character animations, like moving
from a walk to a run, is essential for realistic web-based games and
interactive experiences. This article explains how to seamlessly
crossfade between two different character animations in Three.js using
the built-in AnimationMixer and
AnimationAction APIs. You will learn how to set up your
animation states, prepare actions, and execute a smooth transition with
practical code examples.
1. Setting Up the Animation Mixer
To manage animations in Three.js, you need an
AnimationMixer bound to your 3D character model. Once
loaded, you convert your model’s AnimationClip data into
playable AnimationAction instances.
import * as THREE from 'three';
// Assume 'gltf' is your loaded model
const model = gltf.scene;
const mixer = new THREE.AnimationMixer(model);
// Store animations in an object for easy access
const actions = {};
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
// Set an initial active animation
let currentAction = actions['Idle'];
currentAction.play();2. Implementing the Crossfade Function
To transition from one animation to another, you must manipulate the
weights of the active and target animations. Three.js provides a
built-in method called crossFadeTo on the
AnimationAction prototype to automate this process.
Here is a reusable function to transition between any two states:
function fadeTo(nextActionName, duration = 0.5) {
const nextAction = actions[nextActionName];
// Prevent fading to the already active animation
if (currentAction === nextAction) return;
// 1. Prepare the next action for playback
nextAction.enabled = true;
nextAction.setEffectiveTimeScale(1);
nextAction.setEffectiveWeight(1);
nextAction.time = 0; // Start from the beginning
nextAction.play();
// 2. Crossfade from the current action to the next action
currentAction.crossFadeTo(nextAction, duration, true);
// 3. Update the reference to the active action
currentAction = nextAction;
}Why these steps are necessary:
nextAction.enabled = true: Re-enables the action if it was previously disabled.nextAction.setEffectiveWeight(1): Ensures the destination animation reaches full visibility.nextAction.time = 0: Resets the animation track time so the animation starts from the first frame.currentAction.crossFadeTo(...): Interpolates the weight of the current action down to 0 while driving the weight of the next action up to 1 over the specified duration. The third argument (warp) scales the time of the animations to synchronize their steps, preventing sliding feet.
3. Updating the Mixer in the Loop
For the transitions and animations to play over time, you must
continuously update the AnimationMixer within your
requestAnimationFrame loop using a delta time clock.
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (mixer) {
mixer.update(delta);
}
renderer.render(scene, camera);
}
animate();4. Triggering the Transition
You can trigger the transition based on user inputs or application events:
// Example: Transition to 'Walk' when pressing 'W'
window.addEventListener('keydown', (event) => {
if (event.key === 'w' || event.key === 'W') {
fadeTo('Walk', 0.5); // Fades from 'Idle' to 'Walk' over 0.5 seconds
}
});
// Example: Return to 'Idle' when releasing 'W'
window.addEventListener('keyup', (event) => {
if (event.key === 'w' || event.key === 'W') {
fadeTo('Idle', 0.5);
}
});