3、相机系统Camera:透视相机与正交相机、相机参数详解、相机控制(OrbitControls)、多相机切换

相机,说白了就是你在3D世界里的「眼睛」。

Three.js里最常用的两双眼睛,就是透视相机和正交相机。我刚开始学的时候,总觉得它们差不多,直到有一次做建筑可视化项目,用错了相机类型,整个模型比例全崩了……嗯,从那以后我再也不敢小看这个选择了。

3.1 透视相机 vs 正交相机

先说说最直观的区别。

透视相机模拟的是人眼效果——近大远小。你站在铁轨上,远处的铁轨会汇聚成一个点。这种相机适合做游戏、漫游、产品展示,因为它看起来「真实」。

正交相机则没有透视效果,物体大小不随距离变化。你想想看,工程图纸、2D游戏、UI界面,用的就是这种。它适合做测量、俯视图、或者需要精确对齐的场景。

一句话总结:要真实感,用透视;要精确性,用正交。

3.2 相机参数详解

先看透视相机的构造。我习惯把它想象成一个「视锥体」——从相机位置向前延伸,形成一个四棱锥,近处切一刀,远处再切一刀,中间的部分就是你能看到的区域。

const camera = new THREE.PerspectiveCamera(
  75,           // 视场角(FOV),单位度
  width/height, // 宽高比
  0.1,          // 近裁剪面
  1000          // 远裁剪面
);

这几个参数,每个都有坑。

参数 作用 我踩过的坑
FOV(视场角) 视野宽度,越大看到的东西越多 设到120度以上,画面边缘会严重畸变,像鱼眼镜头
宽高比 画面宽高比例,通常用canvas尺寸计算 忘记在窗口resize时更新,画面会被拉伸
近裁剪面 离相机多近开始渲染 设太小(比如0.0001),深度精度会出问题,远处物体闪烁
远裁剪面 离相机多远停止渲染 设太大,同样影响深度精度,而且浪费性能

我的个人习惯:近裁剪面设0.1,远裁剪面设1000,够用就行。除非你做的是宇宙场景,否则别动不动设到100000。

正交相机呢?参数更简单,但更容易搞混。

const camera = new THREE.OrthographicCamera(
  -10,  // left
  10,   // right
  10,   // top
  -10,  // bottom
  0.1,  // near
  1000  // far
);

left/right/top/bottom定义的是一个矩形视口。你想想看,这个矩形就是你能看到的「窗口」。物体在这个窗口内就显示,超出就裁剪掉。

我曾经犯过一个低级错误:把left和right设反了,结果场景上下颠倒。排查了半天才发现……嗯,这种低级错误,犯过一次就不会再犯了。

3.3 相机控制(OrbitControls)

光有相机还不够,你得能操控它。OrbitControls是Three.js官方提供的控制器,让你用鼠标拖拽旋转、滚轮缩放、右键平移。

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

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;  // 开启惯性效果
controls.dampingFactor = 0.05; // 惯性系数
controls.autoRotate = false;   // 自动旋转
controls.target.set(0, 0, 0);  // 围绕哪个点旋转

这里有个关键点:记得在动画循环里调用controls.update()。尤其是开启了damping(惯性)之后,不调用update,旋转操作会卡顿。

避坑指南:我曾经在项目中把controls.target设错了位置,结果相机绕着空气转,模型纹丝不动。排查了半小时才发现是target没更新。建议每次移动场景后,都检查一下target是否在正确位置。

OrbitControls还有一些实用配置:

  • minDistance / maxDistance:限制缩放范围,防止穿模
  • minPolarAngle / maxPolarAngle:限制上下旋转角度,避免翻转到地下
  • enablePan:是否允许平移,做展示类项目我一般关掉

3.4 多相机切换

有时候一个相机不够用。比如我做的一个工厂监控项目,需要同时提供「全局俯视图」和「第一人称漫游」两种视角。

多相机切换的核心思路很简单:维护一个相机数组,按需切换当前激活的相机

const cameras = {
  perspective: new THREE.PerspectiveCamera(75, aspect, 0.1, 1000),
  orthographic: new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 1000)
};

let activeCamera = cameras.perspective;

// 切换相机
function switchCamera(type) {
  activeCamera = cameras[type];
  // 别忘了同步控制器的目标
  controls.object = activeCamera;
  controls.target.set(0, 0, 0);
}

// 渲染时使用当前相机
function animate() {
  controls.update();
  renderer.render(scene, activeCamera);
  requestAnimationFrame(animate);
}

这里有个容易忽略的细节:切换相机后,OrbitControls的object属性要同步更新。否则你切到正交相机,控制器还在控制透视相机,操作就会错乱。

我的经验:多相机切换时,最好把每个相机的初始位置和target都存下来。切换时恢复,这样用户不会觉得「视角跳来跳去」。我一般用一个对象来管理:

const cameraConfigs = {
  perspective: { pos: [5, 5, 5], target: [0, 0, 0] },
  orthographic: { pos: [0, 10, 0], target: [0, 0, 0] }
};

还有一个进阶玩法:分屏显示。把渲染区域切成两半,左边透视,右边正交。这种在3D建模软件里很常见。实现方式是用两个renderer,或者用scissor(裁剪)功能。不过这个比较复杂,咱们课程后面会专门讲。

最后说一句:相机系统看似简单,但它是整个3D体验的入口。相机参数设不好,再好的模型也白搭。我建议你动手试试,把FOV从30调到120,看看画面变化;把正交相机的left/right调大调小,感受一下「窗口」的变化。实践出真知嘛。


公众号:蓝海资料掘金营,微信deep3321