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