Three.js Morph Targets for Facial Animations

This article provides a practical guide on how to implement and control morph targets in Three.js to create realistic facial expression animations. You will learn how to load a 3D model with pre-authored morph targets, access and manipulate these targets using JavaScript, and animate transitions between different facial expressions such as smiling, blinking, or speaking.

What are Morph Targets?

Morph targets (also known as blend shapes or shape keys) are alternative versions of a 3D mesh geometry where vertices are displaced to form a new shape. In facial animation, a base mesh representing a neutral face is deformed into specific target shapes—such as closed eyes, a raised eyebrow, or an open mouth. Three.js allows you to smoothly transition between these shapes by interpolating vertex positions based on influence weights ranging from 0 (neutral) to 1 (fully deformed).

Loading a Model with Morph Targets

To use morph targets, you typically author them in a 3D modeling tool like Blender or Maya and export the model as a GLTF/GLB file.

You can load this model into Three.js using the GLTFLoader. Once loaded, you need to traverse the model’s scene graph to locate the specific SkinnedMesh or Mesh containing the morph targets.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
let faceMesh;

loader.load('path/to/facial_model.glb', (gltf) => {
    const model = gltf.scene;
    scene.add(model);

    model.traverse((child) => {
        if (child.isMesh && child.morphTargetInfluences) {
            faceMesh = child;
            console.log(faceMesh.morphTargetDictionary);
        }
    });
});

Accessing and Controlling Morph Targets

Three.js stores morph targets on the mesh using two main properties:

  1. morphTargetDictionary: An object mapping the names of the morph targets (e.g., “smile”, “blink”) to their corresponding index in the influence array.
  2. morphTargetInfluences: An array of numerical weights (floats between 0 and 1) that determine how much each morph target affects the base geometry.

Manual Control

To manually trigger or adjust an expression, look up the target index in the dictionary and update the influence array:

// Function to update a specific expression weight
function setExpression(mesh, targetName, intensity) {
    const index = mesh.morphTargetDictionary[targetName];
    if (index !== undefined) {
        mesh.morphTargetInfluences[index] = intensity;
    }
}

// Example: Make the character smile fully
setExpression(faceMesh, 'smile', 1.0);

// Example: Close the left eye halfway
setExpression(faceMesh, 'blink_L', 0.5);

Animating Morph Targets

For fluid animations, you can animate the influences over time. This can be done programmatically in your render loop or by utilizing the Three.js Animation system.

Option 1: Programmatic Animation (Lerping)

To smoothly transition an expression within your render loop, you can use linear interpolation (lerp):

let targetSmileValue = 1.0;

function animate() {
    requestAnimationFrame(animate);

    if (faceMesh) {
        const smileIndex = faceMesh.morphTargetDictionary['smile'];
        
        // Smoothly transition current weight to the target value
        faceMesh.morphTargetInfluences[smileIndex] = THREE.MathUtils.lerp(
            faceMesh.morphTargetInfluences[smileIndex],
            targetSmileValue,
            0.1 // speed factor
        );
    }

    renderer.render(scene, camera);
}

Option 2: Using Three.js AnimationMixer

If your morph target animations were pre-made and exported inside the GLTF file, you can play them back using the AnimationMixer:

let mixer;

loader.load('path/to/facial_model.glb', (gltf) => {
    const model = gltf.scene;
    scene.add(model);

    mixer = new THREE.AnimationMixer(model);
    
    // Play a specific facial animation clip exported with the model
    const clips = gltf.animations;
    const clip = THREE.AnimationClip.findByName(clips, 'talking_animation');
    
    if (clip) {
        const action = mixer.clipAction(clip);
        action.play();
    }
});

// Update the mixer in the render loop
const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);
    
    const delta = clock.getDelta();
    if (mixer) mixer.update(delta);

    renderer.render(scene, camera);
}

Performance Best Practices