Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
Add this skill
npx mdskills install CloudAI-X/threejs-animationComprehensive reference covering keyframe, skeletal, and morph target animation with extensive examples
1---2name: threejs-animation3description: Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.4---56# Three.js Animation78## Quick Start910```javascript11import * as THREE from "three";1213// Simple procedural animation14const clock = new THREE.Clock();1516function animate() {17 const delta = clock.getDelta();18 const elapsed = clock.getElapsedTime();1920 mesh.rotation.y += delta;21 mesh.position.y = Math.sin(elapsed) * 0.5;2223 requestAnimationFrame(animate);24 renderer.render(scene, camera);25}26animate();27```2829## Animation System Overview3031Three.js animation system has three main components:32331. **AnimationClip** - Container for keyframe data342. **AnimationMixer** - Plays animations on a root object353. **AnimationAction** - Controls playback of a clip3637## AnimationClip3839Stores keyframe animation data.4041```javascript42// Create animation clip43const times = [0, 1, 2]; // Keyframe times (seconds)44const values = [0, 1, 0]; // Values at each keyframe4546const track = new THREE.NumberKeyframeTrack(47 ".position[y]", // Property path48 times,49 values,50);5152const clip = new THREE.AnimationClip("bounce", 2, [track]);53```5455### KeyframeTrack Types5657```javascript58// Number track (single value)59new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);60new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);6162// Vector track (position, scale)63new THREE.VectorKeyframeTrack(".position", times, [64 0,65 0,66 0, // t=067 1,68 2,69 0, // t=170 0,71 0,72 0, // t=273]);7475// Quaternion track (rotation)76const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));77const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));78new THREE.QuaternionKeyframeTrack(79 ".quaternion",80 [0, 1],81 [q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],82);8384// Color track85new THREE.ColorKeyframeTrack(".material.color", times, [86 1,87 0,88 0, // red89 0,90 1,91 0, // green92 0,93 0,94 1, // blue95]);9697// Boolean track98new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);99100// String track (for morph targets)101new THREE.StringKeyframeTrack(102 ".morphTargetInfluences[smile]",103 [0, 1],104 ["0", "1"],105);106```107108### Interpolation Modes109110```javascript111const track = new THREE.VectorKeyframeTrack(".position", times, values);112113// Interpolation114track.setInterpolation(THREE.InterpolateLinear); // Default115track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline116track.setInterpolation(THREE.InterpolateDiscrete); // Step function117```118119## AnimationMixer120121Plays animations on an object and its descendants.122123```javascript124const mixer = new THREE.AnimationMixer(model);125126// Create action from clip127const action = mixer.clipAction(clip);128action.play();129130// Update in animation loop131function animate() {132 const delta = clock.getDelta();133 mixer.update(delta); // Required!134135 requestAnimationFrame(animate);136 renderer.render(scene, camera);137}138```139140### Mixer Events141142```javascript143mixer.addEventListener("finished", (e) => {144 console.log("Animation finished:", e.action.getClip().name);145});146147mixer.addEventListener("loop", (e) => {148 console.log("Animation looped:", e.action.getClip().name);149});150```151152## AnimationAction153154Controls playback of an animation clip.155156```javascript157const action = mixer.clipAction(clip);158159// Playback control160action.play();161action.stop();162action.reset();163action.halt(fadeOutDuration);164165// Playback state166action.isRunning();167action.isScheduled();168169// Time control170action.time = 0.5; // Current time171action.timeScale = 1; // Playback speed (negative = reverse)172action.paused = false;173174// Weight (for blending)175action.weight = 1; // 0-1, contribution to final pose176action.setEffectiveWeight(1);177178// Loop modes179action.loop = THREE.LoopRepeat; // Default: loop forever180action.loop = THREE.LoopOnce; // Play once and stop181action.loop = THREE.LoopPingPong; // Alternate forward/backward182action.repetitions = 3; // Number of loops (Infinity default)183184// Clamping185action.clampWhenFinished = true; // Hold last frame when done186187// Blending188action.blendMode = THREE.NormalAnimationBlendMode;189action.blendMode = THREE.AdditiveAnimationBlendMode;190```191192### Fade In/Out193194```javascript195// Fade in196action.reset().fadeIn(0.5).play();197198// Fade out199action.fadeOut(0.5);200201// Crossfade between animations202const action1 = mixer.clipAction(clip1);203const action2 = mixer.clipAction(clip2);204205action1.play();206207// Later, crossfade to action2208action1.crossFadeTo(action2, 0.5, true);209action2.play();210```211212## Loading GLTF Animations213214Most common source of skeletal animations.215216```javascript217import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";218219const loader = new GLTFLoader();220loader.load("model.glb", (gltf) => {221 const model = gltf.scene;222 scene.add(model);223224 // Create mixer225 const mixer = new THREE.AnimationMixer(model);226227 // Get all clips228 const clips = gltf.animations;229 console.log(230 "Available animations:",231 clips.map((c) => c.name),232 );233234 // Play first animation235 if (clips.length > 0) {236 const action = mixer.clipAction(clips[0]);237 action.play();238 }239240 // Play specific animation by name241 const walkClip = THREE.AnimationClip.findByName(clips, "Walk");242 if (walkClip) {243 mixer.clipAction(walkClip).play();244 }245246 // Store mixer for update loop247 window.mixer = mixer;248});249250// Animation loop251function animate() {252 const delta = clock.getDelta();253 if (window.mixer) window.mixer.update(delta);254255 requestAnimationFrame(animate);256 renderer.render(scene, camera);257}258```259260## Skeletal Animation261262### Skeleton and Bones263264```javascript265// Access skeleton from skinned mesh266const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");267const skeleton = skinnedMesh.skeleton;268269// Access bones270skeleton.bones.forEach((bone) => {271 console.log(bone.name, bone.position, bone.rotation);272});273274// Find specific bone by name275const headBone = skeleton.bones.find((b) => b.name === "Head");276if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head277278// Skeleton helper279const helper = new THREE.SkeletonHelper(model);280scene.add(helper);281```282283### Programmatic Bone Animation284285```javascript286function animate() {287 const time = clock.getElapsedTime();288289 // Animate bone290 const headBone = skeleton.bones.find((b) => b.name === "Head");291 if (headBone) {292 headBone.rotation.y = Math.sin(time) * 0.3;293 }294295 // Update mixer if also playing clips296 mixer.update(clock.getDelta());297}298```299300### Bone Attachments301302```javascript303// Attach object to bone304const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);305const handBone = skeleton.bones.find((b) => b.name === "RightHand");306if (handBone) handBone.add(weapon);307308// Offset attachment309weapon.position.set(0, 0, 0.5);310weapon.rotation.set(0, Math.PI / 2, 0);311```312313## Morph Targets314315Blend between different mesh shapes.316317```javascript318// Morph targets are stored in geometry319const geometry = mesh.geometry;320console.log("Morph attributes:", Object.keys(geometry.morphAttributes));321322// Access morph target influences323mesh.morphTargetInfluences; // Array of weights324mesh.morphTargetDictionary; // Name -> index mapping325326// Set morph target by index327mesh.morphTargetInfluences[0] = 0.5;328329// Set by name330const smileIndex = mesh.morphTargetDictionary["smile"];331mesh.morphTargetInfluences[smileIndex] = 1;332```333334### Animating Morph Targets335336```javascript337// Procedural338function animate() {339 const t = clock.getElapsedTime();340 mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;341}342343// With keyframe animation344const track = new THREE.NumberKeyframeTrack(345 ".morphTargetInfluences[smile]",346 [0, 0.5, 1],347 [0, 1, 0],348);349const clip = new THREE.AnimationClip("smile", 1, [track]);350mixer.clipAction(clip).play();351```352353## Animation Blending354355Mix multiple animations together.356357```javascript358// Setup actions359const idleAction = mixer.clipAction(idleClip);360const walkAction = mixer.clipAction(walkClip);361const runAction = mixer.clipAction(runClip);362363// Play all with different weights364idleAction.play();365walkAction.play();366runAction.play();367368// Set initial weights369idleAction.setEffectiveWeight(1);370walkAction.setEffectiveWeight(0);371runAction.setEffectiveWeight(0);372373// Blend based on speed374function updateAnimations(speed) {375 if (speed < 0.1) {376 idleAction.setEffectiveWeight(1);377 walkAction.setEffectiveWeight(0);378 runAction.setEffectiveWeight(0);379 } else if (speed < 5) {380 const t = speed / 5;381 idleAction.setEffectiveWeight(1 - t);382 walkAction.setEffectiveWeight(t);383 runAction.setEffectiveWeight(0);384 } else {385 const t = Math.min((speed - 5) / 5, 1);386 idleAction.setEffectiveWeight(0);387 walkAction.setEffectiveWeight(1 - t);388 runAction.setEffectiveWeight(t);389 }390}391```392393### Additive Blending394395```javascript396// Base pose397const baseAction = mixer.clipAction(baseClip);398baseAction.play();399400// Additive layer (e.g., breathing)401const additiveAction = mixer.clipAction(additiveClip);402additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;403additiveAction.play();404405// Convert clip to additive406THREE.AnimationUtils.makeClipAdditive(additiveClip);407```408409## Animation Utilities410411```javascript412import * as THREE from "three";413414// Find clip by name415const clip = THREE.AnimationClip.findByName(clips, "Walk");416417// Create subclip418const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);419420// Convert to additive421THREE.AnimationUtils.makeClipAdditive(clip);422THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);423424// Clone clip425const clone = clip.clone();426427// Get clip duration428clip.duration;429430// Optimize clip (remove redundant keyframes)431clip.optimize();432433// Reset clip to first frame434clip.resetDuration();435```436437## Procedural Animation Patterns438439### Smooth Damping440441```javascript442// Smooth follow/lerp443const target = new THREE.Vector3();444const current = new THREE.Vector3();445const velocity = new THREE.Vector3();446447function smoothDamp(current, target, velocity, smoothTime, deltaTime) {448 const omega = 2 / smoothTime;449 const x = omega * deltaTime;450 const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);451 const change = current.clone().sub(target);452 const temp = velocity453 .clone()454 .add(change.clone().multiplyScalar(omega))455 .multiplyScalar(deltaTime);456 velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);457 return target.clone().add(change.add(temp).multiplyScalar(exp));458}459460function animate() {461 current.copy(smoothDamp(current, target, velocity, 0.3, delta));462 mesh.position.copy(current);463}464```465466### Spring Physics467468```javascript469class Spring {470 constructor(stiffness = 100, damping = 10) {471 this.stiffness = stiffness;472 this.damping = damping;473 this.position = 0;474 this.velocity = 0;475 this.target = 0;476 }477478 update(dt) {479 const force = -this.stiffness * (this.position - this.target);480 const dampingForce = -this.damping * this.velocity;481 this.velocity += (force + dampingForce) * dt;482 this.position += this.velocity * dt;483 return this.position;484 }485}486487const spring = new Spring(100, 10);488spring.target = 1;489490function animate() {491 mesh.position.y = spring.update(delta);492}493```494495### Oscillation496497```javascript498function animate() {499 const t = clock.getElapsedTime();500501 // Sine wave502 mesh.position.y = Math.sin(t * 2) * 0.5;503504 // Bouncing505 mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;506507 // Circular motion508 mesh.position.x = Math.cos(t) * 2;509 mesh.position.z = Math.sin(t) * 2;510511 // Figure 8512 mesh.position.x = Math.sin(t) * 2;513 mesh.position.z = Math.sin(t * 2) * 1;514}515```516517## Performance Tips5185191. **Share clips**: Same AnimationClip can be used on multiple mixers5202. **Optimize clips**: Call `clip.optimize()` to remove redundant keyframes5213. **Disable when off-screen**: Stop mixer updates for invisible objects5224. **Use LOD for animations**: Simpler rigs for distant characters5235. **Limit active mixers**: Each mixer.update() has a cost524525```javascript526// Pause animation when not visible527mesh.onBeforeRender = () => {528 action.paused = false;529};530531mesh.onAfterRender = () => {532 // Check if will be visible next frame533 if (!isInFrustum(mesh)) {534 action.paused = true;535 }536};537538// Cache clips539const clipCache = new Map();540function getClip(name) {541 if (!clipCache.has(name)) {542 clipCache.set(name, loadClip(name));543 }544 return clipCache.get(name);545}546```547548## See Also549550- `threejs-loaders` - Loading animated GLTF models551- `threejs-fundamentals` - Clock and animation loop552- `threejs-shaders` - Vertex animation in shaders553
Full transparency — inspect the skill content before installing.