Communitygithub.com

aelyraAI/threejs-animation

Animations Three.js – animation par images clés, animation squelettique, morph targets, mix d'animations. À utiliser pour animer des objets, jouer des animations GLTF, créer du mouvement procédural ou mélanger des animations.

Qu'est-ce que threejs-animation ?

Ce skill fournit des connaissances complètes sur le système d'animation de Three.js. Il couvre les trois composants principaux – AnimationClip, AnimationMixer et AnimationAction – et explique en détail comment créer des animations par images clés (KeyframeTrack), charger et lire des animations GLTF, animer des squelettes et des os, utiliser les morph targets pour le blend de formes, et mélanger plusieurs animations ensemble (y compris en mode additif). Le résumé inclut également des motifs d'animation procédurale (oscillation, ressort, damping lisse) et des conseils de performance pour optimiser l'utilisation des animations. Le skill est conçu pour les développeurs utilisant Three.js sur le web, que ce soit pour des jeux, des visualisations ou des applications interactives, et se base sur l'API officielle de Three.js.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/aelyraAI/aelyra-skills/tree/HEAD/threejs-animation

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Animation Three.js

Démarrage Rapide

import * as THREE from "three";

// Animation procédurale simple
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();

Aperçu du Système d'Animation

Le système d'animation Three.js comporte trois composants principaux :

  1. AnimationClip - Conteneur pour les données d'images clés
  2. AnimationMixer - Joue les animations sur un objet racine
  3. AnimationAction - Contrôle la lecture d'un clip

AnimationClip

Stocke les données d'animation par images clés.

// Créer un clip d'animation
const times = [0, 1, 2]; // Temps des images clés (secondes)
const values = [0, 1, 0]; // Valeurs à chaque image clé

const track = new THREE.NumberKeyframeTrack(
  ".position[y]", // Chemin de propriété
  times,
  values,
);

const clip = new THREE.AnimationClip("bounce", 2, [track]);

Types de KeyframeTrack

// Track numérique (valeur unique)
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);

// Track vectoriel (position, échelle)
new THREE.VectorKeyframeTrack(".position", times, [
  0,
  0,
  0, // t=0
  1,
  2,
  0, // t=1
  0,
  0,
  0, // t=2
]);

// Track Quaternion (rotation)
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],
);

// Track couleur
new THREE.ColorKeyframeTrack(".material.color", times, [
  1, 0, 0, // rouge
  0, 1, 0, // vert
  0, 0, 1, // bleu
]);

// Track booléen
new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);

// Track chaîne (pour morph targets)
new THREE.StringKeyframeTrack(
  ".morphTargetInfluences[smile]",
  [0, 1],
  ["0", "1"],
);

Modes d'Interpolation

const track = new THREE.VectorKeyframeTrack(".position", times, values);

// Interpolation
track.setInterpolation(THREE.InterpolateLinear); // Par défaut
track.setInterpolation(THREE.InterpolateSmooth); // Spline cubique
track.setInterpolation(THREE.InterpolateDiscrete); // Fonction échelon

AnimationMixer

Joue les animations sur un objet et ses descendants.

const mixer = new THREE.AnimationMixer(model);

// Créer une action à partir du clip
const action = mixer.clipAction(clip);
action.play();

