| name | threejs-animation |
| description | Three.js animation system — AnimationMixer, AnimationClip, AnimationAction, keyframe tracks, skeletal animation, morph targets, animation blending, and procedural motion. Use when animating objects, playing GLTF animations, creating procedural motion, blending animations, or working with bones and morph targets. Adapted from CloudAI-X/threejs-skills (MIT). |
| license | MIT |
| compatibility | Portable reference skill for agents that support markdown skills or prompt files. Works best alongside project Three.js source files with animation clips loaded from GLTF. |
| disable-model-invocation | true |
| metadata | {"owner":"game-delivery","version":"2.0.0","language":"en-GB","category":"web-rendering","upstream_references":["https://github.com/CloudAI-X/threejs-skills (MIT — see NOTICE.md)"],"tags":["three-js","animation","animation-mixer","skeletal","morph-targets","keyframes","procedural-animation"],"intents":["clip-playback","skeletal-animation","morph-targets","animation-blending","procedural-motion"],"output_types":["code-example","api-reference","animation-plan"]} |
Three.js Animation
Quick Start
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const mixer = null;
const clock = new THREE.Clock();
new GLTFLoader().load('character.glb', gltf => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
});
function animate() {
requestAnimationFrame(animate);
mixer?.update(clock.getDelta());
renderer.render(scene, camera);
}
animate();
Core Classes
AnimationClip
A named container of keyframe data for one animation (e.g., "walk", "jump").
const posTrack = new THREE.VectorKeyframeTrack(
'.position',
[0, 1, 2],
[0,0,0, 0,2,0, 0,0,0],
);
const clip = new THREE.AnimationClip('bounce', 2, [posTrack]);
THREE.AnimationUtils.makeClipAdditive(clip);
Keyframe Track Types
| Track class | Property type | Example path |
|---|
NumberKeyframeTrack | Float | .morphTargetInfluences[0] |
VectorKeyframeTrack | Vector3 | .position, .scale |
QuaternionKeyframeTrack | Quaternion | .quaternion |
ColorKeyframeTrack | Color | .material.color |
BooleanKeyframeTrack | Boolean | .visible |
StringKeyframeTrack | String | (rarely used) |
const rotTrack = new THREE.QuaternionKeyframeTrack(
'.quaternion',
[0, 1, 2],
[
0, 0, 0, 1,
0, Math.sin(Math.PI/4), 0, Math.cos(Math.PI/4),
0, 0, 0, 1,
],
);
Interpolation Modes
import { InterpolateDiscrete, InterpolateLinear, InterpolateSmooth } from 'three';
track.setInterpolation(InterpolateLinear);
track.setInterpolation(InterpolateSmooth);
track.setInterpolation(InterpolateDiscrete);
AnimationMixer
Controls playback of all clips for one object (or hierarchy).
const mixer = new THREE.AnimationMixer(rootObject);
function animate() {
requestAnimationFrame(animate);
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
mixer.addEventListener('finished', event => {
console.log('Finished:', event.action.getClip().name);
});
mixer.removeEventListener('finished', handler);
AnimationAction
Controls a single clip's playback state.
const action = mixer.clipAction(clip, optionalRootObject);
action.play();
action.pause();
action.stop();
action.reset();
action.timeScale = 1;
action.time = 0.5;
action.setDuration(2);
action.loop = THREE.LoopRepeat;
action.loop = THREE.LoopOnce;
action.loop = THREE.LoopPingPong;
action.repetitions = 3;
action.clampWhenFinished = true;
action.weight = 1.0;
action.enabled = true;
action.fadeIn();
action.();
action.(otherAction, , );
Skeletal Animation
Accessing Bones
loader.load('character.glb', gltf => {
const model = gltf.scene;
let skeleton;
model.traverse(child => {
if (child.isSkinnedMesh) {
skeleton = child.skeleton;
}
});
const spineBone = skeleton.getBoneByName('Spine');
spineBone.rotation.z = Math.sin(Date.now() * 0.001) * 0.2;
});
Attaching Objects to Bones
const handBone = skeleton.getBoneByName('RightHand');
const weapon = new THREE.Mesh(weaponGeo, weaponMat);
handBone.add(weapon);
weapon.position.set(0, 0.1, 0);
Skeleton Helper
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);
Morph Targets
Shape blending between different mesh states (facial expressions, deformation).
loader.load('face.glb', gltf => {
const mesh = gltf.scene.children[0];
console.log(mesh.morphTargetDictionary);
mesh.morphTargetInfluences[0] = 0.5;
mesh.morphTargetInfluences[1] = 1.0;
const idx = mesh.morphTargetDictionary['smile'];
mesh.morphTargetInfluences[idx] = 0.8;
});
function animate() {
const t = Math.sin(clock.getElapsedTime()) * 0.5 + 0.5;
mesh.morphTargetInfluences[0] = t;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
Animation Blending
Weight-Based Blending
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);
[idleAction, walkAction, runAction].forEach(a => {
a.play();
a.weight = 0;
});
idleAction.weight = 1;
function setMovementBlend(speed) {
idleAction.weight = Math.max(0, 1 - speed * 2);
walkAction.weight = speed < 0.5
? speed * 2
: Math.max(0, 1 - (speed - 0.5) * 2);
runAction.weight = Math.max(0, (speed - 0.5) * 2);
}
Additive Blending
Layer a secondary motion (e.g., breathing) on top of a base animation.
THREE.AnimationUtils.makeClipAdditive(breatheClip);
const baseAction = mixer.clipAction(walkClip);
const addAction = mixer.clipAction(breatheClip);
addAction.blendMode = THREE.AdditiveAnimationBlendMode;
baseAction.play();
addAction.play();
Crossfade Between Clips
function crossfade(from, to, duration = 0.3) {
to.reset().play();
from.crossFadeTo(to, duration, false);
}
crossfade(idleAction, jumpAction, 0.2);
Procedural Animation
Smooth Damping (Character Movement)
const currentVelocity = new THREE.Vector3();
const targetPosition = new THREE.Vector3();
function smoothDamp(current, target, velocity, smoothTime, delta) {
const omega = 2 / smoothTime;
const x = omega * delta;
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
const diff = current.clone().sub(target);
const temp = velocity.clone().add(diff.clone().multiplyScalar(omega)).multiplyScalar(delta);
velocity.copy(temp.clone().negate().multiplyScalar(omega).add(velocity));
return target.clone().add(diff.clone().add(temp).multiplyScalar(exp));
}
function animate() {
const delta = clock.getDelta();
player.position.copy(
smoothDamp(player., targetPosition, currentVelocity, , delta)
);
(animate);
renderer.(scene, camera);
}
Spring Physics
class Spring {
constructor(stiffness = 200, damping = 20) {
this.stiffness = stiffness;
this.damping = damping;
this.velocity = 0;
this.position = 0;
this.target = 0;
}
update(delta) {
const force = (this.target - this.position) * this.stiffness;
const dampForce = this.velocity * this.damping;
this.velocity += (force - dampForce) * delta;
this.position += this.velocity * delta;
return this.position;
}
}
const bounceSpring = new Spring(150, 15);
function animate() {
const delta = clock.();
bounceSpring. = isJumping ? : ;
mesh.. = bounceSpring.(delta);
(animate);
renderer.(scene, camera);
}
Oscillation and Circular Motion
function animate() {
const t = clock.getElapsedTime();
mesh.position.y = Math.sin(t * 2) * 0.5;
mesh.position.x = Math.cos(t) * 3;
mesh.position.z = Math.sin(t) * 3;
mesh.rotation.y = -t + Math.PI / 2;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
Common Patterns
Cache Mixers for Multiple Characters
const mixers = new Map();
function addCharacter(model, animations) {
const mixer = new THREE.AnimationMixer(model);
mixers.set(model.uuid, mixer);
const actions = {};
animations.forEach(clip => {
actions[clip.name] = mixer.clipAction(clip);
});
return actions;
}
function update(delta) {
mixers.forEach(m => m.update(delta));
}
Disable Mixer for Off-Screen Objects
function animate() {
const delta = clock.getDelta();
mixers.forEach((mixer, uuid) => {
const obj = scene.getObjectByProperty('uuid', uuid);
if (obj && obj.visible) {
mixer.update(delta);
}
});
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
Performance Tips
- Always pass delta — frame-rate-independent playback
- Reuse
clipAction results — mixer caches them; avoid calling clipAction every frame
- Disable mixer when off-screen — save CPU for non-visible characters
- Limit simultaneous blending — more than 4–5 concurrent blended actions gets expensive
- Optimise clips — remove redundant keyframes with
THREE.AnimationUtils.subclip
const subClip = THREE.AnimationUtils.subclip(fullClip, 'run', 20, 40, 30);
See Also
threejs-loaders — loading GLTF files with animation clips
threejs-fundamentals — scene setup and Clock usage
threejs-shaders — vertex shader-based animation