实时阴影ShadowMap

实时阴影的计算:

ShadowMap:传统实时阴影

我们假设场景中只有一个主光源平行光,我们假设在这个光源架设一个摄像机,顺着光的方向,这时会采集到一张“光源相机”的深度图,这个图就是ShadowMap。场景中的普通摄像机观察到的每一个片元,都会计算它在“光源相机”中的深度(Unity会提供一个unity_WorldToShadow的矩阵),如果当前片元的深度比ShadowMap的深度要小,那么说明当前片元能被光照亮。

打开Project Settings——Quality,从最下面的Shadows中将Shadow Distance改为15,Shadow Cascades关闭。然后进入Project Settings——Graphic,将High(Tier 3)的Use Defaults取消勾选,再将里面的Cascaded Shadows取消勾选。

然后我们使用下面阴影内置函数一,就能实现最基础的实时阴影。

此时打开Frame Debugger,选择到Shadows.RenderShadowMap展开看到里面的Shadows.RenderJob,就能观察到当前场景每个物体的Shadow Map了。此时如果我们修改Project Settings——Quality里面的Shadow Distance,会法线这个参数其实控制的就是当前物体到“光源相机”的距离,对应的Shadow Map也会被缩放。

ShadowMap

在Frame Debugger中可以看到,每一个模型的ShadowMap其实都是在SHADOWCASTER这个Pass中计算的,如果我们需要自定义阴影(自定义半透明阴影),也需要在这个Pass中计算。

Screen Space ShadowMap

Screen Space ShadowMap在传统实时阴影的基础上,模仿联级阴影做了一些改进。

想要实现屏幕空间ShadowMap,先按照上面传统实时阴影设置场景,然后再进入Project Settings——Graphic,将High(Tier 3)的Use Defaults勾选上,此时我们打开Frame Debugger,会发现多了UpdateDepthTexture这一步,这一步就会先渲染屏幕空间的深度图,接下来依然会在Shadows.RenderShadowMap里面渲染各个模型的Shadow Map,只不过接下来,有RenderForwardOpaque.CollectShadow这一步,它将屏幕空间的深度图和ShadowMap结合了起来,经过这一步,我们能将屏幕空间的全阴影计算完毕,最后渲染时只需要读取这一张结合后的图即可。

深一些来讲就是,通过屏幕空间深度图,我们不仅能展现每个片元的深度,还能还原每个片元的世界坐标,这个坐标通过unity_WorldToShadow矩阵,得到在“光源相机”的深度,从而得到屏幕空间的全阴影。

联级阴影(CSM)Cascaded Shadow Mapping

CSM在传统实时阴影的基础上,根据视锥体生成多张ShadowMap,优化性能。

按照上面的Screen Space ShadowMap设置场景,然后再在Project Settings——Quality中将Shadow Cascades设为Four Cascades

打开Frame Debugger,在Shadows.RenderShadowMap中我们可以发现每个模型的ShadowMap有四种

四种ShadowMap

我们可以在Scene窗口中选择Shade Mode为Shadow Cascade,能够看到在视锥体内分成了四块区域(距离模型过远时可能看不见四块区域),根据距离的远近区分了ShadeMap,所以会出现四种ShadowMap,这样能够在屏幕空间ShadowMap的基础上进一步节省性能。

阴影相关内置函数

函数 说明
SHADOW_COORDS(3)
TRANSFER_SHADOW(o)
SHADOW_ATTENUATION(i);
只计算实时阴影
LIGHTING_COORDS(3,4)
TRANSFER_VERTEX_TO_FRAGMENT(o);
LIGHT_ATTENUATION(i)
处理投影、而且帮你判断灯光类型、从而算出光照的衰减范围、cookies等
UNITY_SHADOW_COORDS(2)
UNITY_TRANSFER_SHADOW(o, v.uv1);
UNITY_LIGHT_ATTENUATION(atten,i,s.posWorld);
计算投影、处理实时投影和静态投影的混合、光照范围和衰减、cookies等
ShadowCaster 这是一个Pass,想要使用ShadowMap渲染阴影必须要这个Pass,不写的话可以直接使用Fallback "Diffuse",会自动补全。

函数一的使用

SHADOW_COORDS(3)放在v2f构造体内,括号内的是空闲的Texcoord编号。注意没有分号

TRANSFER_SHADOW(o)放在vert方法体内。注意没有分号

SHADOW_ATTENUATION(i);放在frag方法体内。

在最后加上Fallback "Diffuse",注意和SubShader同级。

