MAKE · WRITE · SHARE

Base-各个坐标空间

同一个点可以在模型、世界、相机和屏幕坐标中描述。变换改变的是参照系或投影关系,不能只看到一个 float3 就认为它可以与另一个 float3 相加。

原记录始于 2018 年 2 月,日期以月初占位;本文合并 2020 年 5 月 19 日的空间记录与 2021 年 3 月 18 日的 MVP 推导。矩阵采用列向量约定,具体引擎函数仍需按管线检查。

从模型坐标经过 MVP 到裁剪、透视除法与屏幕坐标

1. 模型、世界与观察坐标

模型空间由资产选择局部原点,并不要求原点一定处于几何中心。模型矩阵 M 将局部位置变成世界位置,观察矩阵 V 把世界变到相机参照系。

1
2
3
float4 worldPos = mul(M, float4(positionOS, 1));
float4 viewPos = mul(V, worldPos);
float4 clipPos = mul(P, viewPos);

因此 clipPos=P*V*M*positionOS。M、V、P 的乘法顺序来自列向量约定;不能把变量名顺序当作跨引擎公式。

Unity 常见观察空间中相机朝 -Z,而世界空间的前向约定不同。方向定义是构造规则的一部分,不是仅凭“左手/右手”就能确定所有轴向。

模型位置到世界位置的原记录示意

以世界坐标 x 分量着色的旧实验

原笔记配图:以世界坐标 x 分量着色的旧实验;黑白变化来自数值的显示截断。

把相机放到原点并约定朝向,是构造观察坐标系的目标

原笔记配图:把相机放到原点并约定朝向,是构造观察坐标系的目标。

2. 为什么 V 是相机变换的逆

若相机到世界的刚性变换为旋转 R 与位置 c,世界点 p 的相机坐标是 Rᵀ*(p-c)。先减相机位置,再应用逆旋转;齐次形式等于对相机的世界变换求逆。

灯光观察空间遵循相同思想,只是观察者变成了光源。ShadowMap 再叠加光源投影与图集区域变换,不能把“灯光空间”直接等同最终阴影 UV。

世界坐标转换到相机参照系的原记录示意

原记录图解

观察位置各分量及其符号的可视化

原笔记配图:观察位置各分量及其符号的可视化;显示为颜色时会发生钳制。

单独观察 ViewPos.x 的旧实验

原笔记配图:单独观察 ViewPos.x 的旧实验。

单独观察 ViewPos.y 的旧实验

原笔记配图:单独观察 ViewPos.y 的旧实验。

单独观察 ViewPos.z 的旧实验,前方 z 的符号依观察空间约定

原笔记配图:单独观察 ViewPos.z 的旧实验,前方 z 的符号依观察空间约定。

翻转 ViewPos.z 的显示符号后,原来被钳制的部分变得可见

原笔记配图:翻转 ViewPos.z 的显示符号后,原来被钳制的部分变得可见;这不等于改变深度编码。

相机平移与旋转的逆变换

原笔记配图:相机平移与旋转的逆变换;图中用右手系相机朝 −Z 的约定。

3. 投影与齐次裁剪

透视投影利用 w 记录后续透视除法所需尺度。在一种常见相机约定下,w 与 -view.z 相关;正交投影则通常保持 w=1。因此“clip.w 永远是相机深度”不成立。

以常见对称透视视锥为例,横纵方向先按视场与宽高比缩放;深度项再把近远平面映射到 API 需要的裁剪范围。Reversed-Z 改变深度映射方向,但不是横纵投影的另一种几何。

GPU 先在齐次空间裁剪,再进行除以 w。跨相机平面或近平面的图元若先除法,会遇到无穷大和符号翻转。

原记录图解

相机视锥、近远面与物体位置的空间关系

原笔记配图:相机视锥、近远面与物体位置的空间关系。

旧手写投影过程

原笔记配图:旧手写投影过程。图中的 NDC 范围与深度标记采用当时的约定,不能直接代替所有 API 的深度纹理编码。

透视投影与正交投影的视锥形状对比

原笔记配图:透视投影与正交投影的视锥形状对比。

正交投影可以理解为将轴对齐观察盒平移、缩放到标准范围

原笔记配图:正交投影可以理解为将轴对齐观察盒平移、缩放到标准范围。

正交投影的平移和缩放矩阵

原笔记配图:正交投影的平移和缩放矩阵;图中近远面使用该资料自己的符号约定。

