{T}

相机系统

相机(Camera)决定了场景中的哪些内容会被渲染,以及渲染的视角。Three.js 提供了多种相机类型,每种都有其特定的应用场景。

系统架构概述

code
┌─────────────────────────────────────────────────────────────────────┐
│                        相机系统架构                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌──────────────┐                                                  │
│   │    Camera    │ ← 基类(抽象)                                    │
│   │   (Object3D) │                                                  │
│   └──────┬───────┘                                                  │
│          │                                                          │
│   ┌──────┴───────┬─────────────────┬─────────────────┐             │
│   │              │                 │                 │             │
│   ▼              ▼                 ▼                 ▼             │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐           │
│ │Perspective│ │Orthographic│ │  Cube     │ │  Stereo   │           │
│ │  Camera   │ │  Camera   │ │  Camera   │ │  Camera   │           │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘           │
│                                                                     │
│   ┌──────────────────────────────────────────────────────────────┐ │
│   │                      控制器系统                                │ │
│   ├──────────────────────────────────────────────────────────────┤ │
│   │ OrbitControls │ FlyControls │ PointerLockControls │ ...     │ │
│   └──────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

相机类型对比

相机类型透视效果主要用途性能开销复杂度
PerspectiveCamera有(近大远小)3D游戏、可视化、建筑展示★☆☆
OrthographicCamera无(平行投影)2D游戏、CAD、UI渲染★☆☆
CubeCamera6面渲染环境反射、天空盒★★☆
StereoCamera双眼渲染VR、立体显示★★★

PerspectiveCamera(透视相机)

透视相机模拟人眼视觉,近大远小,是最常用的相机类型。

视锥体示意

code
                    Far Plane(远裁剪面)
                    ┌─────────────────────┐
                   ╱                       ╲
                  ╱                         ╲
                 ╱                           ╲
                ╱                             ╲
               ╱                               ╲
              ╱                                 ╲
             ╱                                   ╲
            ╱         可视区域                    ╲
           ╱                                       ╲
          ╱                                         ╲
         ╱                                           ╲
        ╱                                             ╲
       ┌───────────────────┐
       │  Near Plane       │
       │  (近裁剪面)      │
       └───────────────────┘
              ▲
              │
           Camera
           (相机位置)

创建透视相机

javascript
const camera = new THREE.PerspectiveCamera(
  fov,    // 视野角度(Field of View)
  aspect, // 宽高比
  near,   // 近裁剪面距离
  far     // 远裁剪面距离
)

参数详解

参数类型说明推荐值注意事项
fovNumber垂直视野角度(度)45-75太大导致变形,太小视野受限
aspectNumber画布宽高比width/height必须与渲染区域一致
nearNumber近裁剪面距离0.1-1太小导致深度冲突
farNumber远裁剪面距离根据场景太大影响深度精度
javascript
const camera = new THREE.PerspectiveCamera(
  75,                                      // FOV: 75度
  window.innerWidth / window.innerHeight,  // 宽高比
  0.1,                                     // 近裁剪面: 0.1
  1000                                     // 远裁剪面: 1000
)

// FOV(Field of View)视野角度
// - 人眼水平 FOV 约 120-140 度,垂直约 100-120 度
// - 常用值:45 度(自然)、60 度(标准)、75 度(宽广)、90+ 度(超广角)
// - 值越大,看到的范围越广,但边缘变形越明显

// Near 近裁剪面
// - 距离相机小于 near 的对象不会被渲染
// - 太小(如 0.0001)会导致深度冲突(Z-fighting)
// - 推荐值:0.1 - 1.0

// Far 远裁剪面
// - 距离相机大于 far 的对象不会被渲染
// - far/near 比值影响深度缓冲精度
// - 推荐比值不超过 1000-10000

// Aspect 宽高比
// - 必须与 canvas 的实际显示宽高比一致
// - 否则画面会被拉伸或压缩

FOV 对比效果

FOV 值视觉效果适用场景
30-45°平静、自然建筑可视化、产品展示
50-60°标准视角一般游戏、模拟器
70-90°宽广、动感FPS游戏、动作游戏
100-120°超广角、夸张特殊效果、VR

