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:
morphTargetDictionary: An object mapping the names of the morph targets (e.g., “smile”, “blink”) to their corresponding index in the influence array.morphTargetInfluences: An array of numerical weights (floats between0and1) 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
- Limit Morph Target Counts: Keep the number of active morph targets per mesh under control. While modern hardware handles morphing efficiently on the GPU, excessive targets can impact memory and performance, especially on mobile devices.
- Keep Mesh Topology Identical: Ensure that all morph targets share the exact same vertex order and count as the base mesh during export, otherwise Three.js will not be able to interpolate the vertex data correctly.