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:

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);
    }
});