04 · Camera Space:把世界放到相机面前
Camera Space 与 View Space 在本系列中指同一空间:以相机为原点、以相机轴为基底表达位置。它不是屏幕空间,也没有执行透视投影。
1. 为什么要换到相机坐标
拍摄者向右移动,静止物体在取景器里向左移动。为了使用统一投影公式,我们可以先把所有世界点转成“相机看到的位置”,再交给投影矩阵。
相机世界矩阵 C 把相机局部点变到世界,视图矩阵则走反方向:
在相机及其父层级保持单位缩放、没有剪切时,Three.js 中对应 camera.matrixWorld 和 camera.matrixWorldInverse。本系列的相机公式和示例都采用这个前提。
r185 的 Camera 更新方法会从世界矩阵分解位置、旋转和缩放,再排除缩放构造视图矩阵。因此缩放相机或其父节点后,matrixWorldInverse 不一定严格等于 matrixWorld 的数学逆;不要通过缩放相机实现 zoom。使用透视相机的 fov/zoom 或正交相机的视域参数,并调用 updateProjectionMatrix()。
2. 相机轴与深度符号
常规 Three.js 相机沿自身局部 -Z 看,+X 为右、+Y 为上。因此相机前方的点通常有负的 view.z。不要据此断言世界 z 为负的点一定在前面:相机可以转身。
import * as THREE from 'three';
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
camera.updateWorldMatrix(true, false);
const originWorld = new THREE.Vector3(0, 0, 0);
const originView = originWorld.clone().applyMatrix4(camera.matrixWorldInverse);
console.log(originView.toArray()); // [0, 0, -5]
物体在前方 5 个单位。相机移到 z=10,物体的世界位置没有变,但 view.z 变为 -10。
-view.z 是沿视线轴的深度,不是到相机的欧氏距离。view 点 (3,0,-4) 的轴向深度是 4,距离是 5。
3. Model-view 矩阵
对于局部顶点,可以合并两次变换:
在 ShaderMaterial 顶点着色器中,Three.js 提供 modelViewMatrix,它是当前对象的模型世界矩阵与相机视图矩阵的组合:
vec4 positionView = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * positionView;
position 是顶点 attribute,JavaScript 上传几何数据后,每个顶点分别运行这段程序。gl_Position 必须是裁剪坐标,所以还有 projectionMatrix 这一步。
4. 相机空间中的光照与方向
如果表面法线在 view space,光照方向也必须变到 view space 才能点积。仅给变量命名 lightDirection 不足以保证正确。
const lightDirectionWorld = new THREE.Vector3(1, 2, 1).normalize();
const lightDirectionView = lightDirectionWorld.clone()
.transformDirection(camera.matrixWorldInverse);
这表示一个不含平移的单位朝向。点光源应先把光源位置变到 view space,再减去表面 view position。相机通常保持单位缩放;复杂缩放父节点会增加朝向解释和控制器行为的不确定性,应谨慎设计层级。
5. 前方检测的边界
positionView.z < 0 只能说明在相机前方,不能说明它在视锥里,也不能说明没被其他物体挡住。完整显示判断还需要投影后的范围,遮挡则需要射线或深度相关方法。
练习(不附答案)
- 相机位于
(5,0,0)并看向原点,预测原点的 view 坐标,再验证。 - 构造一个在相机前方但在画面外的点,说明单独判断 view.z 的不足。
- 比较 view 点
(0,0,-5)与(4,0,-5)的轴向深度和距离。 - 保持相机及父层级单位缩放,移动相机后验证
matrixWorld * matrixWorldInverse约为单位矩阵。