| name | threejs-animation |
| allowed-tools | Read, Glob |
| description | Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations. |
Three.js Animation
Iron Law
Always call mixer.update(delta) in the animation loop — the AnimationMixer does not advance on its own, even when actions are playing.
Quick Start
import * as THREE from "three";
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
mesh.rotation.y += delta;
mesh.position.y = Math.sin(elapsed) * 0.5;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
Animation System Overview
Three.js animation system has three main components:
- AnimationClip - Container for keyframe data
- AnimationMixer - Plays animations on a root object
- AnimationAction - Controls playback of a clip
AnimationClip
Stores keyframe animation data.
const times = [0, 1, 2];
const values = [0, 1, 0];
const track = new THREE.NumberKeyframeTrack(
".position[y]",
times,
values,
);
const clip = new THREE.AnimationClip("bounce", 2, [track]);
KeyframeTrack Types
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);
new THREE.VectorKeyframeTrack(".position", times, [
0,
0,
0,
1,
2,
0,
0,
0,
0,
]);
const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));
new THREE.QuaternionKeyframeTrack(
".quaternion",
[0, 1],
[q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],
);
new THREE.ColorKeyframeTrack(".material.color", times, [
1,
0,
0,
0,
1,
0,
0,
0,
1,
]);
new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);
new THREE.StringKeyframeTrack(
".morphTargetInfluences[smile]",
[0, 1],
["0", "1"],
);
Interpolation Modes
const track = new THREE.VectorKeyframeTrack(".position", times, values);
track.setInterpolation(THREE.InterpolateLinear);
track.setInterpolation(THREE.InterpolateSmooth);
track.setInterpolation(THREE.InterpolateDiscrete);
AnimationMixer
Plays animations on an object and its descendants.
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
Mixer Events
mixer.addEventListener("finished", (e) => {
console.log("Animation finished:", e.action.getClip().name);
});
mixer.addEventListener("loop", (e) => {
console.log("Animation looped:", e.action.getClip().name);
});
AnimationAction
Controls playback of an animation clip.
const action = mixer.clipAction(clip);
action.play();
action.stop();
action.reset();
action.halt(fadeOutDuration);
action.isRunning();
action.isScheduled();
action.time = 0.5;
action.timeScale = 1;
action.paused = false;
action.weight = 1;
action.setEffectiveWeight(1);
action.loop = THREE.LoopRepeat;
action.loop = THREE.LoopOnce;
action.loop = THREE.LoopPingPong;
action.repetitions = 3;
action.clampWhenFinished = true;
action.blendMode = THREE.NormalAnimationBlendMode;
action.blendMode = THREE.AdditiveAnimationBlendMode;
Fade In/Out
action.reset().fadeIn(0.5).play();
action.fadeOut(0.5);
const action1 = mixer.clipAction(clip1);
const action2 = mixer.clipAction(clip2);
action1.play();
action1.crossFadeTo(action2, 0.5, true);
action2.play();
Loading GLTF Animations
Most common source of skeletal animations.
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
loader.load("model.glb", (gltf) => {
const model = gltf.scene;
scene.add(model);
const mixer = new THREE.AnimationMixer(model);
const clips = gltf.animations;
console.log(
"Available animations:",
clips.map((c) => c.name),
);
if (clips.length > 0) {
const action = mixer.clipAction(clips[0]);
action.play();
}
const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
if (walkClip) {
mixer.clipAction(walkClip).play();
}
window.mixer = mixer;
});
function animate() {
const delta = clock.getDelta();
if (window.mixer) window.mixer.update(delta);
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
Skeletal Animation
Skeleton and Bones
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;
skeleton.bones.forEach((bone) => {
console.log(bone.name, bone.position, bone.rotation);
});
const headBone = skeleton.bones.find((b) => b.name === "Head");
if (headBone) headBone.rotation.y = Math.PI / 4;
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);
Programmatic Bone Animation
function animate() {
const time = clock.getElapsedTime();
const headBone = skeleton.bones.find((b) => b.name === "Head");
if (headBone) {
headBone.rotation.y = Math.sin(time) * 0.3;
}
mixer.update(clock.getDelta());
}
Bone Attachments
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
if (handBone) handBone.add(weapon);
weapon.position.set(0, 0, 0.5);
weapon.rotation.set(0, Math.PI / 2, 0);
Morph Targets
Blend between different mesh shapes.
const geometry = mesh.geometry;
console.log("Morph attributes:", Object.keys(geometry.morphAttributes));
mesh.morphTargetInfluences;
mesh.morphTargetDictionary;
mesh.morphTargetInfluences[0] = 0.5;
const smileIndex = mesh.morphTargetDictionary["smile"];
mesh.morphTargetInfluences[smileIndex] = 1;
Animating Morph Targets
function animate() {
const t = clock.getElapsedTime();
mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
}
const track = new THREE.NumberKeyframeTrack(
".morphTargetInfluences[smile]",
[0, 0.5, 1],
[0, 1, 0],
);
const clip = new THREE.AnimationClip("smile", 1, [track]);
mixer.clipAction(clip).play();
Animation Blending
Mix multiple animations together.
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);
idleAction.play();
walkAction.play();
runAction.play();
idleAction.setEffectiveWeight(1);
walkAction.setEffectiveWeight(0);
runAction.setEffectiveWeight(0);
function updateAnimations(speed) {
if (speed < 0.1) {
idleAction.setEffectiveWeight(1);
walkAction.setEffectiveWeight(0);
runAction.setEffectiveWeight(0);
} else if (speed < 5) {
const t = speed / 5;
idleAction.setEffectiveWeight(1 - t);
walkAction.setEffectiveWeight(t);
runAction.setEffectiveWeight(0);
} else {
const t = Math.min((speed - 5) / 5, 1);
idleAction.setEffectiveWeight(0);
walkAction.setEffectiveWeight(1 - t);
runAction.setEffectiveWeight(t);
}
}
Additive Blending
const baseAction = mixer.clipAction(baseClip);
baseAction.play();
const additiveAction = mixer.clipAction(additiveClip);
additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;
additiveAction.play();
THREE.AnimationUtils.makeClipAdditive(additiveClip);
References
references/advanced-patterns.md — Smooth damping, spring physics, oscillation patterns, animation utilities, custom interpolation, and performance optimization
See Also
threejs-loaders - Loading animated GLTF models
threejs-fundamentals - Clock and animation loop
threejs-shaders - Vertex animation in shaders