{T}

动画基础

动画是 Three.js 开发的核心技能,掌握动画原理和实现方法可以创建生动的 3D 交互体验。

概述

Three.js 中的动画主要通过在渲染循环中不断更新对象属性来实现。本章节介绍动画的基本原理、实现方法和常用技术。

架构概览

code
Three.js 动画系统架构
├── 基础动画(手动控制)
│   ├── 渲染循环
│   │   ├── requestAnimationFrame
│   │   └── Clock 类
│   └── 属性动画
│       ├── 变换动画(位置、旋转、缩放)
│       ├── 材质动画(颜色、透明度)
│       └── 纹理动画(UV 偏移)
│
├── 关键帧动画系统
│   ├── AnimationMixer(动画混合器)
│   ├── AnimationClip(动画片段)
│   ├── AnimationAction(动画动作)
│   └── KeyframeTrack(关键帧轨道)
│
└── 高级动画技术
    ├── 骨骼动画(Skeletal Animation)
    ├── 变形动画(Morph Target Animation)
    └── 粒子动画

渲染循环

requestAnimationFrame

创建平滑动画的基础。浏览器会在下一次重绘之前调用指定的回调函数,通常每秒 60 次(60 FPS)。

javascript
import * as THREE from 'three';

// 场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  // 更新对象
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  
  // 渲染场景
  renderer.render(scene, camera);
}

animate();
TIP

requestAnimationFrame 会自动适配显示器的刷新率,并且当页面不可见时会自动暂停,节省资源。

时间控制

javascript
// 使用时间控制动画速度
let previousTime = 0;

function animate(currentTime) {
  requestAnimationFrame(animate);
  
  // 计算时间差(秒)
  const deltaTime = (currentTime - previousTime) / 1000;
  previousTime = currentTime;
  
  // 使用 deltaTime 控制动画速度
  cube.rotation.x += deltaTime;
  cube.rotation.y += deltaTime;
  
  renderer.render(scene, camera);
}

animate(0);

Clock 类

Clock 是 Three.js 提供的时间管理工具类。

Clock API 参考

方法/属性类型说明
constructor(autoStart = true)构造函数创建 Clock 实例,autoStart 控制是否自动开始计时
start()方法启动计时
stop()方法停止计时
getElapsedTime()方法返回从启动到现在的总时间(秒),保留小数
getDelta()方法返回距上次调用 getDelta() 的时间差(秒),并重置计时
running属性是否正在运行(只读)
autoStart属性是否自动开始计时
javascript
// 使用 Clock 类管理时间
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  // 获取经过的时间
  const elapsedTime = clock.getElapsedTime();
  
  // 获取时间差
  const deltaTime = clock.getDelta();
  
  // 使用时间创建动画
  cube.rotation.y = elapsedTime;
  cube.position.x = Math.sin(elapsedTime);
  cube.position.y = Math.cos(elapsedTime);
  
  renderer.render(scene, camera);
}

animate();
WARNING

getDelta() 每次调用会重置计时器,同一帧内多次调用会导致后续调用返回 0。建议每帧只调用一次并保存结果。

基础动画类型

旋转动画

javascript
// 持续旋转
function animate() {
  requestAnimationFrame(animate);
  
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  cube.rotation.z += 0.01;
  
  renderer.render(scene, camera);
}

// 正弦旋转
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  cube.rotation.x = Math.sin(time) * Math.PI;
  cube.rotation.y = Math.cos(time) * Math.PI;
  
  renderer.render(scene, camera);
}

移动动画

javascript
// 直线移动
function animate() {
  requestAnimationFrame(animate);
  
  cube.position.x += 0.01;
  
  // 循环移动
  if (cube.position.x > 5) {
    cube.position.x = -5;
  }
  
  renderer.render(scene, camera);
}

// 圆周运动
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  const radius = 3;
  
  cube.position.x = Math.cos(time) * radius;
  cube.position.z = Math.sin(time) * radius;
  
  renderer.render(scene, camera);
}

// 弹跳运动
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  cube.position.y = Math.abs(Math.sin(time * 2)) * 2;
  
  renderer.render(scene, camera);
}

缩放动画

javascript
// 脉冲缩放
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  const scale = 1 + Math.sin(time * 3) * 0.2;
  
  cube.scale.set(scale, scale, scale);
  
  renderer.render(scene, camera);
}

// 呼吸效果
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  const scaleX = 1 + Math.sin(time) * 0.3;
  const scaleY = 1 + Math.cos(time) * 0.3;
  
  cube.scale.set(scaleX, scaleY, 1);
  
  renderer.render(scene, camera);
}

颜色动画

javascript
// 颜色渐变
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  
  // HSL 颜色变化
  const hue = (time * 0.1) % 1;
  cube.material.color.setHSL(hue, 1, 0.5);
  
  renderer.render(scene, camera);
}

