0%

【Unity Shader】(四)阴影和衰减

在前面的学习中,我们的场景中都仅有一个光源且光源类型是平行光。但在实际的游戏开发过程中,我们往往需要处理数目更多、类型更复杂的光源。更重要的是,我们想要得到阴影、光照衰减等更加逼真的效果。这一节我们先学习 Unity 中不同的渲染路径和重要的光源类型,再学习如何在前向渲染路径中实现包含了光照衰减、阴影等效果的完整的光照计算。

1 Unity 的渲染路径

在我们之前的代码中,每个 Pass 都有一行重要的设置:

1
Tags { "LightMode"="ForwardBase" }

这就是用来设置渲染路径的代码。在 Unity 里,渲染路径(Rendering Path)决定了光照是如何应用到 Unity Shader 中的。因此,如果要和光源打交道,我们需要为每个 Pass 指定它使用的渲染路径,只有这样才能让 Unity 知道:“哦,原来这个程序员想要这种渲染路径,那么好的,我把光源和处理后的光照信息都放在这些数据里,你可以访问啦!” 也就是说,我们只有为 Shader 正确地选择和设置了需要的渲染路径,该 Shader 的光照计算才能被正确执行。

Unity 支持多种类型的渲染路径,最常用的两种是前向渲染路径(Forward Rendering Path)延迟渲染路径(Deferred Rendering Path)。我们可以对整个项目设置统一的渲染路径也可以单独为相机指定渲染路径。

上面的代码将告诉 Unity,该 Pass 使用前向渲染路径中的 ForwardBase 路径。而前向渲染路径还有一种路径叫做 ForwardAdd。下表给出了 Pass 的 LightMode 标签支持的渲染路径设置选项:

image-20220403210148354

我们在 Pass 中制定了渲染路径后,就可以在后面的代码中通过 Unity 提供的内置光照变量来访问光照属性。如果我们没有指定任何渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。

接下来我们详细认识一下不同的渲染路径是如何实现的。

1.1 前向渲染

前向渲染路径是传统的渲染方式,也是我们最常用的一种渲染路径。每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区, 一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。我们可以用下面的伪代码来描述前向渲染路径的大致过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pass {
for (each primitive in this model) {
for (each fragment covered by this primitive) {
if (failed in depth test) {
// 如果没有通过深度测试,说明该片元不可见,舍弃
discard;
}
else {
// 如果该片元可见,就进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
//更新帧缓冲
writeFrameBuffer(fragment, color);
}
}
}
}

对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个 Pass 计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设,场景中有 N 个物体,每个物体受 M 个光源的影响,那么要渲染整个场景一共需要 N*M 个 Pass。可以看出,如果有大量逐像素光照,那么需要执行的 Pass 数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。

事实上,一个 Pass 不仅仅可以用来计算逐像素光照,它也可以用来计算逐顶点等其他光照。这取决于光照计算所处流水线阶段以及计算时使用的数学模型。当我们渲染一个物体时,Unity 会计算哪些光源照亮了它,以及这些光源照亮该物体的方式。在 Unity 中,前向渲染路径有 3 种处理光照(即照亮物体)的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)。光源的渲染模式可以在光源面板的 Render Mode 属性中设置。

在前向渲染中,当我们渲染 一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、 光源强度等)对这些光源进行一个重要度排序。其中,一定数目的光源会按逐像素的方式处理,然后最多有 4 个光源按逐顶点的方式处理,剩下的光源可以按 SH 方式处理。Unity使用的判断规则如下:

  • 场景中最亮的平行光总是按逐像素处理的
  • 渲染模式被设置成 NotImportant 的光源,会按逐顶点或者 SH 处理
  • 渲染模式被设置成 Important 的光源,会按逐像素处理
  • 如果根据以上规则得到的逐像素光源数量小于 Quality Setting 中的逐像素光源数量(Pixel Light Count),则会有更多的光源以逐像素的方式进行渲染

那么在哪里进行光照计算呢?当然是在Pass里。前面提到过,前向渲染有两种Pass:Base Pass 和 Additional Pass。通常来说,这两种 Pass 进行的标签和渲染设置以及常规光照计算如下图:

image-20220403212358300

