US3S6L4——让物体接收阴影

本章代码关键字

1
2
3
SHADOW_COORDS            // 阴影坐标宏,主要用于存储阴影纹理坐标
TRANSFER_SHADOW // 转移阴影宏,计算阴影映射纹理坐标,它会在内部去进行计算,并存储到v2f结构体的SHADOW_COORDS(2)内部
SHADOW_ATTENUATION // 阴影衰减宏,该宏会在内部利用v2f中的阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样,计算阴影衰减值

让物体接收阴影的思路

目前我们已经能够让物体投射阴影了,所谓的投射阴影,
其实就是让物体参与到光源的阴影映射纹理计算中,最终才能影响其他物体在接收阴影时的采样结果

由此可见让物体接收阴影的主要思路,其实就是要从阴影隐射纹理中进行采样,然后将采样结果用于最终的颜色计算中

总体的流程就是:

  1. 在顶点着色器中进行顶点坐标转换(将顶点坐标 转换为 阴影映射纹理坐标)
  2. 在片元着色器中使用阴影映射纹理坐标在阴影映射纹理中进行采样,通过得到的深度值判断片元(像素)是否在阴影中,以计算出阴影衰减值
  3. 将采样结果参与到最终的颜色计算中

实现物体接收阴影效果

  1. 创建一个新的 Shader,复用 让物体投射阴影的Shader 到新 Shader 中,在此基础上实现接受阴影的效果

    这里使用 FallBack "Specular"​ 来实现投射阴影的效果

  2. 接受阴影的三剑客(三个宏)

    这里只修改 Bass Pass 中的代码,来感受下接受阴影的流程
    首先我们需要在 Bass Pass 当中引用包含内置文件 #include "AutoLight.cginc"
    该内置文件中,有用于计算阴影时需要使用的三剑客

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
    #pragma multi_compile_fwdbase

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc" //引用包含计算阴影会使用的宏的cginc文件
    // ...
    ENDCG
    1. SHADOW_COORDS​(阴影坐标宏)

      该宏在 v2f​ 结构体(顶点着色器返回值)中使用,本质上就是声明了一个用于对阴影纹理进行采样的坐标
      在内部实际上就是声明了一个名为 _ShadowCoord​ 的阴影纹理坐标变量,需要注意的是:
      在使用时 SHADOW_COORDS(2)​ 传入参数 2​,表示需要时下一个可用的插值寄存器的索引值

      使用此宏时,句末不需要加 ;​ 分号

      1
      2
      3
      4
      5
      6
      7
      struct v2f
      {
      float4 pos: SV_POSITION; //裁剪空间下的顶点坐标
      float3 wNormal: NORMAL; //世界空间下的法线
      float3 wPos: TEXCOORD0; //世界空间下的顶点坐标
      SHADOW_COORDS(2) //阴影坐标宏,主要用于存储阴影纹理坐标
      };
    2. TRANSFER_SHADOW​(转移阴影宏)

      该宏在顶点着色器函数中调用,传入对应的 v2f​ 结构体对象,该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
      最终的目的就是将顶点进行坐标转换并存储到 _ShadowCoord​ 阴影纹理坐标变量中

      使用此宏时,句末不需要加 ;​ 分号

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // appdata_base 包含的顶点成员必须命名为 vertex
      // v2f 包含的顶点位置成员必须命名为 pos
      v2f vert (appdata_base v)
      {
      v2f v2fData;
      v2fData.pos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
      v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
      v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
      TRANSFER_SHADOW(v2fData) //计算阴影映射纹理坐标,它会在内部去进行计算,并存储到v2f结构体的SHADOW_COORDS(2)内部

      return v2fData;
      }

      需要注意的是:

      1. 该宏会在内部使用顶点着色器中传入的结构体,该结构体中顶点的命名必须是 vertex

        上面的示例代码中参数 appdata_base​ 内的顶点就必须命名为 vertex

      2. 该宏会在内部使用顶点着色器的返回结构体,其中的顶点位置命名必须是 pos

        上面的示例代码中 v2f​ 结构体内的顶点位置就必须命名为 pos

    3. SHADOW_ATTENUATION​(阴影衰减宏)

      该宏在片元着色器中调用,传入对应的 v2f​ 结构体对象,
      该宏会在内部利用 v2f​ 中的 阴影纹理坐标变量(ShadowCoord​)对相关纹理进行采样
      将采样得到的深度值进行比较,以计算出一个 fixed3​ 的阴影衰减值,
      我们只需要使用它返回的结果和(漫反射+高光反射)的结果相乘即可

      使用此宏时,句末必须加 ; 分号

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      fixed4 frag (v2f i) : SV_Target
      {
      //计算Blinn-Phong式光照模型需要的各种颜色
      fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
      fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
      fixed3 shadow = SHADOW_ATTENUATION(i); //得到阴影衰减值
      fixed atten = 1; //衰减值,由于Bass Pass只处理平行光,因此衰减值默认为1
      //计算Blinn-Phong式光照模型颜色,衰减值 需要乘以 漫反射颜色 和 高光反射颜色 的和,还要再乘以阴影衰减值
      fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten * shadow;

      return fixed4(blinnPhongColor.rgb, 1); //因为传递过来的颜色变量不包括透明度,因此这里需要手动指定透明度
      }
  3. 注意

    目前处理的方式只是大致了解接受阴影的流程,我们还没有对 Additional Pass 附加渲染通道进行处理
    后续再统一处理光照衰减和阴影,得到最终效果

