39. Unity Shader光照研究(二)

这篇是对Unity Shader光照研究(一)第二篇,主要是《Unity Shader入门精要》的第9章内容总结。

Unity渲染路径

Unity渲染路径主要有三种,前向渲染路径(最常用),延迟渲染路径,顶点照明渲染路径(准备弃用),这三种渲染路径可以在Graphics中设置平台全局渲染路径,也可以在摄像机中单独设置每个摄像机的渲染路径。

如果显卡不支持该渲染路径会自动使用更低一级的渲染路径

渲染路径的设置是通过LightMode标签设置,例如"LightMode" = "ForwardBase"表示该Pass使用前向渲染你的FoorwardBase路径,LightMode选项设置有:

  • Always:不管使用哪种渲染路径,该Pass总会被渲染,但不会计算任何光照
  • ForwardBase:用于前向渲染,会计算环境光,最重要的平行光,逐顶点光源和Lightmap
  • ForwardAdd:用于前向渲染,计算额外的逐像素光源,一个Pass会被每个光源调用
  • Deferred:用于延迟渲染
  • ShadowCast:该Pass会把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中
  • PrepassBase:用于遗留的延迟渲染
  • PrepassFinal:用于遗留的延迟渲染
  • Vertex、VertexLMRGBM和VertexLM:用于遗留的顶点照明渲染

前向渲染路径

原理

用于前向渲染的Shader如果要支持多光源的话一般会有两个Shader,LightMode分别设置为ForwardBaseForwardAdd

  • ForwardBase:该Pass被调用一次,用来渲染最重要的一盏灯光(一般是最亮的平行光),环境光,还有逐顶点的光源,以及当有光照贴图的时候处理光照贴图。
  • ForwardAdd:该Pass会被调用多次(次数取决于Unity最终计算出来的使用逐像素计算的光源数量,可以通过Quality Setting设置最大支持的逐像素光源数量),用来渲染额外的逐像素灯光,可以是其他平行光,点光源或者聚光灯,该Pass计算出来的颜色会利用Blend与ForwardBase计算的颜色进行叠加,所以一般Blend使用的模式是Blend One One

可以看到如果灯光多的情况下,一个物体会被渲染多次,所以前向渲染处理不当会有性能问题。

原理里面个概念需要理解下:光源是逐像素还是逐顶点是怎么判定的

  • 场景中最亮的平行光总是按照逐像素处理(在ForwardBase处理)
  • 如果光照的Render Mode手动设置为Important则会按照逐像素处理,如果设置为Not Important则会按照逐顶点处理
  • 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量,则设置为Auto的Light会有机会以逐像素处理。(如果Important的灯光数量超过设置的值会怎么样?)

如何写前向渲染

首先我们需要两个Pass,一个Pass的Tag设置为ForwardBase

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
Pass
{
Tags {"LightMode" = "ForwardBase"}

CGPROGRAM
#pragma multi_compile_fwdbase // 后面介绍
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
#if defined(LIGHTMAP_ON)
float4 texcoord2 : TEXCOORD1;
#endif
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : NORMAL;
float3 worldVertex : TEXCOORD1;
#if defined(LIGHTMAP_ON)
half2 uv_lightmap : TEXCOORD3;
#endif
SHADOW_COORDS(2) // 阴影相关
};

#pragma vertex vert
#pragma fragment frag

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
#if defined(LIGHTMAP_ON)
o.uv_lightmap = v.texcoord2.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldVertex = mul(unity_ObjectToWorld, v.vertex);

TRANSFER_SHADOW(o); // 阴影相关
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
// 物体本身颜色
fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;

#if defined(LIGHTMAP_ON)
fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.uv_lightmap.xy));
mainColor.rgb *= lm;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);
return fixed4(mainColor * atten,1.0);
#else
// 受环境光之后影响之后的颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientFactor * mainColor;

fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex));
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射光颜色
fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;

