3.1 变换:CPU 算矩阵 vs GPU 做乘法 本节摘要:三维物体要经过模型、视图、投影三套矩阵,才从本地坐标落到裁剪空间,再由 GPU 除以 w 做透视除法进 NDC。矩阵元素几乎总在 CPU 算完;GPU 的工作是在顶点着色器里做矩阵乘向量。可以把三矩阵在 CPU 合成一份 MVP 只传一次,也可以分开传让 VS 分步乘。差别在带宽、是否还要模型矩阵做法线、以及骨骼动画那种“每顶点不同矩阵”的极端。
本节摘要:三维物体要经过模型、视图、投影三套矩阵,才从本地坐标落到裁剪空间,再由 GPU 除以 w 做透视除法进 NDC。矩阵元素几乎总在 CPU 算完;GPU 的工作是在顶点着色器里做矩阵乘向量。可以把三矩阵在 CPU 合成一份 MVP 只传一次,也可以分开传让 VS 分步乘。差别在带宽、是否还要模型矩阵做法线、以及骨骼动画那种“每顶点不同矩阵”的极端。
阅读完本节,你应当能够:
本地坐标是建模时的原点。乘模型矩阵到世界,乘视图到相机,乘投影到裁剪。GPU 硬件接着做透视除法。CPU 若把模型、视图、投影先乘成一份 4x4,VS 只做一次乘向量,算术更少,但世界空间位置丢了,除非你另传模型矩阵。光照常要世界法线或观察空间法线,于是“合成一份”并不总省事。
没有矩阵栈。固定管线记忆里的 pushMatrix 在这里是你自己的数组或引擎节点。引擎遍历场景图,在 CPU 上把父节点矩阵乘下来,得到每个网格的世界矩阵,再与相机的视图投影结合。GPU 仍然只看见 Uniform 或 instance Attribute。把引擎当“GPU 自动算层级”是错的:层级在 CPU。
本地 --M--> 世界 --V--> 观察 --P--> 裁剪 --除w--> NDC --视口--> 像素 CPU常算 M V P 的元素 GPU常做 矩阵 × 顶点
| 投影 | 视觉 | 矩阵特征 | 适合 |
|---|---|---|---|
| 透视 | 近大远小 | 与 z 相关的 w | 世界、产品、游戏 |
| 正交 | 平行不汇聚 | 无透视项 | CAD、2D UI、阴影贴图常用 |
正交不是“低级透视”。阴影贴图的方向光常用正交,HUD 常用正交。用透视画 UI,按钮会随相机 squish。投影选择是产品合同,不是审美。
法线变换:只把模型矩阵左上 3x3 拿来乘法线,遇到非均匀缩放会错。正确是用逆转置。算逆在 CPU 做一次,传到 uNormalMatrix,VS 乘法线。每顶点在 GPU 求逆 4x4 是浪费。骨骼除外:每顶点矩阵不同,只好在 VS 里混多个骨骼矩阵再乘——那是第 4.4 节的通道选择,不是否定“能在 CPU 做的逆就不要放进 VS”。
attribute vec3 aPos; attribute vec3 aN; uniform mat4 uMVP; uniform mat3 uN; varying vec3 vN; void main() { gl_Position = uMVP * vec4(aPos, 1.0); vN = uN * aN; }
这一小段已经是分工:MVP 与法线矩阵在 CPU 准备,GPU 只乘。时间动画若只是绕 Y 转,CPU 每帧改 M 再合成 MVP,仍比每帧改 VBO 便宜。

