Base-渲染管线

渲染管线(Rendering Pipeline)描述的是:一组场景数据,如何一步步变成屏幕上的像素。

传统实时渲染管线通常可以从概念上分成三个阶段:

  1. 应用阶段(Application Stage):CPU 决定“画什么、以什么顺序画”。
  2. 几何阶段(Geometry Stage):GPU 把模型顶点变换成屏幕上的三角形。
  3. 光栅化阶段(Rasterization Stage):GPU 找出三角形覆盖的采样点,计算颜色并写入渲染目标。

这是一种便于理解的划分。真正的 Direct3D、OpenGL、Vulkan 或 Metal 管线会把它继续拆成更多子阶段,并且不同 GPU 的内部实现也不完全相同。

传统图形渲染管线总览

1. 应用阶段:决定画什么

应用阶段主要运行在 CPU 上,它并不是 GPU 固定管线中的某一个硬件阶段,而是游戏引擎在提交绘制命令之前所做工作的统称。

这一阶段通常包括:

  • 更新动画、Transform、粒子和游戏逻辑。
  • 根据相机做视锥剔除、遮挡剔除和 LOD 选择。
  • 收集可见的 Renderer,并按照材质、渲染队列或深度排序。
  • 准备网格、材质、纹理和 Shader 参数。
  • 设置渲染状态,例如 Cull、ZTest、ZWrite 和 Blend。
  • 生成 Draw Call,并通过图形 API 提交给 GPU。

应用阶段的输入是整个场景,输出则是一串 GPU 可以执行的命令。可以把它理解为导演在开拍前整理演员、道具和拍摄顺序,GPU 负责真正把画面拍出来。

1.1 可见性判断

最基础的是视锥剔除:物体的包围盒完全处于相机视锥外时,没有必要提交给 GPU。

除此之外,现代引擎还可能使用:

  • 遮挡剔除:物体被其他物体完全挡住时不绘制。
  • LOD:距离越远,使用顶点更少或计算更简单的版本。
  • 距离剔除:小到无法产生视觉贡献的物体直接忽略。

剔除越早,后面所有阶段的压力就越小。但剔除本身也需要 CPU 或 GPU 计算,因此不是规则越复杂越好。

1.2 排序、合批与 Draw Call

CPU 每次要求 GPU 绘制一批几何体,通常就会产生一次绘制提交。大量 Draw Call、频繁切换材质和渲染状态,会让 CPU 花很多时间组织命令。

常见优化包括:

  • 合并使用相同材质的物体。
  • 使用 GPU Instancing 绘制大量相同网格。
  • 按材质或管线状态排序,减少状态切换。
  • 使用 SRP Batcher、间接绘制或 GPU Driven Rendering。

透明物体通常还需要从远到近排序。这里也能看出:透明渲染并不只是一个 Blend 设置,它同时依赖应用阶段的排序和光栅化末端的混合。

2. 几何阶段:把顶点变成屏幕三角形

几何阶段主要处理顶点和图元。图元(Primitive)是 GPU 能够光栅化的基础几何形状,实时渲染中最常见的是三角形。

这一阶段的核心流程是:

1
顶点数据 → 顶点着色器 → 图元装配 → 裁剪 → 透视除法 → 视口变换

2.1 顶点输入

网格数据一般保存在 Vertex Buffer 和 Index Buffer 中。

Vertex Buffer 保存:

  • 模型空间位置。
  • 法线和切线。
  • UV。
  • 顶点色。
  • 骨骼索引与权重等自定义数据。

Index Buffer 保存三角形引用哪些顶点。多个三角形可以复用同一个顶点,减少重复数据和顶点着色计算。GPU 内部还可能使用顶点缓存复用已经处理过的结果。

2.2 顶点着色器

顶点着色器(Vertex Shader)至少需要计算顶点的裁剪空间位置。最典型的坐标变换是:

1
模型空间 → 世界空间 → 观察空间 → 裁剪空间

