05 · 图形学:渲染管线与空间变换
定位:图形学面试第一题永远是“讲一遍渲染管线”——13 章给了概念版,这里给面试强化版(每个阶段的考点细化)。空间变换则是管线题的第一追问。本章学完,配合 12 章的数学基础,图形学的“地基两题”你就稳了。 学完标准:能从应用阶段一路讲到输出合并,每阶段说出 1-2 个考点(裁剪空间/NDC/Early-Z/插值);能推导“为什么顶点要乘一串 4×4 矩阵”;能解释 mipmap 为什么存在、reversed-Z 为什么流行;能把背面、视锥、遮挡三种剔除按粒度分开;能用重心坐标判断点在不在三角形里,并说清四元数为什么替代欧拉角。
1. 渲染管线
Section titled “1. 渲染管线”每阶段的追问点(面试官会随机戳一个让你展开):
| 阶段 | 高频追问 | 答法要点 |
|---|---|---|
| 应用 | DrawCall 是什么、为什么贵;先剔除再提交 | CPU 向 GPU 提交一组绘制命令;贵在 CPU 侧验证和状态切换——所以先剔除(本章 §5)再合批(13 章) |
| 顶点着色器 | MVP 各是什么 | 见本章 §2 |
| 裁剪 | 在哪个空间做 | 裁剪空间(w 分量参与比较,除法前做,除法后坐标会炸) |
| 透视除法 | 什么是 NDC | xy 是 [-1,1]。z 在 OpenGL 是 [-1,1],D3D / Vulkan / Metal 是 [0,1]。w 正比于观察空间深度,不是倒数 |
| 光栅化 | 顶点属性怎么到像素 | 插值:重心坐标 + 透视校正(按 1/w 加权,否则透视下扭曲) |
| 片元 | 片元和像素的区别 | 片元=候选像素(还没通过深度/模板测试);通过并写入后才是像素 |
| OM | 深度测试/透明为什么最后 | 见本章 §4 |
面试金句:“管线就是’顶点去哪、像素怎么来、谁能写进屏幕’三段旅程;CPU 决定画什么,顶点着色器决定顶点在哪,片元着色器决定像素什么色,输出合并决定谁留下来。”
2. 空间变换链
Section titled “2. 空间变换链”完整链条(背顺序 + 每步的目的):
| 矩阵 | 干什么 | 直觉 |
|---|---|---|
| M 模型 | 物体自己的摆放(缩放旋转平移) | “把剑从工坊坐标系搬到世界里” |
| V 观察 | 世界→相机(相机放原点看 -Z) | “不是相机移向物体,是整个世界反向移向相机” |
| P 投影 | 3D→裁剪空间(fov/宽高比/近远平面) | 透视 P 会把“远处的东西压小”;正交 P 不缩 |
| ÷w | 裁剪→NDC | 透视的本质:透视矩阵把 z 塞进 w,除完近大远小就出来了 |
| 视口映射 | NDC→像素坐标 | [-1,1] 映射到 [0,width]×[0,height] |
三个必考细节:
- 为什么要齐次坐标 / w 分量(衔接 12 章矩阵表):平移不是线性变换,升一维才能写成矩阵乘法;透视除法靠 w 实现“除以深度”。
- API 差异(加分项):NDC 的 z 范围 OpenGL 是 [-1,1]、D3D/Vulkan/Metal 是 [0,1];纹理 v 轴 GL 原点在左下、D3D 在左上。坐标系见下面两张卡。
- 法线为什么不能用 MV 直接变换:非均匀缩放下
(MV)·n不再垂直——要用逆转置矩阵(MV⁻¹)ᵀ·n。经典追问。
旋转怎么存(欧拉角 / 矩阵 / 四元数):欧拉角就是三个角,策划好懂,但两个轴叠到一起会丢掉一个自由度,这是万向节锁,插值还会抄近路甩出怪姿势。矩阵用 9 个数,插值后还要重新正交化。四元数用 4 个数表示旋转,乘法复合,slerp 插值稳定,没有万向节锁;用之前要归一化,否则会漂。Unity 和 UE 的旋转都是四元数。不要用欧拉角做动画插值。
3. 光栅化与纹理(高频追问区)
Section titled “3. 光栅化与纹理(高频追问区)”插值:三角形内部某点的属性 = 三个顶点属性的加权和(重心坐标)。三个权重是三个小三角形的面积比,加起来为 1。三个都在 [0, 1],点就在三角形内(含边上)。这是“点在不在三角形里”的标准答法,叉积看绕序是同一件事的边版本。透视校正插值:屏幕空间线性插值在透视下是错的,要按 1/w 加权再归一——不说这句,面试官会觉得你真写过 shader。
纹理采样三问:
| 问 | 答 |
|---|---|
| 放大怎么办(texel 不够) | 过滤:最近邻(块状)→ 双线性(4 texel 加权,主流默认) |
| 缩小怎么办(一堆 texel 挤一像素,闪烁) | mipmap:预生成 1/2、1/4… 金字塔,按屏幕导数选层;三线性 = 相邻两层各双线性再混合(层间平滑) |
| uv 出 [0,1] 怎么办 | 寻址模式:重复 Repeat / 镜像 Mirror / 夹紧 Clamp |
mipmap 为什么必要(追问率高):远处一个像素覆盖几十个 texel,逐个采样既慢又因采样不足而闪烁(摩尔纹);mipmap 让一次采样代表一块区域的平均值。代价:显存 +33%。各向异性过滤 = 在 mipmap 基础上按视角压扁的覆盖形状多采样几次,斜看地面更清晰。
纹理压缩格式(游戏必考):BCn(PC/主机,即 DXT)/ ASTC(移动)/ ETC(老移动)。为什么不用 PNG/JPG:PNG 解压后仍是未压缩 RGBA(显存爆);BC/ASTC 是固定压缩率、可随机寻址的格式——GPU 采样时按需解压单个块(4×4 像素块),不用整张解开。这是“为显存和随机访问设计的压缩”,与“为传输设计”的 PNG 本质不同。
4. 输出合并:深度、模板、混合 + Early-Z(现代管线必聊)
Section titled “4. 输出合并:深度、模板、混合 + Early-Z(现代管线必聊)”深度测试(Z-Test):片元的 z 和深度缓冲比,“更近”才通过并写入。两个深挖点:
- z 精度问题:透视投影后深度分布近密远疏——近处精度高远处糊,远处的深度冲突(z-fighting,两面闪)由此来。Reversed-Z 把 near 放在 1、far 放在 0,把浮点在 0 附近的高精度让给远处,补上透视丢掉的那一段。要配浮点深度缓冲,整数缓冲没有这个收益。UE 默认开。
- Early-Z(提前深度测试):在不改深度的前提下,硬件在片元着色器之前先测深度,被挡的像素直接不跑 shader——省掉大量无效着色。Z-Prepass:先跑一遍只写深度的廉价 pass,把最终深度铺好,再跑真正着色(此时 Early-Z 全命中)——复杂场景的经典优化。破坏 Early-Z 的操作:alpha test/clip()、片元里改深度——硬件被迫关提前测试。
模板测试:按 stencil buffer 里的值做位运算判断(描边、镜面反射范围、portal 挖洞用)。
混合(Blend)与透明排序:半透明无法排序解决深度冲突(透明片元既要被挡又要挡人),规则是:不透明先画(开深度写,顺序无所谓,Early-Z 吃掉冗余);透明后画、关深度写、由远到近画(混合是非交换的,顺序错颜色错)。深度冲突时透明物体的经典问题“半透明无法完美排序”→ 分场景讨论(粒子的深度峰值排序、dither 半透明等)。
5. 剔除:少画才是真优化(应用阶段的主菜)
Section titled “5. 剔除:少画才是真优化(应用阶段的主菜)”管线第一段写着“剔除 / 合批 / 排序”。只会念名字,追问两句就断。剔除回答的是:这个东西根本不会出现在屏幕上,就别提交 DrawCall。
| 种类 | 判据 | 谁来做 | 便宜程度 |
|---|---|---|---|
| 背面剔除 | 三角形法线背对相机 | GPU 固定功能,一个开关 | 最便宜,默认开。树叶、布料这类双面材质要关 |
| 视锥剔除 | 物体包围盒(AABB 或球)是否与相机视锥的 6 个平面相交 | CPU,或 Compute,在提交前 | 便宜,先做。完全在锥外的整棵子树都能剪掉(场景图 / BVH) |
| 遮挡剔除 | 被别的东西完全挡住 | 软件遮挡(用上一帧深度,必须保守)或 GPU Hi-Z | 贵一些,城市场景收益大。会误杀就宁可不剔除 |
| 距离 / 尺寸 | 太远,或投影到屏幕小于若干像素 | CPU | 配合 LOD:远了换低模,再远直接不画 |
顺序是先做便宜的大范围裁剪(视锥、层级),再做贵的遮挡,最后才是合批和排序。遮挡剔除和 Early-Z 不重复:前者是整件物体不提交,后者是已经提交之后,被挡住的像素不跑 shader。
Compute Shader 插在哪:它不在光栅化管线上,是绕开顶点和片元、直接对缓冲做通用计算的阶段。粒子模拟、Hi-Z 遮挡、光照分簇、后处理模糊都走它。面试可以补一句:剔除可以在 CPU 做,也可以把包围盒丢给 compute,结果写回间接绘制参数。
面试金句:“背面剔除丢掉朝后的三角形,视锥剔除丢掉相机看不见的物体,遮挡剔除丢掉被挡住的物体。三件事的粒度不同:三角形、物体、物体。Early-Z 是像素级的最后一道,替代不了前两个。”
6. 高频面试题 Q&A(合上书能讲)
Section titled “6. 高频面试题 Q&A(合上书能讲)”Q1:讲一遍渲染管线。 三阶段:应用(CPU 剔除/排序/提交 DrawCall)→ 几何(顶点着色器做 MVP,可选细分/几何着色器,裁剪空间裁剪、透视除法得 NDC、屏幕映射)→ 光栅化(三角形遍历找覆盖像素、透视校正插值、片元着色器逐像素着色)→ 输出合并(深度/模板测试、混合写回)。每步我可以说细节(Early-Z、插值公式、NDC 范围……)。
Q2:MVP 三矩阵分别是什么? Model 把物体从局部坐标摆到世界;View 把世界变换到相机原点(世界反向运动);Projection 把视锥压成裁剪空间(透视含近大远小信息,正交不缩)。之后还有两步常被并入“P”叙述:透视除法(÷w 得 NDC)和视口映射(得屏幕像素坐标)。
Q3:为什么要有 w 分量 / 齐次坐标? 平移不是线性变换,2D/3D 矩阵写不出“加常数”,升一维用 w=1 把平移变成矩阵乘法;同时透视投影把深度编码进 w,透视除法(÷w)一步实现“远处收缩”。
Q4:什么是 NDC?透视除法做了什么? 裁剪空间坐标除以 w 之后就是 NDC。xy 一般是 [-1,1];z 在 OpenGL 是 [-1,1],在 D3D / Vulkan / Metal 是 [0,1]。透视矩阵把观察空间的 z 写进 w,w 正比于深度,不是倒数。除以 w 之后才近大远小。裁剪要在这次除法之前做。
Q5:法线变换为什么要用逆转置? 非均匀缩放下直接乘 MV 会破坏垂直关系;数学上方向要乘 (MV⁻¹)ᵀ 才保持点积/垂直。均匀缩放时退化成 MV(再归一化)。
Q6:mipmap 是什么?解决什么问题? 预生成的多级缩小纹理金字塔。解决“屏幕一个像素覆盖多个 texel”时的高频闪烁(欠采样摩尔纹)和采样浪费——按屏幕空间导数选合适层级。三线性=层间也插值;各向异性=斜视角多方向采样更清晰。代价显存 +1/3。
Q7:纹理为什么要用 BC/ASTC 而不是 PNG? BC/ASTC 是固定倍率、分块、可随机寻址的显存友好压缩:GPU 采样时只解压命中的 4×4 块,显存占用可控且带宽省;PNG/JPG 是整张流式压缩,GPU 无法随机取一个 texel,必须整解成 RGBA——显存和带宽都爆。所以资产管线离线转 BC/ASTC,网络传输才用 PNG。
Q8:什么是 Early-Z 和 Z-Prepass? Early-Z:片元不改深度时,硬件把深度测试提前到着色前,被挡像素不跑 shader,省大量算力。Z-Prepass:先画一遍“只写深度”的廉价 pass 铺满深度,正式着色时几乎全被 Early-Z 拦截。注意 alpha test/clip、片元改深度会迫使 Early-Z 失效。
Q9:透明物体为什么最后画、由远到近? 混合是非交换的(src/dst 顺序不同结果不同),所以必须按视觉顺序叠加:先远后近。且透明物体不写深度(否则后面的透明被错误剔除)。不透明物体先画且顺序无关(深度测试天然解决)。
Q10:什么是 z-fighting?怎么缓解? 两个面深度几乎相同,浮点精度区分不了,渲染结果闪烁。缓解:拉大近平面(精度大头在近处)、模型上避免共面、polygon offset;根治是 Reversed-Z 配浮点深度:near=1、far=0,浮点靠近 0 的精度让给远处。
Q11:片元和像素的区别? 片元是光栅化产出、尚未经深度/模板/混合考验的“候选像素”(带各种插值属性);通过输出合并真正写进渲染目标才叫像素。
Q12:视锥剔除、遮挡剔除和 Early-Z 有什么区别? 视锥剔除看包围盒在不在相机六个平面里,完全在外就不提交,便宜,按场景树可以整棵剪掉。遮挡剔除看是不是被别的物体完全挡住,用上一帧深度或 Hi-Z,更贵,必须保守,以免误杀。Early-Z 是已经提交之后、像素着色之前的深度测试,省的是 shader,不是 DrawCall。三者分别是物体级、物体级、像素级,叠着做。背面剔除更早,丢的是朝后的三角形。
Q13:怎么判断一个点在不在三角形里? 用重心坐标:点写成三个顶点的加权和,权重是三个小三角形的面积比,加起来为 1。三个权重都在 0 到 1 之间就在三角形内,含边。光栅化插值用的就是这组权重。透视下不能在屏幕空间直接线性插值,要先对属性除以 w 再插值。
Q14:欧拉角、旋转矩阵、四元数怎么选? 欧拉角三个数,直观,但有万向节锁,插值会甩出怪姿势。矩阵 9 个数,插值后要重新正交化。四元数 4 个数,乘法复合旋转,slerp 稳定,没有万向节锁,用前要归一化。引擎里的旋转存四元数,欧拉角只留给编辑器。
7. 本章自测(10 题,限时 20 分钟,先自己答再看答案)
Section titled “7. 本章自测(10 题,限时 20 分钟,先自己答再看答案)”1. 管线四大部分各产出什么?
2. 裁剪发生在透视除法之前还是之后?为什么?
3. 从局部坐标到屏幕坐标的完整变换链(6 步)?
4. 透视 P 和正交 P 的本质区别?
5. 双线性和三线性差在哪层?
6. 为什么屏幕空间直接线性插值 uv 是错的?
7. BC 压缩为什么能被 GPU"随机访问"?
8. Early-Z 什么情况下失效?
9. 透明物体为什么不写深度?
10. Reversed-Z 解决什么?原理?
📖 答案(先自己答完再展开)
- 应用:DrawCall 命令;几何:屏幕坐标顶点流;光栅化:片元;输出合并:最终像素。
- 之前,在裁剪空间做——除法后近处坐标剧烈放大,比较不便且 w≤0 会除零;裁剪空间里 |x|≤w 判断干净。
- 局部 →(M)→ 世界 →(V)→ 观察 →(P)→ 裁剪 →(÷w)→ NDC →(视口映射)→ 屏幕。
- 透视把“深度影响大小”编码进 w(除完近大远小);正交不产生透视收缩(w 恒 1),平行线仍平行。
- 双线性:一层 mipmap 内 4 texel 插值;三线性:再在相邻两层间插值,缩放动画不跳层。
- 透视投影下屏幕等距 ≠ 3D 等距;要按 1/w 加权(透视校正插值),否则贴图在斜面上拉伸扭曲。
- 固定块(4×4)固定倍率压缩,块内独立解码——取任一 texel 只需定位并解开所在块,无需整张解压。
- 片元着色器改写深度、alpha test/clip、关深度测试的奇技淫巧——硬件无法保证“提前测的深度=最终深度”。
- 写深度会把后面的透明片元当“被挡”剔除,而透明本该和身后内容混合;透明靠排序+混合表达遮挡。
- 传统深度近密远疏,又常把 near 放在 0,浮点精度也堆在近处,远处就糊。Reversed-Z 把 near=1、far=0,把浮点在 0 附近的精度让给远处,并且要配浮点深度缓冲。
8. 进阶追问(答不上来就回来复习)
Section titled “8. 进阶追问(答不上来就回来复习)”- 重心坐标怎么算?见本章 §3(面积比;三个权重都在 0 到 1 就在三角形内。透视校正是 attr/w 线性插值后再乘回 w)
- 视锥剔除和遮挡剔除的区别?见本章 §5(包围盒对六个平面 vs 被别的物体挡住;Early-Z 是像素级,替代不了它们)
- Compute shader 在管线什么位置?见本章 §5(不在光栅化管线上,绕开顶点/片元做通用计算)
- 与 13 章贯通:DrawCall 合批的几种方式?(静态合批/动态合批/实例化/SRP Batcher)
- 与 11 章贯通:为什么说 GPU 是“吞吐换延迟”?(warp 切换掩蔽访存延迟)