需要注意的有以下几点:

  • 除了在 Pass 中设置标签为对应的渲染路径外,还要使用 #pragma multi_compile_fwdbasepragma multi_compile_fwdadd 这样的编译指令,概括来说这些编译指令保证 Unity 可以为相应类型的 Pass 生成所有需要的 Shader 变种,这些变种会处理不同条件下的渲染逻辑,例如是否使用光照贴图(lightmap)、当前使用哪种光源类型等。因此使用了正确的编译指令我们才可以在相关的 Pass 中得到一些正确的光照变量,例如光照衰减值等。
  • Base Pass 中渲染的平行光默认是支持阴影的(如果开启了光源的阴影功能),而 Additional Pass 中渲染的光源在默认情况下是没有阴影效果的,即便我们在它的 Light 组件中设置了有阴影的 Shadow Type。但我们可以在 Additional Pass 中使用 #pragma multi_compile_fullshadows 代替 pragma multi_compile_fwdadd 编译指令,为点光源和聚光灯开启阴影效果。
  • 环境光和自发光也是在 Base Pass 中计算的。这是因为,对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在 Additional Pass 中计算这两种光照,就会造成叠加多次环境光和自发光,这不是我们想要的。
  • 在 Additional Pass 的渲染设置中,我们还开启和设置了混合模式。这是因为,我们希望每个 Additional Pass 可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果。如果我们没有开启和设置混合模式,那么 Additional Pass 的渲染结果会覆盖掉之前的渲染结果,看起来就好像该物体只受该光源的影响。通常情况下,我们选择的混合模式是 Blend One One
  • 对于前向渲染来说,一个 Unity Shader 通常会定义一个 Base Pass(Base Pass 也可以定义多次,例如需要双面渲染等情况)以及一个 Additional Pass。一个 Base Pass 仅会执行一次(定义了多个 Base Pass 的情况除外),而 一个 Additional Pass 会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次 Additional Pass。

实际上,渲染路径的设置用于告诉 Unity 该 Pass 在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一 些内置变量(如 _LightColor0 等),如何使用这些内置变量进行计算完全取决于开发者的选择。 例如,我们完全可以利用 Unity 提供的内置变量在 Base Pass 中只进行逐顶点光照;同样,我们也完全可以在 Additional Pass 中按逐顶点的方式进行光照计算,不进行任何逐像素光照计算。

下表列出了前向渲染中我们可以在 Shader 中访问到的光照变量:

image-20220403213901931

下表列出了部分前向渲染中可以用的函数:

image-20220403213955807

需要说明的是,上面给出的变量和函数并不是完整的,在后面的学习中,我们会使用到一些不在这些表中的变量和函数。

1.2 延迟渲染

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,我们就需要为该区域内的每个物体执行多个 Pass 来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得到最终的光照。然而,每执行一个 Pass 我们都需要重新渲染一遍物体,但很多计算实际上是重复的 。

延迟渲染是一种更古老的渲染方法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为 G 缓冲(G-buffer),其中 G 是英文 Geometry 的缩写。 G 缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。

延迟渲染主要包含了两个 Pass。在第一个 Pass 中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到 G 缓冲区中。然后,在第二个 Pass 中,我们利用 G 缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。

延迟渲染的过程大致可以用下面的伪代码来描述:

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
Pass 1 {
// 第一个 Pass 不进行光照计算
// 仅仅把光照计算所需的信息存储到 G 缓冲中
for (each primitive in this model) {
for (each fragment covered by this primitive) {
if (failed in depth test) {
discard;
}
else {
writeGBuffer(materialinfo, pos, normal);
}
}
}
}

Pass 2 {
// 利用 G 缓冲中的信息进行真正的光照计算
for (each pixel in the screen) {
if (the pixel is valid) {
// 如果该像素有效,读取它对应的 G 缓冲中的信息
readGBuffer(materialinfo, pos, normal);
// 根据读取到的信息进行光照计算
float4 color = Shading(materia1Info, pos, normal, lightDir, viewDir);
// 更新帧缓冲
writeFrameBuffer(pixel, color);
}
}
}

可以看出,延迟渲染使用的 Pass 数目通常就是两个,这跟场景中包含的光源数目是没有关系的。换句话说,延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张 2D 图像,我们的计算实际上就是在这些图像空间中进行的。

对于延迟渲染来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染也有一些缺点:

  • 不支持真正的反走样(抗锯齿)功能,这一点我们在图形学中学习反走样时也提到过
  • 不能处理半透明物体
  • 对显卡有一定要求

