Origin
zhuanlan.zhihu.com
Tags
简悦
项目
收藏夹
创建时间
收藏类型
Cubox 深度链接
更新时间
原链接
描述
简单整理下一般的 Urp 移动端项目中可能会用到的阴影方案以及在实际接入过程中遇到的问题和解决方法,个人向不全且想到哪里记到哪里。就按项目由远到近的阴影来说。基础版本来自 Urp 的 Cascade Shadow。并且不涉及一些特殊的比如电线阴影投射等以及某些爆炸雾效阴影用 Volume 等,也不涉及具体的代码。

Contact Shadow

我们使用 Unity 原生的 Cascade Shadow 会发现,受限于 Shadowmap 分辨率的限制,我们阴影采样会存在各种各样的精度问题,这样会导致即便是第一级的阴影在一些小物体投影有时也无法满足对阴影质量的要求,而且第一级阴影和第二级在小物体投影上(广告牌字体等等)会有明显差异,这个时候就得考虑使用 Contact Shadow 去补足。Contact Shadow 的思路非常简单,每个屏幕点,结合深度图获取原世界坐标。往 Light 方向做 raymarching,判断每个 step 的深度是否有邻近深度。如果有,则表明被这个深度值所对应的几何遮挡了,从而判定在阴影内。
notion image
notion image
在一般 URP 项目中,接入对于普通小物体投影 HDRP 版 Contact Shadow 已经是够用的了,Forward 项目只需要把 PreDepthPass 打开,用屏幕的这张 DepthRT 就可以,相关代码基本可以照搬这里也不细讲,最后 lighting 阶段采生成的 Contactshadowmap 取 min 即可完成一个基础版的近距离硬阴影补充。
但实际上,ContactShadow 可以有更多的补充。首先,我们将其限制在一段很小的距离的原因在于它完全是硬的,这是符合接触阴影这个物理逻辑的。但倘若我们想要它对场景有更多的补充,我们需要做软化版本的 ContactShadow。网上有相关将 RayMarching 方式改为 sphereTrace,在边缘处渐变为 1 的软阴影方案可以采用。我这里再补充一个相关 ContactShadow 软阴影生成方式,该思路来源于 AMD 降噪的光追阴影:
在生成接触阴影的过程中,我们可以使用时序相位偏移 + 空间蓝噪的方式去做这个阴影的软化。这里比较有意思的点在于时序相位偏移的权重是黄金比例分割的,实际上是也是一种噪声 GR, Golden Ratio Noise,对每帧叠加
$$c=\frac{\sqrt{5}-1}{2}=\varphi-1\approx0.618034.$$
$c=\frac{\sqrt{5}-1}{2}=\varphi-1\approx0.618034.$ R1 序列,保证其在时序上的拟合性质。而在空间上算朝阳光方向的偏移角度的时候我们可以使用蓝噪声 Disk 去随机这个偏移(计算这个便宜角度),最后也能在时序上混出来 ContactShadow 比较好的软阴影。当然,对于该软阴影的进一步降噪可以继续学习上文的内容去做,但实际项目一般都用不到,而且需求较高。其实在这里我们就发现了一个较好时序的软阴影的基本生成方式,后续再聊。
接触阴影最大的问题是它是屏幕空间的,存在一定的穿帮现象,并且在实际使用的过程中,基础的 ContactShadow 的阴影有时候视觉效果并不尽如人意,是需要对于场景的调优的,确实增加了场景美术工作量。

级联阴影

说完了比较近的接触阴影,在拉到稍远一些的级联阴影。抛开原神 8 Cascade 的做法不谈(当然原神方案必然是做了级联的分帧更新的),在 Urp 原生的管线里的四级阴影基础上,实际项目大概率会遇到各种各样的问题,包括效果和性能问题。效果问题还是来源于移动端降了 Shadowmap 分辨率,又为了保证近处的阴影质量,一二级阴影总是拉的非常近,对三四级的阴影质量基本上难以控制了,级联边缘分界问题尤为明显,但又总不能在分界区域算一段距离采两侧做 lerp 过度?这样的性能损失也比较大。而且本身 Mainlightshadowmap 这个 Pass 渲染时间也是比较多的。
所以一种解决方式或思路是利用烘焙阴影的方式将阴影图缓存下来,在某些特定条件下进行更新,比如第一级每帧都更新,其他级分帧更新,远的级可以存着相机移动距离超过某个阈值再进行更新,总之设计一系列的更新方式跳过 Urp 版的 MainlightShadowmapPass 进行每帧的更新计算,减少平均的阴影渲染压力,人物等动态阴影用 realtimeshadow 的方式进行处理,只算某一段距离内的动态阴影,最后做个合并,也不会比基础的 Cascade 用更多的图。当然还有一些重写阴影的细节问题也需要处理,简单的 ShadowUtils 函数重构就跳过。
但这样的分帧更新缓存的阴影的问题在于虽然减少了平均的阴影渲染时间,但不可避免的因为更新逻辑存在渲染峰值,有可能加重对于某些帧的渲染压力,这里解决方法只能通过某些设计关闭某些阴影投射 caster 减少压力了。

