遮挡剔除
在游戏场景中,根据游戏类型的不同,有数量不等的 object,如果这些物件同时渲染的话,会对性能造成很大影响。所以对于大部分游戏来说,将 object 提交到渲染流程之前,剔除掉看不到的 object,是非常必要的一个步骤。看不见的 object 可以两类,一类是在相机范围之外的,另一类是被其他 object (比如墙壁,地形等)挡住的。通过判断 object 是否在相机的视锥体外。判断 object 是否在相机的视线范围内有简单的计算方式,但是判断 object 是否被遮挡则相对比较困难。
因为遮挡物有可能是不规则的,一个 object 挡住另一个可能是像素级别的,不能像相机视锥剔除一样,通过计算规则几何体的相交来得出结果。这个问题的解决思路和渲染的深度测试本质上类似,将 object 光栅化到屏幕空间中,进行逐像素的深度比对。
解决这个问题目前大致有三种方式:软件剔除,硬件剔除,预计算方式。
硬件剔除
Hardware Occlusion Queries
图形硬件 API 通常会提供 Hardware Occlusion Queries 的功能,它可以统计出一个 object 在渲染时会有多少个像素会被看到。打开查询功能后,object 的渲染并不会实际写入 frame buffer 或者 depth buffer,它只是简单的进行光栅化后,和 depth buffer (通常是用前一帧)进行深度测试,然后返回通过深度测试的像素数量,读回到内存中,我们可以根据像素的数量,来决定后续是不是真正渲染这个 object。为了性能考虑,通常只渲染 object 的外包围盒,这是一种保守的估计,在简化计算的同时,不会影响结果的正确性。
一般步骤如下:
-
初始化遮挡查询
-
关闭对帧缓冲区和深度缓冲区的写入
-
渲染一个简单但保守的复杂对象近似体,通常是一个包围盒
-
结束遮挡查询
-
获取查询结果
优势:
-
方案比较简单,无需额外的开发
-
结果比较精确
劣势:
-
对硬件有一定要求(ES 3.1/Vulkan/DirectX 9)
-
因为需要从 GPU 将结果回读到 CPU 端,一般会采用流水线的方式,会造成 1 到 2 帧的延迟。
-
增加 draw call 的数量,增加 gpu 的负担
-
只能以 draw call 为颗粒度,如果多个 object 通过一个 instance 渲染,那么不区分单个 object 的遮挡关系。
Hierarchical Z-Buffer Occlusion
和 Hardware Occlusion Queries 类似,不同的地方是,它将 depth buffer 生成 mipmap,让被遮挡物的包围盒在屏幕空间中只匹配到更低精度的 depth buffer ,比如 depth buffer 就能覆盖包围盒,这种方法巧妙的避免了对 object 进行光栅化的过程,也减少了对纹理的读取开销。
优势:
-
减少纹理的读取开销
-
避免对被遮挡物进行光栅化
劣势:
-
有一些 corner case,实现相对复杂
-
这样会减少 cull 的 object 数量
GPU driven
使用 compute shader 等更高级的 api 特性,将遮挡剔除的结果直接在 gpu 内使用,避免了从 gpu 回读到 cpu 过的过程,但是一般实现较为复杂,对硬件有一定的要求。目前 UE 和 unity 并没有提供现成的解决方案。
软件剔除
软件剔除的方式和硬件剔除是类似的,只不过是在 CPU 上进行光栅化。
首先将遮挡 object 光栅化到 depth buffer,然后对被遮挡物的包围盒进行光栅化,将光栅化的结果和 depth buffer 进行对比,判断 object 是否被遮挡。光栅化可以采用传统的扫描线算法。
一般来讲,CPU 上模拟光栅化速度比硬件要慢,这个方案能成立的前提是它对硬件没有要求,另外可以通过多线程,SIMD 等优化方式来提升运算速度。
优点:
-
对硬件没有要求
-
不用从 GPU 读取数据到 CPU 端,延迟相对较小。
-
在 GPU 成为性能瓶颈的时候,可以减轻 GPU 的负担。
-
可以将 culling 运算放入单独的线程,减少对性能的影响。
目前可以参考的开源的软件剔除方案有:
-
Godot 引擎,采用的是 intel 的光线追踪库 embree 进行光栅化。
-
Intel 的 Software Occlusion Culling
-
UE4
预计算
这种方案是在烘培的阶段进行可见性的计算,运行时再利用预计算的结果来加速剔除的判断,这种方案通常会消耗更多的内存。UE 和 Unity 都提供了这种方案。
UE
在相机可能经过的路径上放置一些 cell(Precomputed Visibility Volumes),预先计算从 cell 出发所能看到的物件,并存储在对应的 cell 中。当摄像机在某个 cell 中时,直接读取结果即可。
优势:
- 计算速度较快
劣势:
-
无法处理超出 cell 的部分,只能适用于相机路径比较固定的游戏。
-
无法判断动态的 object,需要同时使用其他的辅助方案
-
需要占用额外的内存
Unity
unity 采用的是第三方的商业化方案 Umbra,相比 UE 提供的多种方案,unity 仅仅只提供了这一种。相比 UE 需要提前在相机可能出现的位置上布置 cell,Unity 的系统会自动处理摄像机在任何位置的遮挡剔除,不需要提前布置 cell。
这套方案会对场景静态几何体进行分析,创建一个空间数据库,这个数据库能够在运行时用来快速判定物体是否可能被遮挡。具体实现方式可以参考 Introduction to Occlusion Culling | by Umbra 3D | Medium
优势:
- 可以查询动态 object 是否被遮挡
劣势:
-
需要指定 object 作为遮挡物, 并且不支持动态 GameObjects 作为遮挡物
-
需要占用额外的内存