显示效果(左为使用 Unity 默认 Shader,中为使用未实现接收阴影的 Shader,右为使用实现接收阴影的 Shader):

image

可见,按照按照上述步骤实现了接收阴影逻辑后,物体就可以受到阴影的影响

完整 Shader 代码如下:

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
Shader "TeachShader/Lesson67_ForwardLighting"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1) //材质的漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) //材质高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 0.5 //光泽度
}
SubShader
{
// Bass Pass 基础渲染通道
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc" //引用包含计算阴影会使用的宏的cginc文件

struct v2f
{
float4 pos: SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal: NORMAL; //世界空间下的法线
float3 wPos: TEXCOORD0; //世界空间下的顶点坐标
SHADOW_COORDS(2) //阴影坐标宏,主要用于存储阴影纹理坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

//计算兰伯特光照模型 颜色相关函数(逐片元)
//参数:
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragLambertColor(in float3 wNormal)
{
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //将光源0的位置标准化,得到方向,用于计算夹角
//兰伯特光照模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
return color;
}

//计算Blinn-Phong高光反射光照模型 颜色相关函数(逐片元)
//参数:
// wPos: 世界空间下顶点坐标
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragSpecularColor(in float3 wPos, in float3 wNormal)
{
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos); //计算观察方向
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //标准化光源方向
float3 halfA = normalize(viewDir + lightDir); //将光源方向和观察方向相加得到其半角向量,并标准化
//Blinn-Phong高光反射模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
return color;
}

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.pos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
TRANSFER_SHADOW(v2fData) //计算阴影映射纹理坐标,它会在内部去进行计算,并存储结构体的SHADOW_COORDS(2)内部

return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
//计算Blinn-Phong式光照模型需要的各种颜色
fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
fixed3 shadow = SHADOW_ATTENUATION(i); //得到阴影衰减值
fixed atten = 1; //衰减值,由于Bass Pass只处理平行光,因此衰减值默认为1
//计算Blinn-Phong式光照模型颜色,衰减值 需要乘以 漫反射颜色 和 高光反射颜色 的和,还要再乘以阴影衰减值
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten * shadow;

return fixed4(blinnPhongColor.rgb, 1); //因为传递过来的颜色变量不包括透明度,因此这里需要手动指定透明度
}
ENDCG
}

// Additional Pass 附加渲染通道
Pass
{
Tags { "LightMode" = "ForwardAdd" }
// 使用线性减淡效果进行光照混合
Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
#pragma multi_compile_fwdadd

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

struct v2f
{
float4 svPos : SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal : NORMAL; //世界空间下的法线
float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
// 漫反射颜色的计算
fixed3 worldNormal = normalize(i.wNormal);
// 光的方向
#if defined(_DIRECTIONAL_LIGHT)
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光 光的方向就是它的位置
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos); // 点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
#endif
fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

// 高光反射颜色的计算
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos.xyz); // 视角方向
fixed3 halfDir = normalize(worldLightDir + viewDir); // 半角向量
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

// 衰减值的计算
#ifdef USING_DIRECTIONAL_LIGHT //如果光源是平行光,Unity就会定义USING_DIRECTIONAL_LIGHT这个宏,因此会进入这段逻辑
// 平行光逻辑
fixed atten = 1;
#else
// 如果未定义USING_DIRECTIONAL_LIGHT,则说明此光源非平行光
#if defined(POINT)
// 点光源逻辑
float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
// 聚光灯逻辑
float4 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1));
fixed atten = (lightCoord.z > 0) *
tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w *
tex2D(_LightTextureB0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#else
// 其他逻辑
fixed atten = 1;
#endif
#endif

// 附加渲染通道内不需要再加上环境光颜色了,因为它只需要计算一次,而之前已经在基础渲染通道中计算了
fixed3 blinnPhongColor = (diffuse + specular) * atten;
return fixed4(blinnPhongColor.rgb, 1);
}
ENDCG
}
}

Fallback "Specular"
}