常用属性和方法

javascript
// ===== 位置与朝向 =====
// 设置位置
camera.position.set(0, 5, 10)
camera.position.x = 5

// 朝向目标点
camera.lookAt(0, 0, 0)
camera.lookAt(new THREE.Vector3(0, 0, 0))

// ===== 投影相关 =====
// 更新投影矩阵(修改参数后必须调用)
camera.fov = 60
camera.updateProjectionMatrix()

// 获取视锥体
const frustum = new THREE.Frustum()
camera.updateMatrixWorld()
frustum.setFromProjectionMatrix(
  new THREE.Matrix4().multiplyMatrices(
    camera.projectionMatrix,
    camera.matrixWorldInverse
  )
)

// ===== 视口大小 =====
// 获取可视区域在指定距离的尺寸
function getVisibleSizeAtDistance(distance) {
  const vFov = THREE.MathUtils.degToRad(camera.fov)
  const height = 2 * Math.tan(vFov / 2) * distance
  const width = height * camera.aspect
  return { width, height }
}

// 计算物体在屏幕上的大小
function getScreenSize(object, camera, renderer) {
  const size = new THREE.Vector3()
  const center = new THREE.Vector3()
  
  new THREE.Box3().setFromObject(object).getSize(size)
  object.getWorldPosition(center)
  
  const distance = camera.position.distanceTo(center)
  const visibleSize = getVisibleSizeAtDistance(distance)
  
  return {
    width: (size.x / visibleSize.width) * renderer.domElement.width,
    height: (size.y / visibleSize.height) * renderer.domElement.height
  }
}

应用示例

javascript
// ===== 第一人称视角 =====
const fpsCamera = new THREE.PerspectiveCamera(
  75,              // 较大 FOV 增加视野
  width / height,
  0.1,
  1000
)
fpsCamera.position.set(0, 1.6, 0)  // 人眼高度约 1.6m

// ===== 第三人称视角 =====
const tpsCamera = new THREE.PerspectiveCamera(
  60,
  width / height,
  0.1,
  1000
)
tpsCamera.position.set(0, 5, 10)
tpsCamera.lookAt(player.position)

// ===== 等距视角(使用透视相机模拟)=====
const isoCamera = new THREE.PerspectiveCamera(
  45,  // 较小 FOV 减少透视变形
  width / height,
  0.1,
  1000
)
isoCamera.position.set(10, 10, 10)
isoCamera.lookAt(0, 0, 0)

// ===== 望远镜/狙击镜效果 =====
function zoomTo(targetFOV, duration = 500) {
  const startFOV = camera.fov
  const startTime = Date.now()
  
  function animate() {
    const elapsed = Date.now() - startTime
    const progress = Math.min(elapsed / duration, 1)
    
    camera.fov = startFOV + (targetFOV - startFOV) * progress
    camera.updateProjectionMatrix()
    
    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }
  animate()
}

// 放大
zoomTo(20)   // 狙击镜效果
// 恢复
zoomTo(75)   // 正常视野

OrthographicCamera(正交相机)

正交相机使用平行投影,没有透视效果,远近大小相同,常用于 2D 游戏、CAD 软件或 UI 渲染。

视图区域示意

code
        Top
         ┌───────────────────┐
         │                   │
  Left   │                   │  Right
         │     可视区域       │
         │                   │
         │                   │
         └───────────────────┘
              Bottom
              
    ┌─────────────────────────────────────┐
    │                                     │  Far
    │         所有深度范围                  │
    │                                     │
    └─────────────────────────────────────┘
                  Near

创建正交相机

javascript
const camera = new THREE.OrthographicCamera(
  left,   // 左边界
  right,  // 右边界
  top,    // 上边界
  bottom, // 下边界
  near,   // 近裁剪面
  far     // 远裁剪面
)

参数详解

参数类型说明注意事项
leftNumber视图左边界负值
rightNumber视图右边界正值
topNumber视图上边界正值
bottomNumber视图下边界负值
nearNumber近裁剪面0 或小正数
farNumber远裁剪面足够大
javascript
const frustumSize = 10  // 可视区域高度的一半
const aspect = window.innerWidth / window.innerHeight

