Unity的阴影
阴影是如何产生的
在实时渲染中,我们最常用的是一种名为Shader Map的技术。这种技术理解起来非常简单 ,它首先会把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。unity就是用的这种技术。
在前向渲染中,如果场景中最重要的平行光开启了阴影,unity就会为该光源计算它的阴影映射纹理。这张阴影映射纹理本质上也是一张深度图,它记录了从光源的位置出发、能看到的场景中距离它最近的表面位置
那么在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按照正常的渲染流程,即调用base pass和Additional pass来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为实际上仅仅需要深度信息而已,而Base Pass和Additional Pass中往往涉及很多复杂的光照模型。因此Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode便签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或是深度纹理)。Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Shader中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影。当找到了一个LightMode为ShaderCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
在传统的阴影映射纹理的实践中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用XY分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由Z分量得到),那么说明改点位于阴影中,但在Unity 5中,Unity使用了不同于这种传统的阴影技术,即屏幕空间的阴影映射技术。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。注意:并不是所有的平台Unity都会使用这种技术,这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性。
当使用了屏幕空间的阴影映射技术时,Unity会通过调用LightMode为ShaderCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图,如果谁相机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要再Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此我们首先要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结:一个物体接收来自其它物体的阴影,以及它向其他物体投射阴影是两个过程。
如果我们想要你个物体接收来自其它物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间中的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。如果使用了屏幕空间的投影映射技术,Unity还会使用这个Pass产生一张摄像机的深度纹理。
不透明物体的阴影
1. 让物体投射阴影
在Unity中我们可以选择是否让一个物体投射或接收阴影,这是通过设置Mesh Renderer组件中的Cast Shadows 和 Receive Shadows属性来实现。
Caste Shadows可以设置为开启或关闭。如果开启了Cast Shadows属性,那么Unity就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程是通过执行物体的ShadowCaster的 Pass来实现的。Receive Shadows则可以选择是否让物体接收来自其他物体的阴影。如果没有开启Receive Shadows,那么当我们调用Unityd的内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。
LightMode为ShadowCaster的Pass实现。 这种自定义的Pass可以让我们更加灵活的控制阴影的产生。但由于这个Pass的功能通常是可以在多个UnityShader间通用的,因此直接Fallback是一个更加方便的用法。
Shader "OldDriver/GM_ShadowCaster"
{
SubShader
{
Pass
{
Tags { "LightMode"="ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#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;
}
fixed4 frag (v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
让物体接收阴影
首先在Base Pass包含有进一个新的内置文件 #include “AutoLight.cginc” 。 计算阴影时所用的宏都是在这个文件中声明的。
在顶点着色器的输出结构体v2f中添加了一个内置宏 SHADOW_COORDS:这个宏的作用就是声明一个用于对纹理采样的坐标。需要注意的是,这个宏的参数需要是下一个可用的插值寄存器的索引值,在下面的代码设置为4.
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 :TEXCOORD1;
float4 TtoW1 :TEXCOORD2;
float4 TtoW2 :TEXCOORD3;
SHADOW_COORDS(4)
};
在顶点着色器计算返回之前添加另一个内置宏 TRANSFER_SHADOW:这个宏用于在顶点着色器中计算上一步声明的阴影纹理坐标。
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
最后,我们在片元着色器中计算阴影值,这同样使用一个内置宏 SHADOW_ATTENUATION:
fixed shadow=SHADOW_ATTENUATION(i);
SHADOW_COORDS TRANSFER_SHADOW SHADOW_ATTENUATION是计算阴影的三个宏,在Autolight.cginc文件中可以看到声明:
// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)
#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
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#endif
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
上面的代码Unity为了处理不同光源类型,不同平台而定义了多个版本的宏。在前向渲染中,宏SHADOW_COORDS实际上就是声明一个名为_ShadowCoord的阴影纹理坐标变量。而TRANSFER_SHADOW会调用内置的ComputeScreenPos 函数来计算_ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW会把顶点坐标从模型空间变换到光源空间后储存到_ShadowCoord中。然后SHADOW_ATTENTION会把顶点坐标从模型空间变换到光源空间后储存到_ShadowCoord中。然后SHADOW_ATTENUATION负责使用_ShadowCoord对相关的纹理进行采样,得到阴影信息。
注意到,上面内置代码的最后定义了在关闭阴影时的处理代码。可以看出,当关闭了阴影后,SHADOW_COORDS和TRANSFER_SHADOW实际没有任何作用,而SHADOW_ATTENUATION会直接等同于数值1。
需要注意的是这些宏中会使用上下文变量进行相关计算,例如 TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标,因此为了让这些宏正确工作,需要自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证;a2f结构体中的顶点坐标变量名必须是vertex,顶点着色器的输入结构体a2v必须命名为v,且v2f中的顶点位置变量必须命名为pos。
在完成上面的操作后,我们只需要把阴影值shadow和漫反射以及高光反射颜色相乘即可。