第 27 章 坐标变换的完整链:从模型局部到屏幕像素的六道关卡
上一章我们聊了深度缓冲,结尾我埋了个钩子:那个”近大远小”和”深度精度近高远的低”,根子都在一个叫 w 的分量、一次叫透视除法的操作上。这一章,我把这个钩子亲手兑现,而且不止于此——我要带你把”一个三维模型的顶点,怎么最终落到你屏幕的某颗像素上”这条流水线,从头到尾走一遍。
你先回想第 2 章我跟你铺垫过的事:同一个炮管,经历了”局部 → 世界 → 视图 → 屏幕”好几道变换才落进你眼睛。当时我说”变换 = 搬坐标,矩阵是搬运工公式”,并埋了伏笔。现在,是兑现伏笔的时候了。
你先猜一下:把一个三维点变成屏幕上的二维像素点,中间要经过几道”换参照系”?你可能会说”世界到屏幕呗,一道”。——错得离谱,但也正常。真实答案是六道,而且每一道都有明确的目的和数学。这一章就给你把这六道关卡,一道一道拆开。理解这条链,等于你拿到了”能手写 shader、能排查穿模和奇怪透视”的钥匙。面试官最爱顺着这条链往下问。
27.1 为什么用 4x4 矩阵和齐次坐标
要搬坐标,得先有趁手的”搬运工”。你写过程序、懂矩阵,但有个细节你大概率没想过:纯 3x3 矩阵,没法表示”平移”。
我给你一个最直观的反例。平移就是把点 (x, y, z) 加上一个偏移 (tx, ty, tz),得到 (x+tx, y+ty, z+tz)。这是加法。
而 3x3 矩阵乘一个三维向量,做的是线性变换:new = M · old。线性变换的本质是什么?是”过原点”的——它把原点 (0,0,0) 还是映射到原点(因为 M·0 = 0)。可平移要把原点搬到 (tx, ty, tz),原点不再待在原点了。所以平移不是线性变换,3x3 矩阵天生表示不了它。
工程师的解法漂亮且统一:把三维点升维成四维齐次坐标(Homogeneous Coordinates),写成 (x, y, z, w)。在这个四维空间里:
- 用一个 4x4 矩阵就能同时表达”旋转 + 缩放 + 平移”——平移被巧妙地塞进了矩阵的最后一列,靠 w 分量撑起来。
- w = 1 表示”这是一个点”(有位置,能被平移);w = 0 表示”这是一个方向 / 向量”(比如法线、光线方向,没有位置,平移对它没意义,移动一个方向没有意义)。
这里连回第 6 章的法线。法线是”表面朝外的方向”,它描述的是朝向、不是位置。所以法线在齐次坐标里是 w = 0 的方向向量。这一点至关重要,本章结尾思考题会请你用它来想”为什么法线不能用模型矩阵直接乘”——先记住:方向用 w=0,点用 w=1,这是齐次坐标的两条铁律。
那为什么要引入 w 这个”多余的”分量?因为它正是透视投影的灵魂(呼应第 26 章 26.4 的伏笔):投影矩阵算完后,顶点的 w 不再是 1,而是和深度相关的一个值;后面做”透视除法”(除以 w)时,正是这个 w 制造了”近大远小”。没有齐次坐标的 w,近大远小在数学上就写不出来。 所以这四维不是炫技,是被逼出来的必需品。
27.2 模型矩阵 Model:局部坐标 → 世界坐标
第一道关卡,也是最接地气的一道。
你还记得第 2 章的”局部坐标”吗?一个坦克的炮管,在坦克自己的坐标系里是 (0, 1, 2)(相对坦克中心)。但坦克被摆在世界 (10, 0, 0) 的位置。要让炮管在”世界”里参与计算,得先把它从”坦克的局部”搬到”世界”。
这个搬运工,就是模型矩阵(Model Matrix)。它做的事,正是你在 Unity 里调的那个 Transform 的平移 + 旋转 + 缩放拼出来的一个 4x4 矩阵。
- 你给坦克设 Position = (10,0,0)、没旋转、没缩放 → Model 矩阵就是”把原点平移到 (10,0,0)“。炮管的局部 (0,1,2) 乘上它,得到世界 (10,1,2)。
- 你给坦克加个旋转、加个放大 → Model 矩阵把旋转和缩放也包进去,局部坐标一次性搬到世界。
这里呼应第 17 章讲的”组件哲学”。Transform 是每个物体强制拥有的地基组件(管 Position/Rotation/Scale),而模型矩阵,就是 Transform 这三件套(外加父子层级)换算出来的”局部→世界”的搬运工。你当时学”父物体移动子物体跟着动”,本质就是:子物体的 Model 矩阵 = 父的 Model 矩阵 × 子自己的局部变换。父子层级链,就是一串 Model 矩阵连乘。所以”层级”在数学上,就是矩阵乘法链——你写 Go 时嵌套 struct 获得能力,Unity 里嵌套 Transform 获得世界坐标,底层都是”一层层往上乘”。
落进引擎:你在代码里写的 transform.position、transform.rotation、transform.localScale,以及父子关系,最终都被引擎合成为每个物体的 Model 矩阵,喂给渲染管线。你不用手写矩阵乘法,但理解”它就是在把局部搬到世界”,你调 Transform 时就不会懵。
27.3 视图矩阵 View:世界坐标 → 相机坐标
第二道关卡,关键直觉特别反直觉,我重点讲。
顶点到了世界坐标,下一步要”从摄像机的视角看”。这就需要一个视图矩阵(View Matrix),把世界坐标变成”以摄像机为原点的相机坐标”。
反直觉的点来了:
移动相机,等价于反向移动整个世界。
你想,要让”世界里的物体”在”相机眼里”的位置变化,有两种办法:①真的把物体挪动;②保持物体不动,把相机挪到相反方向——结果在相机看来完全一样。图形学选了后者在数学上的等价表达:视图矩阵,就是”相机自身的 Transform 的逆矩阵”。
什么意思?相机在世界里也有一个 Transform(位置、朝向)。如果我们把相机的变换直接应用到世界上,那是”把世界按相机的方式摆”;但我们要的是”把世界摆到相机的坐标系里”,所以必须取逆(inverse)——把相机的平移反向、把相机的旋转反向。于是 View = inverse(CameraModel)。
- 相机在世界 (0,0,5) 朝 +z 看:View 矩阵就是把所有世界点”减去 (0,0,5)“(反平移),并相应反旋转,使相机自己落在原点、看向 -z(图形学惯例相机看向 -z)。
- 这连回第 7 章”摄像机是虚拟眼睛”。你当时学”摄像机有位置有朝向,决定看到什么”。现在揭晓:摄像机的那个 Transform,取个逆,就成了 View 矩阵,把全世界的点都”搬进相机的坐标系”。
类比帮你记:你站在一群人中间拍照。要让照片里大家的位置”相对于你”正确,你不必推着大家动,你只要”以自己为原点重新描述每个人的位置”就行。View 矩阵就是”以相机为原点重新描述世界”的那次重述。这也是为什么它叫 View(视图)——它表达的是”从相机的视图看,世界长啥样”。
落进引擎:你不用手动算 View,Camera 组件的 transform 一变,引擎自动算好 inverse 当 View 矩阵。但理解”View = 相机变换的逆”,你才能搞懂为什么”相机往后退,物体看起来变大”(因为 View 的反平移把物体往前推了),以及为什么 shader 里常看到 UNITY_MATRIX_V 这种符号(V 就是 View)。
27.4 投影矩阵 Projection:相机坐标 → 裁剪空间
第三道关卡,是”近大远小”的真正产地,也是和第 26 章 w 伏笔对接的地方。
顶点现在在相机坐标系里(相机在原点,看向 -z)。下一步,要决定”哪些在三角形视锥里、以及它们被怎么压扁成二维”。这个搬运工叫投影矩阵(Projection Matrix)。
它做两件大事,我分透视和正交讲:
透视投影(Perspective Projection)
透视投影模拟人眼 / 相机:近大远小。它对应第 7 章讲的”真实摄像机有透视”。数学意图(不深抠公式,讲清意图):投影矩阵把”相机坐标系下、不同深度的点”,通过乘上一个和深度相关的项,让远的点的 x、y 被压缩——具体机制和那个 w 有关,下一节透视除法才真正兑现。投影矩阵这一步,是”准备好 w”的一步。
透视投影的视锥是一个被截断的金字塔(平头截锥):近裁剪面小、远裁剪面大,离相机越远能看到的横向范围越宽。这正符合”远处视野更开阔、东西显得小”。
正交投影(Orthographic Projection)
正交投影没有近大远小,远近一样大。它对应第 7 章讲的”小地图 / 俯视图 / 2D UI”——你做小地图、做 2D 游戏、做工程制图,都用正交。数学意图:投影矩阵不做”按深度压缩 x、y”那一套,远近的 x、y 直接按比例映射,所以远近物体屏幕上一样大。
两种投影的对比,落回第 7 章你学的”摄像机投影类型”:Perspective 还是 Orthographic,选的就是用哪套投影矩阵。选错,小地图会”近大远小”变形,第一人称会”没有立体感”。
关键直觉(不抠公式但要懂):投影矩阵的核心目的,是把那个”平头截锥形的视锥”,压成一个标准的”单位立方体”(x、y、z 都在 -1 到 1 之间)。为什么要压成单位立方体?因为压成统一形状后,下一步”裁剪”和”除法”就能用一套通用规则处理,不用管每个相机视野多大。这是工程师”先归一化、再统一处理”的典型思路——和你写后端把各种输入先规整成统一结构体,再走同一套逻辑,是一个套路。
这一节的 w 伏笔:透视投影矩阵算出来的顶点,w 分量不再等于 1,而是等于”相机空间下深度的负数”(近似 -z)。这个 w 就是下一节透视除法的主角。正交投影的 w 则保持为 1(所以除完不变,没有近大远小)——你看,透视和正交在本质量上的差别,就藏在 w 等不等于 1 这件事上。
27.5 裁剪空间与视锥裁剪
投影矩阵把顶点送进了一个叫**裁剪空间(Clip Space)**的地方。这个名字的由来,就是下一步要”裁剪”。
投影矩阵把视锥压成了单位立方体(x、y、z ∈ [-1, 1])。在这个立方体之外的顶点 / 三角形,就被切掉(clip)——这就是”视锥裁剪(Frustum Culling / Clipping)”。
连回第 7 章:你当时学”只有近裁剪面和远裁剪面之间的东西才画”,以及视锥还有”左、右、上、下”四个面。这里全部兑现:裁剪空间的单位立方体,六个面(±x, ±y, ±z)正好对应”左、右、上、下、近、远”六个裁剪面。一个三角形只要有一部分超出这个立方体,图形硬件就把它精确切掉超出的部分(不是整块丢弃,是切边),保证不画到视野外、也不画到远近面外。
你可能会问:为什么不在更早(比如世界坐标阶段)就裁?——好问题。因为”视野范围”是相机定义的(透视金字塔),只有在”相机坐标系 + 投影之后”,视野边界才变成简单好算的”单位立方体六面”。早裁的话,边界是斜的金字塔,逐三角形判断成本高且无统一规则。等到投影后的裁剪空间,边界就是 x=±1、y=±1、z=±1 这种直线平面,用一次比较就能判断,便宜又统一。所以裁剪被安排在投影之后、除法之前,是工程上的最优位置。
这一步的输出,仍然是带 w 的齐次坐标(因为还没做除法)。裁剪是在”四维齐次裁剪空间”里做的(用 x/w、y/w、z/w 和 ±1 比),这是图形硬件的标准做法。
27.6 透视除法:w 产生”近大远小”
第四道关卡,本章的华彩,也是第 26 章 26.4 伏笔的兑现点。
顶点经过裁剪后,终于要做那一步了:透视除法(Perspective Division)。把齐次坐标的每个分量,都除以 w:
x_ndc = x / w
y_ndc = y / w
z_ndc = z / w
除完之后得到的,叫 NDC(Normalized Device Coordinates,归一化设备坐标),它的 x、y、z 都落在 [-1, 1] 的规范区间里。
现在揭晓”近大远小”:回顾 27.4,透视投影给远处的顶点设了更大的 w(w 近似等于 -z_相机,越远 |z| 越大,w 越大)。透视除法一除,远处的 x、y 被更大的 w 一除,缩小得更多 → 在屏幕上离中心更近、显得更小。近处的 w 小,除完 x、y 相对放大 → 显得大。“近大远小”就是这一次除法直接产生的。 你能亲手指着流水线说”它是这一步发生的”,这比背”透视投影有透视”强一万倍。
同时,z_ndc = z/w 也得到了一个归一化的深度值(在 [-1,1] 或 [0,1],取决于 API),这个值就是写进深度缓冲(第 26 章那个 Z-buffer)的那个数。而因为它由 z/w 得来、w 又随深度非线性变化,所以深度精度是近密远疏的——这正是第 26 章 z-fighting 成因一的根源。你看,26.4 的伏笔、26.5 的精度分布、27.4 的 w、27.6 的除法,全部咬合成一条因果链:投影设 w → 除法用 w → 屏幕近大远小 + 深度近密远疏。
再强调一次那个反直觉的对称:正交投影的 w = 1,除完 x、y 不变,所以没有近大远小、深度均匀;透视投影的 w 随深度变,所以有近大远小、深度非线性。同一个除法操作,w 的取值不同,就分化出两种投影。这就是为什么我反复说”w 是透视的灵魂”。
27.7 视口变换 Viewport:NDC → 屏幕像素
第五道关卡,离屏幕只差一步。
NDC 的坐标范围是 [-1, 1] 的正方形。但你的屏幕是 1920×1080 的矩形像素。要把 [-1,1] 映射到 [0, width] 和 [0, height],需要视口变换(Viewport Transform)。
它做的事情很朴素:
- x 从 [-1,1] 拉伸映射到 [0, width](乘 width/2,再加 width/2 偏移)。
- y 从 [-1,1] 拉伸映射到 [0, height]。
- z 从 [-1,1] 映射到深度缓冲的区间 [0,1](这个才是最终写进 Z-buffer 的深度)。
连回第 3 章(分辨率 / 像素):你当时学”屏幕是一格格像素,分辨率决定有多少格”。这里就是终点——一个顶点经过视口变换,最终落在这颗具体的像素 (px, py) 上。整条流水线,从”模型里的一个局部坐标点”,到”屏幕上的一颗像素”,到此闭环。 剩下的,是 GPU 在这个像素位置算颜色(着色器、光照、雾),并把颜色写进颜色缓冲、把深度写进深度缓冲。
注意一个生活里常踩的坑:NDC 是正方形、屏幕是矩形,视口变换是各向同性拉伸(x、y 按各自尺寸拉),所以如果你的”投影矩阵”里的宽高比(aspect ratio)没和屏幕宽高比对齐,画面会被拉变形(人变胖或变瘦)。这就是为什么 Camera 上有个 Aspect 参数、或者投影矩阵要喂入宽高比——它就是为了保证”正方形 NDC 拉伸成矩形屏幕后,圆还是圆”。这点排查”物体忽大忽小/变形”时极有用。
27.8 把六步串成一句话流水线
好,六道关卡全部走完。我把它们串成你面试时能一口气背出来的流水线:
模型局部坐标
│ (Model 模型矩阵:局部 → 世界)
▼
世界坐标
│ (View 视图矩阵 = 相机变换的逆:世界 → 相机)
▼
相机坐标
│ (Projection 投影矩阵:相机 → 裁剪空间,顺带设好 w)
▼
裁剪空间(齐次,带 w)
│ (裁剪:超出单位立方体的切掉)
│ (透视除法:除以 w → NDC)
▼
NDC 归一化设备坐标(x,y,z ∈ [-1,1])
│ (Viewport 视口变换:NDC → 屏幕像素)
▼
屏幕像素坐标 (0~width, 0~height)
记这六个名字就够了:Model → View → Projection → 裁剪 → 透视除法 → Viewport。缩写常写成 MVP(Model-View-Projection)加上后面的裁剪/除法/视口。
为什么这条链如此重要?我给你三个你立刻能用的”超能力”:
- 能手写 shader:顶点着色器(Vertex Shader)干的核心事,就是
gl_Position = Projection * View * Model * localPos(矩阵连乘,注意顺序是从右往左作用于点)。你理解了每步在搬什么,写 shader 时每一行都知道自己在干啥,不会盲抄。 - 能排查”穿模”:物体穿模(互相插进去),大概率是深度 / 裁剪相关(第 26 章)——或者某个物体的 Model 矩阵算错、缩放异常;物体忽大忽小,可能是 Projection 的宽高比或 near/far 设错(27.4、27.7)。
- 能排查”奇怪透视”:透视太强像鱼眼(视野角 FOV 太大)、正交里出现近大远小(投影类型选错)、远处糊(深度精度,连回 26.5)——全都能在这条链上定位到某一关。
一句话收:这条链的本质,就是”不断换参照系,把点从模型的局部语言,逐步翻译到屏幕的像素语言”。每一关都是一个 4x4 矩阵(或一次除法/映射),全是”搬坐标”。你学第 2 章时说”变换 = 搬坐标,矩阵是搬运工”,到这一章,那句话终于长成了完整的骨架。
27.9 一次完整推演:把一个顶点亲手送过六道关
光说流程容易”听懂了但落不下笔”。咱们真拿一个顶点,把六道关算一遍,你就彻底踏实了。数字我取整数,只为展示”每一步坐标怎么变”,不必纠结真实精度。
设定场景:
- 一个立方体模型的某个顶点,在模型局部坐标是
P_local = (1, 1, 1)(也就是立方体角上那个点,w 默认为 1,写成齐次(1,1,1,1))。 - 这个立方体被摆在世界
(10, 0, 0)、无旋转、无缩放。所以 Model 矩阵就是平移(+10, 0, 0)。 - 相机在世界
(0, 0, 5),看向 -z(朝场景里看)。所以 View 是把世界减去(0,0,5),即反平移(0, 0, -5)。 - 投影用透视,假设近裁剪面 Near=1、远裁剪面 Far=100、视野角正常。投影矩阵会按深度给 w 赋值(这里简记规则:透视下 w ≈ 相机空间的 -z)。
逐步走:
① Model(局部 → 世界)
P_world = P_local + (10,0,0) = (11, 1, 1)。
这个顶点在世界里,位于 x=11 处。合理——立方体被摆到了 x=10 附近。
② View(世界 → 相机)
相机在 (0,0,5),反平移就是”减 5 的 z”:P_cam = (11, 1, 1-5) = (11, 1, -4)。
现在顶点在相机坐标系里:x=11、y=1、z=-4(z 为负,表示在相机前方,符合”相机看向 -z”的约定)。注意 z=-4 说明它离相机 4 个单位远。
③ Projection(相机 → 裁剪空间,设 w)
透视投影把 x、y 按”离相机远近”做压缩准备,并设 w = -z_cam = 4(这里用简化规则)。实际透视矩阵还会把 x、y 乘上一个和 FOV、宽高比有关的系数;为看清本质,我们先记:投影后得到齐次坐标,其 w = 4,x、y 被某个系数缩放后假设为 (x', y', z', 4)。关键是w 不再是 1,而是等于深度 4。这一步”埋下了 w=4”。
④ 裁剪
检查 (x', y', z', 4) 经过 x'/w, y'/w, z'/w 是否落在 [-1,1] 内。假设我们的 x’、y’ 绝对值不大、z’ 经过映射也落在范围内,则该顶点通过裁剪,不被切掉。(如果它跑到视野外,这一步就被裁了,后面全免。)
⑤ 透视除法(→ NDC)
x_ndc = x'/4,y_ndc = y'/4,z_ndc = z'/4。
注意:因为 w=4(对应离相机 4 单位),x、y 都被除以 4 缩小。如果另一个顶点离相机只有 2 单位(w=2),它除以 2,缩小得更少、在屏幕上显得更大——这就是 27.6 说的”近大远小”,在这个具体数上兑现了。深度 z_ndc 也被写入深度缓冲,且由于 w 随深度非线性,远处精度更低(呼应 26.5)。
⑥ Viewport(NDC → 屏幕像素)
假设屏幕 800×600。NDC 的 x∈[-1,1] 映射到像素 x∈[0,800]:px = (x_ndc+1)/2 * 800;y 同理映射到 [0,600]。最终这个顶点落在某颗具体像素上,GPU 在那算颜色、写颜色缓冲和深度缓冲。
你亲眼看一遍,会发现:每一步都只是”换参照系 + 一次矩阵/除法运算”,没有任何魔法。Model 把局部搬进世界,View 把世界搬进相机,Projection 准备好 w,除法用 w 制造透视,Viewport 落到像素。六道关,道道可解释。
推而广之:一个三角形有三个顶点,每个都独立走一遍这六关,得到三个屏幕像素位置,GPU 再把这三点连成的区域”光栅化”成一片像素、逐像素算颜色。所谓”渲染一帧”,本质就是”场景里几十万个顶点,排队过这六道关,再逐像素上色”。你写 shader 时,顶点着色器就在替每个顶点跑 ①→③,片元着色器在跑 ⑥ 之后的上色。理解了这条链,你就不会再把”画一个模型”当成黑箱。
27.10 收个尾,连回主线
咱们把这章扣回全书的钥匙:复杂的”三维到屏幕”过程,被工程师拆成一串目的单一、可组合的小步骤,每步就是一个矩阵或一次变换。理解每一步”在换哪套参照系、解决什么问题”,你就拥有了排查和创造的能力,而不是只会调滑块。
- 自然现象 / 问题:三维模型的点,要变成二维屏幕像素,中间要经历”局部→世界→相机→裁剪→标准坐标→像素”一整套参照系切换,且要正确产生近大远小、正确裁剪视野外。
- 工程师的方案:六道关卡的变换链(Model / View / Project / 裁剪 / 透视除法 / Viewport),用 4x4 齐次矩阵统一表达平移旋转缩放,用 w 和透视除法制造透视,用单位立方体统一裁剪,用视口映射到像素。
- 落进引擎的参数/设置:Transform(生成 Model)、Camera 的 transform(生成 View)、Camera 的 Projection 类型与 FOV/Near/Far/Aspect(生成 Projection)、视口分辨率(Viewport)。
- 协同:本章的 Projection 设 w、透视除法生深度,直接喂给第 26 章的深度缓冲——两章在”w”这个点上严丝合缝地咬合。
你带走的最核心一句话:“从模型到屏幕”不是一步魔法,而是六道可解释、可调试的关卡;理解这条链,你就能写 shader、能修穿模、能调透视。 下次面试官让你”口述一个顶点怎么到屏幕”,你就把这条六关流水线,配着 w 和透视除法,从头讲到尾。
【思考题】
- 为什么纯 3x3 矩阵表示不了”平移”?工程师引入齐次坐标 (x, y, z, w) 后,w=1 和 w=0 分别代表什么?各举一个例子(点和方向)。
- 视图矩阵(View)为什么是”相机自身 Transform 的逆矩阵”?用”移动相机等价于反向移动世界”这句话,解释为什么取逆而不是取正向。
- 本章说”近大远小是透视除法那一步产生的”。请用自己的话讲:投影矩阵给远处顶点设了更大的 w,除以 w 后为什么会让远处的东西在屏幕上显得更小?正交投影为什么没有近大远小(从 w 的角度说)?
- 法线是”表面朝外的方向”(第 6 章),在齐次坐标里它是 w=0 的向量。如果错误地用”模型矩阵 Model”直接去乘一个法线,会遇到什么问题?为什么法线通常需要用”模型矩阵的逆转置(inverse transpose)“来变换?提示:思考平移对”方向”有没有意义、以及非均匀缩放会不会把法线扭歪。
【参考答案】
- 平移是加法(点 + 偏移),而 3x3 矩阵乘向量是线性变换,必然把原点 (0,0,0) 映射到原点,无法把原点搬到一个非零位置——所以 3x3 表示不了平移。引入齐次坐标后:w=1 表示”点”(有位置,可被平移),如顶点局部坐标 (0,1,2,1);w=0 表示”方向 / 向量”(无位置,平移无意义),如法线 (0,1,0,0)、光线方向。这连回第 6 章:法线是方向不是位置,所以必须是 w=0。
- 视图矩阵的目的是”把全世界的点,重新用相机为原点来描述”。相机在世界里有一个 Transform(它在哪、朝哪)。如果直接把这个 Transform 应用到世界上,那是”把世界按相机的方式摆”,方向反了;我们要的是”让相机待在原点、看向 -z,世界相对它摆放”,所以必须对相机的变换取逆(反平移 + 反旋转)。即 View = inverse(CameraModel)。正如你拍照时不必推着人群动,只需”以自己为原点重述每个人的位置”——取逆就是这次”以相机为原点的重述”。
- 投影矩阵给远处顶点设的 w 近似等于其相机空间深度的负值,越远 |z| 越大、w 越大。透视除法用 x/w、y/w 得到屏幕坐标:远处的 x、y 被更大的 w 一除,被缩小得更多,于是离屏幕中心更近、显得更小;近处 w 小,除完相对放大,显得大。所以近大远小是这次除法直接产生的。正交投影的 w 恒为 1,除完 x、y 不变,远近一样大,因此没有近大远小——透视与正交的本质差别,就藏在 w 是否随深度变化。
- 法线是方向(w=0),平移对它没意义,所以 Model 矩阵里的平移列不该作用于法线(用 3x3 部分才对)。更隐蔽的坑:非均匀缩放(x 放大 2 倍、y 不变)会把法线扭歪——直接乘 Model 会让法线不再垂直于表面,光照(第 6 章 N·L)就错,明暗怪异。正确做法是用法线的”逆转置矩阵” inverse transpose of Model 的 3x3 部分来变换:转置抵消缩放的非均匀扭曲,求逆抵消缩放方向的拉伸,平移列本来就不参与(因为 w=0)。这也是为什么 shader / 法线贴图里常见
normalMatrix = transpose(inverse(mat3(modelMatrix)))。你能讲清”为什么不能简单乘 Model”,说明你真懂了齐次坐标和变换链。