// 高光颜色
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
fixed3 hDir = normalize(worldViewDir + worldLightDir);
fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;

// 计算光照强度以及阴影的影响
UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);

// 受灯光影响之后的颜色
return ambient + (diffuseColor + specularColor) * atten;
#endif
}
ENDCG
}
  1. 这个Pass会使用最亮的平行光处理一次
  2. 该Pass计算了环境光,最亮的平行光对物体的影响(包括漫反射、高光和阴影),还有烘培之后光照贴图(LIGHTMAP_ON中的代码)的影响(也就是说如果该物体烘培过了,那么走的就是LIGHTMAP_ON里面的逻辑)
  3. 关于阴影相关后面介绍

另外一个Pass的Tag设置为ForwardAdd(如果整个场景只有一个灯光,这个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
Pass
{
Tags {"LightMode" = "ForwardAdd"}

Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_fwdadd_fullshadows // 后面介绍

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : NORMAL;
float3 worldVertex : TEXCOORD1;
SHADOW_COORDS(2) // 阴影相关
};

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldVertex = mul(unity_ObjectToWorld, v.vertex);

TRANSFER_SHADOW(o); // 阴影相关
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
// 物体本身颜色
fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex));
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射光颜色
fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;

// 高光颜色
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
fixed3 hDir = normalize(worldViewDir + worldLightDir);
fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;

// 计算光照强度以及阴影的影响
UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);

// 受灯光影响之后的颜色
return (diffuseColor + specularColor) * atten;
}
ENDCG
}

注意两点:

  1. 每个逐像素的光源都会处理一次这个Pass,这个Pass只处理了这个灯光对物体的影响(漫反射、高光和阴影)
  2. 叠加模式使用Blend One One的叠加规则,这样附加的灯光效果会用加的方式叠加到原来的颜色上

#pragma multi_compile作用

当我们写Shader的时候往往随着需求不同需要写不同的Shader,其中某些需求大部分代码都一样,而小部分根据不同需求不一样。这时候如果我们还拆分成不同的Shader维护成本会很高,这时候我们就需要Shader变体,Unity官方给出的介绍

Shader变体使用上有点像C#里面的宏定义,但不同的是Shader会根据选项保留所有Shader变体的代码(变体多的情况下会导致包体大),并且能够动态的根据当前材质上的关键字选择正确的Shader变体进行执行,而C#的宏定义是编译器就确定下来的,运行期无法改变。

下面代码介绍一个使用变体的Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SubShader {
Pass {

CGPROGRAM

// 定义变体关键字
#pragma multi_compile A B

#if defend(A)
// 定义关键字A时会跑的代码
#else
// 没定义关键字A时会跑的代码
#endif

ENDCG
}
}

下面的代码介绍如何在C#里面设置关键字

1
2
3
4
5
6
7
// 直接通过材质球上的方法设置
material.EnableKeyword("A"); // 开启关键字A
material.DisableKeyword("A"); // 关闭关键字A

// 使用全局设置
Shader.EnableKeyword("A"); // 开启关键字A
Shader.DisableKeyword("A"); // 关闭关键字A

shader_feature介绍

  1. shader_feature可以配合Toggle,在Unity面板上直接控制这个关键字,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Properties {
    [Toggle(_A)] _PremulAlpha("Enable A", Float) = 0
    }


    SubShader {
    Pass {

    CGPROGRAM

    #pragma shader_feature _A

    ENDCG
    }
    }
  2. 与multi_compile区别

    shader_feature声明的变体,如果没有地方使用,则在打包的时候不会包含进去。所以如果Shader在使用的时候发现没有跑到对应的变体代码,可以考虑下是否打包的时候没有把这部分打进去。我们在给出的Shader例子中出现的LIGHTMAP_ON就遇到没有跑这块代码的情况,最后再Graphics->Lightmap Modes设置为Custom,勾选使用到的特性之后才表现正确

例子中用到的multi_compile_xxx又是什么

