游戏中常用的裁剪技术
游戏中应用裁剪技术是为了剔除不需要绘制的部分,以达到提升性能的目的。
裁剪技术里有软件的也有硬件的。很显然,在游戏引擎的架构中,越靠近应用层即远离硬件层的裁剪效果越好。所以在这篇文章中就从游戏中裁剪效果从大到小的顺序总结一下常见的裁剪方法。
可视范围裁剪、细节裁剪
可视范围是制定一个人眼能看到的距离,只渲染离摄像机的距离小于这个距离的物体。
细节裁剪是物体如果在屏幕中渲染的像素小于某个阈值,就不绘制这个物体。
这两种方法的思路大体是一样的,就是把既远又小的物体粗暴地裁剪掉。
可视范围裁剪的实现很简单,把包围盒中心与eye点的距离大于可视范围的物体标记为被裁剪。这种处理对包围盒比较大的物体,比如大型建筑、合并后的物体不太友好,可以求包围盒每个顶点与eye点的距离,用它们的最小距离与可视距离比较。
细节裁剪是将物体的包围盒投影到屏幕上,看投影后的包围盒大小是否小于某个阈值。具体做法是先将物体包围盒的八个顶点投影到NDC clip space,再求得投影后八个顶点的包围盒,并将其包围盒乘以视口变换,得到屏幕上的包围盒,如果它所占用的像素数小于阈值就把这个物体标记为裁剪。
视锥裁剪
在渲染中可视范围是一个椎体,我们称它为视锥。而视锥的范围只占空间的一小部分,所以视锥裁剪的效也非常好。
物体与视锥的关系有相交、在视锥外部、在视锥内部。我们只需要渲染与视锥相交和在视锥内部的物体。
最朴素的方法是用物体的包围盒与视锥体的六个面求相交关系。
- 物体在六个面的内侧,说明物体在视锥体内部。
- 物体在六个面的外侧,说明物体在视锥体外部。
- 其余情况说明物体与是椎体相交。
求物体与视锥体的相交有更快的方法,比如用包围盒在面法线上的投影最小和最大的两个点代替包围盒的八个点与视锥体求交,用SIMD指令加速求交。有兴趣的可以查阅资料:
- Frustum Culling
- Geometric Approach – Testing Boxes II
- Optimized View Frustum Culling Algorithms for Bounding Boxes
当然每个物体与视锥体求交这一步也是可以优化的。如果引擎里用到场景的加速结构比如四叉树、八叉树、BVH等管理场景节点,可以用这些加速结构的节点的包围盒与视锥体求交。如果不相交,那么子节点也不需要再求交了,可以起到加速的作用。这就是层次视锥裁剪(Hierarchical View Frustum Culling)的思想。
Portal Culling和Potential Visibility Set
室内场景有很多的房间和走廊,房间、走廊之间是靠门和窗户连通的。人在一个房间中通过某个视角往前看,如果能看到相邻房间的门,那就可能看到相邻房间内的物件;如果看不到相邻房间的门,那们必然看不到相邻房间的物件。Portal Culling就是基于这种思想。
Portal Culling规定房间和走廊为cell,窗户和门是portal,通常需要艺术家在编辑器中指定好cell和portal。
游戏引擎计算出cells的连通信息,用于之后的可见查询。
我们用下面这个场景描述下Portal Culling的执行逻辑。
这个图里共有A~H房间,房间之间并通过portal相连。
-
构建视锥。
-
检查当前cell的物体与视锥的相交,标记物体是否被裁剪。
-
遍历当前cell的portal,检查其与视锥是否相交。
-
若相交,那么通过portal相邻的cell可见。
- 先计算当前视锥通过portal后缩小的视锥。
- 用这个缩小的视锥,这个相邻的cell递归执行2和3, 进行相交检测。
-
若不相交,则通过portal相邻的cell不可见,将这个相邻的cell标记为被裁剪。
-
-
遍历完成之后,就可以将不可见的剔除掉。
这种算法可以应用于镜面反射的可见性计算。在视锥碰到镜子后,计算反射后的视锥,再与cell进行相交计算。
还有一种跟portal culling思想相近的裁剪方法,叫Potential Visibility Set。它也是把场景划分成cell,然后计算出在每个cell中可能看到的物体,并保存下来。运行时可以通过取摄像机所在的格子并查表来判断物体是否是可见的。
预计算可见性的大概做法是:
- 将场景分成N*M个cell。
- 根据美术编辑的可到达区域,选择出可到达的cell。
- 对可到达的cell,生成一组摆放摄像机的位置,在这个位置生成上下左右前后六个机位。
- 每个cell中的摄像机机位,求出可见的物体,并加入到cell的可见集中。
这样预计算的可见集就建立好了。
在运行时,想要知道一个物体是否可见,只需要判断摄像机在哪个cell里,并查询物体在是否存在于这个物体的可见集中。可见PVS剔除的运行时开销是非常低的。
PVS缺点也比较明显:
- 只能处理静态物体,无法处理动态物体。
- 预计算的速度比较慢。
- 由于基于包围盒,无法处理植被、 树木。
遮挡查询
遮挡查询是利用Z-buffer判断物体是否可见的技术。常见的有硬件遮挡查询、软件遮挡查询、层次Z-Buffer查询(Hierarchical Z Buffer,简称HZBO)。
硬件遮挡查询
硬件图形API支持Query命令,来查询一个物体通过深度测试的像素数量。具体做法如下:
-
用depth-only 的pass获得一个场景的深度图。
-
关闭深度写入和颜色写入。
-
对每个物体创建查询命令。
- 开启查询。
- 绘制物体。
- 结束查询。
-
打开深度写入和颜色写入。
-
对每个查询,查询其可见的像素数,如果大于0,就渲染这个物体。
-
结束。
上面的查询方式有一些显而易见的问题:
- CPU与GPU视并行的,CPU像GPU发起查询和绘制命令后,GPU不会立即执行,所以CPU想要获取查询结果,必然也要经过一定时间的等待。
- 查询相当于多画了一遍物体,是不小的开销。
对于第一个问题,由于CPU与GPU是并行的,CPU想要读取查询结果需要等待GPU执行之前提交的命令;而在GPU执行完CPU的查询命令,又由于CPU在这段时间是在等待,并没有提交新的命令,所以GPU在这时也会处于无事可做的状态。这两种情况都是对性能的浪费。
为了尽量减少CPU与GPU之间的stall,可以利用上一帧的查询结果决定当前帧物体是否渲染。
对于第二个问题,解决思路是尽量地减少查询所需的DrawCall和每个DrawCall的开销。
为了减少每个DrawCall的开销,可以在查询时使用包围盒代替物体提交DrawCall。
为了减少查询所需的DrawCall,由于时间和空间上的连贯性,即上一帧可见的物体在下一帧也有很大的可能性是可见的。对可见的物体,不必每帧发起遮挡查询,隔一段时间查询一次就可以减少不少遮挡查询的DrawCall。
下面介绍的软件遮挡查询和HZBO也会面临以上的问题,所以解决方案也适用,下面就不再做重复介绍了。
软件遮挡查询
这种方法是由Frostbite提出来,用于BatteleField的遮挡方案。它的思路是让美术制定一些大的遮挡物如地形和大型建筑等,在一个低分辨率的深度图上绘制这些遮挡物。
之后用物体的包围盒算出屏幕空间的包围盒,再用屏幕空间的包围盒去跟上面的深度图比较是否可见。
这个方案的优点是基于CPU的,不用担心硬件兼容的问题。
缺点是是如果深度图用的分辨率过大,那么CPU端的计算开销就比较高,如果分辨率过小,又导致遮挡查询的效果比较差。且由于遮挡体由美术指定,选取合适的遮挡体对美术有一定的要求。
Hierarchical-Z Buffer Occlusion(HZBO)
Hierarchical Z-Buffer Occlusion(下面简称HZBO)比上面的方案更进一步地降低了遮挡查询的消耗,目前UE4中的遮挡查询就是这种方案。
HZBO基于Hierachical Z-Buffer,顾名思义,就是多层级的Z-Buffer。下面介绍下这种方案的流程:
- 为深度图生成多级mipmap,每下一级mip的像素值,取自上一级mip2x2像素块的最远深度。
- 将模型包围盒的投影到屏幕空间,并得到包围盒的最近深度。另外根据投影到屏幕空间后点可以得到屏幕空间的包围盒大小。
- 根据屏幕空间的包围盒大小计算出需要与与比较的Z-Buffer的mip级数。
- 采样对应mip级数的Z-Buffer,采样包围盒中心周围的四个点得到最远的Z值,并与包围盒的最近深度比较得到遮挡关系。
可以把包围盒和mip级数的计算都放在GPU中,进一步提高效率。UE4是将模型的包围盒和包围盒中心分别存储到两张贴图中,在shader中采样这两张图计算屏幕空间的包围盒和Mip值,并与Z-Buffer比较,将遮挡结果存储在shader的输出中,供CPU读取。这样一个DrawCall就完成了遮挡查询的工作。
延伸
剔除:从软件到硬件,这篇博客了介绍了基于GPU的一些裁剪,有兴趣的可以看下。我之后再补充。