const camera = new THREE.OrthographicCamera(
  frustumSize * aspect / -2,  // left   = -width/2
  frustumSize * aspect / 2,   // right  = width/2
  frustumSize / 2,            // top    = height/2
  frustumSize / -2,           // bottom = -height/2
  0.1,                        // near
  1000                        // far
)

// 正交相机的视图范围由 left, right, top, bottom 决定
// 对象大小不受距离影响
// frustumSize 控制缩放级别:值越小,物体显示越大

应用示例

javascript
// ===== 2D 游戏/界面相机 =====
const camera2D = new THREE.OrthographicCamera(
  -window.innerWidth / 2,
  window.innerWidth / 2,
  window.innerHeight / 2,
  -window.innerHeight / 2,
  0.1,
  100
)
camera2D.position.z = 10

// ===== 像素完美的 2D 渲染 =====
const pixelSize = 1  // 像素大小
const camera = new THREE.OrthographicCamera(
  -window.innerWidth / pixelSize / 2,
  window.innerWidth / pixelSize / 2,
  window.innerHeight / pixelSize / 2,
  -window.innerHeight / pixelSize / 2,
  0.1,
  100
)

// ===== 等距视角(Isometric)=====
const isoCamera = new THREE.OrthographicCamera(
  -10, 10, 10, -10, 0.1, 100
)
isoCamera.position.set(10, 10, 10)
isoCamera.lookAt(0, 0, 0)

// 等距视角的特定角度
const angle = Math.atan(1 / Math.sqrt(2))  // 约 35.264 度
isoCamera.position.set(10, 10 * Math.tan(angle), 10)
isoCamera.lookAt(0, 0, 0)

// ===== 小地图相机 =====
const minimapCamera = new THREE.OrthographicCamera(
  -50, 50, 50, -50, 0.1, 100
)
minimapCamera.position.set(0, 100, 0)
minimapCamera.lookAt(0, 0, 0)

// ===== 缩放控制 =====
function zoomOrthographic(camera, zoomLevel) {
  const frustumSize = 10 / zoomLevel
  const aspect = camera.right / camera.top
  
  camera.left = frustumSize * aspect / -2
  camera.right = frustumSize * aspect / 2
  camera.top = frustumSize / 2
  camera.bottom = frustumSize / -2
  camera.updateProjectionMatrix()
}

CubeCamera(立方体相机)

用于创建环境贴图或反射效果,从中心点向 6 个方向渲染。

创建立方体相机

javascript
// 创建渲染目标
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
  format: THREE.RGBAFormat,
  generateMipmaps: true,
  minFilter: THREE.LinearMipmapLinearFilter
})

// 创建立方体相机
const cubeCamera = new THREE.CubeCamera(
  1,      // 近裁剪面
  1000,   // 远裁剪面
  cubeRenderTarget
)
scene.add(cubeCamera)

// 使用环境贴图
const reflectiveMaterial = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  metalness: 1,
  roughness: 0,
  envMap: cubeRenderTarget.texture
})

// 在渲染循环中更新
function animate() {
  // 先隐藏反射对象
  reflectiveObject.visible = false
  
  // 更新环境贴图
  cubeCamera.update(renderer, scene)
  
  // 显示反射对象
  reflectiveObject.visible = true
  
  // 渲染场景
  renderer.render(scene, camera)
}

应用示例:动态反射

javascript
// 创建一个反射球体
const sphereGeometry = new THREE.SphereGeometry(2, 32, 32)
const sphereMaterial = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  metalness: 1,
  roughness: 0,
  envMap: cubeRenderTarget.texture
})
const reflectiveSphere = new THREE.Mesh(sphereGeometry, sphereMaterial)

// 立方体相机跟随球体位置
cubeCamera.position.copy(reflectiveSphere.position)

StereoCamera(立体相机)

用于 VR 设备或 3D 立体显示,渲染左右两个视角。

javascript
const stereoCamera = new THREE.StereoCamera()

// 配置参数
stereoCamera.eyeSep = 0.064  // 瞳距(米)

