动画系统
three.js 有两套动画方式:模型自带的关键帧由 AnimationMixer 播放, 代码驱动的位移、旋转、透明度变化走时间步进。混用时要保证两者共用同一个节奏。
关键帧:AnimationMixer
glTF 里的动画会被解析成 AnimationClip,每个剪辑包含若干轨道。
AnimationMixer 绑定到一个根对象,按时间推进所有激活的剪辑。
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const mixers = [];
new GLTFLoader().load('/models/turbine.glb', function (gltf) {
const model = gltf.scene;
scene.add(model);
const mixer = new THREE.AnimationMixer(model);
mixers.push(mixer);
gltf.animations.forEach(function (clip) {
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat, Infinity);
action.clampWhenFinished = false;
action.play();
});
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
const dt = clock.getDelta();
mixers.forEach(function (m) { m.update(dt); });
renderer.render(scene, camera);
});
mixer.update(dt) 的参数是秒,不是毫秒。
用 Date.now() 差值直接传入会让动画快上千倍,
必须换成秒或使用 clock.getDelta()。
剪辑之间的切换
多个动作(行走、待机、旋转)并存时,直接 play()
会同时叠加。切换状态要用 crossFadeTo 做过渡,
并在结束后停掉旧动作。
// 承接上一段:gltf 已加载,mixer 已创建
const actions = {};
gltf.animations.forEach(function (clip) {
actions[clip.name] = mixer.clipAction(clip);
});
function switchTo(name, duration) {
const next = actions[name];
if (!next) return;
Object.keys(actions).forEach(function (key) {
const action = actions[key];
if (action !== next && action.isRunning()) {
action.crossFadeTo(next, duration || 0.4, false);
}
});
next.reset().setEffectiveWeight(1).fadeIn(duration || 0.4).play();
}
switchTo('rotate', 0.6);
先 reset() 再 fadeIn(),
否则切回一个播放完毕的动作时会停在最后一帧。
crossFadeTo 的第三个参数为 warp,用于对齐不同时长的动作。
| 需求 | 方式 | 要点 |
|---|---|---|
| 循环播放 | setLoop(LoopRepeat, Infinity) | 默认就是循环 |
| 只播一次 | setLoop(LoopOnce, 1) | 配合 clampWhenFinished = true |
| 往返播放 | LoopPingPong | 适合来回摆动的机械动作 |
| 动作切换 | crossFadeTo | 需要先 reset 目标动作 |
| 整体变速 | mixer.timeScale | 负数表示倒放 |
程序动画:自己算时间
没有关键帧的内容(旋转的风机、呼吸的标注、流动的材质)按时间累加计算。
用 clock.getDelta() 保证不同刷新率下速度一致。
const animClock = new THREE.Clock();
const rotors = [];
function addRotor(mesh) {
mesh.userData.phase = Math.random() * Math.PI * 2;
rotors.push(mesh);
}
renderer.setAnimationLoop(function () {
const dt = animClock.getDelta();
const elapsed = animClock.getElapsedTime();
rotors.forEach(function (rotor) {
rotor.rotation.y += dt * 0.8; // 匀速旋转
rotor.position.y = 40 + Math.sin(elapsed + rotor.userData.phase) * 1.5;
});
renderer.render(scene, camera);
});
一个时钟,两种动画。关键帧与程序动画都使用同一帧的
clock.getDelta(),否则模型动作与位移速度会各走各的,
在低帧率设备上尤其明显。
注意事项
- 切换页面或暂停时调用
mixer.stopAllAction(),避免后台继续消耗时间。 setAnimationLoop在 XR 会话中同样工作,比requestAnimationFrame更通用。- 模型克隆后动画不能共享同一个 mixer,每个实例需要独立的
AnimationMixer。 - 剪辑名称来自建模软件,可能带有命名空间前缀,取用前先打印
gltf.animations。 - 大量同时播放的动画会带来明显 CPU 开销,远处的实例可以停止更新。
经纬志(2026)。《动画系统》。https://www.jingweizhi.com/three/tutorial/animation.html(访问日期 2026-11-03)。