速览卡片
| 概念 | 一句话说明 |
|---|---|
| 渲染流水线 | 从三维场景出发生成一张二维图像,CPU 和 GPU 共同完成 |
| 应用阶段 | CPU 主导:准备数据 → 设置渲染状态 → 提交 Draw Call,输出渲染图元 |
| 几何阶段 | GPU 主导:顶点着色器(MVP 变换)→ 裁剪 → 屏幕映射 |
| 光栅化阶段 | GPU 主导:三角形遍历生成片元 → 片元着色器 → 测试与混合 |
| 渲染图元 | GPU 唯一能直接处理的几何单位:点、线、三角形 |
| Draw Call | CPU 发给 GPU 的命令,只指向图元列表,不含材质信息 |
| 齐次裁剪空间 | 顶点着色器的输出空间,四维坐标 (x,y,z,w),w≠1 |
| 屏幕映射 | NDC [-1,1]³ → 屏幕像素坐标,z 不处理 |
| 片元 | 光栅化生成的”候选像素”,插值顶点属性的状态集合,≠ 像素 |
| 逐片元操作 | 模板测试 → 深度测试 → 混合,高度可配置但不可编程 |
| Early-Z | 深度测试提前到片元着色器之前,与透明度测试冲突 |
| 双重缓冲 | 渲染在后置缓冲,完成后交换前置缓冲,画面连续 |
| 命令缓冲区 | CPU 添命令、GPU 取命令,二者并行工作的关键 |
| 批处理 | 减少 Draw Call:合并网格,适合静态物体 |
2.1 综述
洋娃娃工厂例子:流水线各工序并行,产量取决于最慢的工序(bottleneck)。
渲染流程分三个概念阶段:应用阶段、几何阶段、光栅化阶段。划分依据是谁拥有控制权——应用阶段 CPU 控制,后两个阶段 GPU 接管;各阶段之间流水线并行,”应用阶段”在整个帧的时间内持续发生。
A:图元 = GPU 能直接处理的基本几何单位(点/线/三角形,Mesh 和四边形必须拆成这三种);片元 = 光栅化阶段对被覆盖像素生成的状态集合(插值得到的法线、UV、深度等)。链条:CPU 把 Mesh 拆成图元 → GPU 把图元变片元 → 片元过测试变像素。
2.2 CPU 和 GPU 之间的通信
应用阶段三个任务:准备场景数据(摄像机、视锥体、模型、光源);粗粒度剔除(culling);设置渲染状态。输出是渲染图元。
数据流向:硬盘 → 内存(全部渲染数据)→ 显存(GPU 要用的子集)。显卡对显存访问更快、且大多数显卡不能直接访问 RAM;数据进显存后 RAM 副本通常可删,但 CPU 还要做碰撞检测/动画的数据要保留。
A:不相等,是"全集 ⊃ 子集"的关系。第一步是全集(几何、纹理、Shader、材质参数全进 RAM);第二步是子集——只有 GPU 真正消费的数据进显存:顶点/索引(顶点着色器用)、纹理(片元着色器采样)、参数(MVP、光照等)。碰撞体、骨骼、CPU 剔除用的包围盒留在 RAM。判断标准:这数据接下来被 CPU 用还是被 GPU 用。
渲染状态定义网格怎么被渲染:用哪个顶点着色器/片元着色器、光源属性、材质等。不改状态,所有网格用同一种状态渲染。
Draw Call 就是一个命令,发起方是 CPU,接收方是 GPU,只指向图元列表,材质信息上一步已设好。
2.3 GPU 流水线
先给出书中图 2.6 的完整流程,后文小节按此流程逐站展开:
着色器 → 几何
着色器 → 裁剪 → 屏幕映射 → 三角形
设置 → 三角形
遍历 → 片元着色器 → 逐片元
操作 → 屏幕图像
实线框 = 必需 虚线框 = 可选(细分、几何着色器)
2.3.2 顶点着色器
顶点着色器(Vertex Shader):每顶点调用一次;不能创建/销毁顶点,无法得知顶点间关系,因此可高速并行。两个主要工作:坐标变换和逐顶点光照。
1 | o.pos = mul(UNITY_MVP, v.position); // 模型空间 → 齐次裁剪空间 |
MVP = 模型变换(模型空间→世界空间,即 Transform 组件)、V 观察变换(世界→相机空间,相机变换的逆)、P 投影变换(相机→齐次裁剪空间)。输出仍是四维、w≠1,”压扁”发生在后面的透视除法。
A:顶点着色器输出只是齐次裁剪空间坐标,不含屏幕分辨率信息,算不出像素位置。之后链路:裁剪(在齐次裁剪空间,裁剪面 x=±w 等六个平面)→ 透视除法(xyz/w → NDC [-1,1]³,固定硬件自动做)→ 屏幕映射。
A:"顶点颜色"只是举例——输出统称 varying,"易变"指会被光栅化器插值。顶点着色器可输出颜色、UV、法线、切线等。现代管线颜色多在片元着色器算(纹理×光照),顶点色通道常被挪用:蒙皮权重、动画参数、遮罩。历史原因:固定管线时代属性槽位定死(位置/法线/颜色/UV),颜色槽常空着就借用了;现在可自定义顶点属性(mesh.uv2~uv8),但复用颜色通道零成本、工具链成熟,仍常见。片元颜色 = 顶点属性按重心坐标加权插值的产物。
2.3.3 裁剪
裁剪(Clipping)处理三种关系:完全在内直接传递、完全在外剔除、部分在内则在边界处生成新顶点(位于图元与视野边界的交点)。不可编程,可配置(自定义裁剪平面)。
"裁剪"有两个:CPU 在应用阶段做视锥体剔除(物体级,被剔除的不发 Draw Call)和 GPU 在几何阶段做图元裁剪(部分在内会生成新顶点)。一前一后、一个 CPU 一个 GPU。
2.3.4 屏幕映射
屏幕映射(Screen Mapping):x、y 从 [-1,1] 缩放到窗口分辨率(如 1080×1920)。z 不处理,与屏幕坐标组成窗口坐标系传给光栅化。
坑:OpenGL 以左下角为 (0,0),DirectX 以左上角为 (0,0)——图像倒转常因于此。
屏幕映射只负责 NDC → 屏幕像素坐标。"归一化到 [-1,1]³"是独立的透视除法(裁剪后、屏幕映射前,固定硬件自动完成),不在任何可编程阶段。
2.3.5 & 2.3.6 三角形设置与遍历
进入光栅化概念阶段。三角形设置(Triangle Setup)计算光栅化所需信息(边界表示方式);三角形遍历(Triangle Traversal)判断覆盖哪些像素,并对覆盖区域插值顶点信息(图 2.12 的深度插值例子)。都是固定函数。
定义:三角形 ABC 内一点 P,存在唯一一组 (λ_A, λ_B, λ_C) 满足:
快速计算:2D 叉积 = 2×2 行列式 = 带符号平行四边形面积。分母 cross(B−A, C−A) 算一次,分子各算一次,第三个用 1 减出来。三个 λ 全非负 ⇔ P 在三角形内——覆盖测试与插值权重是同一组计算;硬件用等价的边函数沿扫描线递推,所以快。
任意属性都走同一公式:
2.3.7 片元着色器
片元着色器(Fragment Shader)实现逐片元(Per-Fragment)着色,完全可编程。最重要技术:纹理采样(UV 是插值来的)。DirectX 里叫像素着色器,但”片元”更准确(此时还不是像素)。
输入 = 插值顶点属性 + 自己的屏幕坐标(SV_POSITION),不知道自己在哪个三角形——无三角形 ID、无索引,每次调用互相独立。定位自己靠:屏幕坐标(像素位置效果);隐式屏幕空间导数(ddx/ddy,GPU 以 2×2 quad 执行可得,mipmap 选级靠它,cross(ddx,ddy) 可重建面法线——书上说的"唯一例外")。理解"信息孤岛"就通了:为什么做不了"到三角形顶点的距离"、为什么透视校正插值重要。
2.3.8 逐片元操作
逐片元操作(Per-Fragment Operations):如修改颜色、深度缓冲、进行混合等,不可编程但可配置性很高。流程:片元 → 模板测试 → 深度测试 → 混合 → 颜色缓冲区。
A:模板测试 = 绘画软件里的剪贴蒙版:上层内容只显示在下层有内容的区域,涂出界的部分自动被裁掉。模板测试就是把这套逻辑搬到每个像素上。
机制:每像素一个模板缓冲区(整数 0~255),相当于每格的"蒙版形状";片元带着参考值来"对蒙版",条件满足 → 内容显示(继续走深度测试、写颜色);不满足 → 被裁掉。
经典配合(两次绘制):第一遍画 A,把 A 的区域"定义为蒙版"(模板=1);第二遍画 B,按条件被这个蒙版裁剪——B 画不进 A 的区域。
例 1 · 描边:剪贴蒙版的反向用法。先正常画角色 = 定义蒙版(模板=1);再画放大 3% 的红色角色,条件设为"模板==1 就丢弃"——等于把红色内容从角色轮廓里裁掉,只剩轮廓外多出来的一圈红边。
例 2 · 镜子:标准正向剪贴。先画镜面 = 铺出蒙版形状(模板=1);再画镜中世界,条件设为"只有模板==1 才许画"——镜像内容被剪贴进镜面的形状里,镜外的部分全部裁掉。
要点:① 模板缓冲全帧共享,用完要恢复状态;② 值是整数,可当计数器(阴影体:正面+1、背面−1,非零即阴影);③ 只能过滤内容,不能生产内容(内部线靠贴图/rim)。
深度测试 = Z-Buffer 算法:近者胜,胜者写颜色+深度。一句话分工:深度测试管前后遮挡(自动),模板测试管区域通行(程序员布局)。
A:不是。硬件混合是通用公式:C_final = C_src ⊗ S + C_dst ⊗ D,S/D 可配置。α-blend 只是一种配置:S=SrcAlpha、D=OneMinusSrcAlpha(C_src·α + C_dst·(1−α))。其他:加法混合(S=D=One,直接相加,火焰/光效,越加越亮);不混合(直接覆盖)——不透明物体走这个,也是初学者开不出透明的原因。半透明物体必须从后往前画:混合对象是"颜色缓冲区已有的颜色",顺序错了就混无可混(渲染队列的存在原因);透明物体通常关深度写入,否则近的玻璃会挡掉后面的东西。
两个补充:
- Early-Z 技术:测试逻辑上在片元着色器之后,但 GPU 会尽量提前(避免算完颜色才发现被舍弃)。与透明度测试(clip)冲突——着色器可能推翻提前测试的结果,GPU 检测到冲突就禁用提前测试,这就是透明度测试性能下降的原因。
- 双重缓冲(Double Buffering):渲染在后置缓冲(Back Buffer),完成后与前置缓冲(Front Buffer)交换,画面连续。
2.4 容易困惑的地方
2.4.1 OpenGL/DirectX 与显卡驱动
OpenGL/DirectX 是图像应用编程接口,架起应用与 GPU 的桥梁:应用发命令 → 接口转给显卡驱动 → 驱动翻译成 GPU 语言 + 把纹理转成 GPU 支持的格式(驱动像”显卡的操作系统”)。
2.4.2 HLSL、GLSL、CG
HLSL(DirectX)、GLSL(OpenGL)、CG(NVIDIA),先编译成与机器无关的中间语言(IL),再由驱动翻译。GLSL 跨平台但编译依赖硬件厂商(结果可能不一致);HLSL 微软统一编译但几乎仅限微软平台;CG 语法近 HLSL、可无缝移植,但可能用不上 OpenGL 新特性。
2.4.3 Draw Call
误区:以为元凶是 GPU 状态切换。其实”拖后腿”的是 CPU。
CPU/GPU 靠命令缓冲区(Command Buffer)并行:CPU 添命令、GPU 取命令。每次 Draw Call 前 CPU 要做大量准备(检查渲染状态、打包数据),GPU 渲染极快(200 和 2000 个三角网格区别不大),提交速度跟不上渲染速度——Draw Call 太多,CPU 卡在提交里,GPU 空闲。类比:复制 1 万个 1KB 小文件远比复制一个 10MB 文件慢,开销在"动作"不在数据量。对策是批处理(Batching):CPU 内存中合并小网格一次画完。注意:① 合并耗 CPU,适合静态物体(动态物体每帧重合并反而亏);② 避免大量小网格、避免过多材质(材质不同合不了批)。
2.4.4 固定管线渲染
早期 GPU 只能配置不能编程;如今固定管线已退出历史舞台(OpenGL 3.2 Core Profile 完全移除),维护老项目才需要学。
2.5 什么是 Shader
一句话:Shader 就是 GPU 流水线上一些可高度编程的阶段(顶点/片元着色器必写,细分/几何着色器可选)。出色画面需所有阶段配合:渲染状态、混合函数、深度测试/写入的开关。
易混淆点对比汇总
| 易混淆对 | 一句话辨析 |
|---|---|
| 图元 vs 片元 | 图元 = GPU 直接处理的几何单位(点/线/三角形);片元 = 光栅化后覆盖像素的状态集合 |
| CPU 视锥剔除 vs GPU 裁剪 | 前者应用阶段、物体级、不发 Draw Call;后者几何阶段、图元级、会生成新顶点 |
| 可编程 vs 可配置 | 可编程 = 写 Shader 代码;可配置 = 只能填参数(模板/深度/混合都是后者) |
| 深度测试 vs 模板测试 | 深度管前后遮挡(近者胜,自动);模板管区域通行(挡板,程序员布局) |
| 投影变换 vs 透视除法 | 投影矩阵只把深度编进 w;压扁成 NDC 的是之后的透视除法 |
| 屏幕映射的范围 | 只负责 NDC → 屏幕像素坐标;透视除法到 NDC 是独立一步 |
| Blend vs α-blend | Blend 是通用混合公式;α-blend 只是一种因子配置(还有加法混合、不混合) |
| 模板测试 vs 深度学习 mask | 同一思想:逐元素挡板、入口过滤 |
| Gouraud vs Phong 着色 | 逐顶点光照+插值 vs 插值法线后逐片元光照(主流,避免大三角面明暗跳变) |