multi_compile_xxx是unity提供的快捷变体定义,相当于使用这个之后就会用multi_compile定义预先设置好的关键字

  • multi_compile_fwdbase:ForwardBase Pass用到的所有需要的变体,用到的关键字有DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON SHADOWS_SCREEN SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING LIGHTPROBE_SH
  • multi_compile_fwdadd:FowardAdd Pass(不带阴影)用到的所有需要的变体,用到的关键字有POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE
  • multi_compile_fwdadd_fullshadows:FowardAdd Pass(带阴影)用到的所有需要的变体,用到的关键字有POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE SHADOWS_DEPTH SHADOWS_SCREEN SHADOWS_CUBE SHADOWS_SOFT SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING

阴影介绍

Unity使用Shadow Map的技术实现阴影,简单的工作原理就是会在灯光处生成一个摄像机,用这个摄像机照射物体并生成一张阴影映射问题,最后在真实渲染的时候对阴影问题进行采样,拿到阴影参数系数,最后做相乘处理。

所以阴影的产生需要两步

需要投射阴影的物体

投射阴影的物体需要有个带"LightMode" = "ShadowCaster"的专门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
Pass {
Tags { "LightMode"="ShadowCaster" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"

struct v2f
{
V2F_SHADOW_CASTER;
};

v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}

float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}

ENDCG
}

接受阴影的物体

Unity封装好了很多宏供我们快捷使用,例如我们例子中的SHADOW_COORDS(2)用来声明阴影用到的变量,2是TEXCOORD的序号;TRANSFER_SHADOW(o);用来在顶点着色器中转换阴影用到的变量;UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);用来获取光照强度以及阴影的影响,如果只是希望获取阴影的影响可以通过SHADOW_ATTENUATION(i)获取

阴影的其他知识点

  1. 如果背面对着光照,这个面片将不会产生阴影,可以尝试将物体Mesh Renderer中的Cast Shadows改成Two Sided
  2. ShadowCaster可以不用每个Shader都写,可以在Fallback中指定VertexLit,这样Unity在本Shader中找不到ShadowCaster就会在VertexLit中找,另外如果是Cutout的版本,可以Fallback到Transparent/Cutout/VertexLit(需要Shader带有”_Cutoff”参数)
  3. 带透明度的物体的阴影会比较麻烦,慎用

完整的前向渲染Shader

给出我们项目用到比较完整的前向渲染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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
Shader "Babu/Model/Normal"
{
Properties
{
_MainTex("主贴图", 2D) = "white" {}
_MainColor("主颜色", Color) = (1, 1, 1, 1)
_MainFactor("不受光照影响的比例", Range(0, 1)) = 1

_AmbientFactor("环境光强度", float) = 1
_DiffuseFactor("漫反射强度", float) = 1
_SpecularFactor("高光强度", float) = 1
_SpecularColor ("高光颜色", Color) = (1, 1, 1, 1)
_Gloss ("光泽度", Range(8, 255)) = 20

_InSideRimColor("内边缘光颜色", Color) = (1,1,1,1)//内边缘光颜色
_InSideRimPower("边缘光强度", Range(0.0,5)) = 5 //边缘光强度 ,这个值可以控制菲涅尔影响范围的大小,这个值越大,效果上越边缘化
_InSideRimIntensity("边缘光强度系数", Range(0.0, 10)) = 0 //边缘光强度系数 这个值是反射的强度, 值越大,返回的强度越大,导致边缘的颜色不那么明显
}

SubShader
{
Tags { "RenderType" = "Qpaque" }

CGINCLUDE

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

sampler2D _MainTex;
fixed4 _MainTex_ST;
fixed4 _MainColor;
fixed _MainFactor;

float _AmbientFactor;
fixed _DiffuseFactor;
fixed _SpecularFactor;
fixed4 _SpecularColor;
float _Gloss;

float _OutLightStrength;
fixed4 _OutLightColor;

float _ShadowStrength;

uniform float4 _InSideRimColor;
uniform float _InSideRimPower;
uniform float _InSideRimIntensity;

ENDCG

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

CGPROGRAM
#pragma multi_compile_fwdbase
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
#if defined(LIGHTMAP_ON)
float4 texcoord2 : TEXCOORD1;
#endif
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : NORMAL;
float3 worldVertex : TEXCOORD1;
#if defined(LIGHTMAP_ON)
half2 uv_lightmap : TEXCOORD3;
#endif
SHADOW_COORDS(2)
};

#pragma vertex vert
#pragma fragment frag

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
#if defined(LIGHTMAP_ON)
o.uv_lightmap = v.texcoord2.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldVertex = mul(unity_ObjectToWorld, v.vertex);

TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
// 物体本身颜色
fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;

#if defined(LIGHTMAP_ON)
fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.uv_lightmap.xy));
mainColor.rgb *= lm;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);
return fixed4(mainColor * atten,1.0);
#else
// 受环境光之后影响之后的颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientFactor * mainColor;

fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex));
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射光颜色
fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;

// 高光颜色
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
fixed3 hDir = normalize(worldViewDir + worldLightDir);
fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);

// 受灯光影响之后的颜色
fixed3 lightColor = ambient + (diffuseColor + specularColor) * atten;

// 内轮廓光
fixed3 ViewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldVertex.xyz);
fixed bright = 1.0 - max(0, dot(worldNormal, ViewDir));
float fresnel = pow(bright, _InSideRimPower) * _InSideRimIntensity;//使用上面的属性参数,这里不多说
float3 emissive = _InSideRimColor.rgb * fresnel; //配置上属性里面的内边缘光颜色

// 最终颜色
fixed4 color = fixed4(mainColor * _MainFactor + lightColor * (1 - _MainFactor) + float4(emissive, 1), 1);
return color;
#endif
}
ENDCG
}

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

Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_fwdadd_fullshadows

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : NORMAL;
float3 worldVertex : TEXCOORD1;
SHADOW_COORDS(2)
};

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldVertex = mul(unity_ObjectToWorld, v.vertex);

TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
// 物体本身颜色
fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex));
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射光颜色
fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;

// 高光颜色
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
fixed3 hDir = normalize(worldViewDir + worldLightDir);
fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);

// 受灯光影响之后的颜色
fixed3 lightColor = (diffuseColor + specularColor) * atten;

// 最终颜色
fixed4 color = fixed4(lightColor * (1 - _MainFactor), 1);
return color;
}
ENDCG
}
}
FallBack "Diffuse"
}

顶点照明渲染路径

顶点光照渲染是前向渲染的一个子集,所有看恶意在顶点照明渲染路径实现的功能都可以在前向渲染中实现。(项目中未使用,所以没有深究里面的内容,只是了解一点点原理)

如果使用顶点光照渲染路径,Unity只会填充逐顶点相关的光源变量。

顶点光照渲染路径通常一个Pass就可以完成物体渲染,这个Pass中会处理所有光源对物体的照明,并且计算是按逐顶点处理的。所以这是Unity中最快捷的渲染路径。

延迟渲染路径

延迟渲染的出现是因为前向渲染会有性能瓶颈问题(一个物体每个光照都会调用一次Pass)。(项目中未使用,所以没有深究里面的内容,只是了解一点点原理)

延迟渲染会有两个Pass,第一个Pass不计算光照,只计算那些片元可见(使用深度缓存),如果可见就把相关信息(表面法线、视角方向、漫反射系数等)写入到G缓冲区;第二个Pass利用各个片元的信息进行真正的光照计算

延迟渲染会有一些限制:

  1. 不支持真正的抗锯齿功能(啥叫真正的抗锯齿?)
  2. 不能处理半透明物体
  3. 对显卡有要求,显卡必须支持MRT,Shader Mode 3.0以上(如果显卡不支持,则会退回到前向渲染)