论睡觉解决多光源实时阴影,“时空滤波”MSDFShadow

先上效果,没有ShadowMap,是实时Trace的SDFShadow:

移动3070上100来帧。
如果没有这个MSDFShadow的优化,也就是每一帧硬去遍历所有光源Trace阴影,在桌面2060上10帧不到:

一 论睡觉
整个算法是我刚躺下去半梦半醒的时候想到的。因为脑子有执念回路,而睡觉恰好是一个碎片整理,清楚杂物的过程,所以可能自动工作了。这在我身上发生了不止一次了,其他例子我就不多举了。就怕心贪嗔痴,堵在那硬求,解决不了的问题就和解,先睡一觉再说。不再多说,睡觉大家都会。
二 论多光源实时阴影
SDFShadow应该都见过,shadertoy上很多。如果只有1个光源,那每个像素只需要在hit点向光源方向再Trace一遍Scene,看是否相交就可以了。
问题就出在我的场景有多个光源,如果要正确的阴影,那要对每个光源都要Trace一遍,帧率很快就下降了。
由于我一直都用的SDFShadow,没有存成RT,所以开Indirect的时候更糟,因为要把Indirect的光源混合阴影(纯阴影里的像素不能成为间接光的光源),也要走一遍Shadow。
三 论时间滤波
很快我就想到了,shadow遍历光源的过程和收集indirect的过程类似,所以每帧只去trace一个光源,然后历史混合就行了。
我很naive地用frameID%lightNum去决定每帧trace哪个光源。
很快实现了,效果也很显著,在2060上从原来的10FPS升到60FPS。
不过有一个显著的缺点。假设我场景有6个灯,那么等到6灯都收集完Shadow,就要等到6帧渲染以后才能完成。那比如在第一帧时,和第一个灯Trace,不是0就是1,和最终收集的结果比如2/6有很大的误差。这样导致前6帧场景的明暗一直在跳变,到6帧结束这个“积分过程”才算结束,才能稳定。
一开始我以为只有6帧,视觉上应该还好。但是第一,人眼有延时效果;第二,为了避免ghost(拖影),和indirect一样,在相机大幅度(实际是稍微有变)改变位姿时,就得舍弃历史数据,重新计算。假设玩家一直在前进,那么永远就是第1帧的计算结果,可想而知不是全亮就是全暗。
(也许你会想利用起来历史数据,而不是直接丢弃重算,就像TAA,Motion Vector那样。我是自己实现过TAA的,实际效果一言难尽,我直接丢弃了,如果你想试试,那我只能祝你好运)
睡觉之后,我想到我真是傻,和indirect一样用低差异序列(以下简称LD)不就行了吗?于是改写。改完瞬间接近完美,没有跳变。(frameID*rand01_LD(seed = float3(id.xy,frameID)))
如果相机停留超过6帧,也会收敛到正确结果,并且跳变不十分明显。(由于LD的特性,随机能均匀遍布Vector Space,使得即使在第一帧的时候,由于每个像素都分布是不同id的灯,导致全局平均眼球看起来阴影度和最终相差不大)
四 论空间滤波
虽然使用LD已经很不错了,但还是会有明显的LD分布形状(也就是“噪声”的形状)。还有一点,在相机持续移动时,实际上每一帧都只有第一帧,没有历史数据(因为避免Ghost,移动了要重新积分),所以光用LD还不够。
之前在B站上看到有人在unity里实现了好像是寒霜引擎的实时GI,提到了“时空滤波”,我就把名字和思路借鉴了过来。由于咱不想搞Surfel啥的,也别3d空间滤波了,就屏幕空间处理一下得了。额外工作是把Shadow分离成RT,不过这样也好,indirect的时候也省的重新计算了,可以直接从RT上读。
对于Indirect,如果简单地用Blur或者Dialation,也许与收敛结果相比还有些跳变;但是咱Shadow,Blur一下没有太大的问题。我没有太仔细地去处理这事,如果设定好mask,然后按lightNum去采样自然数顺序前后像素(由于LD),取平均,应该就能完美解决。不过灯多了肯定就不行了。我就图简单,直接使用带间隔的BoxBlur对Shadow处理一下。(由于我的Shadow,0为黑,1为亮,所以实际应该对1-Shadow的Texture进行Blur,再反回来;但是我图简单,直接对Shadow进行Blur,再*085来和收敛结果一致,也看不出来;如果不乘0.85,显然会把为1的像素Blur到阴影里,变成过曝的效果了:

)
结尾
天天睡不好,但还要天天睡。睡觉很重要。

