yukamu 发表于 2022-1-9 21:30

常见几种Shadowmap在unity实现

先看最终效果
效果在 封面图(新开窗口查看) 和这视频 完整项目在文章最后

各种常见 shadowmap 效果对比
https://www.zhihu.com/video/1463586605797269504

另外补充2张图



unity 自带pcf 在通用距离内效果



vsm 效果

目标
因为之前看了好几篇相关概念的文章和论文,都不是很明确到底怎么实现,没理解实现细节的应该有不少人。所以我想动手做一次,并写明白,需要哪些数据,分别存放到哪个通道,这些数据是怎么来的,哪些方式要做prefilter,如何做prefilter,哪些要实时filter。能不能用d均值的 exp(cd) 来代替exp(cd)均值,从而减少存储的精度需求减少容量等。基础的概念我已经找到一个讲的很好的大佬,所以我主要讲解实现的代码段。不了解基础概念的可以先看这个链接。
Clawko:影子传说——三种Shadowmap改进算法的原理与在Unity中的实现
<hr/>代码实现部分
创建基础shadowmap图
需要一个计算出灯光相机深度值的 shader,和利用这个shader渲染出rt的脚本。一般有2种方式 渲染后 去CameraDepthTex的后效处理。或直接利用RenderWithShader 在渲染对象阶段直接输出深度值。这里采用后者方式 只需要一句执行指定shader渲染。


然后是对应的shader内容,主要就是在vert里 做了一下法线反向小尺度偏移,避免模型同一个位置深度太接近产生摩尔纹。frag里 除了输出深度,需要做个 clip 好让不透明对象比如树叶等 可以产生镂空的阴影。


<hr/>采样shadowmap 通用过程
为了统一作用所有材质光照的阴影计算 所以选用延迟管线来示范更清晰些,找到公用的光照计算shader,根据当前 shaderpoint的世界坐标 转到灯光空间(红色框内内容),这部分代码是几种算法多需要的操作,主要是为了获得2个数据,一个是 shaderpoint 在灯光相机空间下的深度d,一个是 对应shadowmap的screenPos,这个灯光空间上的uv,因为按照 普通相机渲染的转换做 所以也叫 screen了。只要根据这个uv采样shadowmap获得遮挡物深度和d做比较就可以了。主要区别是各种比较算法的不同。





基础shadowmap

<hr/>用软件pcf5x5 方式
这里示范了 5x5 最简单的平均采样的软件pcf代码。实际上用泊松分布软pcf+硬件pcf更常用些,因为硬件考虑过这种需求的普遍性已经做了优化。具体是dx11的 SampleCmpLevelZero ,但是在unity封装的更简单些。用RenderTextureFormat.Shadowmap 格式参考 Internal-ScreenSpaceShadows写法就可以实现。




只做软件的pcf可以看到一开始的效果图,是表现最差的。纯软件层需要好几十次采样才能把pcf做好看。这个开销太费了。所以后面几种主要是为了预计算而减少采样次数。严格来说 这个shadowmap 是要point采样的,因为深度的直接插值是无意义或错误的。比如图中的坐垫网孔,按地面为深度0 坐垫处为深度是h来考虑,(与实际深度大小相反不影响这个)如果直接插值 那么可以看成大量深度都是h/2的深度。最后渲染的时候 地面的0 总是小于h/2,甚至小于h/100.所以地表就全黑了。而不插值。可以得到几个1和几个0,对他们平均就能表现漏光的灰色值了。但是一般自己写pcf效果都不太好 很模糊所以这里常常被写错而没发觉 ,就错误的用了默认Bilinear的shadowmap格式。
<hr/>用esm方式
上面说了pfc纯软件层需要做几十次采样才能比较好的实现软阴影过渡,所以就有人提出适合把这一步提前计算的方式。并不是简单的说 exp函数 比 跳跃函数(d>z?0:1)具有过渡,而是他还支持prefitler。否则 还是用shadowmap图。直接取出z。然后做exp(-c*(d-z)) 就好了。那么esm就变成只改一行代码的事情了。这样做效果非常差,中间过程就不发图了。实际上我们要创建出一份不一样的shadowmap数据。以下的 _MainTex就是原始shadowmap。对他做一个非均匀卷积操作。可以简单理解为 求附近的 exp(cd) (这里叫d到了采样阶段他从图里取出就叫z,采样阶段的d是 采样点的世界坐标在灯光相机内深度。大家都是这样的习惯 导致看起来很晕,我也保存这个习惯 但加个说明吧)。exp(cd)的平均值 不等于 exp(c*d的平均值)所以不能只存d的平均。要存exp(cd)的平均导致对float范围和精度要求较高。另外我写的这个距离平方成反比贡献度的算法,比平均贡献好些。但是没严谨的物理依据,只是我记忆里 万有引力 电荷库仑力 点光光通量衰减 都是按距离平方成反比的。所以就这样写了。但与距离成反比这个是符合物理的只是选一种反比方式。


调用它的C# 也很简单 用blit或 computeshader 都可以,如果选computeshader要记得开 rt的随机写入功能。





esm的 数据图 因为exp后基本都是大于1的值 所以预览全红

