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 :
- AnimationClip - Conteneur pour les données d'images clés
- AnimationMixer - Joue les animations sur un objet racine
- 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
- Partager les clips : Le même AnimationClip peut être utilisé sur plusieurs mixers
- Optimiser les clips : Appeler
clip.optimize()pour supprimer les images clés redondantes - Désactiver hors écran : Arrêter les mises à jour du mixer pour les objets invisibles
- Utiliser le LOD pour les animations : Des armatures plus simples pour les personnages éloignés
- 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ésthreejs-fundamentals- Horloge et boucle d'animationthreejs-shaders- Animation de vertex dans les shaders