Unity 中使用延迟渲染需要我们提供两个 Pass。第一个 Pass 用于渲染 G 缓冲。在这个 Pass 中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的 G 缓冲区中。对于每个物体来说,这个 Pass 仅会执行一次;第二个 Pass 用于计算真正的光照模型。这个 Pass 会使用上一个 Pass 中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

默认的G缓冲区(不同 Unity 版本的渲染纹理存储内容会有所不同)包含了以下几个渲染纹理(Render Texture,RT):

  • RT0:格式是 ARGB32,RGB 通道用于存储漫反射颜色,A 通道没有被使用
  • RT1:格式是 ARGB32,RGB 通道用千存储高光反射颜色,A 通道用于存储高光反射的指数部分
  • RT2:格式是 ARGB2101010,RGB 通道用于存储法线,A 通道没有被使用
  • RT3:格式是 ARGB32(非 HDR )或 ARGBHalf(HDR),用于存储自发光 + lightmap + 反射探针(reflection probes)
  • 深度缓冲和模板缓冲

当在第二个 Pass 中计算光照时,默认情况下仅可以使用 Unity 内置的 Standard 光照模型。如果我们想要使用其他的光照模型,就需要替换掉原有的 lnternal-DeferredShading.shader 文件。

延迟渲染中可访问的内置变量:

image-20220404161402442

2 Unity 的光源类型

Unity 一共支持 4 种光源类型:平行光、点光源、聚光灯和面光源。由于每种光源的几何定义不同,因此它们对应的光源属性也就各不相同。这就要求我们要区别对待它们。本节主要讨论前三种类型的光源。

在此之前先明确我们的 Shader 中使用了哪些光源属性。最常使用的光源属性有光源的位置、方向(更具体说就是,到某点的方向)、颜色、强度以及衰减(更具体说就是,到某点的衰减,与该点到光源的距离有关)这 5 个属性。而这些属性和它们的几何定义息息相关。

2.1 平行光

我们之前使用的都是平行光,它的几何定义是最简单的。平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。

image-20220404162749907

平行光之所以简单,是因为它没有一个唯一的位置,也就是说,它可以放在场景中的任意位置。它的几何属性只有方向,我们可以调整平行光的 Transform 组件中的 Rotation 属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念,也就是说,光照强度不会随着距离而发生改变。

2.2 点光源

点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。 点光源可以表示由一个点发出的、向所有方向延伸的光。

image-20220404162727091

球体的半径可以由面板中的 Range 属性来调整,也可以在 Scene 视图中直接拖拉点光源的线框(如球体上的黄色控制点)来修改它的属性。点光源是有位置属性的,它是由点光源的 Transform 组件中的 Position 属性定义的。对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在 Light 组件面板中调整。同时,点光源也是会衰减的,随着物体逐渐远离点光源,它接收到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处的最弱,值为 0 。其中间的衰减值可以由一个函数定义。

2.3 聚光灯

聚光灯是这 3 种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。

image-20220404162711673

这块锥形区域的半径由面板中的 Range 属性决定,而锥体的张开角度由 Spot Angle 属性决定。我们同样也可以在 Scene 视图中直接拖拉聚光灯的线框(如中间的黄色控制点以及四周的黄色控制点)来修改它的属性。聚光灯的位置同样是由 Transform 组件中的 Position 属性定义的。对于方向属性,我们需要用聚光灯的位置减去某点的位置来得到它到该点的方向。聚光灯的衰减也是随着物体逐渐远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为 0 。其中间的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式要更加复杂,因为我们需要判断一个点是否在锥体的范围内。

2.4 在前向渲染中处理不同类型的光源

首先我们渲染一个有两个点光源(一个绿色、一个红色)和一个平行光(黄色)的场景,场景的 2D 视图如下:

image-20220404163828406

为此我们需要分别编写 Base Pass 和 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
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
Shader "Unity Shaders Book/Chapter 9/Forward Rendering" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" }

