US3S6L6——光照衰减和阴影
US3S6L6——光照衰减和阴影
知识回顾
多种光源的效果实现
在 Additional Pass(附加渲染通道)中根据光源类型,计算出不同的光源衰减值,参与到最终的颜色计算中
计算阴影时需要使用的三剑客
SHADOW_COORDS
(阴影坐标宏)在
v2f
结构体中声明,本质是声明了一个表示阴影映射纹理坐标的变量
TRANSFER_SHADOW
(转移阴影宏)在顶点着色器中调用,本质是将顶点进行坐标转换并存储到
_ShadowCoord
阴影纹理坐标变量中
SHADOW_ATTENUATION
(阴影衰减宏)本质是利用
_ShadowCoord
对阴影映射纹理进行采用,将采样得到的深度值进行比较,以计算出一个fixed3
的阴影衰减值最终之所以能够接受阴影,主要就是因为利用了最终得到的阴影衰减值参与到了最终的颜色计算中
本章代码关键字
1 | UNITY_LIGHT_ATTENUATION // Unity内计算光源和阴影的综合衰减值的宏 |
光照衰减和阴影
通过之前的学习,我们发现光照衰减和接受阴影相关的计算是类似的
关键点都是通过计算出一个衰减值,参与到颜色计算中,都是用 (漫反射+高光反射) 的结果乘以对应的衰减值
由于它们对最终颜色影响的计算非常类似,都是通过乘法进行运算
因此 Unity
中专门提供了对应的宏,来综合处理光照衰减和阴影衰减的计算
AutoLight.cginc
内置文件中的 UNITY_LIGHT_ATTENUATION
(Unity 光照衰减宏)
该宏中会统一的对 光照衰减进行计算,并且也会计算出 阴影衰减值,
最后将两者相乘得到综合衰减值,我们只需要利用该宏来处理 光照和阴影的衰减即可
我们可以在 Unity 安装目录的 Editor/Data/CGIncludes
中找到该内置文件,查看该宏的逻辑
下列代码是光源为点光源情况下 UNITY_LIGHT_ATTENUATION
的定义:
1 |
|
可见,UNITY_LIGHT_ATTENUATION
需要三个参数:
-
destName
:衰减值变量名称 -
input
:结构体 -
worldPos
:世界坐标系下的坐标
分析 UNITY_LIGHT_ATTENUATION
的定义的逻辑,不难看出它进行了三步计算:
- 通过 unity_WorldToLight 进行矩阵乘法将世界坐标系坐标转换为光源坐标系下的坐标
- 使用
UNITY_SHADOW_ATTENUATION
来计算阴影衰减值 - 使用光源坐标从 _LightTexture0 采样,获取
下列代码是光源为聚光灯和平行光情况下 UNITY_LIGHT_ATTENUATION
的定义:
1 | // 聚光灯 |
1 | // 平行光 |
以上宏定义的的主要区别是出现在平行光,聚光灯,点光源的光源衰减值计算逻辑上,其他的步骤是差不多的
因此,我们完全可以直接使用 UNITY_LIGHT_ATTENUATION
来计算综合处理光照和阴影的衰减逻辑
光照衰减和阴影的综合实现
我们将利用 AutoLight.cginc
内置文件中的 UNITY_LIGHT_ATTENUATION
(Unity光照衰减宏)来综合处理光照衰减和阴影相关的逻辑
-
创建一个新的 Shader 并复用上节课中的代码
-
将 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
17struct 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;
} -
为了让 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
// 帮助我们编译所有支持和不支持阴影光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
// ...
ENDCG
} -
修改两个
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
29fixed3 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
23fixed4 frag (v2f i) : SV_Target
{
// 漫反射颜色的计算
fixed3 worldNormal = normalize(i.wNormal);
// 光的方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光 光的方向就是它的位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos); // 点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
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),用于测试后两个立方体是否能够接收来自其他光源投射过来的光源
可见,左边的立方体能够投射各个光源的阴影,也能接受平行光的阴影,但是不能接受点光源的阴影
而右边的立方体能够接受来自点光源的阴影,也能接受平行光的阴影,也能投射阴影
完整 Shader 代码如下:
1 | Shader "TeachShader/Lesson68_ForwardLighting" |