Three.js 动画 #

动画是 3D 场景的灵魂,让静态的场景变得生动有趣。Three.js 提供了多种动画实现方式,从简单的属性变化到复杂的骨骼动画。

动画概述 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Three.js 动画方式                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │   基础动画    │  │   补间动画   │  │   骨骼动画   │         │
│  ├─────────────┤  ├─────────────┤  ├─────────────┤         │
│  │ requestAnim │  │   GSAP      │  │AnimationMixer│        │
│  │ Clock       │  │   Tween.js  │  │KeyframeTrack│         │
│  │ 属性变化     │  │             │  │             │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

基础动画 #

渲染循环 #

所有动画都基于渲染循环:

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

使用 Clock #

Clock 用于精确计时:

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = clock.getDelta();
  
  console.log('总时间:', elapsedTime);
  console.log('帧间隔:', deltaTime);
  
  renderer.render(scene, camera);
}
animate();

Clock 方法 #

方法 返回值 描述
getElapsedTime() Number 总运行时间(秒)
getDelta() Number 距上次调用的时间差
start() - 开始计时
stop() - 停止计时
running Boolean 是否运行中

旋转动画 #

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  
  mesh.rotation.x = elapsedTime;
  mesh.rotation.y = elapsedTime * 0.5;
  
  renderer.render(scene, camera);
}
animate();

位置动画 #

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  
  mesh.position.x = Math.sin(elapsedTime) * 2;
  mesh.position.y = Math.cos(elapsedTime) * 2;
  mesh.position.z = Math.sin(elapsedTime * 0.5) * 2;
  
  renderer.render(scene, camera);
}
animate();

缩放动画 #

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  
  const scale = Math.sin(elapsedTime) * 0.5 + 1;
  mesh.scale.set(scale, scale, scale);
  
  renderer.render(scene, camera);
}
animate();

弹跳动画 #

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  
  mesh.position.y = Math.abs(Math.sin(elapsedTime * 3)) * 2;
  
  renderer.render(scene, camera);
}
animate();

补间动画 #

使用 GSAP #

GSAP 是最流行的 JavaScript 动画库:

javascript
import gsap from 'gsap';

gsap.to(mesh.position, {
  x: 5,
  y: 2,
  z: 3,
  duration: 2,
  ease: 'power2.inOut',
  onComplete: () => {
    console.log('动画完成');
  }
});

gsap.from(mesh.position, {
  x: -5,
  duration: 1
});

gsap.fromTo(mesh.position,
  { x: -5 },
  { x: 5, duration: 2 }
);

GSAP 缓动函数 #

javascript
gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'none'
});

gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'power1.in'
});

gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'power2.out'
});

gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'power3.inOut'
});

gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'elastic.out(1, 0.3)'
});

gsap.to(mesh.position, {
  x: 5,
  duration: 2,
  ease: 'bounce.out'
});

GSAP 时间线 #

javascript
const tl = gsap.timeline();

tl.to(mesh.position, { x: 2, duration: 1 })
  .to(mesh.position, { y: 2, duration: 1 })
  .to(mesh.rotation, { y: Math.PI, duration: 1 })
  .to(mesh.scale, { x: 2, y: 2, z: 2, duration: 1 });

tl.pause();
tl.play();
tl.reverse();
tl.restart();

GSAP 控制 #

javascript
const tween = gsap.to(mesh.position, {
  x: 5,
  duration: 2
});

tween.pause();
tween.resume();
tween.reverse();
tween.restart();
tween.kill();
tween.progress(0.5);
tween.timeScale(2);

使用 Tween.js #

javascript
import * as TWEEN from '@tweenjs/tween.js';

const coords = { x: 0, y: 0 };

const tween = new TWEEN.Tween(coords)
  .to({ x: 5, y: 5 }, 2000)
  .easing(TWEEN.Easing.Quadratic.Out)
  .onUpdate(() => {
    mesh.position.x = coords.x;
    mesh.position.y = coords.y;
  })
  .onComplete(() => {
    console.log('动画完成');
  })
  .start();

function animate() {
  requestAnimationFrame(animate);
  TWEEN.update();
  renderer.render(scene, camera);
}
animate();

关键帧动画 #

AnimationMixer #

AnimationMixer 用于播放动画:

javascript
const mixer = new THREE.AnimationMixer(mesh);

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const delta = clock.getDelta();
  mixer.update(delta);
  
  renderer.render(scene, camera);
}
animate();

创建关键帧动画 #

javascript
const positionKF = new THREE.VectorKeyframeTrack(
  '.position',
  [0, 1, 2],
  [0, 0, 0, 2, 2, 0, 0, 0, 0]
);

const rotationKF = new THREE.QuaternionKeyframeTrack(
  '.quaternion',
  [0, 1, 2],
  [0, 0, 0, 1, 0, 0, 0.707, 0.707, 0, 0, 0, 1]
);

const scaleKF = new THREE.VectorKeyframeTrack(
  '.scale',
  [0, 1, 2],
  [1, 1, 1, 2, 2, 2, 1, 1, 1]
);

const clip = new THREE.AnimationClip('action', 2, [positionKF, rotationKF, scaleKF]);

const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();

KeyframeTrack 类型 #

类型 描述
VectorKeyframeTrack Vector3 动画
QuaternionKeyframeTrack 四元数旋转动画
NumberKeyframeTrack 数值动画
ColorKeyframeTrack 颜色动画
BooleanKeyframeTrack 布尔动画
StringKeyframeTrack 字符串动画

AnimationAction 控制 #

javascript
const action = mixer.clipAction(clip);

