跳转到内容

05 · 图形学:渲染管线与空间变换

定位:图形学面试第一题永远是“讲一遍渲染管线”——13 章给了概念版,这里给面试强化版(每个阶段的考点细化)。空间变换则是管线题的第一追问。本章学完,配合 12 章的数学基础,图形学的“地基两题”你就稳了。 学完标准:能从应用阶段一路讲到输出合并,每阶段说出 1-2 个考点(裁剪空间/NDC/Early-Z/插值);能推导“为什么顶点要乘一串 4×4 矩阵”;能解释 mipmap 为什么存在、reversed-Z 为什么流行;能把背面、视锥、遮挡三种剔除按粒度分开;能用重心坐标判断点在不在三角形里,并说清四元数为什么替代欧拉角。


1 应用 · CPU 剔除、合批、排序,提交 DrawCall 2 几何 · 顶点着色器 MVP,裁剪,再 ÷w 得到 NDC 3 光栅化 找覆盖像素,透视校正,片元着色 4 输出合并 深度、模板、混合,写入目标

每阶段的追问点(面试官会随机戳一个让你展开):

阶段 高频追问 答法要点
应用 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 决定画什么,顶点着色器决定顶点在哪,片元着色器决定像素什么色,输出合并决定谁留下来。”

完整链条(背顺序 + 每步的目的):

1 局部 物体自己的坐标 2 Model → 世界 缩放、旋转、平移,摆进场景 3 View → 相机 世界反向移到相机前 4 Projection → 裁剪 把深度写进 w,这一步不除 5 ÷w → 像素 得到 NDC,再映射到屏幕
矩阵 干什么 直觉
M 模型 物体自己的摆放(缩放旋转平移) “把剑从工坊坐标系搬到世界里”
V 观察 世界→相机(相机放原点看 -Z) “不是相机移向物体,是整个世界反向移向相机”
P 投影 3D→裁剪空间(fov/宽高比/近远平面) 透视 P 会把“远处的东西压小”;正交 P 不缩
÷w 裁剪→NDC 透视的本质:透视矩阵把 z 塞进 w,除完近大远小就出来了
视口映射 NDC→像素坐标 [-1,1] 映射到 [0,width]×[0,height]

三个必考细节:

  1. 为什么要齐次坐标 / w 分量(衔接 12 章矩阵表):平移不是线性变换,升一维才能写成矩阵乘法;透视除法靠 w 实现“除以深度”。
  2. API 差异(加分项):NDC 的 z 范围 OpenGL 是 [-1,1]、D3D/Vulkan/Metal 是 [0,1];纹理 v 轴 GL 原点在左下、D3D 在左上。坐标系见下面两张卡。
  3. 法线为什么不能用 MV 直接变换:非均匀缩放下 (MV)·n 不再垂直——要用逆转置矩阵 (MV⁻¹)ᵀ·n。经典追问。
右手 · OpenGL +X +Y +Z 朝外 相机通常看向 -Z 左手 · D3D / Unity +X +Y +Z 朝里 绕序和法线一起反

旋转怎么存(欧拉角 / 矩阵 / 四元数):欧拉角就是三个角,策划好懂,但两个轴叠到一起会丢掉一个自由度,这是万向节锁,插值还会抄近路甩出怪姿势。矩阵用 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 解决什么?原理?

📖 答案(先自己答完再展开)
  1. 应用:DrawCall 命令;几何:屏幕坐标顶点流;光栅化:片元;输出合并:最终像素。
  2. 之前,在裁剪空间做——除法后近处坐标剧烈放大,比较不便且 w≤0 会除零;裁剪空间里 |x|≤w 判断干净。
  3. 局部 →(M)→ 世界 →(V)→ 观察 →(P)→ 裁剪 →(÷w)→ NDC →(视口映射)→ 屏幕。
  4. 透视把“深度影响大小”编码进 w(除完近大远小);正交不产生透视收缩(w 恒 1),平行线仍平行。
  5. 双线性:一层 mipmap 内 4 texel 插值;三线性:再在相邻两层间插值,缩放动画不跳层。
  6. 透视投影下屏幕等距 ≠ 3D 等距;要按 1/w 加权(透视校正插值),否则贴图在斜面上拉伸扭曲。
  7. 固定块(4×4)固定倍率压缩,块内独立解码——取任一 texel 只需定位并解开所在块,无需整张解压。
  8. 片元着色器改写深度、alpha test/clip、关深度测试的奇技淫巧——硬件无法保证“提前测的深度=最终深度”。
  9. 写深度会把后面的透明片元当“被挡”剔除,而透明本该和身后内容混合;透明靠排序+混合表达遮挡。
  10. 传统深度近密远疏,又常把 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 切换掩蔽访存延迟)