// 渲染
function render() {
  stereoCamera.update(camera)
  
  const size = renderer.getSize(new THREE.Vector2())
  
  renderer.setScissorTest(true)
  
  // 渲染左眼
  renderer.setScissor(0, 0, size.width / 2, size.height)
  renderer.setViewport(0, 0, size.width / 2, size.height)
  renderer.render(scene, stereoCamera.cameraL)
  
  // 渲染右眼
  renderer.setScissor(size.width / 2, 0, size.width / 2, size.height)
  renderer.setViewport(size.width / 2, 0, size.width / 2, size.height)
  renderer.render(scene, stereoCamera.cameraR)
  
  renderer.setScissorTest(false)
}

相机控制器

Three.js 提供了多种控制器,用于实现不同的相机交互方式。

控制器对比

控制器交互方式适用场景特点
OrbitControls鼠标拖拽旋转、缩放模型展示、编辑器最常用、功能全面
FlyControls键盘移动、鼠标朝向飞行模拟、漫游自由度高
FirstPersonControls第一人称移动行走漫游简单易用
PointerLockControls鼠标锁定FPS游戏精确控制
TrackballControls自由旋转科学可视化无旋转限制
MapControls2D地图操作地图应用限制垂直旋转

OrbitControls(轨道控制器)

最常用的控制器,允许围绕目标点旋转、缩放、平移。

javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

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

// ===== 基础配置 =====
// 阻尼(惯性)
controls.enableDamping = true      // 开启阻尼
controls.dampingFactor = 0.05      // 阻尼系数

// 缩放
controls.enableZoom = true         // 允许缩放
controls.zoomSpeed = 1.0           // 缩放速度
controls.minDistance = 2           // 最小距离
controls.maxDistance = 100         // 最大距离

// 旋转
controls.enableRotate = true       // 允许旋转
controls.rotateSpeed = 1.0         // 旋转速度
controls.minPolarAngle = 0         // 最小垂直角度
controls.maxPolarAngle = Math.PI / 2  // 最大垂直角度(限制在地面上方)

// 平移
controls.enablePan = true          // 允许平移
controls.panSpeed = 1.0            // 平移速度
controls.screenSpacePanning = true // 屏幕空间平移

// 目标点
controls.target.set(0, 0, 0)       // 相机围绕的目标点

// ===== 高级配置 =====
// 自动旋转
controls.autoRotate = true         // 自动旋转
controls.autoRotateSpeed = 2.0     // 自动旋转速度

// 按键控制
controls.keys = {
  LEFT: 'KeyA',
  UP: 'KeyW',
  RIGHT: 'KeyD',
  BOTTOM: 'KeyS'
}

// 鼠标按钮
controls.mouseButtons = {
  LEFT: THREE.MOUSE.ROTATE,
  MIDDLE: THREE.MOUSE.DOLLY,
  RIGHT: THREE.MOUSE.PAN
}

// 触摸配置
controls.touches = {
  ONE: THREE.TOUCH.ROTATE,
  TWO: THREE.TOUCH.DOLLY_PAN
}

// ===== 更新控制器 =====
function animate() {
  requestAnimationFrame(animate)
  
  controls.update()  // 必须在阻尼开启时调用
  renderer.render(scene, camera)
}

// ===== 事件监听 =====
controls.addEventListener('change', () => {
  console.log('控制器变化')
})

controls.addEventListener('start', () => {
  console.log('开始交互')
})

controls.addEventListener('end', () => {
  console.log('结束交互')
})

FlyControls(飞行控制器)

第一人称飞行控制,适用于飞行模拟器或大型场景漫游。

javascript
import { FlyControls } from 'three/examples/jsm/controls/FlyControls.js'

const controls = new FlyControls(camera, renderer.domElement)

// 配置参数
controls.movementSpeed = 10        // 移动速度
controls.rollSpeed = 0.5           // 滚转速度
controls.autoForward = false       // 自动前进
controls.dragToLook = true         // 拖拽查看

// 更新控制器(需要 deltaTime)
const clock = new THREE.Clock()

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

