UE4.27实现彩虹六号中的Shadow Cache HZB Culling

游戏场景中经常需要打一些动态的光,实时投射动态阴影,这通常会让ShadowDepth Pass的绘制压力变得很大。

一种优化的思路是做ShadowCache,以UE4中局部灯的ShadowCache为例,对于场景中的每一盏局部灯,它把固定物体的ShadowDepth渲染到一张单独的ShadowMap中,在满足如下条件时:

  1. 动态光源没有移动,投射属性没有发生变化。

  2. 动态光源离相机距离变化不大。(当相机移动导致动态光源离相机很近时,原有固定物体的ShadowDepth分辨率可能不够用,因此UE4会重新渲染一张新的高分辨率ShadowDepth,同样,远离相机时,原有动态光缓存的ShadowDepth的分辨率可能过高造成浪费,UE4会渲染一张小的新ShadowDepth,如果灯光不可见,还会释放掉缓存的ShadowDepth)。

以上条件均满足时,ShadowDepth将一直作为ShadowCache存在显存中,下次渲染阴影时,固定物体直接跳过,拷贝ShadowCache到新的RT上,仅需在这张新的RT上继续渲染动态物体的ShadowDepth。

彩虹六号也是这个思路,不过他们更进一层楼,直接在ShadowCache上做动态物体的HZB Culling,然后直接Gpu DrawIndirect绘制动态物体。

彩虹六号沿用之前大革命的GPU Driven管线,所以他们可以直接在当前帧内搞定所有的流程,比较简单。

而在UE4里做这一套流程就很苦逼,非常的绕,思路可以参考UE4 HZB Occlusion Culling的流程,使用延迟一帧,CPU回读,并手动Fence同步的做法。反正各种别扭。实际上也是纯搬砖,没一点新意。

但最终实现后剔除效率还是挺不错的:

Image

UE4原本的ShadowCache流程大概如下,仅做局部光MainView可见性测试和相交性测试:

Image

要在UE4中做ShadowCache HZB Cull 流程,则绕很多,额外加了四个Pass和一个Fence:

Image

Image

流程大概如下:

首先是对每张尚未生成HZB的ShadowCache生成HZB,这个比较简单,不过要注意两点,一是ShadowCache有一个PixelSize为4的Border,因此,第一级HZB要把EdgeClamp保留4个Pixel。二是最高的三级HZB没啥意义,实际上可以跳过1x1和2x2,4x4的级别的HZB。这样,用好LDS,一个pass能生成4级HZB,通常只需两个pass就搞定了,并且这一步可以放到AsyncCompute中,相当于免费。

然后是Movable Bounds收集并上传到GPU,这一步是直接Copy UE4 HZB Occlusion的,另外可以直接用GPU Scene中的数据, Submit这里只需收集PrimitiveId即可,这样就减少Upload的数据量了。(另外,这一步同样可以放到AsyncCompute中。做好同步的插入基本等于免费。)

然后苦逼的一点是UE4不支持BIndless,所以每盏灯的剔除都得单独开一个ComputePass,尽管可能某一盏灯就他喵的有几个剔除物体,但还是得开一组线程来用。

Feedback这里最好用Texture,别用StructureBuffer,实测StructureBuffer真的真的非常慢。

另外我还搞了一个PackUint8操作,这样可以最大幅度的减少CopyResultToCPU的花费时间。

无论如何,这些Pass全部都可以放到AsyncCompute中,除开最后的FeedBack Read之外,几乎免费。

© - 2024 · 月光下的旅行。 禁止转载