ShadowMap是Unity目前使用的实时阴影技术,还包含了Screen Shadow Map。非常惭愧,做自研引擎的时候我并没有自己去实现这套特性,那么现在在看Unity,刚好就阅读以下Unity是如何实现ShadowMap的。下面会有部分内容转载自冯乐乐,目前她的书《Unity Shader入门精要》已经被我们组列为入组必读书籍之一,值得读三遍以上(我真的读了三遍)。
传统的ShadowMap
ShadowMap的技术理解起来非常简单,首先把摄像机的位置放在光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。
在Unity的前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的ShadowMap,这张ShadowMap本质上也就是一张深度图,记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
Unity中实现了一个额外的Pass来专门更新光源的ShadowMap,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的RenderTarget不是帧缓存,而是ShadowMap(一张深度纹理图)。
然后在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后根据xy分量对ShadowMap进行采样,得到ShadowMap中该位置的深度信息。把这个两个深度信息做对比,判断该点是否应该在阴影中。
延迟渲染中的光照计算绝大部分都是在屏幕空间里进行的,同样也包括阴影。这种屏幕空间的阴影实现主要有这么几个步骤:
Unity5使用了Screenspace ShadowMap,会先通过LightMode为ShadowCaster的pass得到可投影阴影的光源的ShadowMap以及相机的深度纹理。然后根据这两张纹理得到屏幕空间的阴影图。阴影图包含了屏幕空间中所有有阴影的区域。如果希望一个物件接收阴影,那么在PS的最后根据其屏幕坐标对阴影图采样即可。
ScreenSpace ShadowMap原本是延迟渲染中产生阴影的办法,并非所有的平台都支持(目前Mobile平台默认不使用,虽然部分手机已经支持了),因为需要显卡支持MRT。
理论上ScreenSpace ShadowMap更快一些,毕竟不用把所有物件都转换到光源空间去判断是否在阴影中,只需要把深度阴影先计算出来,然后把深度阴影图转到光源空间去和ShadowMap对比即可。而且理论上有了深度阴影,也可以在渲染的时候预防overdraw。不过目前还没有看到具体数据信息,所以只能上理论上是这样的。
LightMode为ShadowCaster的pass
既然这个Pass如此重要,既可以用来计算ShadowMap,还可以计算相机的深度纹理,那么来看一下它的代码。(ScreenShadowMap的计算是通过另外一个Shader,在Unity5.5.x中被称为Hidden/Internal-ScreenSpaceShadows)
// Pass to render object as a shadow caster Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; }; v2f vert( appdata_base v ) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } float4 frag( v2f i ) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG }
代码很短,内容都在三个宏定义中。
#ifdef SHADOWS_CUBE // Rendering into point light (cubemap) shadows #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); #else // Rendering into directional or spot light shadows #define V2F_SHADOW_CASTER_NOPOS #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex.xyz, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif #define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS float4 pos : SV_POSITION #define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
当光源为方向光或者聚光灯的时候,在VS中只是做了一定的偏移(这些偏移大多数是用于防止self-shadowing)之后,将模型空间的坐标转为了相机空间的坐标作为VS的输出。PS直接返回0。这样就可以得到相机空间的深度图。我想ShadowMap肯定就是Unity在内部把相机位置移动到了光源位置,然后得到的深度图。这样,ShadowMap和相机的深度图都得到了。
光源为点光源的时候,vs多了一个输出,模型与光源的距离。在深度图中记录了这个值偏移后的信息。
需要注意的是,是否会渲染到生成深度纹理不仅仅和LightMode为ShadowCaster的Pass相关,也和渲染队列有关。正如Unity文档中所说:Note that only “opaque” objects (that which have their materials and shaders setup to use render queue <= 2500) are rendered into the depth texture.
Hidden/Internal-ScreenSpaceShadows挺长的就不放了,主要思路就是从摄像机的深度纹理里采样得到该fragment的深度值,然后利用矩阵变换计算得到该点对应的世界空间的世界坐标(利用CameraToWorld矩阵),然后再变换到光源空间下的坐标(利用WorldToShadow矩阵),最后拿这个坐标对光源的ShadowMap采样计算阴影。
在生成ShadowMap的时候,有时候为了效果和性能也会使用Shadow Cascades,也就是为什么用Frame Debug看到会绘制好多遍,相应的在生成ScreenShadowMap的时候就会用生成的多层Shadow Cascades去生成阴影图。(具体分层机制和场景的FarPlane值以及物体离摄像机有关,当FarPlane值较大,离摄像机都很近的时候,会有些层绘制不出来)
物体接收阴影
接收阴影就比较简单了,因为不管是使用ShadowMap还是ScreenspaceShadowmap,接收阴影的时候,只需要根据坐标转换得到纹理坐标,然后对纹理采样即可得到所需要的阴影信息。
如果使用ShadowMap,那么只接收不产生阴影的物件就不需要LightMode为ShadowCaster的pass,因为只需要在接收阴影的时候,将物件坐标转换,再根据ShadowMap计算阴影即可。
但是如果使用ScreenSpace ShadowMap的话,只接收不产生阴影的物件也需要LightMode为ShadowCaster的pass,因为如果没有这个Pass,在计算相机深度图的时候就没有这个物件的信息,如果这个物件的Z值对深度图产生了影响,那么得到的就是错误的深度图,进而得到的ScreenSpace ShadowMap也是错误的,因为得到的ScreenSpace ShadowMap是根据这个物件后面的物件的Z值,映射到ShadowMap上对比得到的,而万一后面的物件在阴影中,而当前这个物件的Z值比较靠前不在阴影中,那么得到的Screen ShadowMap的值却是在阴影中,那么就错误了。
然而因为光的种类很多,所以简单的理论实现起来需要考虑的情况还是比较多的。
#define unityShadowCoord4 float4 #define unityShadowCoord3 float3 #if defined (SHADOWS_SCREEN) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #if defined(UNITY_NO_SCREENSPACE_SHADOWS) UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) ); inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) { #if defined(SHADOWS_NATIVE) fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz); shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r); return shadow; #else unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy); // tegra is confused if we use _LightShadowData.x directly // with "ambiguous overloaded function reference max(mediump float, float)" unityShadowCoord lightShadowDataX = _LightShadowData.x; unityShadowCoord threshold = shadowCoord.z; return max(dist > threshold, lightShadowDataX); #endif } #else // UNITY_NO_SCREENSPACE_SHADOWS sampler2D _ShadowMapTexture; #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos); inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) { fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r; return shadow; } #endif #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) #endif // ---- Spot light shadows #if defined (SHADOWS_DEPTH) && defined (SPOT) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex)); #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ---- Point light shadows #if defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ---- Shadows off #if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) #define TRANSFER_SHADOW(a) #define SHADOW_ATTENUATION(a) 1.0 #endif
SHADOW_COORDS是用于声明VS向PS传递的值。
TRANSFER_SHADOW是用于计算纹理坐标,当使用ScreenShadowMap的时候,纹理坐标为屏幕空间的坐标;当使用ShadowMap或者光源为聚光灯的时候,纹理坐标为阴影空间(光源空间)的坐标;当点光源的时候,纹理坐标为与光源之间的距离。
SHADOW_ATTENUATION就是根据纹理坐标进行采样。
虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2017 年 8 月 5 日发表,原文链接(http://geekfaner.com/unity/blog3_ShadowMap.html)