action.play();
action.stop();
action.reset();
action.pause();
action.timeScale = 2;
action.setLoop(THREE.LoopRepeat, 3);
action.setLoop(THREE.LoopOnce);
action.clampWhenFinished = true;

加载动画模型 #

GLTF 动画 #

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

const loader = new GLTFLoader();
let mixer;

loader.load('model.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);
  
  mixer = new THREE.AnimationMixer(model);
  
  gltf.animations.forEach((clip) => {
    const action = mixer.clipAction(clip);
    action.play();
  });
});

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const delta = clock.getDelta();
  if (mixer) mixer.update(delta);
  
  renderer.render(scene, camera);
}
animate();

多动画切换 #

javascript
let mixer;
let actions = {};
let currentAction;

loader.load('model.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);
  
  mixer = new THREE.AnimationMixer(model);
  
  gltf.animations.forEach((clip) => {
    actions[clip.name] = mixer.clipAction(clip);
  });
  
  currentAction = actions['Idle'];
  currentAction.play();
});

function switchAnimation(name) {
  if (currentAction) {
    currentAction.fadeOut(0.5);
  }
  
  currentAction = actions[name];
  currentAction.reset().fadeIn(0.5).play();
}

变形动画 #

Morph Targets #

javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);

const morphPositions = [];
const positionAttribute = geometry.attributes.position;

for (let i = 0; i < positionAttribute.count; i++) {
  morphPositions.push(
    positionAttribute.getX(i) * 2,
    positionAttribute.getY(i) * 2,
    positionAttribute.getZ(i) * 2
  );
}

geometry.morphAttributes.position = [
  new THREE.Float32BufferAttribute(morphPositions, 3)
];

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

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

function animate() {
  requestAnimationFrame(animate);
  
  const time = Date.now() * 0.001;
  mesh.morphTargetInfluences[0] = (Math.sin(time) + 1) / 2;
  
  renderer.render(scene, camera);
}
animate();

粒子动画 #

基础粒子动画 #

javascript
const particleCount = 1000;
const positions = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * 10;
  positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
  positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
}

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

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

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 + 1] += 0.01;
    
    if (positions[i * 3 + 1] > 5) {
      positions[i * 3 + 1] = -5;
    }
  }
  
  particles.geometry.attributes.position.needsUpdate = true;
  
  renderer.render(scene, camera);
}
animate();

骨骼动画 #

骨骼结构 #

javascript
const bones = [];

const root = new THREE.Bone();
bones.push(root);

const child = new THREE.Bone();
root.add(child);
bones.push(child);

const skeleton = new THREE.Skeleton(bones);

const geometry = new THREE.CylinderGeometry(0.1, 0.1, 5, 8, 5);
geometry.translate(0, 2.5, 0);

const mesh = new THREE.SkinnedMesh(geometry, material);
mesh.add(root);
mesh.bind(skeleton);

scene.add(mesh);

动画曲线 #

自定义缓动 #

javascript
function easeInOutQuad(t) {
  return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}

function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

function easeOutElastic(t) {
  const c4 = (2 * Math.PI) / 3;
  return t === 0 ? 0 : t === 1 ? 1 :
    Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}

const clock = new THREE.Clock();
const duration = 2;
let startTime = null;

function animate(timestamp) {
  requestAnimationFrame(animate);
  
  if (!startTime) startTime = timestamp;
  const progress = Math.min((timestamp - startTime) / (duration * 1000), 1);
  
  const easedProgress = easeInOutQuad(progress);
  mesh.position.x = easedProgress * 5;
  
  renderer.render(scene, camera);
}
animate();

动画性能优化 #

使用 Object3D #

javascript
const group = new THREE.Group();
for (let i = 0; i < 100; i++) {
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(Math.random() * 10, Math.random() * 10, Math.random() * 10);
  group.add(mesh);
}
scene.add(group);

function animate() {
  requestAnimationFrame(animate);
  
  group.rotation.y += 0.01;
  
  renderer.render(scene, camera);
}
animate();

减少更新 #

javascript
let needsUpdate = false;

function triggerUpdate() {
  needsUpdate = true;
}

function animate() {
  requestAnimationFrame(animate);
  
  if (needsUpdate) {
    mesh.rotation.y += 0.01;
    needsUpdate = false;
  }
  
  renderer.render(scene, camera);
}
animate();

完整示例 #

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

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

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

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff6b6b });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

const sphereGeometry = new THREE.SphereGeometry(0.3, 32, 16);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x6bffb8 });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(2, 0, 0);
scene.add(sphere);

const torusGeometry = new THREE.TorusGeometry(0.3, 0.1, 16, 32);
const torusMaterial = new THREE.MeshStandardMaterial({ color: 0x6bb8ff });
const torus = new THREE.Mesh(torusGeometry, torusMaterial);
torus.position.set(-2, 0, 0);
scene.add(torus);

gsap.to(cube.position, {
  y: 2,
  duration: 1,
  ease: 'bounce.out',
  repeat: -1,
  yoyo: true
});

gsap.to(cube.rotation, {
  y: Math.PI * 2,
  duration: 2,
  repeat: -1,
  ease: 'none'
});

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  
  const elapsedTime = clock.getElapsedTime();
  
  sphere.position.x = Math.sin(elapsedTime) * 2 + 2;
  sphere.position.y = Math.cos(elapsedTime * 2) * 0.5;
  
  torus.rotation.x = elapsedTime;
  torus.rotation.y = elapsedTime * 0.5;
  
  controls.update();
  renderer.render(scene, camera);
}
animate();

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

下一步 #

现在你已经掌握了动画系统,接下来学习 交互,添加用户交互功能!

最后更新:2026-03-28