软阴影

出于性能的考虑,像软阴影这种需要多次采样的必然受到严格限制。这里说个题外话,软阴影 SAMPLE_TEXTURE2D_SHADOW 宏调用 textureName.SampleCmpLevelZero,在 Dx 上支持硬件 2x2 PCF 等。所以采出来的阴影是软的,当你用深度图替代,直接用 SAMPLE_TEXTURE2D 去采图发现是硬的。并且烘焙相机 camera render 的时候没法直接知道具体物体的 shadowcaster 是否关闭,所以后续做 BakeShadow 还是和上述缓存阴影的逻辑相同接管线。
所以在移动端上,Urp 给了我们几个选择,用的最多的是 PCF4 和 SampleShadow_ComputeSamples_Tent_5x5,采样 4 个点或者采样 9 个点,后面那个算法可以直接看 URP 文档。但很多情况下,这样的软阴影质量大概率会被项目指摘,首先这样的软阴影不是物理正确的(意味着需要上 PCSS),并且这样的软阴影效果也不太优秀(但用更多的采样次数在性能上又无法满足),所以这里又得考虑时序上做 Jitter 去抖让 TAA 去混。
然后聊一下 PCSS,具体的原理跳过,BlockSearch+PCF,源码可以直接借鉴 HDRP 版本的效果,需要在 shadowpass 提前根据 VP 计算级联的参数。(HDRP 版有一点小 BUG 在级联边缘处会采外面去,需要做 clamp)基本上效果是没问题的,但由于参数和级联的 VP 有关,会导致级联过度的问题,远处的级联很容易不软了,可以简单采取回退的方法缓解此问题。最后呢,PCSS 的性能消耗来自两个地方,一个是 BlockSearch 的次数,也就是计算这个平均深度的精确度(这个过少会导致阴影直接噪掉),另一个就是 PCF 的次数,(有优化),直接影响软阴影质量。所以这里又可能需要基于场景、时间的参数调整。但实际测下来消耗还可以。
最后聊一下时序软阴影,这是一个比较好的基于时序 + 噪声的 Trick 方案。我当时在做时序上的软阴影的时候是按着上面的 AMD 光追阴影的思路来做的,后来在听蓝噪声的课发现 GDCBeyond Hard Shadows: Moment Shadow Maps for Single Scattering, Soft Shadows and Translucent Occluders 最后聊一下时序软阴影,这是一个比较好的基于时序 + 噪声的 Trick 方案。我当时在做时序上的软阴影的时候是按着上面的 AMD 光追阴影的思路来做的,后来在听蓝噪声的课发现 SIGGRAPH 2016 有这么一篇软阴影文章,与我的实现比较类似,这里可能需要对各种噪声有一些了解,比如上面的 GR 噪声和蓝噪声 。时序阴影的确可以在基本不损性能的条件下获得一个较好的软阴影效果,但正因为时序它的问题也是存在的,比如在运动的过程中的投影采样又得重新累积计算,具体表现在运动过程中存在一定的分层现象,但阴影的 motion?emmm 确实比较难以处理。不过总体上还是很有价值的,毕竟采四个点就很不错的效果了。
(对于 variance shadow map,只在课上学过,并未实际使用,就不提了,当然在 202 里有更多的软阴影生成方式,因为本人水平有限,虽有了解但还未真正实践过,也跳过,,)

BakeShadow

对于远处的阴影实际上我们不太需要他进行更新并且对他的质量要求也比较低,有 BakeShadow 我们可以让近处阴影有更高的精度,具体的实现上面也讲到了,其实都差不多。但烘焙阴影问题在于如果我跟上面一样根据当前相机的 cullresults 算当前视锥下的 bound 去确定一套参数,很有可能无法完全覆盖场景。所以只能离线根据完整的场景包围盒去算,确定一套参数,并且需要保证在场景多个时间段下都能烘焙正确,特别是那种狭长的场景 + 奇怪角度的阳光可能需要场景美术进一步调整烘焙相机。

VSM(VirtualShadowMap UE5)

以上为参考资料,光空间的 ShadowMapTexture 不是所有部分都会在 shadowMap 中使用到的,所以我们只需要保存 Virtual Shadow Map Texture 中会被使用到的一部分阴影,通过超高分辨率的虚拟纹理,camera 的 DepthPass,通过视图变换之后就可以知道屏幕空间上的每个点对应在这个虚拟纹理的哪个 Tile 上,这些 Tile 是我们后面 Lighting 的时候需要用到的 Tile,以此来做空间和效率的平衡。

总结

基本上上面的由近到远的阴影已经基本上够用了,当然有什么更好的思路和优化点也值得进一步讨论。或许之后可以再聊一下阴影剔除相关的内容。

▎本文由 简悦 SimpRead 转码。