Pass {
// Base Pass计算环境光和最重要的平行光
// 如果场景中包含了多个平行光,Unity会选择最亮的平行光传递给Base Pass进行逐像素计算
// 其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理
// 如果场景中没有任何平行光,那么Base Pass会当成全黑的光源处理
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

// 别忘了声明编译指令
#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
// Base Pass中一定处理的是平行光,因此只需要获取光线方向
// 使用_WorldSpaceLightPos0获取光线方向,平行光的光线方向到场景中任何顶点都一样
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 环境光只在 Base Pass 中计算一次,后面的 Additional Pass 不再计算
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 漫反射,通过_LightColor0获取平行光的强度和颜色(已经是二者相乘后的结果)
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
// Blinn-Phong 高光
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
//平行光没有衰减,因此衰减因子设为 1 即可
fixed atten = 1.0;

return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

ENDCG
}

Pass {
// Additional Pass 计算其他光照,不再计算环境光
Tags { "LightMode"="ForwardAdd" }
// 定义混合因子,同时开启颜色混合
Blend One One

CGPROGRAM

// 别忘了声明编译指令
#pragma multi_compile_fwdadd

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
// 如果是平行光,光线方向到场景中任何点都一样,_WorldSpaceLightPos0存储的是光照方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
// 点光源和聚光灯的光线方向要根据顶点位置计算,_WorldSpaceLightPos0存储的是光源位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
// 漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
// Blinn-Phong 高光
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

// 处理不同光源的衰减
#ifdef USING_DIRECTIONAL_LIGHT
// 平行光无衰减
fixed atten = 1.0;
#else
// 点光源和聚光灯的衰减因子计算非常复杂,因此 Unity 使用一张衰减纹理来作为查找表得到衰减因子
// 使用光源空间下的坐标对衰减纹理采样
#if defined (POINT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif

return fixed4((diffuse + specular) * atten, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}

当使用上面的场景时,得到的渲染效果如下:

image-20220404170534630

可以看到左边绿色的光照,右边红色的光照和整体的黄色的平行光。

现在我们在场景中重新定义一些光源,包含 1 个绿色的平行光和 4 个红色的点光源,他们的布局如下:

image-20220404171010142

同样使用上面的 Shader 得到的渲染效果:

image-20220404171047163

都得到这样的结果,是由于当我们创建一个光源时,默认情况下它的 Render Mode 是 Auto,这意味着 Unity 会自动判断哪些光源逐像素处理哪些光源逐顶点处理或 SH 处理,由于我们也没有更改 Pixel Light Count 中的数值,因此默认会处理 4 个逐像素光照,而我们的场景中有 5 个光源,其中绿色的平行光会在 Base Pass 中逐像素处理,剩下四个光源刚好不超过 Pixel Light Count ,因此会在 Additional Pass 中逐像素处理。4 个光源会调用 4 次 Additional Pass,这可以在帧调试器中看到:

image-20220404171558807

可以看到相机的渲染一共有 6 个渲染事件,其中第一个事件是 Clear,是为了清除颜色、深度和模板缓冲,为后面的渲染做准备,之后的 5 个渲染事件分别是一个 Base Pass 和 4 个 Additional Pass。

如果我们把点光源的 Render Mode 设置为 Not Important:

image-20220404171859381

那么意味着我们不希望把该光源当作逐像素光照去处理,因此也就不会再调用 Additional Pass 去计算这个光源。当我们把四个点光源全部设置为不重要时,得到的渲染结果:

image-20220404172100048

就只有绿色的平行光了。至于多个 Auto 光源之间 Unity 如何自动判断重要程度,与光源的强度、距离物体的远近有关。

3 Unity 的光照衰减

在上面的代码中我们提到 Unity 使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。这样的好处在于,计算衰减不依赖千数学公式的复杂性,我们只要使用一个参数值去纹理中采样即可。但使用纹理查找来计算衰减也有一些弊端:

  • 需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度
  • 不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其他数学公式来计算衰减

但由于这种方法可以在一定程度上提升性能,而且得到的效果在大部分情况下都是良好的,因此 Unity 默认就是使用这种纹理查找的方式来计算逐像素的点光源和聚光灯的衰减的。

Unity 在内部使用一张名为 _LightTexture0 的纹理来计算光源衰减,我们通常只关心 _LightTexture0 对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。例如,(0, 0) 点表明了与光源位置重合的点的衰减值,而 (1, 1) 点表明了在光源空间中所关心的距离最远的点的衰减。

为了对 _LightTexture0 纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这可以通过 unity_WorldToLight 矩阵直接计算得到:

1
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));