如果想要应用阴影,我们需要将阴影和diffuse颜色进行混合,这里的混合是让阴影和NdotL的结果取最小值。

1
2
half diff_term = min(shadow, max(0.0, dot(normal_dir, light_dir)));
half3 diffuse_color = diff_term * _LightColor0.xyz * base_color.xyz;//_LightColor0表示主光源的颜色

同时,我们也需要让高光也乘diff_term,防止出现在暗部不受光的地方会出现高光的错误。

1
half3 spec_color = pow(max(0.0, NdotH),_Shininess) * diff_term * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;

ForwardBase Pass

经过一番更改后,我们目前的ForwardBase Pass如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
Pass
{
Tags {"LightMode" = "ForwardBase"} //在Pass内再添加一个Tag,说明这个是ForwardBasePass
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_dir : TEXCOORD1;//世界空间法线
float3 tangent_dir : TEXCOORD3;//世界空间切线
float3 binormal_dir : TEXCOORD4;//世界空间副法线
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
SHADOW_COORDS(5)
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _AOMap;
sampler2D _SpecMask;//高光遮罩贴图
sampler2D _NormalMap;
sampler2D _ParallaxMap;//视差偏移贴图
float _Parallax;

float4 _LightColor0;//定义后才能获取到主光源的颜色
float _Shininess;//控制高光的范围
float _SpecIntensity;//控制高光的强度
float _NormalIntensity;

float3 ACESFilm(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x*(a*x + b)) / (x*(c*x + d) + e));
}

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;//使用tangent.w来解决不同平台次法线的翻转问题
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o)
return o;
}

half4 frag (v2f i) : SV_Target
{
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
//计算TBN切线空间矩阵
half3 normal_dir = normalize(i.normal_dir);
half3 tangent_dir = normalize(i.tangent_dir);
half3 binormal_dir = normalize(i.binormal_dir);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
//应用视差贴图
half3 view_tangentspace = normalize(mul(TBN, view_dir));

half2 uv_parallax = i.uv;
for(int j = 0; j < 10; j++)
{
half height = tex2D(_ParallaxMap, uv_parallax);
uv_parallax = uv_parallax - (0.7 - height) * (view_tangentspace.xy / view_tangentspace.z + 0.42) * _Parallax * 0.01;
}

half4 base_color = tex2D(_MainTex, uv_parallax);
//将基础色从gamma空间转换到线性空间,为了在ACES色调映射中得到更好的效果
base_color = pow(base_color,2.2);
half4 ao_color = tex2D(_AOMap, uv_parallax);
half4 spec_mask = tex2D(_SpecMask, uv_parallax);
half4 normal_map = tex2D(_NormalMap, uv_parallax);
half3 normal_data = UnpackNormal(normal_map);//xyz分别记载了世界空间下当前像素切线、副法线、法线的偏移量
normal_data.xy = normal_data.xy * _NormalIntensity;

//应用法线贴图,注意要右乘TBN矩阵
normal_dir = normalize(mul(normal_data, TBN));
//将从法线贴图中获取到的normal_data应用到normal_dir中,下面是一个方便理解的算式
//normal_dir = normalize(tangent_dir * normal_data.x * _NormalIntensity + binormal_dir * normal_data.y * _NormalIntensity + normal_dir * normal_data.z);
//获取阴影
half shadow = SHADOW_ATTENUATION(i);

//获取世界主光源的方向,这里正好是指向光源的
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
half diff_term = min(shadow, max(0.0, dot(normal_dir, light_dir)));
half3 diffuse_color = diff_term * _LightColor0.xyz * base_color.xyz;//_LightColor0表示主光源的颜色

//使用内部提供的reflect函数求反射光的方向,用于计算高光,记住这里使用负的light_dir
//half3 reflect_dir = reflect(-light_dir, normal_dir);
//half RDotV = dot(view_dir, reflect_dir);

//求Blin-Phong的半角向量
half3 half_dir = normalize(light_dir + view_dir);
half NdotH = dot(normal_dir, half_dir);

half3 spec_color = pow(max(0.0, NdotH),_Shininess) * diff_term * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;

//使用Unity提供的参数获取环境光
half3 ambient_color = UNITY_LIGHTMODEL_AMBIENT.rgb * base_color.rgb;
half3 final_color = (diffuse_color + spec_color + ambient_color) * ao_color;//AO贴图和最终结果乘算
half3 tone_color = ACESFilm(final_color);
//将计算结果转换到gamma空间
tone_color = pow(tone_color,1.0/2.2);
return half4(tone_color, 1.0);
}
ENDCG
}

