Update Morph Target Weights in Three.js Manually

This article explains how to manually update morph target weights in Three.js to achieve real-time, procedural facial animations. You will learn how to locate morph targets within your 3D model, access their influence arrays, and programmatically adjust their values within the render loop to drive facial expressions dynamically.

Understanding Morph Targets in Three.js

Morph targets (also known as blend shapes or shape keys) allow a single mesh to deform into different shapes. In facial animation, these shapes represent individual expressions like “jawOpen,” “blinkLeft,” or “smile.”

Three.js manages these deformations using two main properties on a Mesh object: * mesh.morphTargetDictionary: A key-value map containing the names of the morph targets and their corresponding index in the influence array. * mesh.morphTargetInfluences: An array of floating-point numbers (typically between 0.0 and 1.0) that determines how much a specific morph target influences the base mesh.

Step 1: Accessing the Mesh and Morph Targets

Before you can update the weights, you must locate the mesh containing the morph targets. When loading a model (e.g., a GLTF file), traverse the scene graph to find the correct SkinnedMesh or Mesh.

let facialMesh;

loader.load('path/to/model.gltf', (gltf) => {
    gltf.scene.traverse((child) => {
        if (child.isMesh && child.morphTargetInfluences) {
            facialMesh = child;
        }
    });
    scene.add(gltf.scene);
});

Step 2: Manually Updating the Weights

To change a specific facial expression, look up its index using the morphTargetDictionary and update the corresponding value in the morphTargetInfluences array.

function setMorphWeight(mesh, targetName, weight) {
    if (!mesh || !mesh.morphTargetDictionary || !mesh.morphTargetInfluences) {
        return;
    }

    const index = mesh.morphTargetDictionary[targetName];
    if (index !== undefined) {
        // Clamp the weight between 0 and 1
        mesh.morphTargetInfluences[index] = Math.max(0, Math.min(1, weight));
    } else {
        console.warn(`Morph target "${targetName}" not found on this mesh.`);
    }
}

// Example usage:
setMorphWeight(facialMesh, 'jawOpen', 0.8);
setMorphWeight(facialMesh, 'eyeBlinkLeft', 1.0);

Step 3: Driving Procedural Animation in the Render Loop

For procedural animation (such as lip-syncing to audio, random eye blinking, or reactive expressions), update the weights inside your application’s animation/render loop. Three.js automatically uploads the updated weight values to the GPU on every frame.

function animate(time) {
    requestAnimationFrame(animate);

    if (facialMesh) {
        // Procedurally simulate a breathing or talking jaw movement using a sine wave
        const jawMovement = Math.abs(Math.sin(time * 0.005)) * 0.5;
        setMorphWeight(facialMesh, 'jawOpen', jawMovement);

        // Procedurally simulate an occasional eye blink
        const blinkValue = Math.sin(time * 0.01) > 0.98 ? 1.0 : 0.0;
        setMorphWeight(facialMesh, 'eyeBlinkLeft', blinkValue);
        setMorphWeight(facialMesh, 'eyeBlinkRight', blinkValue);
    }

    renderer.render(scene, camera);
}

By directly manipulating the morphTargetInfluences array, you bypass the standard keyframe animation clips, giving you complete, programmatic control over your character’s facial expressions in real-time.