Three.js 相机:透视与正交,决定你"怎么看"这个世界
原文出处:Three.js Manual – Cameras
本文基于官方手册 Cameras 章节整理,用通俗方式讲清 three.js 里两种相机(透视 / 正交)到底是什么、near/far 为什么不能乱设。
写在前面
前面我们学会了给场景打光,模型有立体感了。但还有一个绕不开的东西——相机。它决定了"观众的眼睛"在哪、能看到多大范围。
three.js 里相机我们其实一直在用(PerspectiveCamera),但之前的文章没细讲。这篇把两种相机掰开揉碎说清楚,并顺带解决一个新手必踩的坑:near 和 far 为什么不能设成 0 和无穷大。
一、PerspectiveCamera:透视相机,越近越大
这是 three.js 最常用的相机,也是现实人眼的投影方式——远处的东西看起来小,近处的大。
它靠四个参数定义一个"平截头体"(frustum,一个被削掉尖的金字塔)。near和far是前后两个截面的距离,fov(field of view,视场角)决定这个金字塔有多"宽",aspect是宽高比。
| 参数 | 含义 | 常见值 |
|---|---|---|
| fov | 垂直视场角(度) | 45~75 |
| aspect | 画面宽高比 | canvas 宽/高 |
| near | 近裁剪面距离 | 0.1 |
| far | 远裁剪面距离 | 100~1000 |
constfov=45;constaspect=2;constnear=0.1;constfar=100;constcamera=newTHREE.PerspectiveCamera(fov,aspect,near,far);改了任何参数都要调updateProjectionMatrix(),否则画面不会更新。手册用一个小工具类MinMaxGUIHelper保证far永远大于near,很贴心:
functionupdateCamera(){camera.updateProjectionMatrix();}constgui=newGUI();gui.add(camera,'fov',1,180).onChange(updateCamera);constminMaxGUIHelper=newMinMaxGUIHelper(camera,'near','far',0.1);gui.add(minMaxGUIHelper,'min',0.1,50,0.1).name('near').onChange(updateCamera);gui.add(minMaxGUIHelper,'max',0.1,50,0.1).name('far').onChange(updateCamera);二、直观看清"平截头体":双相机 + 剪刀测试
光看参数很难想象。手册给了一个超直观的 demo:用 three.js 的**剪刀测试(scissor)**把画布切成两半,左边是正常视角,右边是"从第二个相机看第一个相机的平截头体"。
核心是把renderer.setScissorTest(true),再分别给左右两块区域设置视口和剪刀框:
functionsetScissorForElement(elem){constcanvasRect=canvas.getBoundingClientRect();constelemRect=elem.getBoundingClientRect();constright=Math.min(elemRect.right,canvasRect.right)-canvasRect.left;constleft=Math.max(0,elemRect.left-canvasRect.left);constbottom=Math.min(elemRect.bottom,canvasRect.bottom)-canvasRect.top;consttop=Math.max(0,elemRect.top-canvasRect.top);constwidth=Math.min(canvasRect.width,right-left);constheight=Math.min(canvasRect.height,bottom-top);constpositiveYUpBottom=canvasRect.height-bottom;renderer.setScissor(left,positiveYUpBottom,width,height);renderer.setViewport(left,positiveYUpBottom,width,height);returnwidth/height;}把near调大到 20,你会看到物体"前半截"突然消失——因为它们跑出了平截头体;把far调小到 35 以下,地面开始消失。一下子就懂了 near/far 是干嘛的。
三、near/far 精度与 Z-fighting(深度冲突)
你可能会想:那我把near设成 0.0000001、far设成 100000000000 不就啥都能看了?
不行,这就是新手经典大坑。GPU 用来判断"谁在前谁在后"的精度(深度缓冲)是有限的,而且默认在相机附近精度高、远处精度低。这个精度要在near到far之间分配。两者差距拉太大,中间物体的前后关系就判断不准,出现Z-fighting(深度闪烁)——两个物体叠在一起时边缘疯狂抖动、互相穿模。
手册用 20 个彩色球演示了这个现象。一个缓解办法是创建 renderer 时开logarithmicDepthBuffer:
constrenderer=newTHREE.WebGLRenderer({antialias:true,canvas,logarithmicDepthBuffer:true,});但别迷信它:第一,很多移动端 GPU 不支持;第二,它比默认方案慢。最根本的解法还是——near 尽量远、far 尽量近,刚好框住你要显示的内容就行。
四、OrthographicCamera:正交相机,没有近大远小
第二种常用相机是OrthographicCamera(正交相机)。它不定义金字塔,而是定义一个长方体盒子,参数是left/right/top/bottom/near/far。因为是正交投影,没有透视变形——物体多大就是多大,不受距离影响。
constleft=-1;constright=1;consttop=1;constbottom=-1;constnear=5;constfar=50;constcamera=newTHREE.OrthographicCamera(left,right,top,bottom,near,far);camera.zoom=0.2;它最常见的用途是画 2D。你想让"画布上 1 像素 = 相机里 1 单位"?直接把盒子设成画布尺寸:
camera.left=0;camera.right=canvas.width;camera.top=0;camera.bottom=canvas.height;camera.near=-1;camera.far=1;camera.zoom=1;这样左上角就是(0,0),跟普通 2D canvas 的坐标系一模一样。手册还用正交相机做了个"图片在画布里像素级反弹"的小动画,跟写 2D 游戏一模一样。另一个经典用途是 3D 建模软件里的"顶/前/左/右"三视图。
gui.add(camera,'zoom',0.01,1,0.01).listen();// 加 .listen() 让滚轮缩放也同步到 GUI总结
| 相机 | 投影方式 | 有透视吗 | 典型用途 |
|---|---|---|---|
| PerspectiveCamera | 平截头体 | 有 | 绝大多数 3D 场景 |
| OrthographicCamera | 长方体盒子 | 无 | 2D 绘制、三视图 |
记住三条:① 改相机参数后调updateProjectionMatrix();② near/far 别拉太极端,小心 Z-fighting;③ 要做 2D 或工程三视图就用正交相机。
延伸阅读
- PerspectiveCamera – docs
- OrthographicCamera – docs
- CameraHelper – docs
- 下一篇可以看 Shadows(阴影)