Unity 渲染原理(十四)Unity的光源

只有一个平行光的世界很美好,但是,我们实际上就是需要在Unity Shader中处理更复杂的光源类型以及数目更多的光源。

Unity 一共支持4种光源,平行光,点光源,聚光灯和面光源。面光源只在烘焙的时候才有用。

由于每种光源的几何定义不同,因此它们对应的光源属性也不同,这就要求我们区别对待它们。好在Unity为我们提供了很多函数可以处理它们。

光源类型有什么影响

光源的类型会对Shader带来了影响,我们可以考虑Shader种使用了光源的哪些属性。最常使用的有光源的位置,方向,颜色,强度以及衰减这五个属性,而这五个属性与他们的几何信息息息相关。

平行光

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

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

点光源

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

  • 球体的半径可以由面板的Range属性来调整
  • 它的位置信息也可以通过Transform组件来修改
  • 方向属性,我们需要用点光源的位置减去某点的位置来得到该点的方向
  • 而光源的颜色和强度可以在Light组件中调整
  • 同时点光源是会衰减的,随着物体逐渐远离点光源,它接受的光照强度也会逐渐缩小。点光源球心处的光照强度最强,边界处最弱,中间的衰减值可以通过函数计算。

聚光灯

它的照亮空间是有限的,但不再是简单的球体,而是一个锥体,它的属性计算方式与点光源大体上相似,只是光源的衰减计算函数中需要判断一下是否在椎体范围内。

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

在了解了3中光源的几何定义之后,我们来看一下如何在Unity Shader中访问它们的五个属性,位置,方向,颜色,强度以及衰减

Shader 实践

(1)首先我们要定义第一个Pass——Base Pass,为此,我们需要设置改Pass的渲染路径的标签

1
2
3
4
5
6
Pass {
Tags{"LightMode"="ForwardBase"}

CGPROGRAM
#pragma mulit_compile_fwdbase
}

除了设置渲染路径外,我们还使用了prgama编译指令,该指令可以保证我们可以在Shader中使用光照衰减等关照变量可以被正确赋值,这是不可缺少的。

(2)在Base Pass的片元着色器中,我们首先计算了场景中的环境光

1
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

我们希望环境光计算一次即可,因此在后面的Additional Pass中就不会再计算这个部分,与之类似,还有物体的自发光,但是我们暂时不考虑自发光

(3)然后,我们在Base Pass中处理了场景中最重要的平行光。在这个例子中,场景中只有一个平行光。如果有多个平行光,Unity会选择最亮的平行光传递给Base Pass进行逐像素处理,其他平行光会按照逐顶点或在Additional Pass中按照逐像素的方式处理。如果场景中没有任何平行光,那么Base Pass会当成全黑的光源处理。

我们提到过,每一个光源都有5个属性,位置,方向,颜色,强度以及衰减。对于Base Pass来说,它处理的逐像素光源类型一定是平行光,我们可以使用_WorldSpaceLightPos0来得到这个平行光的方向(位置对平行光没有意义),使用_LightColor0来得到它的颜色和强度(_LightColor0已经是颜色和强度相乘的结果),由于平行光可以认为是没有衰减的,所以我们直接令衰减值为1

1
2
3
4
5
6
7
8
9
10
11
12
13
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.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);

fixed atten = 1.0;

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

至此,Base Pass的工作就完成了

(4)接下来,我们需要为场景中其他逐像素光源定义Additional Pass。为此,我们首选需要设置Pass的渲染路径标签

1
2
3
4
5
6
Pass {
Tags{"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
}

除了设置渲染路径之外,我们同样使用了#pragma multi_compile_fwdadd指令,以保证我们可以在Additional Pass中访问到正确的光照变量。

与Base Pass不同,我们还开启了Blend命令和设置了混合模式,这是因为,我们希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加。如果没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果。我们现在使用的混合系数是Blend One One,这不是必须的,我们可以设置成任何Unity支持的系数

(5)通常来说,Additional Pass的光照处理和Base Pass的处理方式是一样的,因此我们只需要把Base Pass的代码粘贴到Additonal Pass中,然后稍微修改一下。这些修改往往是为了去掉Base Pass中的环境光,自发光,逐定点光照,SH光照部分等,并添加一些不同光源的支持。

因此,我们没有再计算场景中的环境光。由于Additional Pass处理的光源类型可能是平行光,点光源或者是聚光灯,因此再计算光源的5个属性——位置,方向,颜色,强度以及衰减时,颜色和强度我们仍然可以使用_LightColor0来得到,但对于位置,方向和衰减等属性,需要根据光源类型分别计算。

首先我们看一下,如何计算不同光源的方向

1
2
3
4
5
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
#endif

上面的代码首先通过判断当前处理的逐像素光源的类型,这是通过使用#ifdef指令判断是否定义了USING_DIRECTIONAL_LIGHT来得到的。如果当前前向渲染Pass处理的光源类型是平行光,那么Unity的底层渲染引擎就会定义USING_DIRECTIONAL_LIGHT。如果判断得知是平行光的话,光源方向可以直接由_WorldSpaceLightPos0.xyz得到。如果是点光源或聚光灯,那么_WorldSpaceLigthPos0.xyz表示的是世界空间下光源的位置,想要得到光源的方向,需要用这个位置减去世界空间下顶点的位置

(6)最后我们需要处理不同光源的衰减

1
2
3
4
5
6
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, LightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif

我们同样使用USING_DIRECTIONAL_LIGHT来判断是否是平行光,非平行光的衰减值尽管可以用数学表达式来计算给定点相对于点光源和聚光灯的衰减,但是这些计算往往涉及到开根号,除法等计算量相对较大的操作,因此Unity选择使用一张纹理作为查找表(Lookup Table, LUT),以在片元着色器中得到光源的衰减。我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值

最终的代码如下:

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 "Unlit/ForwardRendering"
{
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" }
LOD 100

Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

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 = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.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);

fixed atten = 1.0;

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

ENDCG
}
Pass
{
Tags{"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdbase
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

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 = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif

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

#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, LightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif

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

ENDCG
}
}

Fallback "Diffuse"
}

上面的代码只适用于Standard流水线,对于URP流水线不再适用,具体改造方法可以参看这篇博客:https://zhuanlan.zhihu.com/p/336428407

Base Pass和Additional Pass的调用

我们在场景中创建一个平行光以及4个点光源

当我们创建一个光源的时候,默认情况下它的Render Mode时Auto,意味着Unity在背后会为我们判断哪些光源会按照逐像素处理,而哪些会按照逐定点或者SH的方式处理。由于我们没有改Piexl Light Color中的数值,因此默认情况下一个物体可以接受除平行光之外的4个最亮的逐像素光照,在这个例子中,场景中包含了5个光源,其中一个是平行光,它会在Base Pass中按照逐像素处理,其他的4个点光源会在Additional Pass中逐像素处理。

Unity在处理这些光源的顺序是按照重要度的,我们场景中的所有点光源的颜色和强度都相同,因此它们的重要度取决于它们距离胶囊体的远近。

对于场景中的一个物体,如果他不在光源影响范围内,Unity不会为这个物体调用Pass

当场景中逐像素光源数目很多的话,该物体的Additonal Pass会被调用多次,影响性能,所以我们可以把光源的Render Mode设置为 “Not Important”,这样Unity就不会把该光源当作逐像素处理。

而我们的例子中,顶点着色器中并没有处理光源,所以如果设置4个点光源都是“Not Important”结果就是这4个光源并不会产生效果。