// 闪烁效果
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  const intensity = (Math.sin(time * 10) + 1) / 2;
  
  cube.material.emissive.setHSL(0, 1, intensity * 0.5);
  
  renderer.render(scene, camera);
}

透明度动画

javascript
// 淡入淡出
const material = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  transparent: true,
  opacity: 1
});

function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  material.opacity = (Math.sin(time) + 1) / 2;
  
  renderer.render(scene, camera);
}
TIP

启用透明度(transparent: true)会增加渲染开销,仅在需要时启用。

相机动画

相机旋转

javascript
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  
  // 相机绕 Y 轴旋转
  camera.position.x = Math.sin(time) * 5;
  camera.position.z = Math.cos(time) * 5;
  camera.lookAt(0, 0, 0);
  
  renderer.render(scene, camera);
}

相机跟随

javascript
function animate() {
  requestAnimationFrame(animate);
  
  // 移动物体
  const time = clock.getElapsedTime();
  cube.position.x = Math.sin(time) * 3;
  cube.position.z = Math.cos(time) * 3;
  
  // 相机跟随
  const cameraOffset = new THREE.Vector3(0, 2, 5);
  camera.position.copy(cube.position).add(cameraOffset);
  camera.lookAt(cube.position);
  
  renderer.render(scene, camera);
}

光源动画

javascript
const pointLight = new THREE.PointLight(0xff0000, 1, 100);
scene.add(pointLight);

function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  
  // 光源移动
  pointLight.position.x = Math.sin(time) * 5;
  pointLight.position.z = Math.cos(time) * 5;
  
  // 光源强度变化
  pointLight.intensity = 1 + Math.sin(time * 2) * 0.5;
  
  // 光源颜色变化
  const hue = (time * 0.1) % 1;
  pointLight.color.setHSL(hue, 1, 0.5);
  
  renderer.render(scene, camera);
}

缓动函数

缓动函数使动画更自然、更有表现力。

常用缓动函数类型

code
缓动函数曲线示意

线性(Linear)
  ┌────────────┐
  │          / │
  │         /  │
  │        /   │
  │       /    │
  │      /     │
  │     /      │
  │    /       │
  └────────────┘

缓入(Ease In)- 开始慢,结束快
  ┌────────────┐
  │          / │
  │        /   │
  │       /    │
  │      /     │
  │    /       │
  │  /         │
  │/           │
  └────────────┘

缓出(Ease Out)- 开始快,结束慢
  ┌────────────┐
  │          / │
  │        /   │
  │      /     │
  │    /       │
  │  /         │
  │ /          │
  │/           │
  └────────────┘

缓入缓出(Ease In Out)- 两端慢,中间快
  ┌────────────┐
  │        _/  │
  │      _/    │
  │    _/      │
  │  _/        │
  │_/          │
  │            │
  │            │
  └────────────┘

缓动函数实现

javascript
// 常用缓动函数
const Easing = {
  // 线性
  Linear: t => t,
  
  // 二次
  QuadIn: t => t * t,
  QuadOut: t => t * (2 - t),
  QuadInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  
  // 三次
  CubicIn: t => t * t * t,
  CubicOut: t => (--t) * t * t + 1,
  CubicInOut: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  
  // 正弦
  SineIn: t => 1 - Math.cos(t * Math.PI / 2),
  SineOut: t => Math.sin(t * Math.PI / 2),
  SineInOut: t => -(Math.cos(Math.PI * t) - 1) / 2,
  
  // 弹性
  ElasticIn: t => t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI) / 3),
  ElasticOut: t => t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1,
  
  // 弹跳
  BounceOut: t => {
    const n1 = 7.5625;
    const d1 = 2.75;
    if (t < 1 / d1) return n1 * t * t;
    if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
    if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
    return n1 * (t -= 2.625 / d1) * t + 0.984375;
  }
};

// 使用缓动函数
function animate() {
  requestAnimationFrame(animate);
  
  const duration = 2000;  // 毫秒
  const time = (Date.now() % duration) / duration;
  
  // 应用缓动
  const easedTime = Easing.CubicInOut(time);
  
  cube.position.x = -5 + easedTime * 10;  // 从 -5 移动到 5
  
  renderer.render(scene, camera);
}

缓动函数速查表

缓动类型效果描述适用场景
Linear匀速运动机械运动、匀速旋转
QuadIn开始慢,逐渐加速启动动画、蓄力效果
QuadOut开始快,逐渐减速停止动画、刹车效果
QuadInOut两端慢,中间快自然移动、开门关门
CubicIn/Out/InOut比二次更明显需要更强加速度的场景
SineIn/Out/InOut柔和的加速减速平滑过渡、UI动画
ElasticIn/Out弹性效果弹跳、强调效果
BounceOut弹跳效果落地、弹球效果