然后,我们使用这个坐标模的平方对衰减纹理进行采样得到衰减值:

1
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

可以发现,在上面的代码中,我们使用了光源空间中顶点距离的平方(通过 dot 函数来得到)来对纹理采样,之所以没有使用距离值来采样是因为这种方法可以避免开方操作。 然后,我们使用宏 UNITY_ATTEN_CHANNEL 来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。

4 Unity 的阴影

之前我们已经学习过阴影映射(Shadow Mapping)的知识,Unity 的阴影使用的就是这种方法。

4.1 阴影的实现原理

在前向渲染路径中,如果场景中最重要的平行光开启了阴影, Unity 就会为该光源计算它的阴影映射纹理(Shadow Map)。 这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。

那么,在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用 Base Pass 和 Additional Pass 来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而 Base Pass 和 Additional Pass 中往往涉及很多复杂的光照模型计算。因此,Unity 选择使用一个额外的 Pass 来专门更新光源的阴影映射纹理,这个 Pass 就是 LightMode 标签被设置为 ShadowCaster 的 Pass 。这个 Pass 的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。Unity 首先把摄像机放置到光源的位置上,然后调用该 Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。 因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的 Unity Shader 中找到 LightMode 为 ShadowCaster 的 Pass,如果没有,它就会在 Fallback 指定的 Unity Shader 中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个 LightMode 为 ShadowCaster 的 Pass 后,Unity 会使用该 Pass 来更新光源的阴影映射纹理。

传统的阴影映射纹理的实现中,我们会在正常渲染的 Pass 中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用 xy 分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由 z 分量得到),那么说明该点位于阴影中。

在高版本的 Unity 中,使用的是不同于传统方法的阴影映射——屏幕空间的阴影映射技术(Screenspace Shadow Map),屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。当使用了屏幕空间的阴影映射技术时,Unity 首先会通过调用 LightMode 为 ShadowCaster 的 Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在 Shader 中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

总结来说,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程:

  • 如果我们想要一个物体接收来自其他物体的阴影,就必须在 Shader 中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
  • 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。 在 Unity 中,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的阴影映射技术,Unity 还会使用这个 Pass 产生一张摄像机的深度纹理。

4.2 不透明物体的阴影实现

首先在光源属性面板设置阴影类型,这里使用软阴影:

image-20220404175527903

然后要在物体的 Mesh Render 组件中设置物体是否投射阴影和接收阴影:

image-20220404175627624

如果开启了 Cast Shadows 属性,那么 Unity 就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。这里正方体先使用我们之前的代码定义的材质,做完这些设置后我们就可以看到如下场景:

image-20220404180300107

因为地面和墙面的材质是默认的材质,所以已经可以接收到正方体投射的阴影效果,但是为什么正方体会向其他物体投射阴影呢?我们之前的代码并没有为物体定义 LightMode 为 ShadowCaster 的 Pass,但是最后的 Fall Back 中我们回调了默认的 Specular Shader,而 Specular Shader 又回调了默认的 VertexLit Shader,在 VertexLit.shader 中定义了 LightMode 为 ShadowCaster 的 Pass:

image-20220404183015901

上面的代码非常短, 尽管有一些宏和指令是我们之前没有遇到过的,但它们的用处实际上就是为了把深度信息写入渲染目标中。在 Unity 中这个 Pass 的渲染目标可以是光源的阴影映射纹理,也可以是摄像机的深度纹理。

同时我们注意到上面的渲染结果中,右边的墙面并没有正确的投射阴影,这是因为在默认情况下,我们在计算光源的阴影映射纹理时会剔除掉物体的背面。但对于内置的平面来说,它只有一个面,因此在本例中当计算阴影映射纹理时,由于右侧的平面在光源空间下没有任何正面,因此就不会添加到阴影映射纹理中。我们可以将 Cast Shadows 设置为 Two Sided 来允许对物体的所有面都计算阴影信息:

image-20220404180534729

更改设置后就可以正确投射阴影了:

image-20220404180607465

接下来我们自己编写代码使得正方体也可以接受其他物体投射的阴影:

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
Shader "Unity Shaders Book/Chapter 9/Shadow" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" }

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
// 计算阴影需要的文件
#include "AutoLight.cginc"

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
// 声明一个用于对阴影纹理采样的坐标
// 需要注意的是,这个宏的参数是下一个可用的插值寄存器的索引值,在这里就是2
SHADOW_COORDS(2)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 阴影纹理坐标转换,传递给片元着色器
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