// Mettre à jour dans la boucle d'animation
function animate() {
  const delta = clock.getDelta();
  mixer.update(delta); // Obligatoire !

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

Événements du Mixer

mixer.addEventListener("finished", (e) => {
  console.log("Animation terminée :", e.action.getClip().name);
});

mixer.addEventListener("loop", (e) => {
  console.log("Boucle d'animation :", e.action.getClip().name);
});

AnimationAction

Contrôle la lecture d'un clip d'animation.

const action = mixer.clipAction(clip);

// Contrôle de lecture
action.play();
action.stop();
action.reset();
action.halt(fadeOutDuration);

// État de lecture
action.isRunning();
action.isScheduled();

// Contrôle temporel
action.time = 0.5; // Temps courant
action.timeScale = 1; // Vitesse de lecture (négatif = inversé)
action.paused = false;

// Poids (pour le blending)
action.weight = 1; // 0-1, contribution à la pose finale
action.setEffectiveWeight(1);

// Modes de boucle
action.loop = THREE.LoopRepeat; // Par défaut : boucle infinie
action.loop = THREE.LoopOnce; // Joue une fois et s'arrête
action.loop = THREE.LoopPingPong; // Alterne avant/arrière
action.repetitions = 3; // Nombre de répétitions (Infini par défaut)

// Clamping
action.clampWhenFinished = true; // Conserver la dernière image à la fin

// Modes de blending
action.blendMode = THREE.NormalAnimationBlendMode;
action.blendMode = THREE.AdditiveAnimationBlendMode;

Fondu d'Entrée/Sortie

// Fondu d'entrée
action.reset().fadeIn(0.5).play();

// Fondu de sortie
action.fadeOut(0.5);

// Crossfade entre animations
const action1 = mixer.clipAction(clip1);
const action2 = mixer.clipAction(clip2);

action1.play();

// Plus tard, crossfade vers action2
action1.crossFadeTo(action2, 0.5, true);
action2.play();

Chargement d'Animations GLTF

Source la plus courante d'animations squelettiques.

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);

  // Créer le mixer
  const mixer = new THREE.AnimationMixer(model);

  // Obtenir tous les clips
  const clips = gltf.animations;
  console.log(
    "Animations disponibles :",
    clips.map((c) => c.name),
  );

  // Jouer la première animation
  if (clips.length > 0) {
    const action = mixer.clipAction(clips[0]);
    action.play();
  }

  // Jouer une animation spécifique par son nom
  const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
  if (walkClip) {
    mixer.clipAction(walkClip).play();
  }

  // Stocker le mixer pour la boucle de mise à jour
  window.mixer = mixer;
});

// Boucle d'animation
function animate() {
  const delta = clock.getDelta();
  if (window.mixer) window.mixer.update(delta);

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

Animation Squelettique

Squelette et Os

// Accéder au squelette depuis un maillage skin
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;

// Accéder aux os
skeleton.bones.forEach((bone) => {
  console.log(bone.name, bone.position, bone.rotation);
});

// Trouver un os spécifique par son nom
const headBone = skeleton.bones.find((b) => b.name === "Head");
if (headBone) headBone.rotation.y = Math.PI / 4; // Tourner la tête

// Aide visuelle du squelette
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);

Animation Programmatique des Os

function animate() {
  const time = clock.getElapsedTime();

  // Animer un os
  const headBone = skeleton.bones.find((b) => b.name === "Head");
  if (headBone) {
    headBone.rotation.y = Math.sin(time) * 0.3;
  }

  // Mettre à jour le mixer si des clips sont également joués
  mixer.update(clock.getDelta());
}

Attachements aux Os

// Attacher un objet à un os
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
if (handBone) handBone.add(weapon);

// Décaler l'attachement
weapon.position.set(0, 0, 0.5);
weapon.rotation.set(0, Math.PI / 2, 0);

Morph Targets

Transition entre différentes formes de maillage.

// Les morph targets sont stockés dans la géométrie
const geometry = mesh.geometry;
console.log("Attributs morph :", Object.keys(geometry.morphAttributes));

// Accéder aux influences des morph targets
mesh.morphTargetInfluences; // Tableau de poids
mesh.morphTargetDictionary; // Correspondance nom -> index

// Définir un morph target par index
mesh.morphTargetInfluences[0] = 0.5;

// Définir par nom
const smileIndex = mesh.morphTargetDictionary["smile"];
mesh.morphTargetInfluences[smileIndex] = 1;

Animer les Morph Targets

// Procédural
function animate() {
  const t = clock.getElapsedTime();
  mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
}

// Avec animation par images clés
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();

Mélange d'Animations

Mélanger plusieurs animations ensemble.

// Configuration des actions
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);

// Jouer toutes avec des poids différents
idleAction.play();
walkAction.play();
runAction.play();

// Définir les poids initiaux
idleAction.setEffectiveWeight(1);
walkAction.setEffectiveWeight(0);
runAction.setEffectiveWeight(0);