粒子动画

javascript
// 创建粒子系统
const particleCount = 1000;
const positions = new Float32Array(particleCount * 3);
const velocities = [];

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = Math.random() * 20 - 10;
  positions[i * 3 + 1] = Math.random() * 20 - 10;
  positions[i * 3 + 2] = Math.random() * 20 - 10;
  
  velocities.push({
    x: (Math.random() - 0.5) * 0.1,
    y: (Math.random() - 0.5) * 0.1,
    z: (Math.random() - 0.5) * 0.1
  });
}

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const material = new THREE.PointsMaterial({
  color: 0xff0000,
  size: 0.1
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

function animate() {
  requestAnimationFrame(animate);
  
  const positions = particles.geometry.attributes.position.array;
  
  for (let i = 0; i < particleCount; i++) {
    positions[i * 3] += velocities[i].x;
    positions[i * 3 + 1] += velocities[i].y;
    positions[i * 3 + 2] += velocities[i].z;
    
    // 边界检测
    if (Math.abs(positions[i * 3]) > 10) velocities[i].x *= -1;
    if (Math.abs(positions[i * 3 + 1]) > 10) velocities[i].y *= -1;
    if (Math.abs(positions[i * 3 + 2]) > 10) velocities[i].z *= -1;
  }
  
  particles.geometry.attributes.position.needsUpdate = true;
  
  renderer.render(scene, camera);
}
TIP

大量粒子动画建议使用 GPU Shader 实现,可显著提升性能。

形变动画

Morph Targets

javascript
// 创建多个形态目标
const geometry = new THREE.BoxGeometry(1, 1, 1);

// 目标形态 1
const morphTarget1 = new THREE.BoxGeometry(1, 2, 1);
// 目标形态 2
const morphTarget2 = new THREE.BoxGeometry(2, 1, 1);

// 设置形态目标
geometry.morphAttributes.position = [
  new THREE.Float32BufferAttribute(morphTarget1.attributes.position.array, 3),
  new THREE.Float32BufferAttribute(morphTarget2.attributes.position.array, 3)
];

const material = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  morphTargets: true
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 动画
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  
  // 形态目标权重
  mesh.morphTargetInfluences[0] = (Math.sin(time) + 1) / 2;
  mesh.morphTargetInfluences[1] = (Math.cos(time) + 1) / 2;
  
  renderer.render(scene, camera);
}

纹理动画

UV 动画

javascript
const texture = textureLoader.load('water.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;

const material = new THREE.MeshStandardMaterial({
  map: texture
});

function animate() {
  requestAnimationFrame(animate);
  
  // UV 偏移
  texture.offset.x += 0.01;
  texture.offset.y += 0.005;
  
  renderer.render(scene, camera);
}

纹理混合动画

javascript
const texture1 = textureLoader.load('texture1.jpg');
const texture2 = textureLoader.load('texture2.jpg');

const material = new THREE.ShaderMaterial({
  uniforms: {
    texture1: { value: texture1 },
    texture2: { value: texture2 },
    mixRatio: { value: 0 }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D texture1;
    uniform sampler2D texture2;
    uniform float mixRatio;
    varying vec2 vUv;
    
    void main() {
      vec4 color1 = texture2D(texture1, vUv);
      vec4 color2 = texture2D(texture2, vUv);
      gl_FragColor = mix(color1, color2, mixRatio);
    }
  `
});

function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  material.uniforms.mixRatio.value = (Math.sin(time) + 1) / 2;
  
  renderer.render(scene, camera);
}

动画工具函数

值插值

javascript
// 线性插值
function lerp(start, end, t) {
  return start + (end - start) * t;
}

// 使用 lerp 创建平滑移动
const targetPosition = new THREE.Vector3(5, 0, 0);

function animate() {
  requestAnimationFrame(animate);
  
  // 平滑移动到目标位置
  cube.position.x = lerp(cube.position.x, targetPosition.x, 0.1);
  cube.position.y = lerp(cube.position.y, targetPosition.y, 0.1);
  cube.position.z = lerp(cube.position.z, targetPosition.z, 0.1);
  
  renderer.render(scene, camera);
}

角度插值

javascript
// 角度归一化到 -PI 到 PI
function normalizeAngle(angle) {
  while (angle > Math.PI) angle -= Math.PI * 2;
  while (angle < -Math.PI) angle += Math.PI * 2;
  return angle;
}

// 角度插值(最短路径)
function lerpAngle(start, end, t) {
  start = normalizeAngle(start);
  end = normalizeAngle(end);
  
  const diff = normalizeAngle(end - start);
  
  return start + diff * t;
}

性能优化

减少计算

javascript
// 不推荐:每次都计算
function animate() {
  requestAnimationFrame(animate);
  
  cube.rotation.x = Math.sin(Date.now() * 0.001) * Math.PI;
  cube.rotation.y = Math.cos(Date.now() * 0.001) * Math.PI;
  
  renderer.render(scene, camera);
}

// 推荐:缓存计算
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  const sinTime = Math.sin(time);
  const cosTime = Math.cos(time);
  
  cube.rotation.x = sinTime * Math.PI;
  cube.rotation.y = cosTime * Math.PI;
  
  renderer.render(scene, camera);
}

降低更新频率

javascript
// 某些动画不需要每帧更新
let frameCount = 0;

function animate() {
  requestAnimationFrame(animate);
  
  frameCount++;
  
  // 每 3 帧更新一次
  if (frameCount % 3 === 0) {
    // 更新某些属性
  }
  
  renderer.render(scene, camera);
}

使用 Object3D.frustumCulled

javascript
// 对于始终在视野内的对象,禁用视锥剔除检测
cube.frustumCulled = false;

动画类型对比

动画类型实现方式适用场景性能影响
属性动画直接修改属性简单变换、颜色变化
关键帧动画AnimationMixer复杂动画、模型动画
骨骼动画SkinnedMesh角色动画
变形动画Morph Targets表情动画、形态变化中-高
粒子动画Points + BufferGeometry特效、大量小物体中-高
Shader 动画自定义着色器高性能特效低(GPU计算)

常见问题解答(FAQ)

Q: 为什么动画在不同设备上速度不一致?

A: 不同设备的刷新率可能不同(60Hz、120Hz 等)。使用 deltaTime 来控制动画速度,而不是固定增量值:

javascript
// 错误:固定增量
cube.rotation.x += 0.01;

// 正确:基于时间
const delta = clock.getDelta();
cube.rotation.x += delta;  // 每秒旋转 1 弧度

Q: 如何让动画暂停和恢复?

A: 使用 Clock 的 start/stop 方法或自定义暂停逻辑:

javascript
let isPaused = false;
const clock = new THREE.Clock();
let elapsedTime = 0;

function animate() {
  requestAnimationFrame(animate);
  
  if (!isPaused) {
    const delta = clock.getDelta();
    elapsedTime += delta;
    
    // 使用 elapsedTime 进行动画
    cube.rotation.y = elapsedTime;
  }
  
  renderer.render(scene, camera);
}

// 暂停/恢复
function togglePause() {
  isPaused = !isPaused;
  if (!isPaused) {
    clock.start();  // 恢复时重新启动时钟
  }
}

Q: 如何实现平滑的相机过渡?

A: 使用 lerp 插值:

javascript
const targetPosition = new THREE.Vector3(5, 5, 5);

function animate() {
  requestAnimationFrame(animate);
  
  // 平滑过渡相机位置
  camera.position.lerp(targetPosition, 0.05);
  
  renderer.render(scene, camera);
}

Q: 动画卡顿如何排查?

A: 检查以下几点:

  1. 使用 requestAnimationFrame 而非 setInterval
  2. 减少 draw call 数量(合并几何体)
  3. 检查是否有内存泄漏(及时释放资源)
  4. 使用 Chrome DevTools 的 Performance 面板分析
  5. 对于复杂场景,考虑使用 LOD 技术

Q: 如何创建循环动画但每次播放有间隔?

A: 使用 AnimationAction 的 repetitions 属性,或在动画完成事件中添加延迟:

javascript
function animateWithDelay(delay) {
  const duration = 2000;  // 动画持续时间
  const totalTime = duration + delay;
  const time = (Date.now() % totalTime) / duration;
  
  if (time < 1) {
    // 播放动画
    cube.position.x = Easing.CubicInOut(time) * 5;
  }
}

完整示例

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 初始化
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
camera.position.z = 5;

const clock = new THREE.Clock();

// 创建多个动画对象
const objects = [];

for (let i = 0; i < 10; i++) {
  const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
  const material = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL(i / 10, 1, 0.5)
  });
  
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = (i - 5) * 1.2;
  
  objects.push({
    mesh,
    rotationSpeed: {
      x: Math.random() * 0.02,
      y: Math.random() * 0.02,
      z: Math.random() * 0.02
    },
    bouncePhase: Math.random() * Math.PI * 2
  });
  
  scene.add(mesh);
}

// 光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);

const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  const time = clock.getElapsedTime();
  
  // 更新每个对象
  objects.forEach((obj, i) => {
    obj.mesh.rotation.x += obj.rotationSpeed.x;
    obj.mesh.rotation.y += obj.rotationSpeed.y;
    obj.mesh.rotation.z += obj.rotationSpeed.z;
    
    // 弹跳
    obj.mesh.position.y = Math.sin(time * 2 + obj.bouncePhase) * 0.5;
  });
  
  controls.update();
  renderer.render(scene, camera);
}

animate();

相关链接