// 键盘控制
// W/S: 前进/后退
// A/D: 左右移动
// R/F: 上下移动
// 鼠标移动: 视角控制

FirstPersonControls(第一人称控制器)

简单的第一人称控制,适用于行走漫游。

javascript
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls.js'

const controls = new FirstPersonControls(camera, renderer.domElement)

// 配置参数
controls.movementSpeed = 10        // 移动速度
controls.lookSpeed = 0.1           // 查看速度
controls.lookVertical = true       // 允许垂直查看
controls.constrainVertical = true  // 限制垂直角度
controls.verticalMin = 0           // 最小垂直角度
controls.verticalMax = Math.PI     // 最大垂直角度

// 禁用上下查看(保持水平)
controls.lookVertical = false

const clock = new THREE.Clock()

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

PointerLockControls(指针锁定控制器)

用于 FPS 游戏,鼠标锁定在屏幕中心,实现精确的视角控制。

javascript
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js'

const controls = new PointerLockControls(camera, document.body)

// 点击开始锁定
document.addEventListener('click', () => {
  controls.lock()
})

// 监听锁定状态
controls.addEventListener('lock', () => {
  console.log('鼠标已锁定,游戏开始')
  // 显示游戏 UI
})

controls.addEventListener('unlock', () => {
  console.log('鼠标已解锁,游戏暂停')
  // 显示暂停菜单
})

// 移动控制
const velocity = new THREE.Vector3()
const direction = new THREE.Vector3()
const moveState = {
  forward: false,
  backward: false,
  left: false,
  right: false
}

document.addEventListener('keydown', (event) => {
  switch (event.code) {
    case 'KeyW': moveState.forward = true; break
    case 'KeyS': moveState.backward = true; break
    case 'KeyA': moveState.left = true; break
    case 'KeyD': moveState.right = true; break
  }
})

document.addEventListener('keyup', (event) => {
  switch (event.code) {
    case 'KeyW': moveState.forward = false; break
    case 'KeyS': moveState.backward = false; break
    case 'KeyA': moveState.left = false; break
    case 'KeyD': moveState.right = false; break
  }
})

function animate() {
  requestAnimationFrame(animate)
  
  if (controls.isLocked) {
    // 计算移动方向
    direction.z = Number(moveState.forward) - Number(moveState.backward)
    direction.x = Number(moveState.right) - Number(moveState.left)
    direction.normalize()
    
    // 应用移动
    const speed = 0.1
    if (moveState.forward || moveState.backward) {
      controls.moveForward(direction.z * speed)
    }
    if (moveState.left || moveState.right) {
      controls.moveRight(direction.x * speed)
    }
  }
  
  renderer.render(scene, camera)
}

TrackballControls(轨迹球控制器)

类似 OrbitControls,但旋转更自由,可以翻转相机。

javascript
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'

const controls = new TrackballControls(camera, renderer.domElement)

controls.rotateSpeed = 1.0
controls.zoomSpeed = 1.2
controls.panSpeed = 0.8
controls.noZoom = false
controls.noPan = false
controls.staticMoving = true
controls.dynamicDampingFactor = 0.3

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

相机动画

相机路径动画

javascript
// 创建路径
const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(0, 5, 10),
  new THREE.Vector3(10, 5, 0),
  new THREE.Vector3(0, 5, -10),
  new THREE.Vector3(-10, 5, 0)
], true)  // true = 闭合路径

// 创建路径可视化
const points = path.getPoints(50)
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const material = new THREE.LineBasicMaterial({ color: 0xff0000 })
const curveObject = new THREE.Line(geometry, material)
scene.add(curveObject)

// 相机沿路径移动
let t = 0
const speed = 0.001

function animate() {
  requestAnimationFrame(animate)
  
  t += speed
  if (t > 1) t = 0
  
  // 获取路径上的位置
  const position = path.getPointAt(t)
  camera.position.copy(position)
  
  // 获取路径切线方向,让相机朝向前方
  const tangent = path.getTangentAt(t)
  const lookAtPoint = position.clone().add(tangent)
  camera.lookAt(lookAtPoint)
  
  renderer.render(scene, camera)
}

