1.概述
在Unity中创建一个默认物体会带有一个默认的材质球和Shader,实现了一个PBR材质,里面已经实现了阴影,反射等功能。而我们不用自带的材质来实现阴影,但是实现原理还是Unity中的Shadow Map 技术。
在前向渣染路径中,如果场景中最重要的平行光开启了阴影, Unity 就会为该光源计算它的阴影映射纹理( shadow map)。 这张阴影映射纹理本质上也是一张深度图, 它记录了从该光源的位置出发、 能看到的场景中距离它最近的表面的深度信息。
Unity 选择使用一个额外的 Pass 来专门更新光源的阴影映射纹理, 这个 Pass 就是 LightMode 标签被设置为 ShadowCaster 的 Pass。
2.准备
创建一个shader文件,并且命名为Shadow,写上最基础的代码。代码如下:
Shader "MyShader/Shadow" {
Properties{
_MainTex("Main Texture", 2D) = "white" {}
}
SubShader{
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 frag(v2f i) : COLOR
{
float4 clr = tex2D(_MainTex, i.uv);
return clr;
}
ENDCG
}
}
}
然后创建一个材质球名为cube,并且关联上Shadow的shader文件。然后在Unity场景中创建一个Plane用来接受阴影,并且Receive Shadows要勾选。然后创建一个cube,Cast Shadows要开启,将cube材质球替换cube默认的材质球,如图所示:
此时这个cube上没有任何效果,没有阴影。
3.添加ShadowCaster
前面中我们说过,Unity会选择一个额外的Pass来更新阴影映射纹理,而这个 Pass 就是 LightMode 标签被设置为 ShadowCaster 的 Pass。那么我们就添加如下代码:
Pass
{
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(o)
return o;
}
float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
保存之后,我们回到Unity场景中看到了cube产生了阴影。如下图所示:
但是,我们发现这个阴影只有在Plane上产生,在cube本身上并没有接受阴影。
4.阴影三剑客
为了使自身能够接受阴影,使用阴影三剑客来实现,使用内置宏来计算阴影,需要注意的是三个内置宏都是在AutoLight.cginc文件中。在Base Pass上计算阴影。
- SHADOW_COORDS:声明了一个名字_ShadowCoord的阴影纹理坐标变量。
- TRANSFER_SHADOW:会根据平台选择阴影生成技术后,会把顶点从模型空间转到光源空间后存到_ShadowCoord中。
- SHADOW_ATTENUATION:把上一个宏存到_ShadowCoord的内容进行采样,并返回阴影衰减。
(1)包含新的内置文件:#include "AutoLight.cginc"。
(2)在顶点着色器的输出结构体 v2f 中添加内置宏 SHADOW_COORDS
。声明一个对阴影纹理坐标的采样。
(3)定点着色器返回之前添加另一个内置宏 TRANSFER_SHADOW
:
(4)片元着色器中依然使用内置宏来处理,采用内置宏 SHADOW_ATTENUTATION
:
fixed shadow = SHADOW_ATTENUATION(i);
(5)最后在返回值的时候,把变量 shadow与颜色相乘。
注意:要注意的是,这些宏会使用上下文变量进行相关计算,所以需要使自定义的变量名与这些宏中使用的变量名一致,如a2f中顶点坐标变量必须交vertex,vert输出必须为v2f且命名为v,顶点位置变量需要叫pos。
在添加了上述代码后,代码如下:
Shader "MyShader/Shadow" {
Properties{
_MainTex("Main Texture", 2D) = "white" {}
}
SubShader{
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
//对阴影纹理采样的坐标生成并存在_ShadowCoord变量中,参数是下一个可以寄存器的索引
SHADOW_COORDS(2)
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//自动判断平台,如果不支持阴影映射则使用传统的映射,把值存到_ShadowCoord中
TRANSFER_SHADOW(o)
return o;
}
float4 frag(v2f i) : COLOR
{
float4 clr = tex2D(_MainTex, i.uv);
//添加环境光
float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.rgb;
//阴影衰减
fixed shadow = SHADOW_ATTENUATION(i);
return float4(shadow * clr.xyz+ ambientLighting, 1);
}
ENDCG
}
Pass
{
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(o)
return o;
}
float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
为了更好的看效果,我复制了一个cube,将复制的cube放到cube斜上方,以便能够遮挡光线,在cube上生成阴影。当然cube自身在光源背面也会生成阴影。效果如下:
可以看到在cube上自身能够接受阴影了。
5.总结
- 一个物体向其他物体投射阴影
1.在 Shader 中添加标签为 ShadowCaster 的 Pass 。
2.打开Cast Shadows属性。
- 一个物体接收来自其他物体的阴影:
1.在 Shader 中对阴影映射纹理( 包括屏幕空间的阴影图) 进行采样, 把采样结果和最后的光照结果相乘来产生阴影效果。
2.打开Receive Shadows属性