fixed atten = 1.0;
// 计算阴影
fixed shadow = SHADOW_ATTENUATION(i);
// 阴影值和漫反射、高光反射相乘即可
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}

ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }

// 和之前的代码完全相同
// Additional Pass的阴影处理和 Base Pass 是一样的,这里就不写了
}
}
FallBack "Specular"
}

上面的代码中我们使用了三个宏定义:SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATION,这些内置宏帮助我们在必要时计算光源的阴影,他们的定义都可以在 AutoLight.cginc 中找到。在前向渲染中,宏 SHADOW_COORDS 实际上就是声明了一个名为_ShadowCoord 的阴影纹理坐标变量。而 TRANSFER_SHADOW 的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了 UNITY_NO_SCREENSPACE_SHADOWS 来得到),TRANSFER_SHADOW 会调用内置的 ComputeScreenPos 函数来计算 _ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到 _ShadowCoord中。然后,SHADOW_ATTENUATION 负责使用 _ShadowCoord 对相关的纹理进行采样,得到阴影信息。此外,当关闭了阴影后,SHADOW_ COORDSTRANSFER_SHADOW 实际没有任何作用,而 SHADOW_ATTENUATION 会直接等同于数值1。

需要格外注意的是,由于这些宏中会使用上下文变量来进行相关计算,例如 TRANSFER_SHADOW 会使用 v.vertexa.pos 来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2v 结构体中的顶点坐标变量名必须是 vertex,顶点着色器的输入结构体 v2f 必须命名为 v,且 v2f 中的顶点位置变量必须命名为 pos。

最后的渲染效果如下:

image-20220404182349779

我们可以在帧调试器中详细看到渲染过程。首先是绘制摄像机深度纹理和阴影贴图:

image-20220404184251457

然后根据摄像机深度纹理和平行光的阴影贴图生成屏幕空间的阴影图:

image-20220404184342934

最后是逐个物体的渲染整个场景:

image-20220404184409860

image-20220404184419098

image-20220404184432084

4.3 统一管理光照衰减和阴影

在前面的代码中我们了解到,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果,因此 Unity 提供了同时计算两个信息的方式,这主要是通过内置的 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
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
Shader "Unity Shaders Book/Chapter 9/Attenuation And Shadow Use Build-in Functions" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" }

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
// 同样要使用SHADOW_COORDS定义阴影纹理坐标
SHADOW_COORDS(2)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 同样使用TRANSFER_SHADOW进行阴影纹理坐标转换
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

// 唯一的不同是使用UNITY_LIGHT_ATTENUATION计算阴影和衰减
// 第一个参数是阴影和衰减的乘积,UNITY_LIGHT_ATTENUATION内部会帮我们声明,因此我们无需声明
// 第二个参数是 v2f 结构体,用来传递给SHADOW_ATTENUATION计算阴影
// 第三个参数是世界空间的顶点位置,用于转换到光源空间在衰减纹理中采样衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }

Blend One One

CGPROGRAM

// 使用下面的声明将为额外的逐像素光源计算阴影
#pragma multi_compile_fwdadd_fullshadows

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

// 使用内置函数使得我们可以不用自己判断光源类型,只需要一行代码
// 同时这一行代码是Base Pass和Additional Pass通用的,对我们来说不必考虑其他细节
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

return fixed4((diffuse + specular) * atten, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}

4.4 透明物体的阴影实现

对于大多数不透明物体来说,把 Fallback 设为 VertexLit 就可以得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的 Fallback。

透明度测试的处理比较简单,但如果我们仍然直接使用 VertexLit、Diffuse、Specular 等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而 VertexLit 中的阴影投射纹理并没有进行这样的操作。我们先使用 VertexLit 回调实现透明度测试的阴影:

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
Shader "Unity Shaders Book/Chapter 9/Alpha Test With Shadow" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader {
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

Pass {
Tags { "LightMode"="ForwardBase" }

Cull Off

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
// 由于我们已经使用了 3 个插值寄存器TEXCOORD0,TEXCOORD1,TEXCOORD2
// 因此SHADOW_COORDS传入的参数是3,表明使用TEXCOORD3存储阴影纹理坐标
SHADOW_COORDS(3)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
clip (texColor.a - _Cutoff);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + diffuse * atten, 1.0);
}

