官方文档中的流程图如下:

因为步骤比较多,涉及到的源文件也非常繁杂,所以此处仅仅只是梳理一遍大概的渲染流程;之后我想我会更加具体地深入某一阶段的源码进行学习……

1. 场景准备与遮蔽#

核心:决定“什么东西要画,什么东西不要画”

在现代引擎的架构下,进行绘制操作时,性能的瓶颈往往不是着色器本身,而是 CPU 与 GPU 之间的高频通信,更具体地说是 CPU 向 GPU 提交的一个个 Draw Call(绘制指令)。我们都知道,CPU 想要将信息传递到 GPU,需要跨过一条漫长的 PCIE 总线,这一过程是极其耗费时间的(相对于 GPU 高速的绘制操作来说)

因此,提前筛选出最终可能会用到的信息是非常关键的

步骤#

具体操作由 CPU 的两个线程(抽象意义上的线程)来执行 —— 游戏线程 与 绘制线程

游戏线程#

  1. 处理动画、物理模拟、AI……
  2. 遍历场景树,收集所有挂载着渲染组件的物体(如 StaticMesh、Skeletal Mesh……)
  3. 计算每个物体的世界矩阵,并将数据打包

之后 UE 会将需要渲染的物体同步给绘制线程

绘制线程#

这一步主要执行剔除工作

  1. 距离剔除

根据事先设定好的“最大绘制距离”将超出这个阈值的物体直接从渲染列表中剔除

应用:草丛、地上的碎石……

  1. 视锥剔除

场景中的每个可见物体都拥有包围盒 / 包围球,这一阶段会将包围盒与视锥体进行快速地相交测试,只要包围盒完全位于视锥体之外(比如玩家背面),就会被剔除

  1. 遮挡剔除

相对于前两步的“暴力删除”操作来说,遮挡剔除会更加复杂与耗时;并且不同于传统渲染管线中的 Depth-Test,UE 使用的是基于 HZB(Hierarchical Z-Buffer,分层深度缓冲)的遮挡剔除技术

在传统的 Z-Buffer 中,对于物体在屏幕上的每一个像素,都需要与 Z-Buffer 中对应的像素进行比较,虽然在工程实现上比较容易,但是一旦当物体占据了屏幕上的大量像素比如 500 x 500 个像素点时,那么光是测试这一个物体就需要读取与比较 250000 次深度值,开销极大

HZB 则借鉴了 Mipmap 的思想,将原本巨大的深度图进行了降采样分层(每 2 x 2 个像素生成一个新像素,这个新像素的值则为其中最远的像素深度值)

如图,从左至右 Mip 层级分别为 4、5、6

https://miketuritzin.com/post/hierarchical-depth-buffers/
https://miketuritzin.com/post/hierarchical-depth-buffers/

这样一来,对于巨型物体(在屏幕上占据大量像素)使用低分辨率层级 HZB,较小的物体使用高分辨率层级 HZB,从而使得无论物体多大,引擎总能找到一个恰好匹配其大小的 HZB 层级,使得该物体的包围盒在那个层级下,只覆盖极少数的几个像素

但是这样不会丢失精度吗?

答案是基本不会损失精度。因为上一步仅仅只是决定用什么深度图,还没有进行比较。

而在深度比较中,引擎会提取该物体距离摄影机最近的点与 HZB 中的深度值进行比较。而如果最近的点都比最远的遮挡物还要远,那么这个物体必不可能被摄影机看到,可以直接丢掉

2. 几何体渲染#

3. 光栅化与几何缓冲#

4. 渲染纹理#

5. 像素着色器与材质#

6. 反射#

7. 静态光照与阴影#

8. 动态光照与阴影#

9. 雾与透明度#

10. 后处理效果#

【Reference】https://dev.epicgames.com/documentation/unreal-engine/introduction-to-rendering-in-unreal-engine-for-unity-developers#rendering-in-unreal-engine

原文:UE 渲染管线(桌面端延迟渲染)