用矩阵表示就是:

1
positionCS = Projection × View × Model × positionOS

除了位置,顶点着色器还会输出 UV、颜色、世界空间法线等数据。这些输出会在三角形内部插值,再交给片元着色器。

顶点着色器适合做:

  • 顶点变换。
  • 骨骼蒙皮。
  • 顶点动画和风摆动。
  • 计算传给片元阶段的数据。

它通常一次只知道当前顶点,不能直接知道整个三角形最终覆盖了哪些像素。

2.3 可选的细分和几何处理

在传统可编程管线中,顶点着色器之后还可能存在:

  • 曲面细分阶段(Tessellation):把较粗的图元细分成更多三角形。
  • 几何着色器(Geometry Shader):以完整图元为输入,可以丢弃或生成图元。

它们不是每次绘制都必须使用。移动端尤其要谨慎使用,因为生成大量几何体可能迅速增加带宽、缓存和光栅化压力。更新的硬件还可能提供 Mesh Shader,用另一种方式组织几何处理,但它不属于所有平台都支持的传统基础管线。

2.4 图元装配

图元装配(Primitive Assembly)根据索引,把处理后的顶点组合成点、线或三角形。

例如索引序列 0, 1, 2 表示第一个三角形引用 0、1、2 三个顶点。顶点绕序还会决定哪一面是正面,从而支持背面剔除。

2.5 裁剪与剔除

这两个概念很容易混淆:

  • 剔除(Culling):整个图元不需要绘制,例如背面剔除。
  • 裁剪(Clipping):图元有一部分超出可见范围,需要在边界上生成新顶点,留下可见部分。

GPU 会在齐次裁剪空间中判断三角形是否处于可见体内。完全在外面的三角形被丢弃,跨越边界的三角形被切开。

必须在透视除法之前裁剪。特别是穿过相机或近裁剪面的三角形,如果先除以 w,坐标可能变得无穷大或方向翻转。

2.6 透视除法与 NDC

顶点着色器输出的裁剪空间位置是四维齐次坐标 (x, y, z, w)。裁剪后执行透视除法:

1
(x, y, z)NDC = (x / w, y / w, z / w)

结果进入 NDC(Normalized Device Coordinates,归一化设备坐标)。透视投影中,远处物体之所以显得更小,关键就来自除以 w

不同图形 API 对 NDC 的 Y 轴方向和 Z 范围存在差异,例如 Z 可能是 [-1, 1][0, 1]。使用引擎提供的变换函数,通常可以避免手动处理这些平台差异。

2.7 视口变换

视口变换把 NDC 映射到渲染目标的屏幕坐标:

1
NDC → Viewport → 屏幕坐标与深度范围

到这里,GPU 已经知道三角形三个顶点在屏幕上的位置。几何阶段的输出不是最终颜色,而是一批等待光栅化的屏幕空间图元,以及随后需要插值的顶点属性。

3. 光栅化阶段:把三角形变成片元

光栅化不是“给模型贴一张图”这么简单。它首先回答一个非常具体的问题:这个屏幕空间三角形覆盖了哪些采样点?

三角形光栅化与片元处理

3.1 三角形设置与覆盖测试

GPU 根据三个屏幕空间顶点建立三角形的边方程,并遍历它可能覆盖的区域。采样点满足三条边的内外判断时,就被认为处于三角形内部。

GPU 并不一定逐个像素笨拙地判断,而会以并行的小块进行处理。屏幕上的三角形越大,覆盖的采样点越多,后续片元着色工作的上限也越高。

3.2 Fragment 不完全等于 Pixel

光栅化产生的是 Fragment(片元),可以理解为“可能对某个像素采样点产生贡献的一次候选记录”。它包含:

  • 屏幕位置和深度。
  • 插值后的 UV、法线、颜色等属性。
  • 正反面信息。
  • 多重采样时的覆盖信息。

