Three.js 相机系统简介
引言
在 Three.js 的三维世界中,相机(Camera)是连接用户与三维场景的桥梁,它决定了我们如何观察和呈现虚拟世界。理解相机的原理和参数配置,对于创建沉浸式的 3D 体验至关重要。本文将深入剖析 Three.js 中的相机系统,涵盖透视相机、正交相机的工作原理、参数配置、使用场景以及最佳实践。
1. 相机基础概念
1.1 什么是相机?
在 Three.js 中,相机定义了三维空间到二维屏幕的投影方式。它决定了哪些对象可见、如何可见,以及观察者的视角。
1.2 相机类型概览
Three.js 提供了多种相机类型,最常用的是:
- PerspectiveCamera(透视相机):模拟人眼视觉效果
- OrthographicCamera(正交相机):无透视效果的平行投影
- CubeCamera(立方体相机):用于环境映射
- ArrayCamera(阵列相机):用于多视口渲染
2. 透视相机 (PerspectiveCamera)
2.1 构造函数与参数
new THREE.PerspectiveCamera(fov: number, aspect: number, near: number, far: number)
参数详解
fov (Field of View) - 视野角度
- 类型: number
- 单位: 度(degrees)
- 范围: 通常 30° - 120°
- 作用: 控制垂直方向的视野范围
- 视觉效果:
- 小角度(30°-50°): 望远镜效果,视野狭窄
- 标准角度(60°-75°): 接近人眼自然视野
- 大角度(90°-120°): 鱼眼效果,视野宽广
aspect (Aspect Ratio) - 纵横比
- 类型: number
- 计算: 渲染区域宽度 / 高度
- 作用: 保持画面比例,防止拉伸变形
- 最佳实践: 动态响应窗口尺寸变化
function updateCameraAspect(): void {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); // 必须调用!
}
near (Near Clipping Plane) - 近裁剪面
- 类型: number
- 作用: 定义距离相机多近的物体开始被渲染
- 取值范围: 根据场景规模调整
- 注意事项:
- 值过小可能导致深度缓冲精度问题
- 值过大会裁剪掉近处物体
far (Far Clipping Plane) - 远裁剪面
- 类型: number
- 作用: 定义距离相机多远的物体停止被渲染
- 性能影响:
- 值过大会影响深度精度,可能导致 Z-fighting
- 值过小会裁剪掉远处物体
- 优化建议: 根据实际可见范围设置合理值
2.2 透视相机示例代码
import * as THREE from 'three';
// 创建透视相机
const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(
75, // fov: 75度视野
window.innerWidth / window.innerHeight, // aspect: 窗口比例
0.1, // near: 最近可见距离 0.1单位
1000 // far: 最远可见距离 1000单位
);
// 设置相机位置和朝向
camera.position.set(0, 2, 5);
camera.lookAt(0, 0, 0);
// 响应窗口尺寸变化
const handleResize = (): void => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', handleResize);
2.3 透视相机的使用场景
- 3D 游戏和交互应用
- 虚拟现实和增强现实
- 建筑可视化和室内设计
- 产品展示和电子商务
- 数据可视化和科学模拟
3. 正交相机 (OrthographicCamera)
3.1 构造函数与参数
new THREE.OrthographicCamera(
left: number,
right: number,
top: number,
bottom: number,
near: number,
far: number
)
参数详解
left, right, top, bottom - 边界参数
- 类型: number
- 作用: 定义视锥体的左右上下边界
- 特点: 保持物体尺寸不随距离变化
- 坐标系统: 原点通常在视口中心
near, far - 裁剪面
- 作用: 与透视相机相同,定义渲染范围
- 区别: 在正交投影中,距离不影响物体大小
3.2 正交相机配置方法
基于窗口尺寸的配置
const aspect: number = window.innerWidth / window.innerHeight;
const frustumSize: number = 10; // 可视区域高度
const camera: THREE.OrthographicCamera = new THREE.OrthographicCamera(
-frustumSize * aspect / 2, // left
frustumSize * aspect / 2, // right
frustumSize / 2, // top
-frustumSize / 2, // bottom
0.1, // near
1000 // far
);
基于像素坐标的配置
// 适用于 2D UI 和 HUD
const camera: THREE.OrthographicCamera = new THREE.OrthographicCamera(
0, // left: 从画布左边缘开始
window.innerWidth, // right: 到画布右边缘
window.innerHeight, // top: 从画布上边缘开始
0, // bottom: 到画布下边缘
-100, // near
100 // far
);
3.3 正交相机示例代码
import * as THREE from 'three';
// 创建正交相机
const aspect: number = window.innerWidth / window.innerHeight;
const frustumSize: number = 5;
const camera: THREE.OrthographicCamera = new THREE.OrthographicCamera(
-frustumSize * aspect / 2,
frustumSize * aspect / 2,
frustumSize / 2,
-frustumSize / 2,
0.1,
100
);
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);
// 响应式更新
const handleResize = (): void => {
const aspect: number = window.innerWidth / window.innerHeight;
camera.left = -frustumSize * aspect / 2;
camera.right = frustumSize * aspect / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', handleResize);
3.4 正交相机的使用场景
- 2D 游戏和用户界面
- CAD 和工程制图应用
- 等距投影游戏
- 数据图表和信息可视化
- 文字渲染和 HUD 系统
4. 相机控制与动画
4.1 相机位置和朝向控制
基本位置设置
// 设置相机位置
camera.position.set(x, y, z);
// 设置相机朝向的三种方法
// 方法1: lookAt() 函数
camera.lookAt(targetPosition);
// 方法2: 设置 up 向量 + lookAt
camera.up.set(0, 1, 0); // Y轴向上(默认)
camera.lookAt(targetPosition);
// 方法3: 手动设置旋转
camera.rotation.x = Math.PI / 4; // 45度
camera.rotation.y = Math.PI / 6; // 30度
相机动画示例
import * as THREE from 'three';
// 相机路径动画
const animateCamera = (): void => {
const time: number = Date.now() * 0.001;
// 圆周运动
camera.position.x = Math.cos(time) * 8;
camera.position.z = Math.sin(time) * 8;
camera.position.y = 2 + Math.sin(time * 2) * 1;
camera.lookAt(0, 0, 0);
};
// 平滑相机移动
class CameraController {
private camera: THREE.Camera;
private targetPosition: THREE.Vector3;
private currentPosition: THREE.Vector3;
constructor(camera: THREE.Camera) {
this.camera = camera;
this.targetPosition = new THREE.Vector3();
this.currentPosition = camera.position.clone();
}
setTarget(x: number, y: number, z: number): void {
this.targetPosition.set(x, y, z);
}
update(deltaTime: number): void {
// 线性插值
this.currentPosition.lerp(this.targetPosition, 0.1);
this.camera.position.copy(this.currentPosition);
}
}
4.2 相机控制系统
Three.js 提供了多种相机控制器:
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
// OrbitControls - 轨道控制器
const controls: OrbitControls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05; // 阻尼系数
controls.screenSpacePanning = false; // 限制在平面内平移
controls.minDistance = 5; // 最小缩放距离
controls.maxDistance = 50; // 最大缩放距离
controls.maxPolarAngle = Math.PI; // 最大垂直角度
// FirstPersonControls - 第一人称控制器
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls';
const controls: FirstPersonControls = new FirstPersonControls(camera, renderer.domElement);
controls.movementSpeed = 100; // 移动速度
controls.lookSpeed = 0.1; // 视角移动速度
5. 高级相机技术
5.1 多相机系统
相机切换
import * as THREE from 'three';
interface CameraCollection {
[key: string]: THREE.Camera;
}
class CameraManager {
private cameras: CameraCollection;
private activeCamera: THREE.Camera;
constructor() {
this.cameras = {
main: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000),
top: new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 1000),
side: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
};
this.activeCamera = this.cameras.main;
this.setupCameras();
}
private setupCameras(): void {
// 主相机
this.cameras.main.position.set(0, 2, 5);
(this.cameras.main as THREE.PerspectiveCamera).lookAt(0, 0, 0);
// 顶视图相机
this.cameras.top.position.set(0, 10, 0);
(this.cameras.top as THREE.OrthographicCamera).lookAt(0, 0, 0);
// 侧视图相机
this.cameras.side.position.set(10, 2, 0);
(this.cameras.side as THREE.PerspectiveCamera).lookAt(0, 0, 0);
}
switchCamera(name: string): void {
if (this.cameras[name]) {
this.activeCamera = this.cameras[name];
}
}
getActiveCamera(): THREE.Camera {
return this.activeCamera;
}
}
画中画效果
import * as THREE from 'three';
function renderPictureInPicture(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
mainCamera: THREE.Camera,
pipCamera: THREE.Camera
): void {
// 清除画布
renderer.clear();
// 渲染主场景(全屏)
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
renderer.render(scene, mainCamera);
// 渲染小窗口场景
const pipWidth: number = 300;
const pipHeight: number = 200;
const pipX: number = window.innerWidth - pipWidth - 20;
const pipY: number = 20;
renderer.setViewport(pipX, pipY, pipWidth, pipHeight);
renderer.setScissor(pipX, pipY, pipWidth, pipHeight);
renderer.setScissorTest(true);
// 添加边框效果
renderer.clearDepth(); // 只清除深度缓冲区
renderer.render(scene, pipCamera);
// 重置视口
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
renderer.setScissorTest(false);
}
5.2 后处理与相机效果
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
// 使用 EffectComposer 添加后处理效果
const composer: EffectComposer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass: UnrealBloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 强度
0.4, // 半径
0.85 // 阈值
);
composer.addPass(bloomPass);
// 在动画循环中使用 composer
const animate = (): void => {
requestAnimationFrame(animate);
composer.render();
};
6. 性能优化与最佳实践
6.1 相机参数优化
合理的裁剪面设置
import * as THREE from 'three';
// 不好的做法 - 范围过大
const badCamera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, aspect, 0.001, 1000000);
// 好的做法 - 根据场景需要设置
const goodCamera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
// 动态调整裁剪面
function updateClippingPlanes(
camera: THREE.PerspectiveCamera,
sceneBoundingBox: THREE.Box3
): void {
const size: THREE.Vector3 = new THREE.Vector3();
const center: THREE.Vector3 = new THREE.Vector3();
sceneBoundingBox.getSize(size);
sceneBoundingBox.getCenter(center);
const maxDim: number = Math.max(size.x, size.y, size.z);
const far: number = maxDim * 10; // 适当的安全边际
const near: number = maxDim / 1000;
camera.near = Math.max(0.1, near);
camera.far = Math.min(10000, far);
camera.updateProjectionMatrix();
}
视锥体剔除
import * as THREE from 'three';
// 启用视锥体剔除
const frustum: THREE.Frustum = new THREE.Frustum();
const cameraViewProjectionMatrix: THREE.Matrix4 = new THREE.Matrix4();
function isInView(camera: THREE.Camera, mesh: THREE.Mesh): boolean {
camera.updateMatrixWorld();
cameraViewProjectionMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix);
if (mesh.geometry.boundingBox) {
return frustum.intersectsBox(mesh.geometry.boundingBox);
}
return true;
}
6.2 内存管理与清理
import * as THREE from 'three';
class CameraLifecycle {
static disposeCamera(camera: THREE.Camera): void {
// 移除所有事件监听器
if ((camera as any).controls) {
(camera as any).controls.dispose();
}
// 清除相机属性
camera.position.set(0, 0, 0);
camera.rotation.set(0, 0, 0);
camera.scale.set(1, 1, 1);
// 如果有自定义属性,也需要清理
for (let key in camera) {
if (camera.hasOwnProperty(key) &&
!['uuid', 'type', 'id', 'parent', 'children'].includes(key)) {
delete (camera as any)[key];
}
}
}
}
7. 实战案例:第一人称相机系统
import * as THREE from 'three';
interface MoveState {
forward: boolean;
backward: boolean;
left: boolean;
right: boolean;
up: boolean;
down: boolean;
}
class FirstPersonCamera {
private camera: THREE.Camera;
private domElement: HTMLElement;
private moveSpeed: number;
private lookSpeed: number;
private moveState: MoveState;
private euler: THREE.Euler;
private velocity: THREE.Vector3;
constructor(camera: THREE.Camera, domElement: HTMLElement) {
this.camera = camera;
this.domElement = domElement;
this.moveSpeed = 5;
this.lookSpeed = 0.002;
this.moveState = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false
};
this.euler = new THREE.Euler(0, 0, 0, 'YXZ');
this.velocity = new THREE.Vector3();
this.bindEvents();
}
private bindEvents(): void {
document.addEventListener('keydown', this.onKeyDown.bind(this));
document.addEventListener('keyup', this.onKeyUp.bind(this));
document.addEventListener('mousemove', this.onMouseMove.bind(this));
document.addEventListener('click', this.lockPointer.bind(this));
}
private onKeyDown(event: KeyboardEvent): void {
switch (event.code) {
case 'KeyW': this.moveState.forward = true; break;
case 'KeyS': this.moveState.backward = true; break;
case 'KeyA': this.moveState.left = true; break;
case 'KeyD': this.moveState.right = true; break;
case 'Space': this.moveState.up = true; break;
case 'ShiftLeft': this.moveState.down = true; break;
}
}
private onKeyUp(event: KeyboardEvent): void {
switch (event.code) {
case 'KeyW': this.moveState.forward = false; break;
case 'KeyS': this.moveState.backward = false; break;
case 'KeyA': this.moveState.left = false; break;
case 'KeyD': this.moveState.right = false; break;
case 'Space': this.moveState.up = false; break;
case 'ShiftLeft': this.moveState.down = false; break;
}
}
private onMouseMove(event: MouseEvent): void {
if (!document.pointerLockElement) return;
this.euler.y -= event.movementX * this.lookSpeed;
this.euler.x -= event.movementY * this.lookSpeed;
this.euler.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, this.euler.x));
}
private lockPointer(): void {
(this.domElement as any).requestPointerLock();
}
update(deltaTime: number): void {
// 更新相机旋转
this.camera.quaternion.setFromEuler(this.euler);
// 计算移动方向
this.velocity.set(0, 0, 0);
if (this.moveState.forward) this.velocity.z -= 1;
if (this.moveState.backward) this.velocity.z += 1;
if (this.moveState.left) this.velocity.x -= 1;
if (this.moveState.right) this.velocity.x += 1;
if (this.moveState.up) this.velocity.y += 1;
if (this.moveState.down) this.velocity.y -= 1;
// 应用相机朝向
this.velocity.applyQuaternion(this.camera.quaternion);
this.velocity.normalize().multiplyScalar(this.moveSpeed * deltaTime);
this.camera.position.add(this.velocity);
}
}
// 使用示例
const fpsCamera: FirstPersonCamera = new FirstPersonCamera(camera, renderer.domElement);
const clock: THREE.Clock = new THREE.Clock();
const animate = (): void => {
requestAnimationFrame(animate);
const deltaTime: number = clock.getDelta();
fpsCamera.update(deltaTime);
renderer.render(scene, camera);
};
animate();
8. 总结
Three.js 的相机系统提供了强大而灵活的工具来创建各种 3D 视觉效果。通过深入理解透视相机和正交相机的原理、参数配置和使用场景,开发者可以:
- 创建逼真的 3D 体验:通过合理配置透视相机参数
- 实现精确的 2D 渲染:利用正交相机的无透视特性
- 优化性能:通过合理的裁剪面和视锥体设置
- 增强交互性:结合相机控制器和自定义相机系统