ENDCG
}
}
FallBack "VertexLit"
}

阴影效果如下:

image-20220404195958714

可以看到镂空区域也存在阴影,整个物体像是一个完整的正方体,这是因为我们使用内置的 VertexLit 中提供的 ShadowCaster 来投射阴影,而这个 Pass 中并没有进行任何透明度测试的计算,因此,它会把整个物体的深度信息渲染到深度图和阴影映射纹理中。所以如果我们想要得到经过透明度测试后的阴影效果,就需要提供一个有透明度测试功能的 Shadow Caster Pass,于是我们把回调改为:

1
FallBack "Transparent/Cutout/VertexLit"

就得到了正确的阴影:

image-20220404200813976

但需要注意的是,由于 Transparent/Cutout/VertexLit 中计算透明度测试时,使用了名为 _Cutoff 的属性来进行透明度测
试,因此,这要求我们的 Shader 中也必须提供名为 _Cutoff 的属性。否则,同样无法得到正确的阴影结果。

但是,这样的结果仍然有一些问题,例如出现了一些不应该透过光的部分。出现这种情况的原因是,默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑物体的正面。但对于本例的正方体来说,由于一些面完全背对光源,因此这些面的深度信息没有加入到阴影映射纹理的计算中。为了得到正确的结果,我们可以将正方体的 Mesh Renderer 组件中的 Cast Shadows 属性设置为 Two Sided,强制 Unity 在计算阴影映射纹理时计算所有面的深度信息。然后就得到了正确的渲染结果:

image-20220404200138111

与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的 Unity Shader, 如 Transparent/VertexLit 等,都没有包含阴影投射的 Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接收来自其他物体的阴影。下面是使用 Transparent/VertexLit 回调的透明度混合代码,在以前的透明度混合代码中加上了阴影计算:

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
Shader "Unity Shaders Book/Chapter 9/Alpha Blend With Shadow" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}

Pass {
Tags { "LightMode"="ForwardBase" }

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
SHADOW_COORDS(3)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale);
}

ENDCG
}
}
FallBack "Transparent/VertexLit"
// Or force to apply shadow
// FallBack "VertexLit"
}

得到的效果如下:

image-20220404201541037

Unity 会这样处理半透明物体是有它的原因的。由千透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且也会影响性能。因此,在 Unity 中,所有内置的半透明 Shader 是不会产生任何阴影效果的。

当然,我们可以使用一些 dirty trick 来强制为半透明物体生成阴影,这可以通过把它们的 Fallback 设置为 VertexLit、 Diffuse 这些不透明物体使用的 Unity Shader, 这样 Unity 就会在它的 Fallback 找到 一个阴影投射的 Pass。然后,我们可以通过物体的 Mesh Renderer 组件上的 Cast Shadows 和 Receive Shadows 选项来控制是否需要向其他物体投射或接收阴影。下图是把Fallback 设为 VertexLit 并开启阴影投射和接收阴影后的半透明物体的渲染效果。

image-20220404201901772

5 总结

到此为止我们已经学习了全部的基础光照计算,包括多光源、阴影、光照衰减,现在我们可以实现一个标准的光照着色器了,使用的都是之前学习过的代码,下面是使用 Blinn-Phong 光照模型、法线纹理、阴影和光照衰减的完整的着色器代码:

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
175
176
177
178
179
180
181
182
183
184
185
Shader "Unity Shaders Book/Common/Bumped Specular" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_Specular ("Specular Color", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
fixed4 _Specular;
float _Gloss;

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

struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

TANGENT_SPACE_ROTATION;

float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

TRANSFER_SHADOW(o);

return o;
}

fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

UNITY_LIGHT_ATTENUATION(atten, i, worldPos);

return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }

Blend One One

CGPROGRAM

#pragma multi_compile_fwdadd
// Use the line below to add shadows for point and spot lights
// #pragma multi_compile_fwdadd_fullshadows

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

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

struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

TRANSFER_SHADOW(o);

return o;
}

fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

UNITY_LIGHT_ATTENUATION(atten, i, worldPos);

return fixed4((diffuse + specular) * atten, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。