相机跟随

javascript
// 跟随目标对象
const target = player
const cameraOffset = new THREE.Vector3(0, 5, 10)

function animate() {
  requestAnimationFrame(animate)
  
  // 更新目标位置(例如玩家移动)
  // player.position.add(velocity)
  
  // 相机跟随目标(平滑过渡)
  const targetCameraPos = target.position.clone().add(cameraOffset)
  camera.position.lerp(targetCameraPos, 0.1)  // 0.1 = 跟随平滑度
  
  // 相机朝向目标
  camera.lookAt(target.position)
  
  renderer.render(scene, camera)
}

// 第三人称相机跟随
class ThirdPersonCamera {
  constructor(camera, target, offset = new THREE.Vector3(0, 5, 10)) {
    this.camera = camera
    this.target = target
    this.offset = offset
    this.currentPosition = new THREE.Vector3()
    this.lookAtPosition = new THREE.Vector3()
  }
  
  update(deltaTime) {
    // 计算目标相机位置
    const targetPosition = this.target.position.clone().add(this.offset)
    
    // 平滑移动相机
    this.currentPosition.lerp(targetPosition, 0.1)
    this.camera.position.copy(this.currentPosition)
    
    // 平滑朝向目标
    this.lookAtPosition.lerp(this.target.position, 0.1)
    this.camera.lookAt(this.lookAtPosition)
  }
}

平滑过渡动画

javascript
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'

// 相机动画到目标位置
function animateCamera(camera, targetPosition, targetLookAt, duration = 2000) {
  const startPosition = camera.position.clone()
  const startLookAt = new THREE.Vector3()
  camera.getWorldDirection(startLookAt)
  startLookAt.multiplyScalar(10).add(camera.position)
  
  const tween = new TWEEN.Tween({ t: 0 })
    .to({ t: 1 }, duration)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .onUpdate((obj) => {
      // 更新位置
      camera.position.lerpVectors(startPosition, targetPosition, obj.t)
      
      // 更新朝向
      const lookAt = new THREE.Vector3().lerpVectors(startLookAt, targetLookAt, obj.t)
      camera.lookAt(lookAt)
    })
    .start()
  
  return tween
}

// 使用
animateCamera(
  camera,
  new THREE.Vector3(5, 5, 5),
  new THREE.Vector3(0, 0, 0),
  2000
)

// 动画循环中更新 TWEEN
function animate() {
  requestAnimationFrame(animate)
  TWEEN.update()
  renderer.render(scene, camera)
}

相机特效

相机震动

javascript
class CameraShake {
  constructor(camera) {
    this.camera = camera
    this.originalPosition = camera.position.clone()
    this.shakeIntensity = 0
    this.shakeDecay = 0.95
  }
  
  shake(intensity = 0.5) {
    this.shakeIntensity = intensity
  }
  
  update() {
    if (this.shakeIntensity > 0.001) {
      // 添加随机偏移
      this.camera.position.x = this.originalPosition.x + 
        (Math.random() - 0.5) * this.shakeIntensity
      this.camera.position.y = this.originalPosition.y + 
        (Math.random() - 0.5) * this.shakeIntensity
      this.camera.position.z = this.originalPosition.z + 
        (Math.random() - 0.5) * this.shakeIntensity
      
      // 衰减
      this.shakeIntensity *= this.shakeDecay
    } else {
      // 恢复原位
      this.camera.position.copy(this.originalPosition)
      this.shakeIntensity = 0
    }
  }
}

// 使用
const cameraShake = new CameraShake(camera)

// 触发震动
cameraShake.shake(0.5)  // 爆炸效果

// 更新
function animate() {
  cameraShake.update()
  renderer.render(scene, camera)
}

景深效果(通过 FOV 变化)

javascript
class FocusEffect {
  constructor(camera) {
    this.camera = camera
    this.targetFOV = camera.fov
    this.currentFOV = camera.fov
    this.focusSpeed = 0.1
  }
  
