相机系统
相机(Camera)决定了场景中的哪些内容会被渲染,以及渲染的视角。Three.js 提供了多种相机类型,每种都有其特定的应用场景。
系统架构概述
code
┌─────────────────────────────────────────────────────────────────────┐
│ 相机系统架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Camera │ ← 基类(抽象) │
│ │ (Object3D) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴───────┬─────────────────┬─────────────────┐ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │Perspective│ │Orthographic│ │ Cube │ │ Stereo │ │
│ │ Camera │ │ Camera │ │ Camera │ │ Camera │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 控制器系统 │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ OrbitControls │ FlyControls │ PointerLockControls │ ... │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘相机类型对比
| 相机类型 | 透视效果 | 主要用途 | 性能开销 | 复杂度 |
|---|---|---|---|---|
| PerspectiveCamera | 有(近大远小) | 3D游戏、可视化、建筑展示 | 低 | ★☆☆ |
| OrthographicCamera | 无(平行投影) | 2D游戏、CAD、UI渲染 | 低 | ★☆☆ |
| CubeCamera | 6面渲染 | 环境反射、天空盒 | 中 | ★★☆ |
| StereoCamera | 双眼渲染 | VR、立体显示 | 高 | ★★★ |
PerspectiveCamera(透视相机)
透视相机模拟人眼视觉,近大远小,是最常用的相机类型。
视锥体示意
code
Far Plane(远裁剪面)
┌─────────────────────┐
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ 可视区域 ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
┌───────────────────┐
│ Near Plane │
│ (近裁剪面) │
└───────────────────┘
▲
│
Camera
(相机位置)创建透视相机
javascript
const camera = new THREE.PerspectiveCamera(
fov, // 视野角度(Field of View)
aspect, // 宽高比
near, // 近裁剪面距离
far // 远裁剪面距离
)参数详解
| 参数 | 类型 | 说明 | 推荐值 | 注意事项 |
|---|---|---|---|---|
fov | Number | 垂直视野角度(度) | 45-75 | 太大导致变形,太小视野受限 |
aspect | Number | 画布宽高比 | width/height | 必须与渲染区域一致 |
near | Number | 近裁剪面距离 | 0.1-1 | 太小导致深度冲突 |
far | Number | 远裁剪面距离 | 根据场景 | 太大影响深度精度 |
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 // 远裁剪面
)参数详解
| 参数 | 类型 | 说明 | 注意事项 |
|---|---|---|---|
left | Number | 视图左边界 | 负值 |
right | Number | 视图右边界 | 正值 |
top | Number | 视图上边界 | 正值 |
bottom | Number | 视图下边界 | 负值 |
near | Number | 近裁剪面 | 0 或小正数 |
far | Number | 远裁剪面 | 足够大 |
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 | 自由旋转 | 科学可视化 | 无旋转限制 |
| MapControls | 2D地图操作 | 地图应用 | 限制垂直旋转 |
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 = 1Q4: 相机控制器不工作?
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