片元不一定成为最终像素,因为它可能在深度测试、模板测试或 discard 中被丢弃;多个透明片元也可能混合到同一个像素中。开启 MSAA 后,一个像素还可能包含多个采样点。

3.3 属性插值

三角形只有三个顶点,但内部会产生大量片元。光栅化器使用重心坐标,对顶点着色器输出的数据进行插值。

普通线性插值可以写成:

1
attribute = λ0 × attribute0 + λ1 × attribute1 + λ2 × attribute2

其中 λ0 + λ1 + λ2 = 1

UV 等属性不能简单地在屏幕空间线性插值,否则透视下的纹理会扭曲。因此 GPU 默认使用透视正确插值:

1
attribute = Σ(λi × attributei / wi) ÷ Σ(λi / wi)

这也是顶点阶段传少量数据,就能在整个三角形表面得到连续 UV 和法线的原因。

3.4 片元着色器 / 像素着色器

片元着色器(OpenGL/Vulkan 常用叫法)和像素着色器(Direct3D 常用叫法)在这里可以看作同一阶段。

它通常会:

  • 采样纹理。
  • 计算材质颜色与光照。
  • 使用法线贴图重建法线。
  • 计算透明度并决定是否 discard
  • 输出一个或多个 Render Target,例如延迟渲染的 GBuffer。

顶点着色器决定三角形在哪里,片元着色器决定三角形覆盖区域呈现什么结果。一般的片元着色器不能再改变三角形的形状,但某些 API 允许它修改片元深度。

3.5 Early-Z、深度和模板测试

深度测试比较当前片元深度和 Depth Buffer 中已有的深度,从而决定谁更靠近相机。模板测试则根据 Stencil Buffer 中的值控制片元是否通过。

硬件可能在运行片元着色器之前执行 Early-Z。被遮挡的片元可以提前丢弃,省去昂贵的着色计算。

但下面这些行为可能限制 Early-Z:

  • 片元着色器主动修改深度。
  • 使用 discard 或 Alpha Test。
  • 某些会产生读写顺序依赖的操作。

因此“最终没显示出来”不代表它一定没有运行片元着色器。大量被遮挡表面仍然执行了着色,就是常说的 Overdraw。

3.6 混合与输出合并

通过测试的片元会进入输出合并阶段(Output Merger)。这里会综合:

  • 片元着色器输出。
  • Render Target 中已有的颜色。
  • Depth Buffer 和 Stencil Buffer。
  • Blend、Color Mask、ZWrite 等固定管线状态。

常见 Alpha 混合近似为:

1
2
result = sourceColor × sourceAlpha
+ destinationColor × (1 - sourceAlpha)

不透明物体通常开启深度写入并关闭混合;透明物体通常关闭深度写入、保留深度测试并开启混合。透明表面的结果与绘制顺序有关,所以通常要在应用阶段从远到近排序。

最终结果被写入颜色缓冲、GBuffer、阴影贴图或其他 Render Texture。它不一定立刻显示到屏幕,也可能成为后续渲染 Pass 的输入。

4. 哪些阶段可编程,哪些是固定功能

传统 GPU 管线不是每一步都由 Shader 编写。

阶段 是否通常可编程 主要工作
顶点输入 / 图元装配 固定功能 读取顶点并组成三角形
Vertex Shader 可编程 顶点变换、蒙皮、顶点动画
Tessellation / Geometry 可选、可编程 细分、生成或修改图元
裁剪 / 透视除法 / 视口变换 固定功能 获得屏幕空间图元
光栅化 / 插值 固定功能 生成片元并插值属性
Fragment / Pixel Shader 可编程 材质、纹理和光照计算
深度、模板、混合 固定功能状态 可见性判断与输出合并

所谓“固定功能”不是不能配置,而是我们通过状态选择行为,不需要自己实现对应硬件算法。例如 Cull BackZTest LEqualZWrite OnBlend SrcAlpha OneMinusSrcAlpha 都是在配置固定功能单元。