但是采用的性能非常高 只需要一次就能实现比pcf25次软阴影。这也是esm核心优势的一句代码。因为zbase是当时存入的exp(80*z),所以 这里就是 exp(-80*d)*exp(80*z)=exp(-80*(d-z)); 就是esm原理介绍里的公式。


直接的esm 因为原理上的问题,在深度差小的地方一定会漏光。但深度差小 阴影要淡些 又是软阴影的关键点 不能丢弃。所以这里先提示下解决漏光的关键是 区分深度差小的地方到底是来自 大幅度变化的插值(阴影边缘 从0边到h 差很多会插值出0.01h这样的小深度差) 还是来自自己所在位置,比如椅子脚接触地板处的 深度差很小 并不是因为插值变小。它离投影物本身就是很近。但解决方式放后面谈。



基础esm 漏光问题

<hr/>用vsm方式
类似esm 我们先要对深度图做变化,这次变化需要把原来一个通道的深度数据,存入2个通道。这是因为平均的 d的平方。与d平方的平均值不同。卷积依然用了距离平方成反比的算法。





vsm的 数据图 因为r存d的平均 g存d平方的平均 所以有2个通道颜色

采样就按正常的 切比雪夫不等式来做。







基础vsm的 light bleeding

<hr/>结合vsm esm
虽然听说有比较复杂的结合 EVSM,我没去看过验证过,这里算道听途说吧。但是有个很简单的结合方式,就是 既然esm vsm都漏光或影子变淡。而从来没有在不该有影子的地方出现影子。也就是他们的错误都是让影子局部变淡,而无变深,而这种地方又常常不同。那么很简单 min(vsm,esm) 不就可以了吗?



min(vsm,esm) 解决大量漏光 渗光问题

min(vsm,esm)
<hr/>
存储优化
以上这些都是只考虑采样性能,没考虑存储空间的 所以各种问题都比较好修复。实际上存储空间是考量的重点,特别是做些场景静态阴影存储。为了让运行时帧率提升10-20帧的 高帧率要求的产品。比如我们的FPS,那么满足存储高效的方式 才是 对他们选择的重要依据。
存储的方案有很多很多。常见的或论文有的我就不提了,我就分享1个我自己的方式。
ESM贴图高范围压缩。根据基础原理当exp(c*d) 的c为80时,效果不错。但是马上就要超过float32的范围了。所以我想把他压缩到0,1范围。想法很简单,求完平均后再用exp的反函数计算出此刻的d。注释的那句改成 它下面这句 。而采样处就从 float esm = saturate((exp(-80 * d) * zbase));改成float esm = saturate((exp(-80 * (d- zbase)) ));









压缩后的esm

可以只有原来的1/4 但压缩后效果还行。不仅如此,还能根据某小区域内 d范围的最大最小值,做精度提升 存储为(d-dmin)/(dmax-dmin);然后再做精度有损的压缩 压到BC4格式。这样只有原版的1/8压缩率比较高了。关于这种压缩之前我有写过,生死狙击2项目也在采用。jackie 偶尔不帅:长阴影shadowmap精度优化
<hr/>esm漏光解决方案的显存优化。
上面的esm漏光 简单的用min(vsm,esm)就解决了,但是这种属于理论算法,实际上是不可用的因为显存需要3倍。vsm本身是2个通道。所以有必要做实际工程师需要的解决方式。
思路前面介绍esm的时候介绍过了,就是区分真实深度差小的遮挡中心,还是因为插值才导致的深度差值很小。换句话说,我们只要找出阴影的边界,然后让边界处保留esm的过渡(阴影色变浅),但真实遮挡(非阴影边界处)采用硬阴影即可。所以分2步完成这个功能。

[*]创建一个更硬的阴影备用
[*]找出阴影边界,在非边界使用1的阴影这样就不会有漏光/过渡了
创建一个更硬的阴影
之前exp(-c(d-z)) 函数曲线有个简单规则,c越大越接近硬阴影,比如用300。但是在制作shadowmap的时候写死了c为80,不可能为300再存一张图。所以用反函数逆向求出原始的z,然后用300重新算esm。这里因插值而产生的偏差忽略是要硬阴影不需要很精确。代码如下





用esm 算出硬阴影

找出边界
边界可以根据深度变化差异大小来判断,如果都是阴影内 或都是阴影外那么 深度图深度是差不多的。当然复杂情况下 重叠投影物处会误算 ,但可以根据投影距离与接受处距离差值再做判断。



寻找阴影边界代码



用ddx ddy找出大致阴影边界



多次采样 硬阴影与esm的 lerp



ESM(左)与 不增加存储修正漏光的ESM(右)

<hr/>本文全部工程

pc8888888 发表于 2022-1-9 21:36

老哥,我至今还有你qq,没想到在知乎看到你了

xiaozongpeng 发表于 2022-1-9 21:40

不会是 驱魔录老玩家吧[大笑]

zifa2003293 发表于 2022-1-9 21:41

不是,好早再ilrt群加你的

NoiseFloor 发表于 2022-1-9 21:45

哦 转图形 不再做客户端后 就离开那个技术群了哈
页: [1]
查看完整版本: 常见几种Shadowmap在unity实现