🥕地表最强 Contact Shadow
00 分钟
2024-8-20
2024-8-26
type
status
date
slug
summary
tags
category
icon
password

杂谈

早年间做过 Days Gone 的 Contact Shadow 算法的逆向分析,记得当初第一眼看到 Days Gone 的地表细节的时候我就被震撼到了:
notion image
 
我能确定这个地表阴影效果肯定是 Contact Shadow,但是我始终没法在 UE 中还原出来,所以我认为 Bend Studios 肯定是做了一些深度优化。因为当时在公司里主要做的就是屏幕空间算法的优化,始终还是想复刻他们的效果,于是我从 RenderDoc 截帧得到的 DXBC 汇编里慢慢地进行逆向,总共几百行代码最后差不多花了我一个月时间,虽然过程坎坷,最后我还是成功地在 UE 里还原了他们的算法。还原完代码我就对他们对 Contact Shadow 的理解之深入以及优化手段的高超感到佩服,但因为还是在公司里做的,我始终没法将整个逆向得到的代码进行公开。
 
而最近我发现 Bend Studios 差不多在去年这个时候已经将他们的算法公开了:
 
看了他们的 Presentation 之后我发现跟我当初逆向的一致,自己逆向的结果得到确认之后心里有点暗喜,同时又觉得这么强的算法为什么没听人谈及呢?既然官方都已经公开了,我就能给大家分享一下 Bend Studios 的最强 Contact Shadow。

Contact Shadow 101

Contact Shadow 在有些地方又被称为 Screen Space Shadow,即屏幕空间阴影,之所以被大家称为 Contact Shadow,是因为它可以补足一些 Shadow Map 投射阴影的不足,例如物体接触面的小块阴影。
 
Contact Shadow 只依赖场景深度,所以它还可以做到一些 Shadow Map 做不到的事情,例如材质表面的 Micro Shadow,可以给材质配上 Displacement Map 来影响场景深度图,进而在材质表面投射出像素级别的阴影,而 Shadow Map 则只有网格体级别的精度。
 
Contact Shadow 其实在很多游戏引擎里也会内置,比如 UnrealEngine 的 Contact Shadow,Unity 和 CryEngine 中的 Screen Space Shadow 等。大家看到的游戏里比较新的游戏基本也都会用到 Contact Shadow,如 赛博朋克 2077、对马岛之魂、Days Gone 等,这里就不一一列举了。
 
Contact Shadow 最基础的算法其实相当简单,除开 Sony 那几个 In-house 的引擎外,就我看到的而言,应该基本都是按照 UnrealEngine 的那个算法照搬的,让我们先来看看 UnrealEngine 的算法(最新的 UE5.4 版本里已经把 Bend Studios 的算法抄进来了,这里说的是老的 StochasticJittering 算法,详情见参考资料):
 
 
其实就是逐像素去将光线方向投影到屏幕空间,然后沿着光线方向做 Ray Marching,每步进一步采样对应的 Depth Buffer,与光线的深度做比较,如果采样点被遮挡,则说明起点像素在阴影之中,如果所有采样点都未被遮挡,这说明起点像素不在阴影之中:
 
notion image
 
要注意的是 Contact Shadow 实际上是不能替代 Shadow Map 的,因为 Contact Shadow 只依赖 Depth Buffer,而 Depth Buffer 有个很致命的问题,就是没有场景的厚度信息,这就意味着在判断遮挡时,只能做一个近似,UE 的算法里有一个 CompareTolerance ,它实际上是一个假定的 Depth Buffer 厚度,Scene Depth 和 Ray Depth 相差太大或太小,都会影响阴影的判断。这会产生什么影响呢,一是带空洞的物体或许会投射出实心的阴影,而是过大的物体可能投射出镂空的阴影,大家可以去看一下赛博朋克 2077 或者 UE 里的效果,很容易就会发现这类瑕疵,所以说可不要想着用 Contact Shadow 完全去替代 Shadow Map。它和 Shadow Map 的关系就类比于 DFAO 和 SSAO 的关系。一个是米级精度的技术,一个是厘米级别的技术,两者效果互相补足缺一不可。
 
另外一个值得一提的点,Contact Shadow 是 Ray Marching 类算法,避不开的问题就是 Marching Step,步长会直接影响阴影的精度与性能。步长太长会直接忽略掉 Depth Buffer 小于一个步长的起伏变化,可能本来是在阴影中的像素被判断成无阴影;而步长太短则会增加采样次数,提高性能开销,在屏幕空间算法里,单像素的采样次数是可以直接按比率影响性能的。当然,在使用 TAA 的引擎里(比如 UE),都会给 Ray Start 加一个随机值,然后帧间产生 Dithering,最后再依靠 TAA 做一下平滑,这样可以以一个较低的采样数或者高采样数的效果。不过像这种做法其实也是权衡之计罢了,用过或者改过 Contact Shadow 的哥们都知道,遇到一些比较抽象的 Mesh,你还是没办法获得很好的效果。
 
所以该怎么样进行优化呢?

Days Gone Contact Shadow

前面已经提到,步长是直接影响 Contact Shadow 效果和性能的,我们有可能使用 Dithering 之外的做法再去优化采样吗?Bend Studios 给出了他们的答案。
 
给定两个相邻像素,他们的 Ray Marching 路径是这样的:
notion image
 
我们可以看见其实两条 Ray Marching 路径相当相似,这就意味着两个像素所需要计算的采样点也会相当相似,那我们能不能以某种方式去共享他们的采样结果呢?
 
答案当然是肯定的,只需要做一个简单的近似,我们可以认为沿着光线路径上的一串像素都共享相同的 Ray Marching 路径:
notion image
 
基于上面的近似后,我们可以自定义 Compute Shader 派发的 Wavefronts,从传统的标准屏幕空间矩形 8x8x1 改成沿着相同光线方向的 64x1x1,之后每个线程去负责少数几个采样,然后将可以共享的计算结果暂存到 LDS (Shared Memory) 中,最后再逐像素进行 Ray Marching,这时候访问所需要的数据都只需要从 LDS 中取的,从而把步长提升到理论最小值一像素的同时达到惊人的效率。
notion image
 
最后全屏幕 Wavefronts 的一个可视化:
notion image
 
光源在正中心的情况,别的情况依次类推,最后算法实现的难点就是这个 Wavefronts 划分,原来逆向这块花了我很多时间,只要看到这个 Pattern 其实最后算法就推的差不多了。不过现在 Bend Studios 已经开源了(见参考资料),那就没必要自己再写了,直接爽用就行 🤣
 
值得一提的是,UE 5.4 已经把这个算法抄进来了,有个控制台变量可以操作,代码我也已经贴到参考资料了,想用的小伙伴可以直接参考官方代码集成到自己的引擎里,再次感谢 Bend Studios,对 Contact Shadow 做了极致优化,还不吝啬给大家分享 ❤。

参考资料

上一篇
记给 UnrealEngine 的一次 Contribution
下一篇
利用 C++ Concepts 做编译期检查

评论
Loading...