5. 在 Unity Shader 中如何对应

一段简化的 Unity HLSL,大致能对应到以下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Attributes
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};

Varyings Vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS);
output.uv = input.uv;
return output;
}

half4 Frag(Varyings input) : SV_Target
{
return SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
}

它们在管线中的位置是:

1
2
3
4
5
6
7
8
9
10
11
Mesh / Vertex Buffer

Vert:模型空间 → 裁剪空间

固定管线:裁剪、透视除法、光栅化、插值

Frag:纹理采样和材质计算

固定管线:深度、模板、混合

Render Target

Unity 中常见概念的对应关系:

  • MeshRenderer、剔除和排序:主要属于应用阶段。
  • Mesh、Vertex Buffer、Index Buffer:几何输入。
  • Vert:顶点着色器。
  • Frag:片元着色器。
  • CullZTestZWriteStencilBlend:固定功能状态。
  • RenderTexture、Camera Color、GBuffer:渲染目标。
  • 一个 Shader 中的多个 Pass:多次经过管线,而不是一次管线里自动完成所有效果。

6. 从性能问题反推阶段

知道工作发生在哪个阶段,优化才不会凭感觉乱改。

应用阶段瓶颈

常见表现:CPU Render Thread 或 Main Thread 耗时高,GPU 没有吃满。

优先检查:

  • Draw Call 和状态切换是否过多。
  • 剔除和 LOD 是否有效。
  • 是否存在大量小物体、频繁更新的 Renderer。
  • 合批、Instancing 或命令生成成本。

几何阶段瓶颈

常见表现:顶点量、蒙皮或曲面细分增加后,GPU 时间明显上升。

优先检查:

  • 实际提交的顶点和三角形数量。
  • 顶点着色器是否过重。
  • 骨骼数量、蒙皮顶点和额外顶点属性。
  • 细分倍率是否失控。
  • 小三角形是否过多。小到接近像素尺寸的三角形会让光栅化效率也变差。

光栅化 / 片元阶段瓶颈

常见表现:降低分辨率后帧率明显改善,或屏幕覆盖面积越大越慢。

优先检查:

  • 片元着色器的指令、纹理采样和分支。
  • 透明物、粒子和多层 UI 导致的 Overdraw。
  • Render Target 数量、格式和带宽。
  • MSAA 采样数。
  • 全屏后处理次数。

判断瓶颈时最好使用 Unity Profiler、Frame Debugger、RenderDoc,以及对应平台的 GPU Profiler。只看三角形数量或 Draw Call 其中一个数字,很容易误判。

7. 最容易混淆的几个结论

  1. 应用阶段不等于某个 Shader 阶段。 它主要是 CPU 和引擎组织渲染工作的过程。
  2. 顶点着色器的输出不是屏幕像素。 中间还要经过装配、裁剪、透视除法和光栅化。
  3. 光栅化不等于片元着色。 光栅化负责产生片元和插值数据,片元着色器才计算材质结果。
  4. 片元不等于最终像素。 它可能被测试丢弃,也可能和其他片元混合。
  5. 深度测试不保证一定发生在片元着色之后。 硬件可以使用 Early-Z 提前排除遮挡片元。
  6. 透明渲染跨越多个阶段。 应用阶段负责排序,片元阶段计算颜色和透明度,输出阶段负责混合。

8. 总结

传统渲染管线可以记成一句话:

CPU 选择并提交物体,GPU 把顶点变成三角形,把三角形变成片元,再经过深度、模板和混合写入渲染目标。

进一步压缩就是:

1
2
3
应用阶段:场景 → 绘制命令
几何阶段:顶点 → 屏幕三角形
光栅化阶段:屏幕三角形 → 渲染目标

理解这条链路以后,很多看似独立的概念——Draw Call、坐标空间、裁剪、插值、Overdraw、Early-Z、透明排序和 Blend——都会回到各自正确的位置。

参考资料