  focusOn(object) {
    // 计算距离
    const distance = this.camera.position.distanceTo(object.position)
    
    // 根据距离调整 FOV
    if (distance < 5) {
      this.targetFOV = 30  // 近距离:窄 FOV
    } else if (distance < 15) {
      this.targetFOV = 50
    } else {
      this.targetFOV = 75  // 远距离:宽 FOV
    }
  }
  
  update() {
    // 平滑过渡
    this.currentFOV += (this.targetFOV - this.currentFOV) * this.focusSpeed
    this.camera.fov = this.currentFOV
    this.camera.updateProjectionMatrix()
  }
}

视锥体裁剪

javascript
// 视锥体裁剪检测
class FrustumCulling {
  constructor(camera) {
    this.camera = camera
    this.frustum = new THREE.Frustum()
    this.projScreenMatrix = new THREE.Matrix4()
  }
  
  update() {
    // 更新视锥体
    this.projScreenMatrix.multiplyMatrices(
      this.camera.projectionMatrix,
      this.camera.matrixWorldInverse
    )
    this.frustum.setFromProjectionMatrix(this.projScreenMatrix)
  }
  
  isVisible(object) {
    return this.frustum.intersectsObject(object)
  }
  
  containsPoint(point) {
    return this.frustum.containsPoint(point)
  }
  
  // 检查并更新对象可见性
  cullObjects(objects) {
    this.update()
    objects.forEach((obj) => {
      obj.visible = this.isVisible(obj)
    })
  }
}

// 使用
const culling = new FrustumCulling(camera)

function animate() {
  culling.cullObjects(scene.children)
  renderer.render(scene, camera)
}

响应式相机

javascript
// 响应窗口大小变化
function setupResponsiveCamera(camera, renderer) {
  window.addEventListener('resize', () => {
    const width = window.innerWidth
    const height = window.innerHeight
    
    if (camera.isPerspectiveCamera) {
      // 透视相机
      camera.aspect = width / height
      camera.updateProjectionMatrix()
    } else if (camera.isOrthographicCamera) {
      // 正交相机
      const frustumSize = 10
      const aspect = width / height
      
      camera.left = frustumSize * aspect / -2
      camera.right = frustumSize * aspect / 2
      camera.top = frustumSize / 2
      camera.bottom = frustumSize / -2
      camera.updateProjectionMatrix()
    }
    
    // 更新渲染器
    renderer.setSize(width, height)
  })
}

// 封装响应式相机类
class ResponsiveCamera {
  constructor(type = 'perspective', options = {}) {
    this.type = type
    this.camera = this.createCamera(options)
    this.setupResize()
  }
  
  createCamera(options) {
    const aspect = window.innerWidth / window.innerHeight
    
    if (this.type === 'perspective') {
      return new THREE.PerspectiveCamera(
        options.fov || 75,
        aspect,
        options.near || 0.1,
        options.far || 1000
      )
    } else {
      const frustumSize = options.frustumSize || 10
      return new THREE.OrthographicCamera(
        frustumSize * aspect / -2,
        frustumSize * aspect / 2,
        frustumSize / 2,
        frustumSize / -2,
        options.near || 0.1,
        options.far || 1000
      )
    }
  }
  
  setupResize() {
    window.addEventListener('resize', () => {
      const aspect = window.innerWidth / window.innerHeight
      
      if (this.type === 'perspective') {
        this.camera.aspect = aspect
      } else {
        const frustumSize = 10
        this.camera.left = frustumSize * aspect / -2
        this.camera.right = frustumSize * aspect / 2
        this.camera.top = frustumSize / 2
        this.camera.bottom = frustumSize / -2
      }
      
      this.camera.updateProjectionMatrix()
    })
  }
}

常见问题

Q1: 相机看到黑色画面?

诊断与解决

javascript
// 1. 检查相机位置和方向
console.log('Camera position:', camera.position)
console.log('Camera direction:', camera.getWorldDirection(new THREE.Vector3()))

// 2. 确保相机看向场景中的对象
camera.lookAt(0, 0, 0)

// 3. 检查对象是否在视锥体内
const frustum = new THREE.Frustum()
camera.updateMatrixWorld()
frustum.setFromProjectionMatrix(
  new THREE.Matrix4().multiplyMatrices(
    camera.projectionMatrix,
    camera.matrixWorldInverse
  )
)
console.log('Object in frustum:', frustum.containsPoint(object.position))

