【Unity Shader】Unity中自阴影优化方案

上一篇记录了在Unity中阴影映射的标准流程实现,本篇博客一起来看看阴影映射存在的自遮挡/自阴影问题及其优化方案。

因为在之前学习202的过程中也有记录这方面的内容,所以这里结合我的GAMES202作业1-实现过程详细步骤这篇博客以及百人计划实时阴影那节课再来巩固一下。


1 产生原因

  • 浮点计算精度
  • 采样问题(shadowmap上一个值对应太多物体表面边界点)

由于阴影映射的分辨率有限,或者说采样的时候做判断时“比大小”的过程数值精度比较会有偏差,难免会造成不正确的自遮挡阴影,关键在于“自”!是指物体自己表面上出现了本不该有的阴影锯齿。而且这种不正确的自遮挡阴影往往会出现在物体和光线靠近的那种边缘处,出现这种神奇的自遮挡锯齿:(下图是202那篇文章中我的作业展示图)

这种问题一般称作阴影瑕疵(Shadow Acne),甚至更加形象的称为“阴影粉刺 Surface Acne”,也有叫做Z-Fighting,总之都是表达的上述自遮挡的错误现象。

让我们拿Games202和百人计划的图一起做例子:

 

左右两个图里红色的线可以理解成shadowmap中每个texel记录的深度值的大小,那么texel涵盖物体表面的区域内的深度值都将会是一样的(都取的是左图所示中间红色着色点的深度值)。

那么我们取一个后面部分“被迫涵盖的”蓝色着色点来看:从摄像机视角记录的场景中的深度值记为zp,但它采样shadowmap得到的值zs由于被迫一视同仁而直接取了红色着色点的深度值,于是zp>zs,蓝色点就被列入了阴影中。

根据上述原理不难法线,当光线和物体表面法线夹角越大,一个texel的遮盖区域将越多,shadow acne问题将越严重。

2 优化方案1 提高分辨率

即从shadowmap本身的采样分辨率出发的——提高shadowmap的分辨率(直接体现为texel尺寸变小,即上面图中的红色线变短),我们随便从Unity中搭一个Cube,看看选择不同shadowmap分辨率的效果,下图上为Unity提供的最低分辨率;下为最高分辨率:

但!这个方法太不实际了!且效果还是不如人意(图里还是有一点点锯齿的感觉)。而且在面对一个大世界下的场景,shadowmap再怎么大也还是会出现自遮挡,考虑到性能很多游戏对于纹理大小是有限制的,这个方法直接被PASS掉了。

3 优化方案2 给一个bias

 参考自适应Shadow Bias算法 - 知乎 (zhihu.com)

 Shadow Mapping Summary – Part 1 – The Witness (the-witness.net)

接下来就是最常用的方法。回想起101的作业,代码中凡是涉及到“数值比大小”的内容,都会考虑数值精度问题给一个容许偏差(或者叫容错阈值),即常说的bias。那么既然上述阴影瑕疵的产生原因也是数值偏差问题,解决办法就也会是设置一个bias,一般会设置:

  • 深度偏移 Depth Bias
  • 法线偏移 Normal Bias

但值得注意的是,当这个Bias设置过大时会出现漏光现象(即后面会讲到的Peter Panning),所以这个值是很难一下子适配到场景中的所有物体的,只能尽量取一个合适的大小。

那么这个值怎么给呢?还是拿上面的图:

通用做法是采样shadowmap进行阴影深度测试时,让红线向远离光源方向偏移一个bias,即zs+bias,那摄像机获得的深度值zp和zs的判断就从zp>zs变成zp<zs+bias,这样一来蓝色点就在光源能照射到的范围里啦!

一些必要的参数解释

这部分是参考百人计划里老师给出的一些解释,我认为很用必要:

  • 深度偏移——这个偏移指片元向着光源方向偏移
  • 法线偏移——沿着法线方向向外偏移(后面会有涉及)
  • 偏移量bias的单位——shadowmap的texel大小,例如shadowmap分辨率是256X256,则一个单位的bias就是1/256
  • bias的使用——bias是在逐像素进行阴影深度测试时使用,对物体表面本身的法线是没有影响的

3.1 深度偏移 Depth Bias(tan)

第一种是直接朝着光源的方向平移一段距离就行!由于这个bias的大小牵扯到Light的透视矩阵和shadowmap的分辨率大小,所以bias还需要经过一些计算,我这里直接截图第一篇参考文章里的了,可以自行查看:

bias过大带来的新问题

因为深度判断是在着色器中对整个场景中每个片元都适用的,而且实际中大多数引擎实现Slope Bias时计算bias不会像上面那么复杂,都会给一个固定的bias, 每个片元都会执行这个偏移。用Slope Depth的方法由于不仅如果这个偏移量过大,很有可能让原本就应该处于阴影中的片元被错误判断进了光源照得到的区域,这就会造成阴影悬浮(Peter Panning)问题,例如下图,右图相对于左图给的bias大了很多,右图就会出现阴影悬浮的问题。

3.2 法线偏移 Normal Bias(sin)

为了修正深度偏移bias过大带来的悬浮问题,Normal Bias被提出。

 

计算公式同样参考:

4 Unity中如何实践bias

Light的ShadowMap参数

我们先来看看Light组件包含的ShadowMap相关的参数:

  • Shadow Type——选择Shadow类型
  • Strength——控制阴影的亮度
  • Resolution——shadowmap的分辨率
  • Bias——Depth Bias的大小
  • Normal Bias——Normal Bias的大小

4.1 Normal Bias

Unity把偏移bias这些操作都包装起来了,我们可以在UnityCG.cginc中找到下面的UnityClipSpaceShadowCasterPos,这个就是应用NormalBias的:

float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
    //获得世界空间下的顶点坐标
    float4 wPos = mul(unity_ObjectToWorld, vertex);

    if (unity_LightShadowBias.z != 0.0)
    {
        float3 wNormal = UnityObjectToWorldNormal(normal);
        float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));

        float shadowCos = dot(wNormal, wLight);
        float shadowSine = sqrt(1-shadowCos*shadowCos);
        float normalBias = unity_LightShadowBias.z * shadowSine;

        wPos.xyz -= wNormal * normalBias;
    }
    //世界空间 -> 裁剪空间
    return mul(UNITY_MATRIX_VP, wPos);
}

可以发现Unity是在Shadow Caster阶段生成屏幕空间阴影纹理时,把顶点位置朝着法线方向偏移了一个bias,这个顶点再参与到后续的采样过程。

重点来看看这个unity_LightShadowBias,当我把Normal Bias设置成0.2时,打开Frame Debug可以看到:

 可以推断,Unity内部会根据我们输入的bias值去计算出考虑分辨率和透视矩阵的正确的bias大小。至于Normal Bias相关的计算,就是一个简单的计算cos再算出sin最后应用在法线方向,这里不再赘述。

4.2 Depth Bias

Unity同样提供了深度偏移,偏移裁剪空间的深度值:

float4 UnityApplyLinearShadowBias(float4 clipPos)

{
 
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
    #if defined(UNITY_REVERSED_Z)
       
        clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
    #else
        clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w);
    #endif
#endif

...

如果我们在Light组件中给Bias一个值:

 此时参与真正计算的unity_LightShadowBias.x是个负数:

 相当于也是把顶点的深度值减小了,这样跟shadowmap深度值对比的时候,顶点就从阴影里变到了可以接受光照的区域了。

猜你喜欢

转载自blog.csdn.net/qq_41835314/article/details/127556757