一、前言

实时渲染中Unity使用的是Shadow Map技术,它会把摄像机位置与光源位置重合,那么场景中光源阴影区域就是摄像机看不见的地方。

前向渲染路径中,场景中如果开启了平行光阴影效果,那么Unity会为光源计算它的阴影映射纹理,这个纹理就是一个深度图,用来记录从光源位置出发,能看到的场景中距离它最近的表面位置(深度信息)。

Unity使用了一个一个额外的Pass来计算更新光源的阴影映射纹理,专门来进行渲染阴影映射纹理。
Pass : Tags{"LightMode" = "ShadowCaster"} 当开启了光源的阴影效果后,底层渲染引擎会首先找到这个Pass,找不到就在Fallback的Shader中继续寻找,如果找不到就不会启用阴影效果,如果找到,那么就会用这个Pass进行更新阴影映射纹理。

纹理更新过程:首先将摄像机放在光源位置,调用上面这个Pass,通过顶点变换后得到光源空间下的位置,据此输出深度信息到阴影映射纹理中。

传统阴影映射纹理实现过程(如何使用阴影映射纹理):在正常渲染的Pass中把顶点位置变换到光源空间中,得到顶点在光源空间中的三维位置信息,然后使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息,如果深度值小于该顶点的深度值(这点的深度大于阴影映射纹理中最近平面的),说明该点处于阴影中。

Unity5中采用屏幕空间的阴影映射技术(显卡需要MRT):他本来是作为延迟渲染中产生阴影的方法。

过程:首先调用ShadowCaster这个Pass,得到可投射阴影光源的阴影映射纹理以及摄像机的深度纹理,再根据这两个纹理得到屏幕空间的阴影图(如果一个表面的摄像机深度图的深度大于阴影映射纹理的深度,那就说明这个表面虽然可见,但是是在阴影中)。
这样阴影图中就会包含屏幕空间中所有有阴影的区域,当我们需要一个物体接受来自其他物体的阴影时,只需要在Shader中对阴影进行采样。

阴影图在屏幕空间,所以我们需要将表面坐标转换到屏幕空间中,再使用这个坐标对阴影图进行采样。

总之:

  1. 如果我们需要一个物体向其他物体进行阴影投射,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。这个过程通过执行ShadowCaster这个Pass实现。如果使用了屏幕空间的阴影映射技术,Unity还会使用这个Pass产生一个摄像机的深度纹理。
  2. 如果我们想要一个物体接受其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间阴影图)进行采样,把采样结果和光照结果相乘产生阴影效果。

二、不透明物体的阴影效果实现

Mesh Render组件中Cast Shadows开启物体投射,Receive Shadows接收阴影。
开启Cast Shadows后Unity就会把该物体加入光源的阴影映射纹理中,从而让其他物体在阴影映射纹理采样时得到该物体的相关信息。
Receive Shadows没有开启时,调用内置宏和变量计算阴影时,这些宏会判断物体有没有开启这个接收阴影投射功能,这样就不会在内部计算阴影。

我们使用之前没有 ShadowCaster这个Pass的Shader时,如果开启以上两个选项也会有阴影效果,那是因为Fallback中的”Specular“或者“Diffuse”内部的Fallback会调用VertexLit,其内部是由ShadowCaster这个Pass的。所以会实现阴影效果。

Pass
{
	Name "Shadow Caster"
	Tags {"LightMode"="ShadowCaster"}
	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#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
}

上面代码,宏和一些指令用处就是把深度信息写入渲染目标中。

内置平面会影响灯光判断,认为背面不存在,或许不会产生阴影,可以将Mesh Renderer中的Cast Shadows设置成Two Sided,这样物体的所有面都计算阴影信息,正面代替背面产生阴影效果。

三、让物体接收阴影