// 4. 检查 near/far 设置
console.log('Near:', camera.near, 'Far:', camera.far)

// 5. 检查场景中是否有光源
console.log('Lights:', scene.children.filter(c => c.isLight))

Q2: 画面变形?

javascript
// 原因:宽高比不正确
// 解决:确保宽高比与渲染区域一致

// 错误示例
const camera = new THREE.PerspectiveCamera(75, 16/9, 0.1, 1000)
// 但实际渲染区域不是 16:9

// 正确做法
const width = renderer.domElement.clientWidth
const height = renderer.domElement.clientHeight
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

// 响应式更新
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
})

Q3: 深度冲突(Z-fighting)?

javascript
// 症状:近距离平面出现闪烁条纹
// 原因:near/far 比值过大,深度精度不足

// 解决方案 1:调整近裁剪面
const camera = new THREE.PerspectiveCamera(
  75, aspect,
  1,    // near 从 0.1 改为 1
  1000
)

// 解决方案 2:使用对数深度缓冲
const renderer = new THREE.WebGLRenderer({
  logarithmicDepthBuffer: true
})

// 解决方案 3:调整远裁剪面
const camera = new THREE.PerspectiveCamera(
  75, aspect, 1,
  100   // far 从 1000 改为 100(如果够用)
)

// 解决方案 4:添加深度偏移(材质设置)
material.polygonOffset = true
material.polygonOffsetFactor = 1
material.polygonOffsetUnits = 1

Q4: 相机控制器不工作?

javascript
// 检查控制器是否正确初始化
const controls = new OrbitControls(camera, renderer.domElement)

// 如果开启阻尼,必须在动画循环中调用 update()
controls.enableDamping = true

function animate() {
  controls.update()  // 必须调用
  renderer.render(scene, camera)
  requestAnimationFrame(animate)
}

// 检查事件目标是否正确
// 第二个参数应该是渲染器的 DOM 元素
new OrbitControls(camera, renderer.domElement)  // ✓ 正确
new OrbitControls(camera, document.body)        // ✗ 可能有问题

最佳实践

1. 根据场景选择相机和控制器

javascript
// 3D 游戏/可视化
const camera = new THREE.PerspectiveCamera(60, aspect, 1, 1000)
const controls = new OrbitControls(camera, renderer.domElement)

// 2D 游戏/UI
const camera = new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 100)

// FPS 游戏
const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000)
const controls = new PointerLockControls(camera, document.body)

// 飞行模拟
const camera = new THREE.PerspectiveCamera(60, aspect, 1, 5000)
const controls = new FlyControls(camera, renderer.domElement)

// 反射效果
const cubeCamera = new THREE.CubeCamera(1, 1000, renderTarget)

2. 合理设置相机参数

javascript
// 推荐配置模板
const camera = new THREE.PerspectiveCamera(
  60,   // FOV: 60 度是平衡视野和变形的好选择
  window.innerWidth / window.innerHeight,
  1,    // near: 不要设太小,避免 Z-fighting
  1000  // far: 根据场景大小设置
)

// 不同场景的推荐配置
const configs = {
  // 室内场景
  indoor: { fov: 60, near: 0.5, far: 100 },
  
  // 室外场景
  outdoor: { fov: 60, near: 1, far: 2000 },
  
  // 大型开放世界
  openWorld: { fov: 70, near: 1, far: 10000 },
  
  // 产品展示
  product: { fov: 45, near: 0.1, far: 100 },
  
  // VR 应用
  vr: { fov: 90, near: 0.1, far: 1000 }
}

3. 性能优化

javascript
// 按需渲染
let needsRender = true

controls.addEventListener('change', () => {
  needsRender = true
})

function animate() {
  requestAnimationFrame(animate)
  
  if (needsRender) {
    controls.update()
    renderer.render(scene, camera)
    needsRender = false
  }
}

// 使用视锥体裁剪(自动开启)
object.frustumCulled = true

相关链接

下一步