Unity中的shadows(一)
Unity中的阴影针对不同的光源类型,平行光,点光源,聚光灯有不同的处理方式,casting和receiving的实现都有些区别。我们根据光源类型的不同详细看一下具体的实现。
平行光阴影
如图中所示场景,有两个平行光源,我们打开frame debug查看一下:
可以看到,对于平行光产生的阴影,Unity使用screen space shadow map来保存阴影信息。
首先,Unity对整个场景跑了一遍深度pass,记录场景的深度信息:
Unity在这一阶段定义了SHADOWS_DEPTH关键字,并且把ColorMask设置为0,意思是不会输出任何颜色。另外,只有object的shader中包含ShadowCaster标签,才会跑一遍该pass。ShadowCaster的代码可以很简单:
float4 MyShadowVertexProgram (VertexData v) : SV_POSITION {
return UnityObjectToClipPos(v.position);
}
half4 MyShadowFragmentProgram () : SV_TARGET {
return 0;
}
我们并不需要返回深度信息,z的值引擎底层会记录下来。
screen space的深度图有了之后,Unity会基于每个平行光源渲染一张shadow map,简单来说,就是把平行光源当作一个摄像机,做一次正交投影,将场景中物体的深度信息记录在shadow map上。
之后,Unity会做一次Collect Shadows阴影收集过程,用之前深度图中每个pixel的深度信息,将其重建回世界坐标系下,然后变换到光源空间,对之前光源空间渲染的shadow map进行采样。shadow map采样出的信息就是被光源照射到最近物体的深度,通过比较两个在光源空间下的深度信息,就可以判断出当前pixel是否在阴影内,如果在阴影内则输出0,能被光源照亮则输出1。每个平行光源都对应一张各自的screen space shadow map。
聚光灯阴影
如图中所示场景,有两个聚光灯光源,我们打开frame debug查看一下:
可以看到,与平行光相比,少了深度pass,还有阴影收集的过程,Unity就只是简单地把光源空间内的物体渲染到shadow map上。
点光源阴影
这次场景中有两个点光源,打开frame debug:
可以看到,点光源渲染阴影所用到的pass数量非常多,一个点光源可能需要6次阴影pass,这是因为点光源的shadow map不是一张贴图,而是一个cube map。Unity为点光源阴影定义了SHADOWS_CUBE
关键字。
另外值得注意的是,ShadowCaster的pass需要添加如下定义:
#pragma multi_compile_shadowcaster
用Visual Studio可以看到具体的keywords:
// -----------------------------------------
// Snippet #2 platforms ffffffff:
Builtin keywords used: SHADOWS_DEPTH SHADOWS_CUBE
2 keyword variants used in scene:
SHADOWS_DEPTH
SHADOWS_CUBE
如果要为主平行光(base pass)添加阴影,需要添加SHADOWS_SCREEN
关键字:
#pragma multi_compile _ SHADOWS_SCREEN
如果要想为第二个平行光,点光源,或者聚光灯(即add pass)添加阴影,则需要如下定义:
#pragma multi_compile_fwdadd_fullshadows
用Visual Studio可以看到具体的keywords:
// -----------------------------------------
// Snippet #1 platforms ffffffff:
Builtin keywords used: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING SHADOWS_DEPTH SHADOWS_SOFT SHADOWS_SCREEN SHADOWS_CUBE
13 keyword variants used in scene:
POINT
DIRECTIONAL
SPOT
POINT_COOKIE
DIRECTIONAL_COOKIE
SHADOWS_DEPTH SPOT
SHADOWS_DEPTH SHADOWS_SOFT SPOT
DIRECTIONAL SHADOWS_SCREEN
DIRECTIONAL_COOKIE SHADOWS_SCREEN
POINT SHADOWS_CUBE
POINT SHADOWS_CUBE SHADOWS_SOFT
POINT_COOKIE SHADOWS_CUBE
POINT_COOKIE SHADOWS_CUBE SHADOWS_SOFT
阴影设置参数
在Unity的project settings中的quality项,有如下的若干有关阴影的设置:
Shadows有3个选项,一是完全关闭阴影,二是只使用hard shadows,三是hard shadows和soft shadows都使用。使用软阴影的好处是阴影的边缘更加光滑,减少锯齿。根据不同的选项,以平行光源为例,unity会在收集阴影阶段使用不同的subshader绘制screen space shadow map:
Shadow Distance控制阴影显示的距离,太远的物体往往没有显示阴影的必要,通过控制距离可以降低绘制的draw call:
可以看出,超过阴影显示距离的物体,在光源绘制shadow map时直接就跳过了。
Shadow Cascade控制是否开启级联阴影,以及级联的数量和区域划分。级联阴影的好处是可以根据物体距离的远近,生成不同分辨率的shadow map,近的物体对高分辨率的shadow map进行采样,得到更多的阴影的细节,远的物体只用对低分辨率的shadow map采样即可,避免不必要的性能浪费。不同分辨率的shadow map在阴影收集阶段同样会被绘制到一张screen space shadow map上,因此只是在光源空间渲染shadow map时,draw call会根据选择级联的数量翻倍:
Shadow Projection控制级联阴影的区域生成方式。Close Fit利用摄像机的深度信息来计算区域,而Stable Fit利用到摄像机位置的距离信息来计算区域。在scene窗口下,可以选择Miscellaneous / Shadow Cascades来显示级联阴影区域:
Stable Fit的区域通常更稳定,不会受摄像机自身的位置和旋转影响:
Shadow Acne
由于shadow map的精度问题,shadow map上的一个pixel,实际上是对应了场景上的一片区域。而这片区域中的所有点,在shadow caster阶段,都会去取shadow map同一个pixel的深度值,当这个深度值与区域中的所有点的深度值相近的时候,就会出现一部分点比较深度后,在阴影中,而另一部分点却不在阴影中,导致场景中会出现明显的明暗条纹:
从数学角度上来看,如图所示:
EF为shadw map采样的深度,那么CF区域中的点由于深度都小于EF,因此不在阴影中;而DF区域中的点深度大于EF,会被判定在阴影中。
为了解决这一问题,可以在渲染shadow map的时候人为加上偏移,让阴影位于物体之后。这个偏移就被称作shadow bias。
常用的shadow bias有两种,depth bias和normal bias。depth bias顾名思义,就是在shadow caster阶段,将物体沿着相机空间的z方向往后偏移:
可以看到,我们将CD区域沿着相机空间的z方向平移到了GI区域。此时shadow map采样到的深度值为EH,它等于BD。这样,CD区域中所有的点深度都不会大于EH(因为最大的深度也就是BD,而BD等于EH),从而保证了CD区域中的所有点都不会判定在阴影中,也就可以消除shadow acne。这里,depth bias的值就是CG的大小。
normal bias是在shadow caster阶段,将物体沿着自身的法线方向往后偏移:
原理与depth bias类似,这里normal bias的值就是CG的大小。
shadow bias的参数可以在光源的属性中进行调整:
Reference
[1] Shadows
[2] 自适应Shadow Bias算法
本文是Unity中的shadows系列的第一篇文章,如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)-