Shader "Unity Shaders Book/Chapter 9/Shadow"
{
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1) //漫反射颜色
		_Specular("Specular", Color) = (1,1,1,1) //高光反射颜色    
		_Gloss("Gloss", Range(8.0,256)) = 20 高光区域大小
	}
	SubShader
	{
		Pass    //BasePass  渲染环境光、平行光
		{
			Tags{ "LightMode" = "ForwardBase" } //开启前向渲染模式
			CGPROGRAM
			#pragma vertex vert  
			#pragma fragment frag  
			#pragma multi_compile_fwdbase //保证光照衰减等光照变量可以被正确赋值 
			#include "Lighting.cginc"   //引入Unity内置文件,才可以使用内置变量(如_LightColor0)
			#include "AutoLight.cginc" //计算阴影的宏都在这个文件中
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1; 
				SHADOW_COORDS(2) //添加内置宏,声明一个用于阴影纹理采样的坐标,参数是下一个可用的插值寄存器的索引值
			};
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNTIY_MATRIX_MVP,v.vertex);//转换顶点坐标到裁剪空间  
				o.worldNormal = UnityObjectToWorldNoraml(v.normal);//转换顶点坐标到世界空间  
				o.worldPos = mul(_Object2World,v.vertex).xyz;
				TRANSFER_SHADOW(o);//添加另一个内置宏,用于在顶点着色器中计算上一步中声明的阴影纹理坐标
				return o;
			}
	 
	
			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//获取环境光 ,只计算一次,下面的Pass不计算环境光
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));//计算漫反射光照    
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(viewDir + worldLightDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
				fixed atten = 1.0;//设定平行光衰减值为1
				fixed shadow = SHADOW_ATTENUATION(i);//使用内置宏计算阴影值
				return fixed4(ambient + (diffuse + specular) * atten * shadow,1.0);
			}
			ENDCG
		}
 
		Pass  //渲染其他环境光,点光源等。
		{
			Tags{ "LightMode" = "ForwardAdd" } //设置Addition Pass
			Blend One One//开启混合模式将此光照结果和之前帧缓存中的光照进行叠加
			CGPROGRAM
			#pragma vertex vert  
			#pragma fragment frag  
			#pragma multi_compile_fwdadd//保证我们在Pass中得到正确的光照变量
			#include "Lighting.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
			};
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNTIY_MATRIX_MVP,v.vertex);//转换顶点坐标到裁剪空间  
				o.worldNormal = UnityObjectToWorldNoraml(v.normal);//转换顶点坐标到世界空间  
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				return o;
			}
			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				#else
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
				#endif
				
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(viewDir + worldLightDir); //半兰伯特h
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
	
				#ifdef USING_DIRECTIONAL_lIGHT
					fixed atten = 1.0;
				#else
					float lightCoord = mul(_LightMatrix0,float4(i.worldPosition,1)).xyz;
					fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
					//宏UNITY_ATTEN_CHANNEL是为了得到衰减纹理中衰减值所在的分量。
					//下面这个线性衰减
					//float distance = length(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
					//fixed atten = 1.0 / distance;
				#endif
				
				return fixed4((diffuse + specular) * atten,1.0);
			}
			ENDCG
		}
	}
	Fallback"Specular"
}

上面一些阴影部分解释:
阴影计算的三剑客:

SHADOW_COORDS:它是声名的一个名为_ShadowCoord的阴影纹理坐标变量。

TRANSER_SHADOW():它是用来计算上面这个阴影纹理坐标的。它的实现会根据平台不同而不同,如果使用了屏幕空间的阴影映射技术,它会调用ComputeScreenPos函数来计算_ShadowCoord,没有使用的话,直接进行传统的阴影映射技术。他也会将顶点坐标从模型空间转换到光源空间并且存储到_ShadowCoord中。

SHADOW_ATTENUATION:使用_ShadowCoord对相关纹理采样,得到阴影信息。

以上几个函数会使用之前自定义的变量名,注意不要更改:a2v中顶点坐标变量vertex,顶点着色器输入结构体a2v需要命名为v,v2f中顶点位置变量需要命名为pos。

用帧调试器查看上面的渲染分为4步:首先更新摄像机的深度纹理,再渲染得到平行光阴影映射纹理过程,再根据前两步结果计算得到屏幕空间阴影图,最后如果物体使用的Shader包含了对这张阴影图的采样就会得到阴影效果。

