threejs-animation

v2026.09.24

Implement and debug Three.js motion with AnimationMixer, clips and actions, GLTF animation playback, skeletal rigs, morph targets, cross-fades, and frame-rate-independent procedural animation. Use when playing or blending model animations, driving bones or morphs, creating timeline motion, or fixing animation lifecycle and performance issues. Triggers on: Three.js animation, AnimationMixer, AnimationAction, AnimationClip, GLTF animation, skeletal animation, bones, morph targets, crossfade, procedural motion.

GitHub
Install command
npx skhub add akillness/threejs-animation
Markdown
SKILL.md

Three.js Animation

Use this skill for the animation implementation itself: clip selection, mixer ownership, blending, skeleton/morph driving, and time-step correctness. Use threejs-loaders to load the model and web-game-development when game-state or combat design—not the animation API—is the primary problem.

When to use this skill

  • Play, pause, seek, loop, or blend AnimationClip data through AnimationMixer
  • Attach GLTF clips to a loaded scene and clean mixers up with the model lifecycle
  • Animate skeletal bones, morph target influences, or lightweight procedural movement
  • Fix actions that do not play, snap during transitions, speed up with frame rate, or leak

Instructions

Step 1: Identify the animation owner and clock

  1. Inspect the model's gltf.animations, root object, and current render-loop owner.
  2. Create one mixer per animated root and advance it once per frame with elapsed delta time, not a fixed per-frame amount.
  3. Preserve action references by semantic name; do not index an exported clip array blindly because asset revisions can reorder clips.
const mixer = new THREE.AnimationMixer(gltf.scene);
const clips = new Map(gltf.animations.map((clip) => [clip.name, clip]));
const idle = mixer.clipAction(clips.get("Idle"));
idle.reset().fadeIn(0.2).play();

const clock = new THREE.Clock();
function update() {
  mixer.update(clock.getDelta());
  renderer.render(scene, camera);
  requestAnimationFrame(update);
}
update();

Step 2: Transition actions without visual discontinuities

Set loop and clamp behavior deliberately, reset an incoming action before playing it, and use cross-fades only when clips share a compatible pose/rig.

function transition(from, to, duration = 0.2) {
  to.reset().setEffectiveWeight(1).setEffectiveTimeScale(1).play();
  from.crossFadeTo(to, duration, false);
}

For one-shot actions, listen for the mixer's finished event and transition back to a known idle action. Remove listeners during feature teardown.

Step 3: Choose the narrowest animation mechanism

NeedDefault mechanism
Authored transform/property keysAnimationClip + keyframe tracks
Imported character animationAnimationMixer + clipAction
Per-frame idle motion or UI responseDelta-time procedural update
Facial expressions or mesh deformationmorphTargetInfluences
Runtime bone adjustmentBone transform after mixer update
Layered additive poseAdditive clip only after validating reference pose

Apply procedural bone offsets after mixer.update(delta) so the authored clip does not overwrite them. Keep expensive IK, physics, or retargeting behind a measured budget.

Step 4: Dispose animation state with the feature

Call mixer.stopAllAction() and mixer.uncacheRoot(root) when the animated root is permanently removed. Do not uncache a root shared by another scene or instance.

Step 5: Verify animation behavior

  • Test at a low and high frame rate; duration and travel must remain consistent.
  • Test transition interruption, one-shot completion, and rapid action changes.
  • Confirm named clips exist and report a useful error when an expected asset export changes.
  • Verify cleanup by mounting/unmounting the scene repeatedly without growing listeners or retained mixer actions.

Examples

Animate a morph target

const face = gltf.scene.getObjectByName("Face");
face.morphTargetInfluences[0] = 0.75;

Validate that the mesh has morph targets and use a named index map from the asset rather than a magic number when the export includes multiple expressions.

Drive a simple procedural bob

const elapsed = clock.getElapsedTime();
mesh.position.y = baseY + Math.sin(elapsed * 2) * 0.1;

Store baseY separately. Incremental position changes accumulate drift and make mixing with authored animation difficult.

Best practices

  1. Update mixers exactly once per frame with delta seconds.
  2. Prefer exported clips for authored movement; procedural offsets should be additive and localized.
  3. Name clips at export time and validate names at load time.
  4. Avoid cloning a rig or mixer without checking shared skeleton and material ownership.
  5. Profile skinned meshes and morph targets on target hardware before multiplying them.

References

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

.agent-skills/threejs-animation

Default branch

main

Latest commit

f579bfe

Tree SHA

34a09b3