把透视视锥压成正交盒,再做正交归一化的推导思路

原笔记配图:把透视视锥压成正交盒,再做正交归一化的推导思路。

透视到正交的中间矩阵:齐次 w 承担随后透视除法的分母

原笔记配图:透视到正交的中间矩阵:齐次 w 承担随后透视除法的分母。图中的 unknown 表示尚待约束的量。

利用近裁剪面保持的条件约束透视矩阵,这是中间推导步骤

原笔记配图:利用近裁剪面保持的条件约束透视矩阵,这是中间推导步骤。

利用远裁剪面条件补充深度映射约束

原笔记配图:利用远裁剪面条件补充深度映射约束。

联立近远面条件求矩阵系数

原笔记配图:联立近远面条件求矩阵系数。图中 n、f 是观察空间中的带符号坐标,不应与正的 near/far 距离混用。

原手写矩阵推导照片

原笔记配图:原手写矩阵推导照片。正文采用统一列向量约定,实际 API 矩阵以当前引擎为准。

4. NDC 与深度范围

ndc=clip.xyz/clip.w。通常 x、y 为 [-1,1];z 范围由 API 与投影约定决定,例如 [0,1] 或 [-1,1]。

Reversed-Z 指近远映射反向,并不是“DirectX 必然反向、OpenGL 必然正向”。应分别检查 API 裁剪范围、引擎投影、深度缓冲和采样转换。

旧记录中 OpenGL 风格的 NDC 深度范围示意

原笔记配图:旧记录中 OpenGL 风格的 NDC 深度范围示意。

旧记录中 Direct3D 风格的 NDC 深度范围示意

原笔记配图:旧记录中 Direct3D 风格的 NDC 深度范围示意;实际投影还可能采用 Reversed-Z。

5. 顶点与片元的 SV_POSITION 不同

顶点 Shader 输出的 SV_POSITION 是裁剪位置;片元 Shader 输入的同名语义,已经是光栅化后的屏幕相关坐标,不能继续当作原始 clipPos 再除一次 w。

若想在片元阶段使用某个自定义齐次投影坐标,应通过额外插值量传递,并清楚区分其含义。普通属性默认还涉及透视校正插值,而不是所有值都按屏幕重心直接线性插。

6. 像素坐标与屏幕 UV

概念上 uv=ndc.xy*0.5+0.5,像素坐标再乘视口尺寸。但纹理 Y 朝向、渲染目标翻转、视口偏移、动态分辨率与 XR 都会改变工程公式。

Unity Built-in 的 ComputeScreenPos 保留 w 并处理其约定下的投影翻转;URP 则应优先使用当前包提供的屏幕/深度工具。不要把 UNITY_REVERSED_Z 当成纹理 Y 方向开关。

屏幕坐标映射的原记录示意

NDC 位置映射到渲染目标的示意

原笔记配图:NDC 位置映射到渲染目标的示意;原点和 y 翻转依 API、资源与显示路径而定。

从 NDC 得到归一化屏幕 UV 的原记录

原笔记配图:从 NDC 得到归一化屏幕 UV 的原记录。

7. 深度重建

从深度重建时,先把像素 UV 与深度转换为正确的 NDC,构造四维位置 (ndc.x,ndc.y,ndc.z,1),再乘逆 VP,并执行齐次除法:

1
2
float4 h = mul(inverseVP, float4(ndc, 1));
float3 positionWS = h.xyz / h.w;

逆矩阵必须对应产生这张深度的同一个投影、抖动和视图。原记录中直接把 float3 传入四维矩阵、按反向深度决定 Y 翻转的示例均不应沿用。

重建观察位置用逆 P 即可。透视和正交相机都可通过正确逆矩阵处理,不能只为透视相机硬编码一个方向乘线性深度。

8. 切线与法线的特殊性

T、B、N 组成局部基,TBN 把切线坐标方向转换到选定空间。它们是方向,平移项不应参与;法线在非均匀缩放下还需要逆转置变换。

插值、蒙皮和缩放后,应检查归一化、正交性与手性。法线变换的具体推导见现有法线贴图文章,不以普通位置矩阵代替。

切线空间基向量的原记录示意

总结

每个变量都应标明空间、位置/方向类型和投影阶段。MVP 到屏幕是一条有裁剪和除法的流程,深度重建则需要沿一致的约定逆向处理;把这些边界写清楚,比记忆 API 矩阵名可靠。

参考资料