// Mélanger en fonction de la vitesse
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);
  }
}

Mélange Additif

// Pose de base
const baseAction = mixer.clipAction(baseClip);
baseAction.play();

// Couche additive (ex. respiration)
const additiveAction = mixer.clipAction(additiveClip);
additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;
additiveAction.play();

// Convertir un clip en additif
THREE.AnimationUtils.makeClipAdditive(additiveClip);

Utilitaires d'Animation

import * as THREE from "three";

// Trouver un clip par son nom
const clip = THREE.AnimationClip.findByName(clips, "Walk");

// Créer un sous-clip
const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);

// Convertir en additif
THREE.AnimationUtils.makeClipAdditive(clip);
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);

// Cloner un clip
const clone = clip.clone();

// Obtenir la durée du clip
clip.duration;

// Optimiser le clip (supprimer les images clés redondantes)
clip.optimize();

// Réinitialiser le clip à la première image
clip.resetDuration();

Motifs d'Animation Procédurale

Damping Lisse

// Suivi lisse/lerp
const target = new THREE.Vector3();
const current = new THREE.Vector3();
const velocity = new THREE.Vector3();

function smoothDamp(current, target, velocity, smoothTime, deltaTime) {
  const omega = 2 / smoothTime;
  const x = omega * deltaTime;
  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
  const change = current.clone().sub(target);
  const temp = velocity
    .clone()
    .add(change.clone().multiplyScalar(omega))
    .multiplyScalar(deltaTime);
  velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);
  return target.clone().add(change.add(temp).multiplyScalar(exp));
}

function animate() {
  current.copy(smoothDamp(current, target, velocity, 0.3, delta));
  mesh.position.copy(current);
}

Physique de Ressort

class Spring {
  constructor(stiffness = 100, damping = 10) {
    this.stiffness = stiffness;
    this.damping = damping;
    this.position = 0;
    this.velocity = 0;
    this.target = 0;
  }

  update(dt) {
    const force = -this.stiffness * (this.position - this.target);
    const dampingForce = -this.damping * this.velocity;
    this.velocity += (force + dampingForce) * dt;
    this.position += this.velocity * dt;
    return this.position;
  }
}

const spring = new Spring(100, 10);
spring.target = 1;

function animate() {
  mesh.position.y = spring.update(delta);
}

Oscillation

function animate() {
  const t = clock.getElapsedTime();

  // Onde sinusoïdale
  mesh.position.y = Math.sin(t * 2) * 0.5;

  // Rebond
  mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;

  // Mouvement circulaire
  mesh.position.x = Math.cos(t) * 2;
  mesh.position.z = Math.sin(t) * 2;

  // Figure en 8
  mesh.position.x = Math.sin(t) * 2;
  mesh.position.z = Math.sin(t * 2) * 1;
}

Conseils de Performance

  1. Partager les clips : Le même AnimationClip peut être utilisé sur plusieurs mixers
  2. Optimiser les clips : Appeler clip.optimize() pour supprimer les images clés redondantes
  3. Désactiver hors écran : Arrêter les mises à jour du mixer pour les objets invisibles
  4. Utiliser le LOD pour les animations : Des armatures plus simples pour les personnages éloignés
  5. Limiter les mixers actifs : Chaque mixer.update() a un coût
// Mettre en pause l'animation lorsqu'elle n'est pas visible
mesh.onBeforeRender = () => {
  action.paused = false;
};

mesh.onAfterRender = () => {
  // Vérifier si l'objet sera visible à l'image suivante
  if (!isInFrustum(mesh)) {
    action.paused = true;
  }
};

// Mettre en cache les clips
const clipCache = new Map();
function getClip(name) {
  if (!clipCache.has(name)) {
    clipCache.set(name, loadClip(name));
  }
  return clipCache.get(name);
}

Voir Aussi

  • threejs-loaders - Chargement de modèles GLTF animés
  • threejs-fundamentals - Horloge et boucle d'animation
  • threejs-shaders - Animation de vertex dans les shaders

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills associés