四、统一管理光照衰减和阴影:

//顶点着色器和前面与上面代码相同,也要使用阴影3剑客前两个。
 //片元着色器 直接用下面函数计算衰减+阴影
 
 UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);//i是v2f、传入片元着色器的那个。atten在内部由函数定义,后面可直接使用。
 return fixed4(ambient+(diffuse+specular)*atten,1.0);

使用这个函数我们的Pass:Base和Add可以统一,不需要在Base中处理阴影,也不需要在Add中处理衰减。

如果我们要添加Additional Pass中的阴影效果,那我们就需要使用之前所说的:

#pragma mullti_compile_fwdadd_fullshadows

来进行计算额外的逐像素光源的阴影。

五、透明物体的阴影

1.透明度测试的阴影效果

对于正常物体来说,使用VertexLit设为Fallback就可以得到正确阴影。

但对于透明物体就不一样,他们使用的透明度测试和透明度混合,所以需要小心处理他们的Fallback。

需要注意:我们需要将我们进行透明度测试的模型的MeshRender中Cast Shadows属性进行设置Two Sided。这是为了让Unity计算双面的深度信息,否则阴影的透明效果会只考虑距离摄像头近的一侧。

Shader “Unity Shaders Book/Chapter 8/Alpha Test”
{
	Properties
	{
		_Color("Main Tint",Color)=(1,1,1,1)
		_MainTex("Main Tex",2D)="white"{}
		_Cutooff("Alpha Cutoff",Range(0,1))=0.5	//设置透明度测试使用的判断条件
	}
	Subshader
	{
		Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"} 
		//这里透明度测试使用的渲染队列为AlphaTest,
		//设置忽视投影器的影响
		//RenderType是可以将Shader提前归入定义的组TransparentCutout,
		//指明这是一个使用了透明度测试的Shader,RenderType通常被用来用于着色器替换功能。
		Pass
		{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _Cutoff; //用于判断比较的条件
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			struct v2f
			{
				float4 pos : SV:POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv :TEXCOORD2;
				SHADOW_COORDS(3)
				//(3)是因为已经占用了3个寄存器,阴影纹理坐标将占用第四个寄存器TEXCOORD3。
			}
			vef vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNTIY_MATRIX_MVP,v.vertex);
				o.worldNormal = UnityObjectToWorldNoraml(v.normal);
				o.worldPos = mul(_Object2World,v.vertex).xyz;
				o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); //计算平铺偏移后的纹理坐标
				TRANSFER_SHADOW(o);
				renturn o;
			}
			fixed4 frag(v2f i):SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed4 texColor = tex2D(_Maintex,i.uv);
				clip(texColor.a-_Cutoff); //这里直接使用上面的采样后的纹理a分量进行对比_Cutoff。
				fixed3 albedo = texColor.rgb*_Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
				fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLightDir));
				UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
				return fixed4(ambient+diffuse*atten,1.0);
			}
		}
	}
	Fallback "Transparent/Cutout/VertexLit"
	//这个回调Shader不仅可以充当显卡不能使用时的替换Shader,
	//也可以保证使用透明度测试的物体可以正确的向其他物体投射阴影。
	//它可以使透明度测试的物体得到正确的阴影效果,而直接Fallback “VertexLit”是不能的。
}

2.透明度混合的阴影

所有内置的透明度混合的UnityShader(例如Transparent/VertexLit)都没有提供阴影投射的Pass,意味着这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,他们不会产生阴影,也不会向其他物体投射阴影。

因为处理半透明物体需要关闭深度写入,这同时会带来问题影响阴影的产生。

大多数Unity中的半透明物体都是不接收阴影也不投射阴影的,
如果非要正确产生阴影,必须严格按照从后往前的顺序进行渲染,处理过程非常复杂,很影响性能。

我们也可以将其当成不透明物体强制产生阴影,把他们的Fallback设置成“VertexLit”或者“Diffuse”这些,还要在物体MeshRender上Cast Shadows和Receive Shadows进行控制是否需要需要向其他物体投射阴影和接受阴影。
这也会避免不了有一些小的现象问题。。