US3S6L6——光照衰减和阴影

知识回顾

  1. 多种光源的效果实现

    在 Additional Pass(附加渲染通道)中根据光源类型,计算出不同的光源衰减值,参与到最终的颜色计算中

  2. 计算阴影时需要使用的三剑客

    1. SHADOW_COORDS​(阴影坐标宏)

      v2f​ 结构体中声明,本质是声明了一个表示阴影映射纹理坐标的变量

    2. TRANSFER_SHADOW​(转移阴影宏)

      在顶点着色器中调用,本质是将顶点进行坐标转换并存储到 _ShadowCoord​ 阴影纹理坐标变量中

    3. SHADOW_ATTENUATION​(阴影衰减宏)

      本质是利用 _ShadowCoord​ 对阴影映射纹理进行采用,将采样得到的深度值进行比较,以计算出一个 fixed3​ 的阴影衰减值

最终之所以能够接受阴影,主要就是因为利用了最终得到的阴影衰减值参与到了最终的颜色计算中

本章代码关键字

1
UNITY_LIGHT_ATTENUATION        // Unity内计算光源和阴影的综合衰减值的宏

光照衰减和阴影

通过之前的学习,我们发现光照衰减和接受阴影相关的计算是类似的
关键点都是通过计算出一个衰减值,参与到颜色计算中,都是用 (漫反射+高光反射) 的结果乘以对应的衰减值
由于它们对最终颜色影响的计算非常类似,都是通过乘法进行运算

因此 Unity​ 中专门提供了对应的宏,来综合处理光照衰减和阴影衰减的计算
AutoLight.cginc​ 内置文件中的 UNITY_LIGHT_ATTENUATION​(Unity 光照衰减宏)
该宏中会统一的对 光照衰减进行计算,并且也会计算出 阴影衰减值,
最后将两者相乘得到综合衰减值,我们只需要利用该宏来处理 光照和阴影的衰减即可
我们可以在 Unity 安装目录的 Editor/Data/CGIncludes​ 中找到该内置文件,查看该宏的逻辑

image

下列代码是光源为点光源情况下 UNITY_LIGHT_ATTENUATION​ 的定义:

1
2
3
4
5
6
7
8
#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
#define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif

可见,UNITY_LIGHT_ATTENUATION​ 需要三个参数:

  • destName​:衰减值变量名称
  • input​:结构体
  • worldPos​:世界坐标系下的坐标

分析 UNITY_LIGHT_ATTENUATION​ 的定义的逻辑,不难看出它进行了三步计算:

  1. 通过 unity_WorldToLight​ 进行矩阵乘法将世界坐标系坐标转换为光源坐标系下的坐标
  2. 使用 UNITY_SHADOW_ATTENUATION​ 来计算阴影衰减值
  3. 使用光源坐标从 _LightTexture0​ 采样,获取

下列代码是光源为聚光灯和平行光情况下 UNITY_LIGHT_ATTENUATION​ 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 聚光灯
#ifdef SPOT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
{
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
{
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
}
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
#else
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
#endif
#define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
#endif
1
2
3
4
// 平行光
#ifdef DIRECTIONAL
#define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif

以上宏定义的的主要区别是出现在平行光,聚光灯,点光源的光源衰减值计算逻辑上,其他的步骤是差不多的

因此,我们完全可以直接使用 UNITY_LIGHT_ATTENUATION​ 来计算综合处理光照和阴影的衰减逻辑

光照衰减和阴影的综合实现

我们将利用 AutoLight.cginc​ 内置文件中的 UNITY_LIGHT_ATTENUATION​(Unity光照衰减宏)来综合处理光照衰减和阴影相关的逻辑

  1. 创建一个新的 Shader 并复用上节课中的代码

  2. 将 SHADOW_COORDS​ 和 TRANSFER_SHADOW​,在 Additional Pass 附加渲染通道中也添加上

    注意:需要在附加渲染通道中包含内置文件 AutoLight.cginc​(因为阴影计算相关宏来自于它,UNITY_LIGHT_ATTENUATION​ 光照衰减宏也来自于它)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct v2f
    {
    float4 pos: SV_POSITION; //裁剪空间下的顶点坐标
    float3 wNormal: NORMAL; //世界空间下的法线
    float3 wPos: TEXCOORD0; //世界空间下的顶点坐标
    SHADOW_COORDS(2) //阴影坐标宏,主要用于存储阴影纹理坐标
    };

    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;
    }
  3. 为了让 Additional Pass 附加渲染通道能够添加阴影效果,需要将编译指令进行修改

    将原本的 #pragma multi_compile_fwdadd​,修改为 #pragma multi_compile_fwdadd_fullshadows
    这样 Unity 会生成多个包括支持和不支持阴影的 Shader 变体,从而为额外的逐像素光源计算阴影,并传递给 Shader 了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // Additional Pass 附加渲染通道
    Pass
    {
    Tags { "LightMode" = "ForwardAdd" }
    // 使用线性减淡效果进行光照混合
    Blend One One

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

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"
    // ...
    ENDCG
    }
  4. 修改两个 Pass​ 的片元着色器中衰减计算相关的代码

    使用 UNITY_LIGHT_ATTENUATION​ 宏替代原有逻辑,该宏需要传入3个参数

    • 第一个参数:是用来存储最终衰减值的变量名(不用声明,内部会声明)
    • 第二个参数:是片元着色器中传入的 v2f​ 结构体对象
    • 第三个参数:是顶点相对于世界坐标系的位置

    最终将得到的衰减结果和(漫反射 + 高光反射)的结果相乘即可

    Bass 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
    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;
    }

    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;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    //计算Blinn-Phong式光照模型需要的各种颜色
    fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
    fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
    // 利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算,其中atten是衰减值变量名,宏内部会自动声明,因此我们不需要再自己声明了
    UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)
    fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

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

    Additional Pass:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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);

    // 利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算,其中atten是衰减值变量名,宏内部会自动声明,因此我们不需要再自己声明了
    UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)

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

显示效果(左为 Additional Pass 不进行阴影衰减计算的 Shader,右为 Additional Pass 进行阴影衰减综合计算的 Shader):

中间的立方体会遮挡红色点光源发出的光(光源开启 Shadow),用于测试后两个立方体是否能够接收来自其他光源投射过来的光源

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
Shader "TeachShader/Lesson68_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); //计算高光反射颜色
// 利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算,其中atten是衰减值变量名,宏内部会自动声明,因此我们不需要再自己声明了
UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

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_fullshadows

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

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

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

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
{
// 漫反射颜色的计算
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);

// 利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算,其中atten是衰减值变量名,宏内部会自动声明,因此我们不需要再自己声明了
UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)

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

Fallback "Specular"
}