
一句话答案是的,几何形状被扭曲了。但因为最终屏幕也是扁平的 2D,那些扭曲恰好和透视效果互相抵消**,最终画面完全正确。**这不是巧合,这是精心设计的。先直面你的疑问:是不是扭曲了?是的,数学上绝对扭曲了。想象一下:视锥是个锥形,里面有一个球。我们通过 P 矩阵把视锥掰成立方体,那个球会变成什么?答案:球会被拉长或压扁——离相机远的部分会被放大,离相机近的部分会保持原样(取决于球的位置)。几何上,球不再是球了。那为什么最终画面还是对的?关键洞察:我们根本不需要3D 几何形状是对的这是最反直觉的一点:屏幕是 2D 的。我们只关心最终 2D 屏幕上的像素位置和颜色。3D 空间里的几何形状是什么样,完全不重要,只要投影到 2D 后正确就行。换句话说 矩阵不需要保持3D 形状正确,它只需要保证投影到 2D 后正确。这是一个思维的降维——你要跳出3D 几何完整性的执念,只关心 2D 结果。举个类比:影子想象太阳照射在一个物体上,物体在地面投下影子。问题:如果你只关心地面上影子的形状,物体本身是什么形状重要吗?答案:不重要!一个圆球和一个从太阳看是圆的立体不规则物投出的影子一模一样你无法从影子反推物体的完整 3D 形状只要投影结果相同,原物体形状可以千变万化。P 矩阵就是这样一个投影生成器——它把 3D 空间扭曲成一个新的 3D 空间(裁剪空间),但这个新空间里的物体,投影到屏幕上的结果,和从相机视锥内直接透视投影的结果完全一样。深入:P 矩阵到底扭曲了什么?来看具体的数学。P 矩阵做了两件事:1. 拉伸(把梯形拉宽)原来的视锥:近处窄,远处宽(锥形)。P 矩阵做的:把远处的宽压缩,把近处的窄拉宽,变成等宽的立方体。结果:一个近处的小方块 → 保持大小一个远处的大方块 → 被压缩变小看起来远处的东西被压小了——等等,这不就是透视效果吗?这就是关键 矩阵把透视效果编码进了几何变换里!远处的物体在裁剪空间里被物理压小了。等到投影到 2D 屏幕时,它自然就显得小 —— 因为它真的变小了。2. 变形(Z 值的非线性变换)原来的 Z(深度):从 near 到 far,线性分布。P 矩阵做的:把 Z 值做非线性映射——近处的 Z 精度高,远处的 Z 精度低。结果:深度值在 [0, 1] 之间(或 [-1, 1])但分布是近处密,远处稀的这就是 Z-buffer 精度问题的根源—— 但这是另一个话题,先按下不表。精髓:P 矩阵把透视除法藏在几何里来看一个具体的公式。正确的透视投影该做什么?(在观察空间里)一个点(x, y, z)(相机前方 z 米,右边 x 米,上方 y 米),它在屏幕上的位置应该是:screen_x x / (-z) ← 除以深度,近大远小 screen_y y / (-z)这就是透视效果的核心 —— 除以深度。那 P 矩阵在干什么?P 矩阵(简化版)长这样:P | 1/aspect 0 0 0 | | 0 1 0 0 | | 0 0 ... ...| | 0 0 -1 0 | ← 关键!最后一行注意最后一行:(0, 0, -1, 0)用它乘一个点(x, y, z, 1):clip P * (x, y, z, 1) clip.x x / aspect clip.y y clip.z ... (深度的非线性映射) clip.w -z ← ⭐ 关键!w 分量存的是深度!看到了吗?P 矩阵把观察空间的 z 值,存进了裁剪空间的 w 分量。然后透视除法登场阶段 3.3 的透视除法:ndc clip.xyz / clip.w (x/aspect, y, ...) / (-z) (x / (aspect * -z), y / (-z), ...)就是它!透视除法自动完成了除以深度这个操作。精髓时刻P 矩阵不是直接做透视投影,而是把深度信息藏进 w,让后续的透视除法自动完成透视。这是一个极其精巧的设计:P 矩阵本身只是一个线性变换(矩阵乘法)但透过 w 分量,它引入了非线性效果(除法)在需要的时候(裁剪之后),自动做除法这就是齐次坐标 w 分量的意义—— 它让透视这种非线性操作可以用线性矩阵表达。所以扭曲到底扭曲了什么?现在我们能精确回答了。P 变换后的裁剪空间:XY 方向:被压缩到[-w, w]范围(w -view_z)Z 方向:被非线性映射到[-w, w]范围形状:整个视锥变成了一个齐次立方体(注意:不是标准立方体,是每个点的 w 不同的立方体)透视除法后的NDC 空间:所有分量除以各自的 w变成真正的标准立方体[-1, 1]³此时,每个点的位置就是投影到 2D 屏幕上的位置(xy) 深度(z)关键点:NDC 空间的 xy,就是最终屏幕位置!扭曲完成的那一刻,画面已经是正确的透视效果。举一个具体的例子,让你亲眼看到场景:观察空间里有两个大小相同的方块。方块 A:在相机前方 5 米处,大小 1×1方块 B:在相机前方 10 米处,大小 1×1直觉:方块 B 应该看起来是方块 A 的一半大(近大远小)。观察空间里(P 变换前)方块 A 的顶点:(-0.5, -0.5, -5), (0.5, -0.5, -5), ...方块 B 的顶点:(-0.5, -0.5, -10), (0.5, -0.5, -10), ...它们的 XY 尺寸相同,只有 Z 不同。应用 P 矩阵后(裁剪空间)(用一个简化的 P 矩阵,假设 fov 和 aspect 让 P 的 xy 分量是恒等)clip_A P * (0.5, -0.5, -5, 1) (0.5, -0.5, ?, 5) ← w 5 clip_B P * (0.5, -0.5, -10, 1) (0.5, -0.5, ?, 10) ← w 10XY 依然相同,但w 分量不同了(w |view_z|)。看起来还是一样大?—— 不,秘密在下一步。透视除法后(NDC 空间)ndc_A.xy clip_A.xy / clip_A.w (0.5, -0.5) / 5 (0.1, -0.1) ndc_B.xy clip_B.xy / clip_B.w (0.5, -0.5) / 10 (0.05, -0.05)看!方块 A 在 NDC 里的边长:0.2方块 B 在 NDC 里的边长:0.1方块 B 恰好是方块 A 的一半!这就是透视效果——远处的东西自动变小了。视口变换到屏幕假设屏幕 1920×1080:方块 A 占屏幕 192×108 像素(0.1 × 1080 大概)方块 B 占屏幕 96×54 像素画面完全正确!远处的方块看起来是近处的一半大。那3D 几何被扭曲了对不对?是的,而且严重扭曲。在裁剪空间(或 NDC 空间)里,方块 A 和方块 BXY 尺寸不同了——但它们在 3D 空间里明明是一样大!这就是3D 几何被破坏——但这不重要,因为我们只关心 2D 屏幕结果。但等等:光照怎么办?阴影怎么办?好问题!这就是为什么图形学要保留多个空间。光照计算通常在世界空间或观察空间做,不在裁剪空间做。因为光照需要真实的几何关系(法线方向、光线方向、距离等),而裁剪空间已经扭曲了几何。流程:v2f vert(appdata v) { v2f o; // 世界空间位置(用于光照) o.worldPos mul(unity_ObjectToWorld, v.vertex); // 世界空间法线(用于光照) o.worldNormal mul((float3x3)unity_ObjectToWorld, v.normal); // 裁剪空间位置(用于光栅化) o.pos UnityObjectToClipPos(v.vertex); return o; } half4 frag(v2f i) : SV_Target { // 在世界空间里算光照(几何未扭曲) half3 lighting ComputeLighting(i.worldNormal, i.worldPos); return half4(lighting, 1); }这就是顶点在多个空间里同时存在的原因——不同的计算需要不同的空间:光照:世界空间或观察空间(需要真实几何)裁剪:裁剪空间(硬件优化)纹理映射:模型空间的 uv(不受几何变换影响)P 矩阵扭曲的只是最后用来光栅化的位置,其他数据(法线、世界位置、uv)保持在未扭曲的空间。一个更深刻的洞察:P 矩阵是预处理换个角度理解:如果没有 P 矩阵,我们要在光栅化时对每个像素做:判断像素在不在视锥内(6 个斜面判断)从像素反推 3D 点(除以深度)深度测试(计算 3D 距离)极其昂贵!!!有了 P 矩阵,我们在顶点阶段一次性做完这些扭曲,然后:判断像素在不在视锥内 → 判断在不在 [-1, 1] 立方体内(极简)从像素反推 3D 点 → 直接用 NDC 位置(已经算好)深度测试 → 直接比较 NDC 的 z(已经算好)P 矩阵是把每个像素都要做的昂贵操作提前到每个顶点做一次。一个物体几万个像素,但只有几十个顶点。每顶点一次比每像素一次便宜几百倍。这就是 P 矩阵存在的终极意义:用一次性的顶点端扭曲,换取所有后续操作的极致简化。类比:预处理的思想想象你要给 100 万人发工资,每人的工资 基础工资 × 系数。方案 A(不预处理):每个人都算一遍:工资 基础 × 系数100 万次乘法方案 B(预处理):先算好等效工资 基础 × 系数(1 次乘法)然后发工资时直接给等效工资(0 次运算)总计 1 次乘法P 矩阵就是图形学的预处理—— 把透视效果预先烘焙进几何位置,后续所有操作都不需要重新计算透视。那为什么最终画面不变形?现在你能完整回答自己的问题了:问题 矩阵把梯形硬掰成立方体,画面不会变形吗?答案:P 矩阵确实扭曲了 3D 几何(远处物体被压缩)但这个扭曲不是随便扭的,它精确地编码了透视投影的规律扭曲后的 xy 值,除以 w之后,就是标准透视投影的结果所以最终 2D 屏幕上的位置完全正确而 3D 几何的扭曲不影响 2D 结果(因为屏幕只看到 2D)换个说法 矩阵扭曲的方式,恰好和透视投影的效果完美对应。扭曲不是 bug,是 feature。一个终极思维实验想象一下:如果 P 矩阵没有这种编码透视的设计,而是老老实实保持几何形状,会怎样?流程会变成:观察空间 →保持 3D 形状不变→ 送到光栅化光栅化时,对每个像素,反推它对应的 3D 点除以深度得到 2D 屏幕位置判断是否在视锥内(6 个斜面判断)深度测试这样做的问题:每个像素都要做除法 → 极慢视锥判断复杂 → 硬件难做深度存储和比较麻烦图形学的先驱们想到了:提前把除以深度藏进 w提前把视锥变成立方体提前把深度变成 [0, 1] 的标准值代价:3D 几何被扭曲了。收获:所有后续操作变成平凡的立方体内的判断,硬件可以极致优化。这是一笔完美的交易:用几何完整性换性能。而且这个牺牲根本不影响画面——因为我们只看 2D 屏幕。总结:P 矩阵是面向 2D 结果的最优扭曲【核心思想】 我们最终只关心 2D 屏幕上的画面 ↓ 3D 空间里的几何形状正不正确根本不重要 ↓ 只要 2D 投影的结果正确即可 ↓ 【P 矩阵的设计】 故意扭曲3D 几何 ↓ 但扭曲的方式,恰好让透视效果自动生成 ↓ 2D 投影结果完全正确 ↓ 【收获】 所有后续操作(裁剪、深度、光栅化)极致简化 硬件可以最快执行 ↓ 【代价】 裁剪空间/NDC 空间里的 3D 形状是假的 光照、物理等需要真实几何的操作,必须在世界/观察空间做 ↓ 所以我们保留多个空间,各司其职一个反过来的思考题问题:如果你想在 shader 里根据到相机的距离计算雾效(fog),你应该用哪个空间的坐标?为什么?参考答案应该用观察空间或世界空间的坐标,不能用裁剪空间/NDC 空间。原因:雾效需要真实的物理距离(离相机 10 米、20 米…)。而裁剪空间/NDC 空间里,深度已经被 P 矩阵非线性扭曲(近处密、远处稀),那里的 z 值不代表真实距离。如果错误地用 NDC 的 z 算雾:近处的雾看起来太浓(NDC 里近处 z 变化剧烈)远处的雾看起来太淡(NDC 里远处 z 变化平缓)结果:雾效不真实正确做法:// 顶点着色器 o.viewPos mul(UNITY_MATRIX_MV, v.vertex).xyz; // 观察空间 // 片元着色器 float distance length(i.viewPos); // 真实距离 float fogFactor 1 - exp(-distance * fogDensity); color lerp(color, fogColor, fogFactor);这印证了:光照、雾、物理相关的计算,都必须用未被 P 矩阵扭曲的空间。这就是为什么多个空间并存是必要的。