渲染循环与生命周期
渲染循环是 Three.js 应用的核心,它驱动整个 3D 场景的更新和渲染。理解渲染循环的工作机制对于创建流畅、高效的 3D 应用至关重要。
系统架构概述
code
┌─────────────────────────────────────────────────────────────────────────────┐
│ 渲染循环系统架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 单帧执行流程 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ 开始新帧 │───▶│ 计算时间 │───▶│ 更新逻辑 │───▶│ 渲染场景 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │
│ ▲ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ └─────────────────────│ requestNext │◀───────────────────────┘ │
│ │ Frame │ │
│ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 生命周期阶段 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Init ──▶ Start ──▶ Update(循环) ──▶ Pause ──▶ Resume ──▶ Stop ──▶ Dispose │
│ │
└─────────────────────────────────────────────────────────────────────────────┘requestAnimationFrame 基础
工作原理
requestAnimationFrame 是浏览器提供的 API,用于在下次重绘之前调用指定的回调函数,是实现流畅动画的关键。
code
┌──────────────────────────────────────────────────────────────────┐
│ requestAnimationFrame 工作流程 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 浏览器刷新周期(通常 16.67ms @ 60Hz) │
│ ┌────────┬────────┬────────┬────────┬────────┬────────┐ │
│ │ Frame │ Frame │ Frame │ Frame │ Frame │ Frame │ │
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ │
│ └────────┴────────┴────────┴────────┴────────┴────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 回调执行 │ ───── requestAnimationFrame ────────▶ │ 回调执行 │ │
│ └──────────┘ └──────────┘ │
│ │
│ 特点: │
│ 1. 与显示器刷新率同步(通常 60 FPS) │
│ 2. 标签页不可见时自动暂停 │
│ 3. 比 setInterval 更精确、更省电 │
│ 4. 自动处理帧率波动 │
│ │
└──────────────────────────────────────────────────────────────────┘基础用法
javascript
function animate() {
requestAnimationFrame(animate)
// 更新场景
cube.rotation.x += 0.01
cube.rotation.y += 0.01
// 渲染场景
renderer.render(scene, camera)
}
animate() // 启动渲染循环与 setInterval 对比
| 特性 | requestAnimationFrame | setInterval |
|---|---|---|
| 帧率同步 | ✓ 自动同步显示器 | ✗ 固定间隔 |
| 后台暂停 | ✓ 自动暂停 | ✗ 继续执行 |
| 性能优化 | ✓ 浏览器优化 | ✗ 无优化 |
| 帧率稳定 | ✓ 自动调节 | ✗ 可能丢帧 |
| 电池友好 | ✓ 暂停时不耗电 | ✗ 持续耗电 |
javascript
// ❌ 不推荐:使用 setInterval
setInterval(() => {
// 不会被优化,可能导致卡顿
// 后台标签页仍然执行,浪费资源
renderer.render(scene, camera)
}, 1000 / 60)
// ✓ 推荐:使用 requestAnimationFrame
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}时间控制
javascript
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
// 获取时间信息
const deltaTime = clock.getDelta() // 距上一帧的时间(秒)
const elapsedTime = clock.getElapsedTime() // 总运行时间(秒)
// 使用 deltaTime 控制动画速度(帧率无关)
cube.rotation.x += deltaTime * speed
// 使用 elapsedTime 创建周期性动画
cube.position.y = Math.sin(elapsedTime) * 2
// 创建循环动画
const angle = elapsedTime * Math.PI * 2 / period // period 秒一圈
cube.position.x = Math.cos(angle) * radius
cube.position.z = Math.sin(angle) * radius
renderer.render(scene, camera)
}Clock API
| 方法/属性 | 返回值 | 说明 |
|---|---|---|
getDelta() | Number | 距上次调用的时间(秒) |
getElapsedTime() | Number | 时钟启动后的总时间(秒) |
start() | - | 启动时钟 |
stop() | - | 停止时钟 |
reset() | - | 重置时钟 |
running | Boolean | 是否正在运行 |
javascript
const clock = new THREE.Clock()
// 或设置 autoStart = false
const clock = new THREE.Clock(false)
// 程序控制
clock.start()
console.log('运行时间:', clock.getElapsedTime())
clock.stop()
clock.reset()渲染循环结构
标准渲染循环
javascript
class RenderLoop {
constructor(renderer, scene, camera) {
this.renderer = renderer
this.scene = scene
this.camera = camera
this.clock = new THREE.Clock()
this.isRunning = false
this.callbacks = new Set() // 使用 Set 避免重复
}
start() {
if (!this.isRunning) {
this.isRunning = true
this.clock.start()
this.animate()
}
}
stop() {
this.isRunning = false
this.clock.stop()
}
animate() {
if (!this.isRunning) return
requestAnimationFrame(() => this.animate())
const deltaTime = this.clock.getDelta()
const elapsedTime = this.clock.getElapsedTime()
// 执行所有更新回调
this.callbacks.forEach(callback => {
callback(deltaTime, elapsedTime)
})
// 渲染场景
this.renderer.render(this.scene, this.camera)
}
// 添加更新回调
addCallback(callback) {
this.callbacks.add(callback)
return () => this.callbacks.delete(callback) // 返回移除函数
}
// 移除更新回调
removeCallback(callback) {
this.callbacks.delete(callback)
}
}
// 使用示例
const renderLoop = new RenderLoop(renderer, scene, camera)
// 添加更新逻辑
const removeRotate = renderLoop.addCallback((deltaTime, elapsedTime) => {
cube.rotation.x += deltaTime
cube.rotation.y += deltaTime * 0.5
})
const removeBounce = renderLoop.addCallback((deltaTime, elapsedTime) => {
cube.position.y = Math.sin(elapsedTime * 2) * 2
})
// 启动渲染循环
renderLoop.start()
// 移除特定回调
removeRotate()固定时间步长(Fixed Time Step)
用于物理模拟或需要精确控制的场景,确保不同帧率下物理行为一致。
code
┌──────────────────────────────────────────────────────────────────┐
│ 固定时间步长原理 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 帧时间:变量(如 0.033s, 0.050s, 0.020s) │
│ 固定步长:固定值(如 0.016s = 1/60s) │
│ │
│ 帧 1: deltaTime = 0.050s │
│ ┌─────────────────────────────────────┐ │
│ │ 物理更新(0.016) │ 物理更新(0.016) │ 物理更新(0.016) │ 渲染 │ │
│ │ 累加器剩 0.002s(用于插值) │ │
│ └─────────────────────────────────────┘ │
│ │
│ 帧 2: deltaTime = 0.012s │
│ ┌─────────────────────────────────────┐ │
│ │ 累加器(0.002+0.012=0.014) < 0.016 │ │
│ │ 不进行物理更新,直接渲染 │ │
│ └─────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘javascript
class FixedTimeStepLoop {
constructor(renderer, scene, camera, options = {}) {
this.renderer = renderer
this.scene = scene
this.camera = camera
// 固定时间步长(默认 60Hz)
this.fixedDeltaTime = options.fixedDeltaTime || 1 / 60
// 时间累加器
this.accumulator = 0
this.clock = new THREE.Clock()
// 回调函数
this.physicsCallback = null
this.renderCallback = null
// 最大帧时间(防止死亡螺旋)
this.maxFrameTime = options.maxFrameTime || 0.25
}
setPhysicsCallback(callback) {
this.physicsCallback = callback
}
setRenderCallback(callback) {
this.renderCallback = callback
}
update(deltaTime) {
// 限制最大帧时间,防止积累太多更新
if (deltaTime > this.maxFrameTime) {
deltaTime = this.maxFrameTime
}
// 累加时间
this.accumulator += deltaTime
// 固定步长的物理更新
while (this.accumulator >= this.fixedDeltaTime) {
if (this.physicsCallback) {
this.physicsCallback(this.fixedDeltaTime)
}
this.accumulator -= this.fixedDeltaTime
}
// 插值因子(用于平滑渲染)
const alpha = this.accumulator / this.fixedDeltaTime
// 渲染更新(使用插值)
if (this.renderCallback) {
this.renderCallback(alpha)
}
// 渲染场景
this.renderer.render(this.scene, this.camera)
}
animate() {
requestAnimationFrame(() => this.animate())
const deltaTime = this.clock.getDelta()
this.update(deltaTime)
}
start() {
this.clock.start()
this.animate()
}
}
// 使用示例
const gameLoop = new FixedTimeStepLoop(renderer, scene, camera, {
fixedDeltaTime: 1 / 60, // 60Hz 物理更新
maxFrameTime: 0.25 // 最大帧时间
})
// 设置物理更新回调
gameLoop.setPhysicsCallback((fixedDelta) => {
// 固定步长的物理更新
updatePhysics(fixedDelta)
updateCollisions(fixedDelta)
})
// 设置渲染回调(用于插值)
gameLoop.setRenderCallback((alpha) => {
// 使用 alpha 在上一帧和当前帧之间插值
interpolatePositions(alpha)
})
gameLoop.start()插值渲染示例
javascript
// 位置插值类
class InterpolatedPosition {
constructor(object) {
this.object = object
this.previousPosition = object.position.clone()
this.currentPosition = object.position.clone()
}
// 物理更新中调用
update(newPosition) {
this.previousPosition.copy(this.currentPosition)
this.currentPosition.copy(newPosition)
}
// 渲染时调用
interpolate(alpha) {
this.object.position.lerpVectors(
this.previousPosition,
this.currentPosition,
alpha
)
}
}
// 使用
const playerInterp = new InterpolatedPosition(player)
gameLoop.setPhysicsCallback((fixedDelta) => {
// 更新物理状态
const newPosition = calculateNewPosition()
playerInterp.update(newPosition)
})
gameLoop.setRenderCallback((alpha) => {
// 平滑渲染
playerInterp.interpolate(alpha)
})生命周期管理
对象生命周期
code
┌──────────────────────────────────────────────────────────────────┐
│ 对象生命周期 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Create │──▶│ Active │──▶│ Inactive│──▶│ Destroy │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 初始化资源 更新逻辑 暂停动画 释放资源 │
│ 添加到场景 渲染可见 隐藏对象 从场景移除 │
│ 设置初始值 接受交互 停止更新 清理内存 │
│ │
└──────────────────────────────────────────────────────────────────┘javascript
class ObjectLifecycle {
constructor() {
this.objects = new Map()
this.inactivePool = new Map()
}
// 创建对象
create(id, config) {
// 检查是否可以从池中复用
if (this.inactivePool.has(id)) {
const object = this.inactivePool.get(id)
this.inactivePool.delete(id)
this.objects.set(id, object)
object.visible = true
return object
}
// 创建新对象
const geometry = config.geometry || new THREE.BoxGeometry(1, 1, 1)
const material = config.material || new THREE.MeshStandardMaterial()
const mesh = new THREE.Mesh(geometry, material)
mesh.userData = {
id,
createdAt: Date.now(),
state: 'active',
...config.userData
}
if (config.position) {
mesh.position.copy(config.position)
}
this.objects.set(id, mesh)
scene.add(mesh)
return mesh
}
// 更新对象
update(id, updates) {
const object = this.objects.get(id)
if (!object) return null
if (updates.position) {
object.position.copy(updates.position)
}
if (updates.rotation) {
object.rotation.copy(updates.rotation)
}
if (updates.scale) {
object.scale.copy(updates.scale)
}
if (updates.visible !== undefined) {
object.visible = updates.visible
}
object.userData.updatedAt = Date.now()
return object
}
// 停用对象(放入池中等待复用)
deactivate(id) {
const object = this.objects.get(id)
if (!object) return false
object.visible = false
object.userData.state = 'inactive'
this.objects.delete(id)
this.inactivePool.set(id, object)
return true
}
// 销毁对象
destroy(id) {
const object = this.objects.get(id) || this.inactivePool.get(id)
if (!object) return false
// 从场景移除
scene.remove(object)
// 清理资源
if (object.geometry) {
object.geometry.dispose()
}
if (object.material) {
const materials = Array.isArray(object.material)
? object.material
: [object.material]
materials.forEach(mat => {
for (const key in mat) {
if (mat[key] && typeof mat[key].dispose === 'function') {
mat[key].dispose()
}
}
mat.dispose()
})
}
// 从映射中删除
this.objects.delete(id)
this.inactivePool.delete(id)
return true
}
// 销毁所有对象
destroyAll() {
const allIds = [
...this.objects.keys(),
...this.inactivePool.keys()
]
allIds.forEach(id => this.destroy(id))
}
// 获取对象
get(id) {
return this.objects.get(id)
}
// 获取所有活动对象
getAllActive() {
return Array.from(this.objects.values())
}
}
// 使用示例
const lifecycle = new ObjectLifecycle()
// 创建对象
const enemy = lifecycle.create('enemy_1', {
position: new THREE.Vector3(5, 0, 0),
userData: { health: 100, type: 'enemy' }
})
// 更新对象
lifecycle.update('enemy_1', {
position: new THREE.Vector3(6, 0, 0)
})
// 停用对象(复用池)
lifecycle.deactivate('enemy_1')
// 销毁对象
lifecycle.destroy('enemy_1')场景生命周期
javascript
class SceneManager {
constructor() {
this.scenes = new Map()
this.currentScene = null
this.clock = new THREE.Clock()
}
// 注册场景
register(name, config = {}) {
const scene = new THREE.Scene()
// 场景配置
if (config.background) {
scene.background = new THREE.Color(config.background)
}
if (config.fog) {
scene.fog = new THREE.Fog(...config.fog)
}
if (config.environment) {
scene.environment = config.environment
}
// 生命周期钩子
const sceneData = {
scene,
name,
// 生命周期方法
onInit: config.onInit || (() => {}),
onEnter: config.onEnter || (() => {}),
onUpdate: config.onUpdate || (() => {}),
onExit: config.onExit || (() => {}),
onDestroy: config.onDestroy || (() => {}),
// 状态
isInitialized: false,
isActive: false,
// 资源
resources: []
}
this.scenes.set(name, sceneData)
return sceneData
}
// 切换场景
switch(name) {
const sceneData = this.scenes.get(name)
if (!sceneData) {
console.error(`场景 "${name}" 不存在`)
return false
}
// 退出当前场景
if (this.currentScene && this.currentScene !== sceneData) {
this.currentScene.onExit()
this.currentScene.isActive = false
}
// 初始化新场景(首次进入)
if (!sceneData.isInitialized) {
sceneData.onInit(sceneData.scene)
sceneData.isInitialized = true
}
// 进入场景
sceneData.onEnter()
sceneData.isActive = true
this.currentScene = sceneData
this.clock.start()
return true
}
// 更新当前场景
update() {
if (!this.currentScene) return
const deltaTime = this.clock.getDelta()
const elapsedTime = this.clock.getElapsedTime()
this.currentScene.onUpdate(deltaTime, elapsedTime)
}
// 获取当前场景
getCurrentScene() {
return this.currentScene ? this.currentScene.scene : null
}
// 销毁场景
destroy(name) {
const sceneData = this.scenes.get(name)
if (!sceneData) return false
// 如果是当前场景,先退出
if (this.currentScene === sceneData) {
sceneData.onExit()
this.currentScene = null
}
// 执行销毁回调
sceneData.onDestroy()
// 清理场景资源
sceneData.scene.traverse((child) => {
if (child.geometry) child.geometry.dispose()
if (child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material]
mats.forEach(mat => mat.dispose())
}
})
this.scenes.delete(name)
return true
}
}
// 使用示例
const sceneManager = new SceneManager()
// 注册主菜单场景
sceneManager.register('menu', {
background: 0x333333,
onInit: (scene) => {
// 创建菜单对象
const title = createTitle()
const buttons = createButtons()
scene.add(title, ...buttons)
},
onEnter: () => {
console.log('进入菜单')
showUI()
},
onUpdate: (deltaTime, elapsedTime) => {
// 菜单动画
},
onExit: () => {
console.log('退出菜单')
hideUI()
}
})
// 注册游戏场景
sceneManager.register('game', {
background: 0x87ceeb,
fog: [0xcccccc, 50, 200],
onInit: (scene) => {
// 创建游戏对象
const player = createPlayer()
const enemies = createEnemies()
const terrain = createTerrain()
scene.add(player, ...enemies, terrain)
},
onEnter: () => {
console.log('进入游戏')
startGame()
},
onUpdate: (deltaTime, elapsedTime) => {
updatePlayer(deltaTime)
updateEnemies(deltaTime)
checkCollisions()
},
onExit: () => {
console.log('退出游戏')
pauseGame()
},
onDestroy: () => {
// 清理游戏数据
clearGameData()
}
})
// 切换场景
sceneManager.switch('game')
// 渲染循环
function animate() {
requestAnimationFrame(animate)
sceneManager.update()
const currentScene = sceneManager.getCurrentScene()
if (currentScene) {
renderer.render(currentScene, camera)
}
}动画系统
基础动画函数
javascript
// ===== 旋转动画 =====
function rotateAnimation(mesh, speed = 1) {
const clock = new THREE.Clock()
return {
update: () => {
const delta = clock.getDelta()
mesh.rotation.x += delta * speed
mesh.rotation.y += delta * speed
}
}
}
// ===== 弹跳动画 =====
function bounceAnimation(mesh, amplitude = 1, frequency = 1) {
const clock = new THREE.Clock()
return {
update: () => {
const time = clock.getElapsedTime()
mesh.position.y = Math.abs(Math.sin(time * frequency)) * amplitude
}
}
}
// ===== 往返运动 =====
function pingPongAnimation(mesh, start, end, speed = 1) {
const clock = new THREE.Clock()
return {
update: () => {
const time = clock.getElapsedTime()
const t = (Math.sin(time * speed) + 1) / 2 // 0 到 1
mesh.position.lerpVectors(start, end, t)
}
}
}
// ===== 轨道运动 =====
function orbitAnimation(mesh, center, radius, speed = 1) {
const clock = new THREE.Clock()
return {
update: () => {
const time = clock.getElapsedTime()
mesh.position.x = center.x + Math.cos(time * speed) * radius
mesh.position.z = center.z + Math.sin(time * speed) * radius
mesh.lookAt(center)
}
}
}缓动函数库
javascript
// 常用缓动函数
const Easing = {
// 线性
linear: t => t,
// 二次方
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
// 三次方
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
// 四次方
easeInQuart: t => t * t * t * t,
easeOutQuart: t => 1 - (--t) * t * t * t,
easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
// 正弦
easeInSine: t => 1 - Math.cos(t * Math.PI / 2),
easeOutSine: t => Math.sin(t * Math.PI / 2),
easeInOutSine: t => -(Math.cos(Math.PI * t) - 1) / 2,
// 指数
easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeInOutExpo: t => {
if (t === 0) return 0
if (t === 1) return 1
if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2
return (2 - Math.pow(2, -20 * t + 10)) / 2
},
// 弹性
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
},
// 弹跳
easeOutBounce: t => {
const n1 = 7.5625
const d1 = 2.75
if (t < 1 / d1) {
return n1 * t * t
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375
}
}
}动画类封装
javascript
class Animation {
constructor(options = {}) {
this.target = options.target
this.property = options.property
this.from = options.from !== undefined ? options.from : this.getInitialValue()
this.to = options.to
this.duration = options.duration || 1000 // 毫秒
this.easing = options.easing || 'linear'
this.onStart = options.onStart || (() => {})
this.onUpdate = options.onUpdate || (() => {})
this.onComplete = options.onComplete || (() => {})
this.elapsedTime = 0
this.isRunning = false
this.isComplete = false
}
getInitialValue() {
if (!this.target || !this.property) return 0
const value = this.target[this.property]
// Vector3 或 Color
if (value && typeof value.clone === 'function') {
return value.clone()
}
return value
}
start() {
this.elapsedTime = 0
this.isRunning = true
this.isComplete = false
this.onStart()
return this
}
stop() {
this.isRunning = false
return this
}
reset() {
this.elapsedTime = 0
this.isComplete = false
this.isRunning = false
return this
}
update(deltaTime) {
if (!this.isRunning || this.isComplete) return
this.elapsedTime += deltaTime * 1000 // 转换为毫秒
let t = Math.min(this.elapsedTime / this.duration, 1)
t = Easing[this.easing](t)
// 更新属性
this.updateProperty(t)
this.onUpdate(t)
if (t >= 1) {
this.isComplete = true
this.isRunning = false
this.onComplete()
}
}
updateProperty(t) {
if (!this.target || !this.property) return
const from = this.from
const to = this.to
// Vector3 或 Color
if (from && typeof from.lerp === 'function') {
this.target[this.property].lerpVectors(from, to, t)
}
// 数值
else if (typeof from === 'number') {
this.target[this.property] = from + (to - from) * t
}
}
}
// 使用示例
const moveAnimation = new Animation({
target: mesh.position,
property: 'position',
from: new THREE.Vector3(0, 0, 0),
to: new THREE.Vector3(5, 5, 5),
duration: 2000,
easing: 'easeInOutCubic',
onStart: () => console.log('动画开始'),
onUpdate: (t) => console.log('进度:', t),
onComplete: () => console.log('动画完成')
}).start()
// 在渲染循环中更新
function animate() {
const delta = clock.getDelta()
moveAnimation.update(delta)
renderer.render(scene, camera)
}动画队列
javascript
class AnimationSequence {
constructor() {
this.animations = []
this.currentIndex = 0
this.isPlaying = false
}
// 添加动画
add(animation) {
this.animations.push(animation)
return this
}
// 添加延迟
delay(duration) {
this.animations.push({
update: (deltaTime) => {
this.delayTime = (this.delayTime || 0) + deltaTime * 1000
return this.delayTime >= duration
},
isComplete: true // 标记延迟已添加
})
return this
}
// 并行执行
parallel(...animations) {
this.animations.push({
animations,
update: function(deltaTime) {
let allComplete = true
this.animations.forEach(anim => {
if (!anim.isComplete) {
anim.update(deltaTime)
if (!anim.isComplete) allComplete = false
}
})
this.isComplete = allComplete
return allComplete
}
})
return this
}
// 播放
play() {
this.currentIndex = 0
this.isPlaying = true
this.playNext()
return this
}
playNext() {
if (this.currentIndex >= this.animations.length) {
this.isPlaying = false
return
}
const animation = this.animations[this.currentIndex]
if (animation.start) {
animation.start()
}
}
update(deltaTime) {
if (!this.isPlaying) return
const current = this.animations[this.currentIndex]
if (current.update) {
current.update(deltaTime)
}
if (current.isComplete) {
this.currentIndex++
this.playNext()
}
}
stop() {
this.isPlaying = false
return this
}
reset() {
this.currentIndex = 0
this.isPlaying = false
this.animations.forEach(anim => {
if (anim.reset) anim.reset()
})
return this
}
}
// 使用示例
const sequence = new AnimationSequence()
.add(new Animation({
target: mesh.position,
property: 'position',
from: new THREE.Vector3(0, 0, 0),
to: new THREE.Vector3(5, 0, 0),
duration: 1000,
easing: 'easeOutQuad'
}))
.delay(500)
.add(new Animation({
target: mesh.position,
property: 'position',
from: new THREE.Vector3(5, 0, 0),
to: new THREE.Vector3(5, 5, 0),
duration: 1000,
easing: 'easeInOutCubic'
}))
.add(new Animation({
target: mesh.rotation,
property: 'rotation',
from: new THREE.Vector3(0, 0, 0),
to: new THREE.Vector3(0, Math.PI * 2, 0),
duration: 1000,
easing: 'easeOutElastic'
}))
.play()性能优化
按需渲染
javascript
class OnDemandRenderer {
constructor(renderer, scene, camera) {
this.renderer = renderer
this.scene = scene
this.camera = camera
this.needsRender = true
this.animationId = null
}
// 请求渲染
requestRender() {
this.needsRender = true
}
// 启动渲染循环
start() {
const animate = () => {
this.animationId = requestAnimationFrame(animate)
if (this.needsRender) {
this.renderer.render(this.scene, this.camera)
this.needsRender = false
}
}
animate()
}
// 停止渲染循环
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}
}
// 使用示例
const onDemand = new OnDemandRenderer(renderer, scene, camera)
// 只在交互时渲染
controls.addEventListener('change', () => onDemand.requestRender())
window.addEventListener('resize', () => {
onDemand.requestRender()
})
// 静态场景不需要循环
renderer.render(scene, camera)性能监控
javascript
class PerformanceMonitor {
constructor() {
this.frames = 0
this.lastTime = performance.now()
this.fps = 0
this.frameTime = 0
// 统计数据
this.minFPS = Infinity
this.maxFPS = 0
this.avgFPS = 0
this.fpsHistory = []
this.maxHistoryLength = 60
}
update() {
this.frames++
const currentTime = performance.now()
if (currentTime >= this.lastTime + 1000) {
// 计算 FPS
this.fps = Math.round(
(this.frames * 1000) / (currentTime - this.lastTime)
)
this.frameTime = (currentTime - this.lastTime) / this.frames
// 更新统计
this.minFPS = Math.min(this.minFPS, this.fps)
this.maxFPS = Math.max(this.maxFPS, this.fps)
this.fpsHistory.push(this.fps)
if (this.fpsHistory.length > this.maxHistoryLength) {
this.fpsHistory.shift()
}
this.avgFPS = this.fpsHistory.reduce((a, b) => a + b) / this.fpsHistory.length
// 重置
this.frames = 0
this.lastTime = currentTime
return true // 表示有新的 FPS 数据
}
return false
}
getReport() {
return {
current: this.fps,
frameTime: this.frameTime.toFixed(2) + ' ms',
min: this.minFPS === Infinity ? 0 : this.minFPS,
max: this.maxFPS,
avg: this.avgFPS.toFixed(1),
history: this.fpsHistory.slice()
}
}
// 性能等级判断
getPerformanceLevel() {
if (this.avgFPS >= 55) return 'excellent'
if (this.avgFPS >= 45) return 'good'
if (this.avgFPS >= 30) return 'acceptable'
return 'poor'
}
}
// 使用示例
const monitor = new PerformanceMonitor()
function animate() {
requestAnimationFrame(animate)
if (monitor.update()) {
console.log(monitor.getReport())
}
renderer.render(scene, camera)
}暂停与恢复
javascript
class PausableRenderLoop {
constructor(renderer, scene, camera) {
this.renderer = renderer
this.scene = scene
this.camera = camera
this.clock = new THREE.Clock()
this.isPaused = false
this.isVisible = true
this.callbacks = []
this.setupVisibilityHandler()
}
setupVisibilityHandler() {
// 监听页面可见性
document.addEventListener('visibilitychange', () => {
this.isVisible = !document.hidden
if (this.isVisible) {
this.clock.start()
} else {
this.clock.stop()
}
})
// 监听窗口失焦(可选)
window.addEventListener('blur', () => {
// 可以选择暂停
})
window.addEventListener('focus', () => {
// 可以选择恢复
})
}
pause() {
if (!this.isPaused) {
this.isPaused = true
this.clock.stop()
this.onPause()
}
}
resume() {
if (this.isPaused) {
this.isPaused = false
this.clock.start()
this.onResume()
}
}
onPause() {
// 可以在这里暂停音频等
}
onResume() {
// 可以在这里恢复音频等
}
addUpdateCallback(callback) {
this.callbacks.push(callback)
}
animate() {
requestAnimationFrame(() => this.animate())
// 暂停或不可见时不更新
if (this.isPaused || !this.isVisible) {
return
}
const deltaTime = this.clock.getDelta()
const elapsedTime = this.clock.getElapsedTime()
// 执行更新回调
this.callbacks.forEach(callback => {
callback(deltaTime, elapsedTime)
})
// 渲染
this.renderer.render(this.scene, this.camera)
}
start() {
this.clock.start()
this.animate()
}
}
// 使用示例
const gameLoop = new PausableRenderLoop(renderer, scene, camera)
gameLoop.addUpdateCallback((deltaTime, elapsedTime) => {
updatePlayer(deltaTime)
updateEnemies(deltaTime)
})
// 暂停游戏
pauseButton.addEventListener('click', () => {
gameLoop.pause()
showPauseMenu()
})
// 恢复游戏
resumeButton.addEventListener('click', () => {
gameLoop.resume()
hidePauseMenu()
})
gameLoop.start()常见问题
Q1: 动画不流畅?
javascript
// ❌ 问题:使用固定值更新
function animate() {
cube.rotation.x += 0.01 // 不同帧率下速度不同
renderer.render(scene, camera)
}
// ✓ 解决:使用 deltaTime
const clock = new THREE.Clock()
function animate() {
const deltaTime = clock.getDelta()
cube.rotation.x += deltaTime * speed // 帧率无关
renderer.render(scene, camera)
}Q2: 内存泄漏?
javascript
// 常见内存泄漏原因和解决方案
// 1. 未清理几何体和材质
function disposeObject(object) {
if (object.geometry) object.geometry.dispose()
if (object.material) {
const mats = Array.isArray(object.material)
? object.material
: [object.material]
mats.forEach(mat => {
for (const key in mat) {
if (mat[key] && typeof mat[key].dispose === 'function') {
mat[key].dispose()
}
}
mat.dispose()
})
}
}
// 2. 未清理事件监听
class MyAnimation {
constructor() {
this.onResize = this.onResize.bind(this)
window.addEventListener('resize', this.onResize)
}
dispose() {
window.removeEventListener('resize', this.onResize) // 必须移除
}
}
// 3. 循环中创建对象
// ❌ 每帧创建新对象
function animate() {
const vector = new THREE.Vector3() // 内存泄漏!
vector.set(1, 0, 0)
object.position.add(vector)
}
// ✓ 复用对象
const vector = new THREE.Vector3() // 只创建一次
function animate() {
vector.set(1, 0, 0)
object.position.add(vector)
}Q3: 标签页切换后动画异常?
javascript
// 问题:标签页切换后,deltaTime 可能非常大
// 解决:限制最大 deltaTime
const clock = new THREE.Clock()
function animate() {
let deltaTime = clock.getDelta()
// 限制最大 deltaTime
const maxDelta = 0.1 // 最大 100ms
if (deltaTime > maxDelta) {
deltaTime = maxDelta
}
update(deltaTime)
renderer.render(scene, camera)
}
// 或使用 Clock 的 getDelta 会自动处理
// 但最好还是添加限制Q4: 如何实现慢动作/快进?
javascript
class TimeController {
constructor() {
this.timeScale = 1.0
this.clock = new THREE.Clock()
}
getDelta() {
return this.clock.getDelta() * this.timeScale
}
// 慢动作
slowMotion(scale = 0.5) {
this.timeScale = scale
}
// 快进
fastForward(scale = 2.0) {
this.timeScale = scale
}
// 正常速度
normal() {
this.timeScale = 1.0
}
// 暂停时间
pause() {
this.timeScale = 0
}
}
// 使用
const timeController = new TimeController()
function animate() {
const deltaTime = timeController.getDelta()
update(deltaTime)
renderer.render(scene, camera)
}
// 慢动作
timeController.slowMotion(0.25) // 1/4 速度最佳实践
1. 使用 deltaTime 保证帧率无关
javascript
// ✓ 推荐
const clock = new THREE.Clock()
function animate() {
const deltaTime = clock.getDelta()
object.position.x += speed * deltaTime
renderer.render(scene, camera)
}
// ❌ 不推荐
function animate() {
object.position.x += 0.01
renderer.render(scene, camera)
}2. 避免在循环中创建对象
javascript
// ✓ 推荐:复用对象
const tempVector = new THREE.Vector3()
const tempMatrix = new THREE.Matrix4()
function animate() {
tempVector.set(1, 0, 0)
object.position.add(tempVector)
}
// ❌ 不推荐:循环中创建
function animate() {
const vector = new THREE.Vector3(1, 0, 0) // 每帧创建新对象
object.position.add(vector)
}3. 封装完整的渲染循环类
javascript
class Application {
constructor(options = {}) {
this.scene = new THREE.Scene()
this.camera = this.createCamera(options.camera)
this.renderer = this.createRenderer(options.renderer)
this.clock = new THREE.Clock()
this.updateCallbacks = []
this.isRunning = false
this.init()
}
createCamera(options = {}) {
return new THREE.PerspectiveCamera(
options.fov || 75,
window.innerWidth / window.innerHeight,
options.near || 0.1,
options.far || 1000
)
}
createRenderer(options = {}) {
const renderer = new THREE.WebGLRenderer({
antialias: options.antialias ?? true,
...options
})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
return renderer
}
init() {
document.body.appendChild(this.renderer.domElement)
this.setupResize()
this.initScene()
}
initScene() {
// 子类重写
}
setupResize() {
window.addEventListener('resize', () => {
const width = window.innerWidth
const height = window.innerHeight
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
this.renderer.setSize(width, height)
})
}
addUpdateCallback(callback) {
this.updateCallbacks.push(callback)
return () => {
const index = this.updateCallbacks.indexOf(callback)
if (index > -1) this.updateCallbacks.splice(index, 1)
}
}
update(deltaTime, elapsedTime) {
this.updateCallbacks.forEach(cb => cb(deltaTime, elapsedTime))
}
render() {
this.renderer.render(this.scene, this.camera)
}
animate() {
if (!this.isRunning) return
requestAnimationFrame(() => this.animate())
const deltaTime = this.clock.getDelta()
const elapsedTime = this.clock.getElapsedTime()
this.update(deltaTime, elapsedTime)
this.render()
}
start() {
this.isRunning = true
this.clock.start()
this.animate()
}
stop() {
this.isRunning = false
this.clock.stop()
}
dispose() {
this.stop()
// 清理场景
this.scene.traverse((child) => {
if (child.geometry) child.geometry.dispose()
if (child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material]
mats.forEach(mat => mat.dispose())
}
})
// 清理渲染器
this.renderer.dispose()
this.renderer.domElement.remove()
}
}
// 使用示例
class MyApp extends Application {
initScene() {
this.cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x00ff00 })
)
this.scene.add(this.cube)
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(5, 5, 5)
this.scene.add(light)
this.camera.position.set(0, 0, 5)
// 添加更新逻辑
this.addUpdateCallback((deltaTime) => {
this.cube.rotation.x += deltaTime
this.cube.rotation.y += deltaTime
})
}
}
const app = new MyApp()
app.start()