渲染流水线:从三维场景到屏幕像素的完整旅程——《Unity Shader 入门精要》第 2 章学习笔记

速览卡片

概念 一句话说明
渲染流水线 从三维场景出发生成一张二维图像,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 接管;各阶段之间流水线并行,”应用阶段”在整个帧的时间内持续发生。

Q:图元和片元有什么区别?

A:图元 = GPU 能直接处理的基本几何单位(点/线/三角形,Mesh 和四边形必须拆成这三种);片元 = 光栅化阶段对被覆盖像素生成的状态集合(插值得到的法线、UV、深度等)。链条:CPU 把 Mesh 拆成图元 → GPU 把图元变片元 → 片元过测试变像素。

2.2 CPU 和 GPU 之间的通信

应用阶段三个任务:准备场景数据(摄像机、视锥体、模型、光源);粗粒度剔除(culling);设置渲染状态。输出是渲染图元。

数据流向:硬盘 → 内存(全部渲染数据)→ 显存(GPU 要用的子集)。显卡对显存访问更快、且大多数显卡不能直接访问 RAM;数据进显存后 RAM 副本通常可删,但 CPU 还要做碰撞检测/动画的数据要保留。

Q:内存中的渲染数据 和 GPU 所需的数据相等吗?

A:不相等,是"全集 ⊃ 子集"的关系。第一步是全集(几何、纹理、Shader、材质参数全进 RAM);第二步是子集——只有 GPU 真正消费的数据进显存:顶点/索引(顶点着色器用)、纹理(片元着色器采样)、参数(MVP、光照等)。碰撞体、骨骼、CPU 剔除用的包围盒留在 RAM。判断标准:这数据接下来被 CPU 用还是被 GPU 用。

渲染状态定义网格怎么被渲染:用哪个顶点着色器/片元着色器、光源属性、材质等。不改状态,所有网格用同一种状态渲染。

Draw Call 就是一个命令,发起方是 CPU,接收方是 GPU,只指向图元列表,材质信息上一步已设好。

2.3 GPU 流水线

先给出书中图 2.6 的完整流程,后文小节按此流程逐站展开:

图 2.6 GPU 流水线(几何阶段 → 光栅化阶段)
顶点数据 → 顶点着色器 → 曲面细分
着色器
→ 几何
着色器
→ 裁剪 → 屏幕映射 → 三角形
设置
→ 三角形
遍历
→ 片元着色器 → 逐片元
操作
→ 屏幕图像
绿 可编程  黄 可配置不可编程  蓝 固定实现
实线框 = 必需  虚线框 = 可选(细分、几何着色器)

2.3.2 顶点着色器

顶点着色器(Vertex Shader):每顶点调用一次;不能创建/销毁顶点,无法得知顶点间关系,因此可高速并行。两个主要工作:坐标变换和逐顶点光照。

1
o.pos = mul(UNITY_MVP, v.position);  // 模型空间 → 齐次裁剪空间

MVP = 模型变换(模型空间→世界空间,即 Transform 组件)、V 观察变换(世界→相机空间,相机变换的逆)、P 投影变换(相机→齐次裁剪空间)。输出仍是四维、w≠1,”压扁”发生在后面的透视除法。

Q:顶点着色器做完 MVP 变换,不就得到最终位置了吗?

A:顶点着色器输出只是齐次裁剪空间坐标,不含屏幕分辨率信息,算不出像素位置。之后链路:裁剪(在齐次裁剪空间,裁剪面 x=±w 等六个平面)→ 透视除法(xyz/w → NDC [-1,1]³,固定硬件自动做)→ 屏幕映射。
Q:顶点着色器里"计算顶点颜色"是干什么用的?顶点着色器都能输出哪些数据?

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) 满足:
P = λ_A·A + λ_B·B + λ_C·C  (λ_A + λ_B + λ_C = 1)
几何意义是面积比:λ 的下标是谁,分子就是"把谁换成 P"的子三角形面积:
λ_A = S(BCP) / S(ABC)
即对边与 P 围成的小三角形占整个三角形的比例。P 越靠近某顶点,对应 λ 越大;落在对边上则该 λ = 0。

快速计算:2D 叉积 = 2×2 行列式 = 带符号平行四边形面积。分母 cross(B−A, C−A) 算一次,分子各算一次,第三个用 1 减出来。三个 λ 全非负 ⇔ P 在三角形内——覆盖测试与插值权重是同一组计算;硬件用等价的边函数沿扫描线递推,所以快。

任意属性都走同一公式:
属性|P = λ_A·属性_A + λ_B·属性_B + λ_C·属性_C
法线、UV"凭空出现在片元函数参数里",就是在这站搭的便车。

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):如修改颜色、深度缓冲、进行混合等,不可编程但可配置性很高。流程:片元 → 模板测试 → 深度测试 → 混合 → 颜色缓冲区。

Q:图 2.15 里模板测试、深度测试那一串到底在干什么?

A:模板测试 = 绘画软件里的剪贴蒙版:上层内容只显示在下层有内容的区域,涂出界的部分自动被裁掉。模板测试就是把这套逻辑搬到每个像素上。

机制:每像素一个模板缓冲区(整数 0~255),相当于每格的"蒙版形状";片元带着参考值来"对蒙版",条件满足 → 内容显示(继续走深度测试、写颜色);不满足 → 被裁掉。

经典配合(两次绘制):第一遍画 A,把 A 的区域"定义为蒙版"(模板=1);第二遍画 B,按条件被这个蒙版裁剪——B 画不进 A 的区域。

例 1 · 描边:剪贴蒙版的反向用法。先正常画角色 = 定义蒙版(模板=1);再画放大 3% 的红色角色,条件设为"模板==1 就丢弃"——等于把红色内容从角色轮廓里裁掉,只剩轮廓外多出来的一圈红边。

例 2 · 镜子:标准正向剪贴。先画镜面 = 铺出蒙版形状(模板=1);再画镜中世界,条件设为"只有模板==1 才许画"——镜像内容被剪贴进镜面的形状里,镜外的部分全部裁掉。

要点:① 模板缓冲全帧共享,用完要恢复状态;② 值是整数,可当计数器(阴影体:正面+1、背面−1,非零即阴影);③ 只能过滤内容,不能生产内容(内部线靠贴图/rim)。

深度测试 = Z-Buffer 算法:近者胜,胜者写颜色+深度。一句话分工:深度测试管前后遮挡(自动),模板测试管区域通行(程序员布局)。
Q:Blend 就是 α-blend 吗?

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。

为什么 Draw Call 多会卡?——命令缓冲区与批处理

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 插值法线后逐片元光照(主流,避免大三角面明暗跳变)