求知若饥,虚心若愚
2009年,Unity 的渲染工程师 Aras连续发表了3篇名为《Shaders must die》的博客,在博客里,他认为把渲染流程分为顶点和像素的抽象层面是错误的,这种在顶点/几何/片元着色器上的操作是对硬件友好,但不符合人类的思考方式,应该划分为表面着色器、光照模型和光照着色器这样的层面。
- 表面着色器定义了模型表面的反射率、法线和高光等
- 光照模型则选择是使用兰伯特还是Blinn-Phone等模型
- 光照着色器负责计算光照衰减、阴影等
最终在2010年的 Unity3 中,加入了表面着色器(Surface Shader)。
表面着色器实际上是在顶点/片元着色器上添加了一层抽象,例如只需要直接告诉shader(实际就是unity会根据我们这些关键配置和代码去生成完整的shader,避免我们写那些重复的代码):
- 用这些纹理填充颜色
- 用这个法线纹理填充表面法线
- 使用兰伯特光照模型
- 不用考虑是使用前向渲染路径还是延迟渲染路径,场景中有多少光源,它们的类型是什么,怎么处理这些光源等
表面着色器的一个例子
1 | Shader "Unity Shaders Learn/Chapter17/BumpedDiffuse" |
表面着色器的例子。左图:在一个平行光下的效果。右图:添加了一个点光源(蓝色)和一个聚光灯(紫色)后的效果
相比顶点/片元着色器,表面着色器的代码量更少。而且可以轻松地实现常见的光照模型,甚至不需要和任何光照变量打交道,Unity为我们处理好了每个光源的光照结果。
表面着色器的CG代码直接且必须写在 SubShader 块中。最重要的部分是 两个结构体 以及它的 编译指令。
汇编指令
汇编指令最重要的作用是指明该表面着色器使用的 表面函数 和 光照函数,并设置一些可选参数。编译指令的一般格式如下:
1 | // pragma surface指明该编译指令是用于定义表面着色器的 |
表面函数
surfaceFunction 用于定义反射率、光滑度、透明度等表面属性,通常是名为 surf 的函数(函数名可以是任意的),他的函数格式的固定的:
1 | void surf(Input IN, inout SurfaceOutput o) |
在表面函数中,会使用输入结构体InputIN来设置各种表面属性,并把这些属性储存在输出结构体中,再传递给光照函数计算光照结果。
光照函数
光照函数会使用表面函数中设置的各种表面属性,来应用某些光照模型,进而模拟物体表面的光照效果。
- 基于物理的光照模型函数(UnityPBSLighting.cginc):
- Standard
- StandardSpecular
- 简单的非基于物理的光照模型函数(Lighting.cginc):
- Lambert
- BlinnPhong
还可以自定义光照函数,例如用下面的函数来定义用于前向渲染中的光照函数:
1 | // 用于不依赖视角的光照模型,例如漫反射 |
其他可选参数
可选参数包含了很多非常有用的指令类型,例如:
- 自定义的修改函数
- 表面函数
- 光照模型
- 顶点修改函数(vertex:VertexFunction):例如把顶点颜色传递给表面函数,修改顶点位置,实现某些顶点动画等
- 最后的颜色修改函数(finalcolor:ColorFunction):可以在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义的雾效等
- 阴影
- addshadow:为表面着色器生成一个阴影投射的Pass,可以对阴影的投射进行特殊处理(顶点动画或者透明度测试时)
- fullforwardshadows:可以在前向渲染路径中支持所有光源类型的阴影,默认只有最重要的平行光
- noshadow:禁用阴影
- 透明度混合和透明度测试
- alpha:透明度混合
- alphatest:VariableName:使用名为 VariableName 的变量来剔除不满足条件的片元
- 光照
- noambient:不要应用任何环境光照或光照探针(light probe)
- novertexlights:不要应用任何逐顶点光照
- noforwardadd:去掉所有前向渲染中的额外Pass,只支持一个逐像素平行光,其他按逐顶点或SH光源
- nolightmap/nofog,控制光照烘焙、雾效模拟
- 控制代码的生成
- 默认情况下会为一个表面着色器生成响应的前向渲染路径、延迟渲染路径使用的 Pass,这会导致生成的 Shader 文件比较大
- exclude_path:[deferred/forward/prepass],不要为这些渲染路径生成代码
两个结构体
表面着色器支持最多自定义4种关键的函数:表面函数,光照函数,顶点修改函数,最后的颜色修改函数。
需要通过两个结构体,在他们之间传递信息:
- 表面函数的输入结构体 Input
- 存储表面属性的结构体 SurfaceOutput(以及 SurfaceOutputStandard 和 SurfaceOutputStandardSpecular)
数据来源:Input 结构体
输入结构体Input包含了许多表面属性的数据来源,它会作为表面函数的输入结构体(如果自定义了顶点修改函数还会是顶点修改函数的输出结构体)。其中如果有纹理坐标,那命名规则为uv加纹理名称(当使用第二张纹理坐标时使用uv2加纹理名称)。下表列出了Input结构体中内置的其他变量(不需要手动计算,unity会准备好,但需要在 Input 结构体中严格声明这些变量):
变量 | 描述 |
---|---|
float3 viewDir | 包含了视角方向,可用于计算边缘光照等 |
使用COLOR语义定义的float4变量 | 包含了插值后的逐顶点颜色 |
float4 screenPos | 包含了屏幕空间的坐标,可以用于反射或屏幕特效 |
float3 worldPos | 包含了世界空间下的位置 |
float3 worldRefl | 包含了世界空间下的反射方向。前提是没有修改表面法线o.Normal |
float3 worldRefl;INTERNAL_DATA | 如果修改了表面法线o.Normal,需要使用该变量告诉Unity要基于修改后的法线计算世界空间下的反射方向。在表面函数中,我们需要使用WorldReflectionVector(IN, o.Normal)来得到世界空间下的反射方向 |
float3 worldNormal | 包含了世界空间下的法线方向。前提是没有修改表面法线o.Normal |
float3 worldNormal;INTERNAL_DATA | 如果修改了表面法线o.Normal,需要使用该变量告诉Unity要基于修改后的法线计算世界空间下的法线方向。在表面函数中,我们需要使用WorldNormalVector(IN, o.Normal)来得到世界空间下的法线方向 |
表面属性:SurfaceOutput 结构体
SurfaceOutput SurfaceOutputStandard SurfaceOutputStandardSpecular 作为表面函数的输出,随后作为光照函数的输入来进行光照计算。
相比 Input 结构体的自由性,这个结构体里面的变量是提前就声明好的,不能增加或减少。
SurfaceOutput 的声明可以在 Lighting.cginc 中找到:
1 | struct SurfaceOutput { |
SurfaceOutputStandard 和 SurfaceOutputStandardSpecular 的声明可以在 UnityPBSLighting.cginc 中找到:
1 | struct SurfaceOutputStandard |
使用哪个输出结构体,取决于我们选择使用的光照模型:
- 使用 SurfaceOutput:Unity5 之前的、简单的、非基于物理的光照模型。包括 Lambert 和 BlinnPhong
- 使用 SurfaceOutputStandard:Unity5添加的、基于物理的光照模型,Standard。用于默认的金属工作流程(Metallic Workflow)
- 使用 SurfaceOutputStandardSpecular:Unity5 添加的、基于物理的光照模型,StandardSpecular。用于高光工作流程(Specular Workflow)
以下是 SurfaceOutput 结构体中的变量和含义:
- fixed3 Albedo:对光源的反射率。通常由纹理采样和颜色属性的乘积计算而得
- fixed3 Normal:表面法线方向。
- fixed3 Emission:自发光。c.rgb += o.Emission;
- half Specular:高光反射中指数部分的系数。
- fixed Gloss:高光反射中的强度系数。
1
2// 内置的BlinnPhone光照函数
float spec = pow(nh, s.Specular * 128.0) * s.Gloss - fixed Alpha:透明通道。如果开启了透明度的话,会用该值进行颜色混合,否者默认会被重置为1(可配置)
时刻记着,表面着色器本质上就是包含了很多 Pass 的顶点/片元着色器(自动生成)。
Unity 背后做了什么
表面着色器中的各个函数、编译指令和结构体与顶点/片元着色器之间有什么关系呢?
- 有些 Pass 是为了针对不同的渲染路径
- 会前向渲染路径生成 LightMode 为 ForwardBase 和 ForwardAdd 的 Pass
- 为 Unity5 之前的延迟渲染路径生成 LightMode 为 PrePassBase 和 PrePassFinal 的 Pass
- 为 Unity5 之后的延迟渲染路径生成 LightMode 为 Deferred 的 Pass
- 一些 Pass 是用于产生额外的信息。例如为了给光照映射和动态全局光照提取表面信息,会生成一个 LightMode 为 Meta 的 Pass
- 有些表面着色器由于修改了顶点位置,因此可以使用 addshadow 编译指令为它生成相应的 LightMode 为 ShadowCaster 的阴影投射 Pass
在每个编译完成的表面着色器面板上,都有 Show generated code 按钮,单击可以看到 Unity 为它生成的所有顶点/片元着色器。
查看表面着色器生成的代码
Unity对ForwardBase的Pass自动生成过程大概如下
-
Unity会直接将表面着色器中CGPROGRAM和ENDCG之间的代码赋值过来,这些代码包括了我们对Input结构体,表面函数,光照函数等变量和函数的定义。这些函数和变量会在之后的处理过程中被当成正常的结构体和函数进行调用。
-
Unity会分析上述代码,并生成顶点着色器的输出,即v2f_surf结构体,Unity会分析我们在自定义函数中所使用的变量。而且,即便有时我们在Input中定义了某些变量(如某些纹理坐标),但是并没用到,Unity会舍弃这些变量,这相当于一种自动的优化。
-
生成顶点着色器,生成顶点着色器又可以分为以下几步。
- 如果我们定义了顶点修改函数,Unity会首先调用顶点修改函数来修改顶点数据,或填充自定义的Input结构体中的变量。然后,Unity会分析顶点修改函数中修改的数据,在需要时通过Input结构体将修改结果存储到v2f_surf相应的变量中。
- 计算v2f_surf中其他生成的变量值。这主要包括了顶点位置、纹理坐标、法线方向、逐顶点光照、光照纹理的采样坐标等。当然,我们可以通过编译指令来控制某些变量是否需要计算。
- 最后,将v2f_surf结构体传递给片元着色器。
-
生成片元着色器,生成片元着色器又可以分为以下几步。
- 使用v2f_surf中的对应的变量填充Input结构体,例如纹理坐标,视角方向等。
- 调用我们自定义的表面函数填充SurfaceOutput结构体。
- 调用光照函数得到初始的颜色值。如果使用的是内置的Lambert或者BlinnPhong光照,Unity还会计算动态全局光照,并添加到光照模型的计算中。
- 进行其他的颜色的叠加。例如,如果没有使用光照烘焙,还会添加逐顶点光照的影响。
- 最后,如果自定义了最后颜色修改函数,Unity就会调用它进行最后的颜色修改。其他Pass的生存过程和上面类似,在此不再赘述。
表面着色器的渲染计算流水线。黄色:可以自定义的函数。灰色:Unity自动生成的计算步骤
表面着色器实例分析
实例
实现的效果是对模型进行膨胀。原理是在顶点修改函数中,沿着顶点法线方向扩张顶点位置。本例中对表面着色器中4个可自定义的函数,全部采用了自定义的实现(顶点修改函数、表面函数、光照函数、最后的颜色修改函数)。
沿顶点法线对模型进行膨胀。左图:膨胀前。右图:膨胀后
1 | Shader "Unity Shaders Learn/Chapter17/NormalExtrusion" |
- 在顶点修改函数 myvert():使用顶点法线对顶点位置进行了膨胀
- 在表面函数 surf():使用主纹理设置了表面属性中的反射率,使用法线纹理设置了表面方向
- 在光照函数 LightingCustomLambert():实现了简单的兰伯特漫反射光照模型
- 在最后的颜色修改函数 mycolor():简单的使用了颜色参数对输出颜色进行调整
- pragma surface 编译指令:
- addshadow:生成一个该表面着色器对应的阴影投射 Pass,而不要依赖 FallBack 中找到的阴影投射 Pass
- exclude_path:deferred exclude_path:prepass:不要为延迟渲染路径生成相应的 Pass,为了缩小自动生成的代码量
- nometa:取消对提取元数据的 Pass 的生成
生成代码分析
生成了3个 Pass,分别是 LightMode 为 ForwardBase(前向渲染路径中处理逐像素的平行光)、ForwardAdd(处理其他逐像素光)、ShadowCaster(处理阴影投射)。代码中有大量的#ifdef和#if语句,可以判断一些渲染条件,如是否使用了动态光照纹理、是否使用了逐顶点光照、是否使用了屏幕空间的阴影等,unity会根据这些条件来进行不同的光照计算,把烦人的光照计算都交给Unity
以下分析 Unity生成的ForwardBase(2021.3.4f1)
-
首先指明了一些编译指令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// ---- forward rendering base pass:
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_instancing
#pragma multi_compile_fwdbase
#include "HLSLSupport.cginc"
#define UNITY_INSTANCED_LOD_FADE
#define UNITY_INSTANCED_SH
#define UNITY_INSTANCED_LIGHTMAPSTS
#include "UnityShaderVariables.cginc"
#include "UnityShaderUtilities.cginc"顶点着色器vert_surf和片元着色器frag_surf都是自动生成的
-
之后是一些自动生成的注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// Surface shader code generated based on:
// vertex modifier: 'myvert'
// writes to per-pixel normal: YES
// writes to emission: no
// writes to occlusion: no
// needs world space reflection vector: no
// needs world space normal vector: no
// needs screen space position: no
// needs world space position: no
// needs view direction: no
// needs world space view direction: no
// needs world space position for lighting: no
// needs world space view direction for lighting: no
// needs world space view direction for lightmaps: no
// needs vertex color: no
// needs VFACE: no
// needs SV_IsFrontFace: no
// passes tangent-to-world matrix to pixel shader: YES
// reads from normal: no
// 2 texcoords actually used
// float2 _MainTex
// float2 _BumpMap -
随后定义了一些宏来辅助计算:
1
2
3#define INTERNAL_DATA half3 internalSurfaceTtoW0; half3 internalSurfaceTtoW1; half3 internalSurfaceTtoW2;
#define WorldReflectionVector(data,normal) reflect (data.worldRefl, half3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal)))
#define WorldNormalVector(data,normal) fixed3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal))这些宏是为了修改表面法线的情况下,辅助计算得到世界空间下的反射方向和法线方向,与之对应的是Input结构体中的一些变量
-
接着 Unity 把我们在表面着色器中编写的 CG 代码复制过来,作为 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/* UNITY: Original start of shader */
//surf 指定表面着色器函数
//CustomLambert自定义光照函数
//vertex:myvert 自定义顶点修改函数
//finalcolor:mycolor 自定义颜色修改函数
//addshadow 由于我们修改了顶点位置,所以需要告诉unity重新生成shadowcaster,让阴影配合顶点位移
//exclude_path:deferred exclude_path:prepass不要为延迟路径生成相应的pass
//nometa 不需要为光照映射和全局光照提取表面信息
//#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
// Use shader model 3.0 target, to get nicer looking lighting
//#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _ColorTint;
half _Amount;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
void myvert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amount;
}
void surf (Input IN, inout SurfaceOutput o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten)
{
half NdotL = dot(s.Normal, lightDir);
half4 c;
c.rgb = s.Albedo * _LightColor0 * NdotL * atten;
c.a = s.Alpha;
return c;
}
void mycolor (Input IN, SurfaceOutput o, inout float4 color)
{
color *= _ColorTint;
} -
然后 Unity 定义了顶点着色器到片元着色器的插值结构体(即顶点着色器的输出结构体) v2f_surf。Unity使用#ifdef语句来判断是否使用了光照纹理,并为不同的情况生成不同的结构体:
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// vertex-to-fragment interpolation data
// no lightmaps:
#ifndef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
float4 tSpace0 : TEXCOORD1;
float4 tSpace1 : TEXCOORD2;
float4 tSpace2 : TEXCOORD3;
fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights
UNITY_LIGHTING_COORDS(5,6)
#if SHADER_TARGET >= 30
float4 lmap : TEXCOORD7;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
float4 tSpace0 : TEXCOORD1;
float4 tSpace1 : TEXCOORD2;
float4 tSpace2 : TEXCOORD3;
fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights
UNITY_SHADOW_COORDS(5)
#if SHADER_TARGET >= 30
float4 lmap : TEXCOORD6;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endif
// with lightmaps:
#ifdef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
float4 tSpace0 : TEXCOORD1;
float4 tSpace1 : TEXCOORD2;
float4 tSpace2 : TEXCOORD3;
float4 lmap : TEXCOORD4;
UNITY_LIGHTING_COORDS(5,6)
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
float4 tSpace0 : TEXCOORD1;
float4 tSpace1 : TEXCOORD2;
float4 tSpace2 : TEXCOORD3;
float4 lmap : TEXCOORD4;
UNITY_SHADOW_COORDS(5)
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endifpack0存储了主纹理和法线纹理的采样坐标,tSpace0、tSpace1、tSpace2是切线空间变换世界空间的矩阵以及世界坐标,vlight是逐顶点和SH光照的结果,在顶点着色器计算并在片元着色器与原光照结果叠加
-
随后 Unity 定义了真正的顶点着色器(首先会调用我们自定义的顶点修改函数):
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// vertex shader
v2f_surf vert_surf (appdata_full v) {
UNITY_SETUP_INSTANCE_ID(v);
v2f_surf o;
UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
UNITY_TRANSFER_INSTANCE_ID(v,o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
myvert (v);
o.pos = UnityObjectToClipPos(v.vertex);
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.pack0.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
o.tSpace0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.tSpace1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.tSpace2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
#ifdef DYNAMICLIGHTMAP_ON
o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
#ifdef LIGHTMAP_ON
o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
// SH/ambient and vertex lights
#ifndef LIGHTMAP_ON
#if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
float3 shlight = ShadeSH9 (float4(worldNormal,1.0));
o.vlight = shlight;
#else
o.vlight = 0.0;
#endif
#ifdef VERTEXLIGHT_ON
o.vlight += Shade4PointLights (
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos, worldNormal );
#endif // VERTEXLIGHT_ON
#endif // !LIGHTMAP_ON
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy); // pass shadow and, possibly, light cookie coordinates to pixel shader
return o;
}包含了变量的计算和判断是否使用了光照映射和动态光照映射,并在需要时把两种光照纹理的采样坐标计算结果储存在o.lmap中,如果没有光照映射就计算顶点的SH光照,结果储存在o.vlight,以及四个最重要的逐顶点光照
-
在 Pass 的最后,Unity 定义了真正的片元着色器:
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// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN);
// prepare and unpack data
Input surfIN;
#ifdef FOG_COMBINED_WITH_TSPACE
UNITY_RECONSTRUCT_TBN(IN);
#else
UNITY_EXTRACT_TBN(IN);
#endif
UNITY_INITIALIZE_OUTPUT(Input,surfIN);
surfIN.uv_MainTex.x = 1.0;
surfIN.uv_BumpMap.x = 1.0;
surfIN.uv_MainTex = IN.pack0.xy;
surfIN.uv_BumpMap = IN.pack0.zw;
float3 worldPos = float3(IN.tSpace0.w, IN.tSpace1.w, IN.tSpace2.w);
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
#ifdef UNITY_COMPILER_HLSL
SurfaceOutput o = (SurfaceOutput)0;
#else
SurfaceOutput o;
#endif
o.Albedo = 0.0;
o.Emission = 0.0;
o.Specular = 0.0;
o.Alpha = 0.0;
o.Gloss = 0.0;
fixed3 normalWorldVertex = fixed3(0,0,1);
o.Normal = fixed3(0,0,1);
// call surface function
surf (surfIN, o);
// compute lighting & shadowing factor
UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
fixed4 c = 0;
float3 worldN;
worldN.x = dot(_unity_tbn_0, o.Normal);
worldN.y = dot(_unity_tbn_1, o.Normal);
worldN.z = dot(_unity_tbn_2, o.Normal);
worldN = normalize(worldN);
o.Normal = worldN;
#ifndef LIGHTMAP_ON
c.rgb += o.Albedo * IN.vlight;
#endif // !LIGHTMAP_ON
// lightmaps
#ifdef LIGHTMAP_ON
#if DIRLIGHTMAP_COMBINED
// directional lightmaps
fixed4 lmtex = UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy);
fixed4 lmIndTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, IN.lmap.xy);
half3 lm = DecodeDirectionalLightmap (DecodeLightmap(lmtex), lmIndTex, o.Normal);
#else
// single lightmap
fixed4 lmtex = UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy);
fixed3 lm = DecodeLightmap (lmtex);
#endif
#endif // LIGHTMAP_ON
// realtime lighting: call lighting function
#ifndef LIGHTMAP_ON
c += LightingCustomLambert (o, lightDir, atten);
#else
c.a = o.Alpha;
#endif
#ifdef LIGHTMAP_ON
// combine lightmaps with realtime shadows
#ifdef SHADOWS_SCREEN
#if defined(UNITY_LIGHTMAP_DLDR_ENCODING)
c.rgb += o.Albedo * min(lm, atten*2);
#else
c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
#endif
#else // SHADOWS_SCREEN
c.rgb += o.Albedo * lm;
#endif // SHADOWS_SCREEN
#endif // LIGHTMAP_ON
#ifdef DYNAMICLIGHTMAP_ON
fixed4 dynlmtex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, IN.lmap.zw);
c.rgb += o.Albedo * DecodeRealtimeLightmap (dynlmtex);
#endif
mycolor (surfIN, o, c);
UNITY_OPAQUE_ALPHA(c.a);
return c;
}- 如果是HLSL,则使用更严格的声明方式来声明SurfaceOutput结构体
- 先对表面属性进行初始化,再用表面函数surf来填充这些表面属性
- 进行光照计算
- 判断是否使用光照映射(生成的代码好像与书中有冲突,以代码为主)
- 不使用,就把逐顶点的光照结果叠加到输出颜色中
- 使用,使用之前计算的光照纹理采样坐标,对光照纹理进行采样并解码,得到光照纹理中的光照结果
- 如果使用了动态光照映射,还会对动态光照纹理进行采样
- 调用自定义颜色修改函数
- UNITY_OPAQUE_ALPHA 是用来重置透明通道,默认情况下不透明类型都会被重置,
Surface Shader 的缺点
表面着色器给我们带来了很大的便利,但缺点也不少:
- 任何在表面着色器中完成的事情,都可以在顶点/片元着色器中重现,但反过来不成立。
- 表面着色器往往会对性能造成影响。例如移动平台的版本 Mobile/Diffuse 等只是去掉了额外的逐像素 Pass、不计算全局光照等,但没有深层的优化。
- 表面着色器无法完成一些自定义的渲染效果。例如 10.2.2 节中的透明玻璃效果。
因此,给出一些建议供读者参考:
- 如果你需要和各种光源打交道,尤其是想用 Unity 中的全局光照。可以使用表面着色器,但要时刻小心它的性能;
- 如果你需要处理的光源数目非常少,例如只有一个平行光,那么使用顶点/片元着色器更好;
- 最重要的是,如果有很多自定义的渲染效果,那么请选择顶点/片元着色器。