threejs-animation
Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
By cloudai-x · 14,467 installs
npx skills add cloudai-x/threejs-skills --skill threejs-animation
Source repository · Upstream listing
Three.js Animation
Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.
Quick Start
How the System Fits Together
AnimationClip : keyframe data — tracks of times and values, no playback state.
AnimationMixer : plays clips on one root object and all its descendants.
AnimationAction : playback control for one clip on one mixer.
Each frame, mixer.update(delta) samples the active clips and writes the values into ordinary object properties. The system runs on the CPU and never touches the renderer, so it drives models the same way under either renderer.
AnimationClip and Keyframe Tracks
Track names follow nodeName.objectName[objectIndex].propertyName[propertyIndex] ; all parts except the property are optional. .position binds the root's position, .material.opacity a material property, .position[y] one component, .morphTargetInfluences[smile] one morph target, Hips.position a named descendant — that is how bones are addressed in a skinned model. Object names may be material , materials , bones or map .
Interpolation
AnimationMixer
AnimationAction
Fades and Crossfades
Playing glTF Animations
Skeletal Animation
Procedural bone control runs after mixer.update() , or a clip that animates the same bone overwrites it:
Bones are Object3Ds, so attaching means parenting:
Morph Targets
Adding a morph target by hand:
Driving morph targets:
Blending Clips by Weight
Additive Animation
Clip Utilities
Procedural Motion
Performance
The same AnimationClip can feed any number of mixers — share one clip set across characters instead of copying it.
Call mixer.update() only for characters that are visible or about to be; an un updated mixer simply leaves the last pose in place.
A SkinnedMesh computes its bounding sphere once, the first time culling or raycasting needs it, so a clip that moves it far from that pose can get it culled at the screen edge: after mixer.update() call mixer.getRoot().updateMatrixWorld() then skinnedMesh.computeBoundingSphere() (a CPU pass over every vertex) or set skinnedMesh.frustumCulled = false .
WebGPU and TSL
Everything above — mixer, actions, bone transforms, morph weights — is computed on the CPU and drives a model under WebGPURenderer unchanged. TSL adds animation on the GPU: time (elapsed seconds) and deltaTime (seconds since the previous frame) are float uniforms the renderer refreshes every frame, and the oscillators oscSine , oscSquare , oscTriangle and oscSawtooth return 0 to 1 waves with a one second period from a time node, defaulting to time ( oscSine(time.mul(0.5)) takes two seconds). Per vertex motion needs a node material and a subdivided mesh.
On a skinned mesh, build displacement on positionLocal : skinning, morph targets, batching and instancing are applied to it before positionNode runs, so the mixer driven pose survives. positionGeometry is the raw rest pose attribute — never the base for displacement on an animated mesh.
Common Mistakes
const clock = new THREE.Clock(); → const timer = new THREE.Timer(); timer.connect(document); : the Clock class is deprecated in r183 and Timer is its replacement.
const elapsed = clock.getElapsedTime(); const delta = clock.getDelta(); → timer.update(); const delta = timer.getDelta(); const elapsed = timer.getElapsed(); : getElapsedTime() calls getDelta() internally, so the following delta read is about zero and the mixer stalls.
requestAnimationFrame(animate); → renderer.setAnimationLoop(animate); : both renderers' docs advise it over a manual loop, and it hands frames to the WebXR session when one starts.
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; → import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; : the examples/jsm specifier does not resolve under the standard import map.
new THREE.NumberKeyframeTrack(".material.opacity", [0, 1, 2], [1, 0]) → new THREE.NumberKeyframeTrack(".material.opacity", [0, 1], [1, 0]) : values must hold times.length × valueSize entries; with a fractional ratio play() never returns, and any other mismatch samples wrong values.
new THREE.StringKeyframeTrack(".morphTargetInfluences[smile]", [0, 1], ["0", "1"]) → new THREE.NumberKeyframeTrack(".morphTargetInfluences[smile]", [0, 1], [0, 1]) : morph influences are numbers, and a string track writes "0"/"1" strings into the array.
headBone.rotation.y = Math.sin(t) 0.3; mixer.update(delta); → mixer.update(delta); headBone.rotation.y = Math.sin(t) 0.3; : update() writes the clip's pose over any value set before it.
clip.resetDuration() → action.reset() : resetDuration() only recomputes the clip's duration from its longest track, while reset() rewinds the action to time 0 and re enables it after a one shot finished.
material.positionNode = positionGeometry.add(offset) → material.positionNode = positionLocal.add(offset) : positionGeometry is the raw attribute, so building on it drops skinning, morph targets, batching and instancing.
const uTime = uniform(0); uTime.value = timer.getElapsed(); const wave = uTime.mul(2).sin(); → const wave = time.mul(2).sin(); : the renderer already advances the TSL time node every frame, so a Timer fed uniform is redundant per frame JavaScript.
const model2 = model.clone(); → const model2 = SkeletonUtils.clone(model); : cloning a skinned model shares its skeleton between the copies; each copy needs its own.
import { clone } from "three/addons/utils/SkeletonUtils.js"; → import as SkeletonUtils from "three/addons/utils/SkeletonUtils.js"; : the namespace keeps the call as SkeletonUtils.clone(model), which cannot be mistaken for the skeleton sharing model.clone().
new Promise((resolve, reject) = loader.load(url, resolve, undefined, reject)) → await loader.loadAsync(url) : loaders already expose the promise form, and loadAsync takes the progress callback as its second argument.
See Also
threejs fundamentals — render loop, Timer, renderer and scene setup
threejs loaders — loading glTF models and their animation clips
threejs shaders — GPU vertex displacement with TSL or ShaderMaterial