{T}

渲染循环与生命周期

渲染循环是 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 对比

特性requestAnimationFramesetInterval
帧率同步✓ 自动同步显示器✗ 固定间隔
后台暂停✓ 自动暂停✗ 继续执行
性能优化✓ 浏览器优化✗ 无优化
帧率稳定✓ 自动调节✗ 可能丢帧
电池友好✓ 暂停时不耗电✗ 持续耗电
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()-重置时钟
runningBoolean是否正在运行
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()

相关链接

下一步