游戏中的剔除技术

游戏中的剔除技术

​ 剔除是个比较大的工程,方法众多,同时剔除的目标的粒度也不同,下面我们介绍一下常见的提出技术,有些是软件上的技术(ComputeShader也算软件,非专用硬件电路剔除的都算软件剔除),有些是硬件上的技术,我们会分开来介绍。

一般来看,我们是根据粗粒度逐渐进行剔除的,这一点在GPU-Driven pipeline中可以很明显看出,我们从粗粒度上大概分成以下几种:

  • 对object/instance的剔除通常在CPU进行,目的是减少物体的Draw Call
  • 对cluster/chunk的剔除通常用Compute Shader在GPU进行,目的是为了减少光栅化三角形的数量
  • 对triangle的剔除通常通过硬件或者Compute Shader在GPU进行,目的是为了减少光栅化次数以及shading次数

视锥剔除

image-20240719141116331

这个可以对MeshCluster或者Instance进行剔除,一般用BVH作为空间数据结构,空间加速结构的遍历存在三种情况。

  • 如果包围盒完全位于视锥之内,则其所有子节点都不需要做相交测试了。
  • 如果包围盒部分处于视锥体之内,其子节点都需要进行相交测试。
  • 如果包围盒处于视锥体之外则不会对其子节点进行进一步处理,直接对这颗树进行裁剪

其实对于一个AABB和视锥体是否相交的检测并不是特别直接:

image-20240727195950072

遮挡剔除

PVS预计算遮挡剔除

​ 离线方案,对动态物体无法处理,对室内,建筑等十分合适。

​ 这个东西除了客户端上减少渲染物体从而减少drawcall外,《生死狙击2》中在服务器端做了一套这个方案,主要参考了瓦洛兰特的这篇Blog https://technology.riotgames.com/news/demolishing-wallhacks-valorants-fog-war。在不可见时不同步敌军数据,直接从根源上防止了外挂。

image-20240727200552672

硬件遮挡查询Occlusion Query

​ 简单来说就是对查询物体做一次draw看看通过depth test的有多少,如果全不通过直接剔除即可。可以配合PVS使用,PVS剔除静态,硬件遮挡查询剔除动态物体。

主要问题在于:

  • 查询本身是一种drawcall
  • 虚拟内存VRAM回读到系统内存System RAM

Hi-Z

image-20240719201733599

​ 做一个z-buffer mipmap,但是不做深度平均而是只存储深度最大的。

​ 常用的方式是:对每个物体的AABB做测试即可,如果构建了OCTree也可以从根向下遍历八叉树,这样可以利用树结构的特性剪枝一部分。

​ 但大多数场景都不会用上面完整版本的Hi-Z算法,hi-z的深度需要提前知道当前帧每处深度才能去生成,然而直接一个大的z-prepass对所有物体做是很耗的

《刺客信条大革命》的Hi-z方案:部分物体z-prepass+上一帧z-buffer重投影

  • 《刺客信条大革命》中使用了:首先对部分遮挡物做做一次光栅化,将光栅化结果与前一帧深度1/16分辨率的重投影结果结合起来再构建z-buffer金字塔。

    • 只对最近最大的300个物体做一个z-prepass,这个开销很低
    • 剩下没深度的地方用上一帧reprojection来的信息
    • 这个结合的信息肯定是有问题的,因为一些快速移动物体可能会导致错误(很好理解缺失了TAA中的motion buffer),不过概率低,他们能接受。

    image-20240726111055315

《战地3》寒霜引擎Hi-z方案:CPU仅对大物体做一个低分辨率深度图

hi-z在cpu上做需要低分辨率渲染一个深度图:比如寒霜引擎在做战地3时用了这个方法,只对大物体做一个简单深度图渲染。不过那是2011年,现在有ComputeShader可以更好的做这件事。如果项目cpu有多余算力可以考虑,天刀貌似也用上了这个方法。

image-20240719210527891

GDC2023《明日之后》移动平台上的软光栅遮挡剔除方案

Hi-z最好不要回读

注意Hi-z 如果要异步回读不太靠谱,有滞后性,要么cpu上软光栅,要么Compute Shader不要回读,下面是《星球重启》GDC talk中遇到的回读导致的滞后问题

动图

Two-Phase occlusion culling

image-20240726111609479

 所有物体先用上一帧的ZBuffer测试一遍,得到可见的物体拿到这一帧相机画一遍。因为是上帧的z-buffer那么剩下的我们认为被遮挡的有些其实并没被遮挡怎么办呢?在本帧完成后生成了新的DepthBuffer后,然后拿这个DepthBuffer测试一遍刚才认为遮挡不可见的物体,如果发现有新的物体可见,则给他们打上标记,下一帧渲染回来。 (这个技术在sig2015刺客信条大革命那个gpu-driven talk里提出的,这里的是Instance 和 Cluster)

GPU-Driven rendering pipeline

​ GDC2015刺客信条大革命提出,其实也是遮挡剔除,但是他用了mesh cluster来做表达,还是单独开一个博客讲讲: 链接

Pre-z Pass

​ 提前做一个深度Pass, Pre-Z虽然只写入深度不做shading计算,Pre-Z即提前进行一次代价很小的渲染,这次渲染不做shading只写入深度,有了Pre-Z的深度图就可以用这个深度图去做提前深度测试了,这样虽然会导致DrawCall数量翻倍但是配合Early-Z使用能确保下一个Pass的shading数量和屏幕空间的Pixel数量是一致的,并不会带来多余的shading浪费。Pre-Z主要功耗产生在DrawCall,但却能保证只有1x倍Pixel的shading,相当于用更多的Draw Call去节省Overdraw的时间

局限性:

​ 可以缓解大量的SM压力,但在移动端,功耗的瓶颈在于Draw Call数量,这个时候需要综合考虑是否需要使用Pre-Z以及对Pre-Z做一些额外出来等操作来评估是否值得用double draw call(深度一次pass,绘制一次pass)去减少pixel shading次数。

硬件单元 Z-cull(硬件)

image-20240727195325451

image-20240727215102686

Early-z(硬件)

image-20240719193203898

最后深度测试会使得很多像素被overdraw,于是我们考虑:Early-z在Pixel shader前做。

image-20240719193257174

early-z失效:

​ 但这里有个坑点, 一旦进行了手动写入深度值、开启alpha test或者丢弃像素等操作,那么GPU就会关闭early-z直到下次Clear Z-Buffer后才会重新开启。 原因是:AlphaTest会导致在前面的透明物体z值被写入但是其本身其实不会渲染,这导致后面的物体本来该被渲染但没被渲染。

early-z的问题:

early-z在物体从远到近渲染是没有意义的,这也是主要问题。 最好在CPU按照距离做一个排序决定渲染顺序再提交渲染。

裁剪Clipping(硬件)

硬件上的操作,在Clip Space完成,Sutherland–Hodgman算法:

https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm

image-20240719211834005

背面剔除(硬件)

硬件中做,管线中配置下即可。

小三角形剔除

这是一个为了速度牺牲质量的剔除方法:

image-20240727195325451

​ 在光栅化后,并不是直接就派发到PS执行,而是有一个Z-Cull单元。在进行逐像素光栅化之前,GPU首先会对块进行粗糙光栅化,一般为4*4。Z-Cull单元负责维护每个块的深度最大值。

​ Z-Cull后会进行Fine Raster,以2*2个pixel进行光栅化,这对于小三角形有了很多不必要的开销,会多三倍开销。

image-20240727195420394

这也就是Visibility Buffer的诞生的主要动机,后来用Compute Shader做软件光栅化。

image-20240719232914953

Ref