ForwardAdd Pass

ForwardAdd Pass和ForwardBase Pass相比,修改了几个地方,并且使用了上面表格中第二套计算阴影的方式。同时,使用Lerp函数来判断_WorldSpaceLightPos0.w分量,从而区分平行光和点光,并且由于第二套阴影计算已经包含了灯光衰减,这里区分平行光和点光仅仅是为了计算漫反射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Tags {"LightMode" = "ForwardAdd"}
Blend One One
#pragma multi_compile_fwdadd

//...
struct v2f
{
//...
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
LIGHTING_COORDS(5, 6)
};

//...

v2f vert (appdata v)
{
v2f o;
//...
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
half4 frag (v2f i) : SV_Target
{

half atten = LIGHT_ATTENUATION(i);//这里已经包含了灯光衰减和阴影信息

half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
//...
//获取世界主光源的方向,这里正好是指向光源的
half3 light_dir_point = normalize(_WorldSpaceLightPos0.xyz - i.pos_world);
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
light_dir = lerp(light_dir, light_dir_point, _WorldSpaceLightPos0.w);//_WorldSpaceLightPos0.w分量是1时,代表平行光,否则为0

half diff_term = min(atten, max(0.0, dot(normal_dir, light_dir)));
//...
half3 final_color = (diffuse_color + spec_color) * ao_color;//去掉环境光和ACES色调映射以及线性空间的转换
return half4(final_color, 1.0);
}

完整的ForwardAdd Pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
Pass
{
Tags {"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "AutoLight.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_dir : TEXCOORD1;//世界空间法线
float3 tangent_dir : TEXCOORD3;//世界空间切线
float3 binormal_dir : TEXCOORD4;//世界空间副法线
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
LIGHTING_COORDS(5, 6)
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _AOMap;
sampler2D _SpecMask;//高光遮罩贴图
sampler2D _NormalMap;
sampler2D _ParallaxMap;//视差偏移贴图
float _Parallax;

float4 _LightColor0;//定义后才能获取到主光源的颜色
float _Shininess;//控制高光的范围
float _SpecIntensity;//控制高光的强度
float _NormalIntensity;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;//使用tangent.w来解决不同平台次法线的翻转问题
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}

half4 frag (v2f i) : SV_Target
{

half atten = LIGHT_ATTENUATION(i);

half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
//计算TBN切线空间矩阵
half3 normal_dir = normalize(i.normal_dir);
half3 tangent_dir = normalize(i.tangent_dir);
half3 binormal_dir = normalize(i.binormal_dir);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
//应用视差贴图
half3 view_tangentspace = normalize(mul(TBN, view_dir));

half2 uv_parallax = i.uv;
for(int j = 0; j < 10; j++)
{
half height = tex2D(_ParallaxMap, uv_parallax);
uv_parallax = uv_parallax - (0.7 - height) * (view_tangentspace.xy / view_tangentspace.z + 0.42) * _Parallax * 0.01;
}

half4 base_color = tex2D(_MainTex, uv_parallax);

half4 ao_color = tex2D(_AOMap, uv_parallax);
half4 spec_mask = tex2D(_SpecMask, uv_parallax);
half4 normal_map = tex2D(_NormalMap, uv_parallax);
half3 normal_data = UnpackNormal(normal_map);//xyz分别记载了世界空间下当前像素切线、副法线、法线的偏移量
normal_data.xy = normal_data.xy * _NormalIntensity;

//应用法线贴图,注意要右乘TBN矩阵
normal_dir = normalize(mul(normal_data, TBN));

//获取世界主光源的方向,这里正好是指向光源的
half3 light_dir_point = normalize(_WorldSpaceLightPos0.xyz - i.pos_world);
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
light_dir = lerp(light_dir, light_dir_point, _WorldSpaceLightPos0.w);//_WorldSpaceLightPos0.w分量是1时,代表平行光,否则为0

half diff_term = min(atten, max(0.0, dot(normal_dir, light_dir)));
half3 diffuse_color = diff_term * _LightColor0.xyz * base_color.xyz;//_LightColor0表示主光源的颜色

half3 half_dir = normalize(light_dir + view_dir);
half NdotH = dot(normal_dir, half_dir);

half3 spec_color = pow(max(0.0, NdotH),_Shininess) * diff_term * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;

half3 final_color = (diffuse_color + spec_color) * ao_color;//AO贴图和最终结果乘算
return half4(final_color, 1.0);
}
ENDCG
}