Chapter 06

动画系统

AnimationMixer 关键帧驱动、骨骼蒙皮动画、GSAP 补间动画集成,构建角色 idle/walk/run 状态机

1. Three.js 动画系统核心概念

2. AnimationMixer + AnimationClip

import * as THREE from 'three';

// ── 手动创建关键帧动画 ──

// 位置关键帧轨道:属性名 '.position',时间 [0, 1, 2],值 [x,y,z] * 3
const posTrack = new THREE.VectorKeyframeTrack(
  '.position',
  [0, 0.5, 1, 1.5, 2],              // 关键帧时间点(秒)
  [0,0,0, 0,2,0, 2,2,0, 2,0,0, 0,0,0]  // 对应位置值(每3个一组)
);

// 旋转关键帧轨道(四元数)
const q0 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));
const rotTrack = new THREE.QuaternionKeyframeTrack(
  '.quaternion',
  [0, 1, 2],
  [...q0.toArray(), ...q1.toArray(), ...q0.toArray()]
);

// 缩放关键帧
const scaleTrack = new THREE.VectorKeyframeTrack(
  '.scale',
  [0, 1, 2],
  [1,1,1,  1.5,1.5,1.5,  1,1,1]
);

// 组合成 AnimationClip
const clip = new THREE.AnimationClip(
  'bounce',   // 动画名称
  2,           // 时长(秒),-1 = 自动从轨道推断
  [posTrack, rotTrack, scaleTrack]
);

// 创建 Mixer 并播放
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.loop = THREE.LoopRepeat;  // LoopOnce / LoopRepeat / LoopPingPong
action.play();

// ⚠️ 动画循环中必须更新 mixer
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  mixer.update(delta); // 推进动画时间
  renderer.render(scene, camera);
}

3. 骨骼动画 SkeletonHelper

骨骼动画用于角色/生物动画。骨骼(Bone)形成层级树,蒙皮(Skinned Mesh)的顶点跟随骨骼变形。通常从 GLTF 模型中加载,不需要手动创建:

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
const gltf = await new Promise((res, rej) =>
  loader.load('/models/character.glb', res, undefined, rej)
);

const model = gltf.scene;
scene.add(model);

// 显示骨骼辅助器(调试用)
const skeletonHelper = new THREE.SkeletonHelper(model);
scene.add(skeletonHelper);

// 播放模型内置的动画
const mixer = new THREE.AnimationMixer(model);
console.log('可用动画:', gltf.animations.map(a => a.name));

// 播放第一个动画
const action = mixer.clipAction(gltf.animations[0]);
action.play();

4. GSAP 补间动画集成

GSAP(GreenSock Animation Platform)是 JavaScript 生态最强大的补间动画库,与 Three.js 完美配合:

# 安装 GSAP
npm install gsap
import gsap from 'gsap';

// 移动相机到新位置(2秒,ease 缓动)
gsap.to(camera.position, {
  duration: 2,
  x: 5, y: 3, z: 8,
  ease: 'power2.inOut',
  onUpdate: () => camera.lookAt(0, 0, 0)
});

// 序列动画(timeline)
const tl = gsap.timeline();
tl
  .to(cube.position, { y: 2, duration: 0.5, ease: 'power2.out' })
  .to(cube.rotation, { y: Math.PI * 2, duration: 1 }, '-=0.2')
  .to(cube.position, { y: 0, duration: 0.5, ease: 'bounce.out' });

// 悬浮动画循环
gsap.to(mesh.position, {
  y: '+=0.5',
  duration: 1.5,
  ease: 'sine.inOut',
  yoyo: true,    // 来回播放
  repeat: -1     // 无限循环
});

// 颜色动画(需要特殊处理)
gsap.to(mesh.material.color, {
  r: 1, g: 0.5, b: 0,
  duration: 1
});

// 点击触发入场动画
function enterAnimation(object) {
  object.scale.set(0, 0, 0);
  gsap.to(object.scale, {
    x: 1, y: 1, z: 1,
    duration: 0.6,
    ease: 'back.out(1.7)'
  });
}

5. 实战:角色动画状态机(idle / walk / run)

class CharacterController {
  constructor(model, animations) {
    this.mixer  = new THREE.AnimationMixer(model);
    this.actions = {};
    this.current = null;

    // 加载所有动画 Clip
    animations.forEach(clip => {
      const action = this.mixer.clipAction(clip);
      this.actions[clip.name] = action;
    });

    this.play('idle');
  }

  play(name, fadeDuration = 0.3) {
    const next = this.actions[name];
    if (!next || next === this.current) return;

    if (this.current) {
      // crossFadeTo:从当前动画淡出到新动画
      this.current.crossFadeTo(next, fadeDuration, true);
    }

    next.reset().play();
    this.current = next;
  }

  update(delta) {
    this.mixer.update(delta);
  }
}

// 使用
const character = new CharacterController(gltf.scene, gltf.animations);

// 根据速度切换动画
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  const speed = getCharacterSpeed(); // 0, 1-3, 3+

  if      (speed === 0) character.play('idle');
  else if (speed < 3)  character.play('walk');
  else                 character.play('run');

  character.update(delta);
  renderer.render(scene, camera);
}

本章小结:Three.js 动画系统围绕 AnimationMixer 展开——它负责播放和混合 AnimationClip。来自 GLTF 模型的骨骼动画直接通过 mixer.clipAction() 播放;程序性动画用 GSAP 补间;复杂交互动画用状态机管理过渡。