列主序是 WebGL 的约定。JS 数组 16 个 float 按列排。自己手写索引时 m[12],m[13],m[14] 是平移。引擎与数学库若按行主序存储,上传必须转置或在 uniformMatrix4fv 把 transpose 设对。WebGL 对 transpose 为 true 的支持曾经别扭,实践里我更倾向 CPU 就按列主序排好,transpose 保持 false。这是 CPU 合同,GPU 只吃你给的 16 个数。
| 策略 | CPU 工作 | GPU 工作 | 何时选 |
|---|---|---|---|
| 只传 MVP | 每物体乘三次矩阵 | 一次 mat4 乘 vec4 | 只要裁剪坐标、不要世界位 |
| 传 M 与 VP | VP 每帧一次,M 每物体 | 两次乘 | 要世界位置做光照或雾 |
| 传 M V P | 更清晰 | 三次乘 | 教学;生产通常合并 VP |
| 实例化矩阵 Attribute | 上传实例表 | 每实例取矩阵再乘 | 大量同类物体 |
相机本身是视图矩阵的逆:相机世界变换的逆。CPU 算。lookAt 是便利函数,不是 GPU 功能。Z 近平面过近、远平面过远,深度精度变差,这是投影矩阵参数的 CPU 选择,会在第 3.3 节变成条纹与闪烁。先在这里记住:投影不是“视野角越大越酷”,它是深度缓冲的分配方案。
⚠️ 常见坑:VS 里写成
aPos * uMVP。GLSL 的矩阵乘法不满足你在纸上随便换序的习惯。位置是列向量时必须矩阵在左。画面会切成不可认的条,日志却完全干净。💡 关键直觉:能对整物体做的算,放 CPU;必须对每个顶点不同的算,放 VS。层级变换属于前者,波浪属于后者,骨骼两边都沾。
齐次坐标 w:位置用 1,方向用 0。CPU 构造向量时搞反,平移会污染方向。法线是方向。这又是组装合同。引擎的 Vector3 变换接口通常分 transformPoint 与 transformDirection,裸写时要自己分。
演示可以。物体一动,你就要改着色器字符串并重新编译,把第 2.4 节的 CPU 编译账单拉进动画循环。矩阵 Uniform 存在的理由就是让公式稳定、让参数每帧变。写死三角函数当教学可以,当架构不行。
第一个立方体建议 CPU 用现成数学库生成透视矩阵与 lookAt,VS 只乘 MVP。自己实现透视矩阵作为练习可以,但不要与“第一次看见 3D”绑在同一小时,否则分不清是投影写错还是缓冲绑错。对照驱动:空间错通常是整物体位置怪;缓冲错通常是炸裂或全黑。
相机动画只改视图。第一人称与轨道相机都是 CPU 用四元数或欧拉生成旋转,再放进 lookAt 或直接建视图矩阵。欧拉角万向锁是 CPU 数学问题,不是 GPU。锁了之后表现为视角乱跳,有人去改投影 near,完全找错端。四元数插值在 CPU 做完,GPU 仍然只乘一份矩阵。
深度抖动与 TAA 一类技术会在投影或裁剪空间加小偏移。偏移在 CPU 每帧改投影,或在 VS 加常数。它们为了缓解 3.3 的精度条纹或锯齿。不要把抖动当“变换搞砸了的修复”。先保证 MVP 链正确、near/far 合理,再谈抖动。错误的乘法顺序加抖动,只会让错误会跳舞。
精度:世界很大、物体很远时,矩阵乘法在 GPU 用 mediump 会抖。对策对照:把大世界放到 CPU 的相机相对空间,VS 里只处理相对相机的坐标,提高有效精度;或局部 highp。这是把“大数减大数”从 GPU 挪回 CPU。MMORPG 地图常用这招。产品查看器物体在原点附近,不必上。
父节点带动子节点,引擎在 CPU 遍历。你自己写时,用数组存局部矩阵,每帧从根乘到叶。不要在 VS 里试图用骨骼系统模拟场景图,除非叶子真是蒙皮网格。场景图是物体级,蒙皮是顶点级,粒度不同,通道不同。混用会把 4.4 的账单提前透支。
WebGL 裁剪空间 Z 从 -1 到 1,透视矩阵按这个约定造。有的引擎内部用 0 到 1 的 Z,再转换。混用两份数学库,Z 会翻转,深度测试像反了。看起来是 3.3 的 bug,根在 3.1 的矩阵合同。选定一个数学库,透视函数与深度范围成套。不要从文章 A 抄 lookAt、文章 B 抄 perspective。套件比公式正确性口号重要。
镜像相机或反射矩阵会翻转绕序,要临时改 frontFace。反射平面做镜子时,先改绕序再画反射场景,画完改回。这是变换与剔除的交叉。状态泄漏会让主场景缺面。pass 结束恢复,又是那句老话。矩阵再漂亮,状态表乱了也白乘。
near 过近或矩阵把 z 搞反,裁剪没把它丢掉。也可能深度函数与写入被泄漏。先输出 NDC 的 z 当颜色。z 不合理回 3.1;z 合理仍乱挡回 3.3。不要两节一起改。乘法顺序 aPos 在左会把模型切成条,那是整物体变形,与“在背后”不同。看症状分类。
near 太小、far 太大。把 near 提到 0.05 或 0.1 米级,far 收到关卡半径。条纹是精度饼分薄了。polygon offset 救不了整世界。大世界用相机相对位置,CPU 把大数减掉再上传。这是精度搬家,不是换投影类型那么简单。
CPU 没乘父世界矩阵,或引擎节点没 add 对。VS 不会懂层级。打印子物体最终 M,看是否含父旋转。含了仍不对,查列主序与转置。层级 bug 看起来像着色器,其实是 3.1 的树遍历。单元测试矩阵乘,比盯画面快。
全项目一个数学库。透视、lookAt、逆、转置都用它。测试向量:点 0,0,0 乘模型应到平移量;方向 0,0,1 乘不该被平移。用这两条抓住 w 用错。相机 lookAt 的 up 与视线平行会出 NaN,CPU 要断言。NaN 进 GPU 会整屏烂,日志仍成功。动画里 up 与视线接近时改 up。大世界每帧把物体位置减相机位置再送 VS,CPU 多几次减法,GPU 精度换回来。这是 3.1 对 4.7 的贡献:精度问题先搬家,再谈 highp。
矩阵验收用三件套:单位立方体在原点、父带子午转、相机绕物。三件套过了再上业务模型。业务模型单位错会让三件套看起来“库有问题”。先怀疑资产单位。NaN 检测在 CPU 乘完后扫 16 个数,发现就打物体名。NaN 进 GPU 无日志。镜像 pass 改 frontFace 必须用栈或配对恢复,禁止散落的赋值。反射做完主场景缺面,十次里九次是没恢复。大世界相对化写成数学库的 vec3.sub(cameraPos),VS 只见小值。相对化漏了一个物体,那个物体会抖或飞,像单独的 bug,其实是精度政策没覆盖全。政策要覆盖灯光位置、阴影灯相机,不只是网格。
矩阵调试打印 16 个数没有人爱看。改成打印平移与欧拉近似角。角对了再看缩放。缩放为负就警告绕序。警告比缺面后查 3.3 更早。相机 up 与视线点积接近 1 时改 up。不改会出 NaN,NaN 无日志。把检测放在 lookAt 包装里,业务代码就不会踩。包装是 CPU 卫生,GPU 只乘你给的数。
现场对照时只改一个变量。同时改版本、封装、通道,归因会消失。消失之后只能靠感觉,感觉正是本书要拆掉的东西。把改动写成一条,测完再写下一条。一条一条,四组对照才用得上。用不上的对照表只是装饰。装饰填不满格子,格子空着就不要标完工。矩阵这一格改完,再用三件套验收,不要直接上业务模型。
下一节对照颜色怎么写进帧缓冲:直接覆盖,还是按混合函数与旧像素合成。