06 动画
学习 Three.js 中的各种动画实现方式
requestAnimationFrame 动画循环
所有动画的基础:
js
function animate() {
requestAnimationFrame(animate)
// 在这里更新物体状态
mesh.rotation.y += 0.01
// 渲染
renderer.render(scene, camera)
}
animate()1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
旋转与缩放
持续旋转
js
function animate() {
requestAnimationFrame(animate)
// 绕Y轴旋转
mesh.rotation.y += 0.01
// 绕X轴旋转(更慢)
mesh.rotation.x += 0.005
// 同时缩放(脉冲效果)
const scale = 1 + Math.sin(Date.now() * 0.002) * 0.1
mesh.scale.set(scale, scale, scale)
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
绕轨道运动
js
function animate() {
requestAnimationFrame(animate)
const time = Date.now() * 0.001 // 秒
// 圆形轨道
mesh.position.x = Math.cos(time) * 5
mesh.position.z = Math.sin(time) * 5
mesh.position.y = Math.sin(time * 2) * 1 // 上下起伏
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
THREE.Clock 精确计时
js
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
const elapsedTime = clock.getElapsedTime() // 总时长(秒)
const delta = clock.getDelta() // 距离上一帧的时间
// 匀速旋转(与帧率无关)
mesh.rotation.y = elapsedTime * 0.5
// 上下浮动
mesh.position.y = Math.sin(elapsedTime * 2) * 0.5
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GSAP 动画库(推荐)
GSAP 是最强大的网页动画库,配合 Three.js 非常好用。
bash
pnpm add gsap1
基础用法
js
import gsap from 'gsap'
// 位置动画
gsap.to(mesh.position, {
x: 5,
duration: 2,
ease: 'power2.out'
})
// 旋转动画
gsap.to(mesh.rotation, {
y: Math.PI * 2,
duration: 2,
ease: 'elastic.out(1, 0.5)'
})
// 缩放动画(脉冲)
gsap.to(mesh.scale, {
x: 1.2, y: 1.2, z: 1.2,
duration: 0.3,
yoyo: true,
repeat: -1, // 无限循环
ease: 'power1.inOut'
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
关键帧动画
js
gsap.to(mesh.position, {
keyframes: [
{ x: 0, y: 0, duration: 0 },
{ x: 5, y: 0, duration: 1 },
{ x: 5, y: 3, duration: 1 },
{ x: 0, y: 3, duration: 1 },
{ x: 0, y: 0, duration: 1 }
],
repeat: -1,
ease: 'none'
})1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
相机动画
js
// 相机围绕场景旋转
gsap.to(camera.position, {
x: 10 * Math.cos(Date.now() * 0.001),
z: 10 * Math.sin(Date.now() * 0.001),
duration: 0.1,
onUpdate: () => {
camera.lookAt(0, 0, 0)
}
})1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
动画控制
js
// 创建动画对象
const tween = gsap.to(mesh.position, {
x: 5,
duration: 2
})
// 暂停
tween.pause()
// 恢复
tween.resume()
// 重新开始
tween.restart()
// 跳到结尾
tween.progress(1)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TWEEN.js 补间动画
bash
pnpm add @tweenjs/tween.js1
js
import * as TWEEN from '@tweenjs/tween.js'
// 创建补间
new TWEEN.Tween(mesh.position)
.to({ x: 10, y: 5, z: 0 }, 1000) // 目标位置和时长(ms)
.easing(TWEEN.Easing.Quadratic.Out) // 缓动函数
.onUpdate(() => {
// 每帧更新时调用
})
.onComplete(() => {
// 动画完成时调用
console.log('完成')
})
.start()
// 链式调用
new TWEEN.Tween(mesh.scale)
.to({ x: 0, y: 0, z: 0 }, 500)
.easing(TWEEN.Easing.Back.In)
.start()
.chain(
new TWEEN.Tween(mesh.scale)
.to({ x: 1, y: 1, z: 1 }, 500)
.easing(TWEEN.Easing.Elastic.Out)
)
// 在动画循环中更新
function animate(time) {
requestAnimationFrame(animate)
TWEEN.update(time)
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
常用缓动函数
js
TWEEN.Easing.Linear.None // 匀速
TWEEN.Easing.Quadratic.In // 先慢后快
TWEEN.Easing.Quadratic.Out // 先快后慢
TWEEN.Easing.Quadratic.InOut // 慢-快-慢
TWEEN.Easing.Cubic.In // 更明显的加速
TWEEN.Easing.Elastic.Out // 弹性效果
TWEEN.Easing.Bounce.Out // 弹跳效果
TWEEN.Easing.Back.Out // 先冲出再回弹1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
物体形变动画
顶点动画(手动)
js
const geometry = new THREE.SphereGeometry(2, 32, 32)
const positions = geometry.attributes.position
function animate() {
requestAnimationFrame(animate)
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i)
const y = positions.getY(i)
const z = positions.getZ(i)
// 在顶点着色器中计算会更快,这里是CPU端演示
const offset = Math.sin(x * 2 + Date.now() * 0.002) * 0.1
positions.setZ(i, z + offset)
}
positions.needsUpdate = true
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
序列帧动画
js
// 加载序列帧图片
const textures = []
const loader = new THREE.TextureLoader()
const frameCount = 30
for (let i = 1; i <= frameCount; i++) {
const texture = loader.load(`/frames/frame${i.toString().padStart(3, '0')}.png`)
textures.push(texture)
}
const material = new THREE.MeshBasicMaterial({ map: textures[0] })
const mesh = new THREE.Mesh(planeGeometry, material)
scene.add(mesh)
let currentFrame = 0
function animate() {
requestAnimationFrame(animate)
currentFrame = (currentFrame + 1) % frameCount
material.map = textures[currentFrame]
material.needsUpdate = true
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
粒子动画
js
// 创建粒子系统
const particleCount = 1000
const geometry = new THREE.BufferGeometry()
const positions = new Float32Array(particleCount * 3)
const velocities = []
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 10
positions[i * 3 + 1] = 0
positions[i * 3 + 2] = (Math.random() - 0.5) * 10
velocities.push({
x: (Math.random() - 0.5) * 0.02,
y: Math.random() * 0.05 + 0.02,
z: (Math.random() - 0.5) * 0.02
})
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
const material = new THREE.PointsMaterial({
color: 0x4ecdc4,
size: 0.05,
transparent: true,
opacity: 0.8
})
const particles = new THREE.Points(geometry, material)
scene.add(particles)
function animate() {
requestAnimationFrame(animate)
const pos = particles.geometry.attributes.position
for (let i = 0; i < particleCount; i++) {
let y = pos.getY(i)
y += velocities[i].y
// 超出范围重置
if (y > 5) {
y = 0
}
pos.setY(i, y)
}
pos.needsUpdate = true
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
动画性能优化
1. 使用固定时间步
js
const clock = new THREE.Clock()
let accumulator = 0
const fixedDelta = 1 / 60 // 固定60fps
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
accumulator += delta
while (accumulator >= fixedDelta) {
// 用 fixedDelta 计算,保证物理模拟稳定
updatePhysics(fixedDelta)
accumulator -= fixedDelta
}
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. 离屏物体暂停动画
js
// 使用 Page Visibility API
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
renderer.setAnimationLoop(null) // 暂停
} else {
renderer.setAnimationLoop(animate) // 恢复
}
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
3. 实例化动画(大量相似物体)
js
const count = 100
const mesh = new THREE.InstancedMesh(geometry, material, count)
const dummy = new THREE.Object3D()
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * 10,
Math.random() * 5,
(Math.random() - 0.5) * 10
)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
}
mesh.instanceMatrix.needsUpdate = true
scene.add(mesh)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15