第十七章 Unity的表面着色器探秘
* 表面着色器(Surface Shader)*实际上就是在顶点/片元着色器之上又添加了一层抽象。
17.1 表面着色器的一个例子
Shader "Unity Shaders Book/Chapter 17/Bumped Diffuse" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _Color;
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf (Input IN, inout SurfaceOutput o) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb * _Color.rgb;
o.Alpha = tex.a * _Color.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}
从上面的例子可以看出,相比之前所学的顶点/片元着色器技术,表面着色器的代码量很少,而且,我们可以非常轻松地实现常见的光照模型,甚至不需要和任何光照变量打交道。
Shader "Unity Shaders Book/Common/Bumped Diffuse" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
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 = mul(UNITY_MATRIX_MVP, 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(_Object2World, 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));
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4(ambient + diffuse * 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;
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 = mul(UNITY_MATRIX_MVP, 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(_Object2World, 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));
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4(diffuse * atten, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
和顶点/片元着色器需要包含到一个特定的Pass块中不同,表面着色器的CG代码是直接而且也必须写在SubShader块中,Unity会在背后为我们生成多个Pass。当然,可以在SubShader一开始处使用Tags来设置该表面着色器使用的标签。在Chapter17-BumpedDiffuse中,我们还使用LOD命令设置了该表面着色器的LOD值(16.8.1)。然后,我们使用CGPROGRAM和ENDCG定义了表面着色器的具体代码。
一个表面着色器中最重要的部分是两个结构体以及他的编译指令。其中,两个结构体是表面着色器中不同函数之间信息传递的桥梁,而编译指令是我们和Unity沟通的重要手段。
17.2 编译指令
编译指令最重要的作用是指明该表面着色器使用的表面函数和关照函数,并设置一些可选参数。表面着色器的CG快中的第一句代码往往就是它的编译指令。格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
其中,#pragma surface 用于指明编译指令是用于定义表面着色器的。在它的后面需要指明使用的表面函数(surfaceFunction)和光照模型(lightModel),同时,还可以使用一些可选参数来控制表面着色器的一些行为。
17.2.1 表面函数
一个对象的表面属性定义了它的反射率、光滑度、透明度等值。而编译指令中的surfaceFunction就用于定义这些表面属性。surfaceFunction通常就是名为surf的函数(函数名可以是任意的),它的函数格式是固定的:
//表面输出
void surf (Input IN, inout SurfaceOutput 0)
//标准表面输出
void surf (Input IN, inout SurfaceOutputStandard 0)
//表面输出标准镜面
void surf (Input IN, inout SurfaceOutputStandardSpecular 0)
其中,后两个是Unity5中由于引入了基于物理的渲染而新添加的两种结构体。SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular都是Unity内置的结构体,它们需要配合不同的光照模型使用。
在表面函数中,会使用输入结构体Input IN来设置各种表面属性,并把这些属性存储在输出结构体SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular中,再传递给光照函数计算光照结果。我们可以在Unity手册中的表面着色器的例子一文(http://docs.unity3d.com/Manual/SL_SurfaceShaderExamples.html)中找到更多的实例表面函数。
17.2.2 光照函数
除了表面话函数,我们还需要指定另一个非常重要的函数——光照函数。光照函数会使用表面函数中设置的各种表面属性,来应用某些光照模型,进而模拟物体表面到光照效果。Unity内置了基于物理的光照模型函数Standard和StandardSpecular(在Unity PBSLightiong.cginc文件中被定义)。例如。在Chapter17-BumpedDiffuse中,我们就指定了Lambert光照函数。
当然,我们也可以定义自己的光照函数。例如,可以使用下面的函数来定义用于前向渲染中的光照函数:
//用于不依赖视角的光照模型,例如漫反射
half4 Lighting<Name>(SurfaceOutput s,half3 lightDir, half atten);
//用于依赖视角的光照模型,例如高光反射
half4 Lighting<Name>(SurfaceOutput s,half3 lightDir,half3 viewDir,half atten)‘
我们可以在Unity手册的表面着色器中的自定义光照模型一文(http://docs.unity3d.com/Manual/SL_SurfaceShaderLighting.html)中找到更全面的自定义光照模型的介绍。而一些例子可以参见手册中的表面着色器的光照例子一文(http://docs.unity3d.com/Manual/SL_SurfaceShaderLightingExamples.html),这篇文档展示了如何使用表面着色器来定义常见的漫反射、高光反射、基于光照纹理等常用的光照模型。
17.2.3 其他可选参数
在编译指令的租后,我们还可以设置一些可选参数(optionalparams)。这些可选参数包含了很多非常有用的指令类型,例如。开启/设置透明度混合/透明度测试,指定自定义的顶点和颜色修改含函数,控制生成的代码等。下面,我们选取了一些比较重要和常用的参数进行更深入的说明。在Unity官方手册的编写表面着色器一文(http://docs.unity3d.com/Manual/SL_SurfaceShaders.html)中找到更加详细的参数和设置说明。
·自定义的修改函数。除了表面函数和光照模型外,表面着色器还可以支持其他两种自定义的函数顶点修改函数(Vertex:VertexFunction)和最后到颜色修改函数(finalcolor:ColorFunction)。顶点修改函数允许我们自定义一些顶点属性,例如,把顶点颜色传递给表面函数,或是修改顶点位置,实现某些顶点动画等。最后的颜色修改函数则可以在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义的雾效等。
·阴影。我们可以通过一些指令来控制和阴影相关的代码。例如,addshadow参数会为表面着色器生成一个阴影投射的Pass。通常情况下,Unity可以直接在FallBack中找到通用的光照模式为ShadowCaster的Pass,从而将物体正确地渲染到深度和阴影纹理中(9.4)。但对于一些进行了顶点动画、透明度测试的物体,我们就需要对阴影的投射的特殊处理,来为他们产生正确的阴影,正如在11.3.3中看到的一样。fullforwardshadows参数则可以在前向渲染路径中支持所有光源类型的阴影。默认情况下,Unity只支持最重要的平行关的阴影效果。如果我们需要让点光源或聚光灯在前向渲染中也可以有阴影,就可以添加这个参数。相反的,如果我们不想对使用这个Shader的物体进行任何阴影计算,就可以使用noshadow参数来禁用阴影。
·透明度混合和透明度测试。我们可以通过alpha和alphatest指令来控制透明度混合和透明度测试。例如,alphatest:VariableName指令会使用名为VariableName的变量来剔除不满足条件的片元。此时,我们可能还需要使用上面提到的addshadow参数来生成正确的阴影投射的Pass。
·光照。一些指令可以控制光照对物体的影响,例如,noambient参数会告诉Unity不要应用任何环境光照或光照探针(light probe)。novertexlights参数告诉Unity不要应用任何逐顶点光照。noforwardadd会去掉所有前向渲染中的额外的Pass。也就是说,这个Shader只会支持一个逐像素的平行光,而其他的光源会按照逐顶点或SH的方法来计算光照影响。这个参数通常会用于移动平台版本的表面着色器中。还有一些用于控制光照烘焙、雾效模拟的参数,如nolightmap、nofog等。
·控制代码得到生成。一些指令还可以控制由表面着色器自动生成的代码,默认情况下,Unity会为一个表面着色器生成相应的前行渲染路径、延迟渲染路径使用的Pass,这会导致生成的Shader文件比较大。如果我们确定该表面着色器只会在某些渲染路径中使用,就可以exclude_path:deferred、exclude_path:forward和exclude_path:prepass来告诉Unity不需要为某些渲染路径生成代码。
17.3 两个结构体
表面着色器支持最多自定义4种关键的函数:表面函数(用于设置各种表面性质,如反射率、法线等),光照函数(定义表面使用的光照模型),顶点修改函数(修改或传递顶点属性),最后的颜色修改函数(对最后的颜色进行修改)。那么,这些函数之间的信息传递怎么实现的呢?例如,我们想把顶点颜色传递给表面颜色,添加到表面反射率的计算中,要怎么做呢?这就是两个结构体的工作。
一个表面着色器需要使用两个结构体:表面函数的输入结构体Input,以及存储了表面属性的结构体SurfaceOutput(Unity5新引入了另外两个同种的结构体SurfaceOutputStandard和SurfaceOutputStandardSpecular)。
17.3.1 数据来源:input结构体
Input结构体包含了许多表面属性的数据来源,因此,它会作为表面函数的输入结构体(如果自定义了顶点修改函数,他还会是顶点修改函数的输出结构体)。Input支持很多内置的变量名,通过这些变量名,我们告诉Unity需要使用的数据信息。例如,在Chapter17-BumpedDiffuse中,Input结构体中包含了主纹理和法线纹理的采样坐标uv_MainTex和uv_BumpMap。这些采样坐标必须以“uv”为前缀(实际上也可用“uv2”为前缀,表面使用次纹理坐标集合),后面紧跟纹理名称。以主纹理_MainTex为例,如果需要使用它的采样坐标,就需要在Input结构体中声明float2 uv_MainTex来对应它的采样坐标。
需要注意的是,我们并不需要自己计算上述的各个变量,而只需要在input结构体中按上述名称严格声明这些变量即可,unity会在背后为我们准备好这些数据,而我们只需要在表面函数中直接使用他们即可。一个例外情况是,我们自定义了顶点修改函数,并需要向表面函数中传递一些自定义的数据。例如,为了自定义雾效,我们可能需要在顶点修改函数中根据顶点在视角空间下的位置信息计算雾效混合系列,这样我们就可以在Input结构体中定义一个名为halffog的变量,把计算结果存储在该变量后进行输出。
17.3.2 表面属性:SurfaceOutput结构体
有了Input结构体来提供所需要的数据后,我们就可以据此计算各种表面属性。因此,另一个结构体就是用于存储这些表面属性的结构体,即SurfaceOutput、SurfaceOutputStandard、和SurfaceOutputStandardSpecular,它会作为表面函数的输出,随后会作为光照函数的输入来进行各种光照计算。相比与Input结构体的自由性,这个结构体里面的变量是前提就声明好的,不可以增加也不会减少(如果没有对某些赋值,就会使用默认值)。SurfaceOutput的声明可以在Lighting.cginc文件中找到:
struct SurfaceOutput{
fixed3 Albedo; //反射率
fixed3 Normal; //法线
fixed3 Emission; //自发光
half Specular; //镜面
fixed Gloss; //光泽
fixed Alpha; //α
}
而SurfaceOutputStandard和SurfaceOutputStandardSpecular的声明可以在UnityPBSLighting.cginc中找到:
struct SurfaceOutputStandard{
fixed3 Albedo; //基地(漫射光或反射光)的颜色
fixed3 Normal; //切线空间正常,如果写的
fixed3 Emission;
half Metallic; //0 =非金属,1 =金属
half Smoothness; //0 =粗糙,1 =光滑
half Occlusion; // 闭塞(默认为1)
fixed Alpha; //α为幻灯片
}
struct SurfaceOutputStandardSpecular{
fixed3 Albedo; //漫射颜色
fixed3 Specular; //高光颜色
fixed3 Normal; //切线空间正常,如果写的
fixed3 Emission;
half Smoothness; //0 =粗糙,1 =光滑
half Occlusion; // 闭塞(默认为1)
fixed Alpha; //α为幻灯片
}
在一个表面着色器中,只需要选择上述三者中的其一即可,这取决于我们选择使用的光照模型。Unity内置的光照模型有两种,一种是Unity5之前的、简单的、非基于物理的光照模型,包括了Lambert和BlinnPhong;另一种是Unity5添加的、基于物理的光照模型,包括Standard和StandardSpecular,这种模型会更加符合物理规律,但计算也会复杂很多。如果使用了非基于物理的光照模型,我们通常会使用SurfaceOutput结构体,而如果使用了基于物理的光照模型Standard或StandardSpecular,我们会分别使用SurfaceOutputStandard或SurfaceOutputStandardSpecular结构体。其中,SurfaceOutputStandard结构体用于默认的金属工作流程(Metallic Workflow),对应了Standard光照模型;而SurfaceOutputStandardSpecular结构体用于高光工作流程(Specular Workflow),对应了StandardSpecular光照函数。更多关于基于物理的渲染内容,我们会在第18章中了解到。
这里我们着重了解一下SurfaceOutput结构体中的变量和含义。在表面函数中,我们需要根据Input结构体传递的各个变量计算表面属性。在SurfaceOutput结构体,这些表面属性包括了。
·fixed3 Albedo:对光源的反射率。通常由纹理采样和颜色属性的乘积计算而得。
·fixed3 Normal:表面法线方向。
·fixed3 Emission:自发光。Unity通常会在片元着色器最后输出前(并在最后的顶点函数被调用前,如果定义了的话),使用类似下面的语句惊进行简单的颜色叠加:
c.rgb += o.Emission;
·half Specular:高光反射中的指数部分的系数,影响高光反射的计算。例如。如果使用了内置的BlinnPhong光照函数,它会使用如下语句计算高光反射的强度:
float spec = pow (nh,s.Specular*128.0)*s.Gloss;
·fixed Gloss:高关反射中的强度系数。和上面的Specular类似,计算公式见上面的代码。一般在包含了高关反射的光照模型里使用。
·fixed Alpha:透明通道。如果开启了透明度的话,会使用该值进行颜色混合。
(时刻记着,表面着色器本质上就是包含了很多Pass的顶点/片元着色器)。
17.4 Unity背后做了什么
Unity在背后会根据表面着色器生成一个包含了很对Pass的顶点/片元着色器。这些Pass有些是为了针对不同的渲染路径,例如,默认情况下Unity会为前向渲染路径生成LightMode为ForwardBase和ForwardAdd的Pass,为Unity5之后的延迟渲染路径生成LightMode为Deferred的Pass。还有一些Pass是用于产生额外的信息,例如,为了给光照映射和动态全局光照提取表面信息,Unity会生成一个LightMode为Meta的Pass。有些表面着色器由于修改了顶点位置,因此,我们可以利用adddshadow编译指令为它生成相应的LightMode为ShadowCaster的阴影投射Pass。这些Pass的生成都是基于我们在表面着色器中的编译指令和自定义的函数,这是由规律可循的。Unity提供了一个功能,让我们可以对表面着色器自动生成的代码一探究竟:在每个编译完成的表面着色器的面板上,都有一个“Show generated code”的按钮。我们只要单击一下它就可以看到Unity为这个表面着色器生成的所有顶点/片元着着色器。
Unity对该Pass的自动生成过程大致如下。
(1)直接将表面着色器中CGPROGRAM和ENDCG之间的代码复制过来,这些代码包括了我们对Input结构体、表面函数、光照函数(如果自定义了的话)等变量和函数的定义。这些函数和变量会在之后的处理过程中被当成正常的结构体和函数进行调用。
(2)Unity会分析上述代码,并据此生成顶点着色器的输出——v2f_surf结构体,用于在顶点着色器和片元着色器之间进行数据传递。Unity会分析我们在自定义函数中所使用的变量,例如,纹理坐标、视角方向、反射方向等。如果需要,它就会在v2f_surf中生成相应的变量。而且,即便有时我们在Input中定义了某些变量(如某些纹理坐标),但Unity在分析后续代码时发现我们并没有使用这些变量,那么这些变量实际上是不会在v2f_surf中生成的。也就是说,Unity做了一些优化。v2f_surf中还包含了一些其他需要的变量,例如阴影纹理坐标、光照纹理坐标、逐顶点光照等。
(3)接着,生成顶点着色器。
1.如果我们自定义了顶点修改函数,unity会首先调用顶点修改函数来修改顶点数据,或填充自定义的Input结构体中的变量。然后,Unity会分析顶点修改函数中修改的数据,在需要时通过Inout结构体将修改结果存储到v2f_surf相应的变量中。
2.计算v2f_surf中其他生成的变量值。这主要包括了顶点位置、纹理坐标、法线方向、逐顶点光照、光照纹理的采样坐标等。当然,我们可以通过编译指令来控制某些变量是否需要计算。
3.最后,将v2f_surf传递给接下来的片元着色器。
(4)生成片元着色器。
1.使用v2f_surf中的对应变量填充Input结构体,例如,纹理坐标、视角方向等。
2.调用我们自定义的表面函数填充SurfaceOutput结构体。
3.调用光照函数得到初始的颜色值。如果使用的是内置的Lambert或BlinnPhong光照函数,Unity还会计算动态全局光照,并添加到光照模型的计算中。
4进行其他的颜色叠加。例如。如果没有使用光照烘焙,还会添加逐顶点光照的影响。
5最后,如果自定义了最后的颜色修改函数,Unity就会调用它进行最后的颜色修改。
我们将场景Scene_17_1中的代码进行分析源代码如下:
Shader "Unity Shaders Book/Chapter 17/Bumped Diffuse" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _Color;
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf (Input IN, inout SurfaceOutput o) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb * _Color.rgb;
o.Alpha = tex.a * _Color.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}
17.5 表面着色器实例分析
我们将场景Scene_17_4中的代码进行分析,它实现的效果是对模型进行膨胀,如下图所示:
这种效果的实现非常简单,就是在顶点修改函数中沿着顶点法线方向扩张顶点位置。为了分析表面着色器中4个可定义函数(顶点修改函数、表面函数、光照函数和最后的颜色修改函数)的原理,在本例中我们对这四个函数全部采用了自定义的实现。在Chapter17-NormalExtrusion文件中找到该表面着色器,它的代码如下:
Shader "Unity Shaders Book/Chapter 17/Normal Extrusion" {
Properties {
_ColorTint ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
//冲浪-哪个表面功能。
//CustomLambert(定制的兰伯特)-使用的照明模型。
//顶点:myvert-使用自定义顶点修改函数。
//finalcolor:mycolor——使用自定义的最终颜色修改功能。
//添加阴影-产生一个影子投射者通过。因为我们修改了顶点的位置,shder需要特殊的阴影处理。
//独占路径:延迟/排除路径:prepas——不要为延迟/遗留递延渲染路径生成通行证。
//nometa-不要产生一个“元”通道(这是由光映射和动态全局照明来提取表面信息)。
#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
#pragma target 3.0
fixed4 _ColorTint;
sampler2D _MainTex;
sampler2D _BumpMap;
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) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb;
o.Alpha = tex.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.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
//光照函数实现了简单的兰伯特漫反射光照模式
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
color *= _ColorTint;
}
//在最后的颜色修改函数中,我们简单地使用了颜色参数对输出颜色进行调整。
//除了四个函数外,我们在#pragma surface的编译指令一行中还指定了一些额外的参数。由于我们
//修改了顶点位置,因此,要对其他物体产生正确的阴影效果并不能直接依赖FallBack中找到的阴影
//投射Pass,addshadow参数可以告诉Unity要生成一个该表面着色器对用的阴影投射Pass。默认情况下,
//Unity会为所有支持的渲染路径生成相应的Pass,为了缩小自动生成的代码量,我们
//使用exclude_pach:deferred和exclude_pach:prepass来告诉Unity不要为延迟渲染路径生成相应的
//Pass。最后,我们使用nometa参数取消对提取原数据的Pass的生成。
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}
然后单击“Show generated code”按钮后,我们可以看到unity生成的顶点/片元着色器。
在这个将近600行代码的文件中,Unity一共为该表面着色器生成了3个Pass,它们的LightMode分别是ForwardBas、ForwardAdd和ShadowCaster,分别对应了前向渲染路径中的处理逐像素平行光的Pass、处理其他逐像素光的Pass、处理阴影投射的Pass。这些Pass的原理可以回顾9.1.1和9.4中的相关内容。读者可以在这些代码中看到大量的#ifdef和#if语句,这些语句可以判断一些渲染条件,例如,是否使用了动态光照纹理、是否使用了逐顶点光照、是否使用了屏幕空间的阴影等,Unity会根据这些条件来进行不同的光照计算,这正是表面着色器的魅力之一——把这些烦人的光照计算交给Unity来做!
Shader "Unity Shaders Book/Chapter 17/Normal Extrusion" {
Properties {
_ColorTint ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
// ------------------------------------------------------------
// Surface shader code generated out of a CGPROGRAM block:
// ----前方呈现基地通过:
//首先指明了一些编译指令:
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_fwdbase
#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"
//自动生成的注释:
//表面着色代码,基于:
//顶点修饰符:“myvert”
//写入每个像素的正常值:是的
//写到排放:不
//需要世界空间反射矢量:不
//需要世界空间法向量:不
//需要屏幕空间位置:不
//需要世界空间位置:不
//需要视图方向:不
//需要世界空间视图方向:不
//需要世界空间照明的位置:不
//需要世界空间视角的照明方向:不
//需要世界空间视角的光图方向:不
//需要顶点颜色:不
//需要VFACE:没有
//通过切线到世界的矩阵到像素着色器:是的
//从正常情况下阅读:不
//2个texcoords实际使用
// float2 _MainTex
// float2 _BumpMap
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
//顶点着色器vert_surf和片元着色器frag_surf都是自动生成的。
//尽管这些对渲染结果没有影响,但我们可以从这些注释中理解到Unity的分析过程和它的分析救结果。
//随后,Unity定义了一些宏来辅助计算:
#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结构体中的一些变量(17.3.1)/
//接着,Unity把我们在表面着色器中编写的CG代码复制过来,作为Pass的一部分,以便后续调用
// 原始表面着色器代码片段:
#line 10 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif
// surf - which surface function.
// CustomLambert - which lighting model to use.
// vertex:myvert - use custom vertex modification function.
// finalcolor:mycolor - use custom final color modification function.
// addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling.
// exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path.
// nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information).
//#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
//#pragma target 3.0
fixed4 _ColorTint;
sampler2D _MainTex;
sampler2D _BumpMap;
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) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb;
o.Alpha = tex.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.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
color *= _ColorTint;
}
//然后,Unity定义了顶点着色器到片元着色器的插值结构体(即顶点着色器的输出结构体)
//v2f_surf。在定义之前,unity使用#ifdef语句来判断是否使用了光照纹理,并为不同的情况生成
//不同的结构体。主要的区别是,如果没有使用光照纹理,就需要定要一个储存逐顶点和SH光照的变量
// vertex-to-fragment插值数据
// 没有光照贴图
#ifdef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
fixed3 tSpace0 : TEXCOORD1;
fixed3 tSpace1 : TEXCOORD2;
fixed3 tSpace2 : TEXCOORD3;
fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights
SHADOW_COORDS(5)
#if SHADER_TARGET >= 30
float4 lmap : TEXCOORD6;
#endif
};
#endif
//使用光照贴图
#ifndef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
fixed3 tSpace0 : TEXCOORD1;
fixed3 tSpace1 : TEXCOORD2;
fixed3 tSpace2 : TEXCOORD3;
float4 lmap : TEXCOORD4;
SHADOW_COORDS(5)
};
#endif
//上面很多变量名看起来很陌生,但实际上大部分变量的含义我们在之前都碰到过,只是这里使用了不同的
//名称而已。例如,在下面我们会看到,pack0中实际上存储的就是主纹理和法线纹理的采样坐标,
//而tSpace0、tSpace1和tSpace2存储了从切线空间到世界空间的变换矩阵。一个比较陌生
//的变量是vlight,Uinty会把逐顶点和SH光照的结果存储到该变量里,并在拍
//片元着色器中和原光照结果进行叠加。
float4 _MainTex_ST;
float4 _BumpMap_ST;
//随后,Unity定义了真正的顶点着色器。顶点着色器首先会调用我们自定义的顶点修改函数来修改一些顶点属性:
// 顶点着色器
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
myvert (v);
//之后的代码是用于计算v2f_surf中各个变量的值。例如,计算经过MVP矩阵变换后的顶点坐标;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
//使用TRANSFORM_TEX内置宏计算两个纹理的采样坐标,并分别储存在 o.pack0的xy分量和zw分量中;
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.pack0.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
//计算从切线空间到世界空间的变换矩阵,并把矩阵的每一行分别存储在 o.tSpace0、 o.tSpace11和o.tSpace2变量中
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 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 = fixed3(worldTangent.x, worldBinormal.x, worldNormal.x);
o.tSpace1 = fixed3(worldTangent.y, worldBinormal.y, worldNormal.y);
o.tSpace2 = fixed3(worldTangent.z, worldBinormal.z, worldNormal.z);
//判断是否使用了光照映射和动态光照映射,并在需要时把两种光照纹理的采样坐标计算结果
//存储在o.lmap.xy和o.lmap.zw分量中;
#ifndef DYNAMICLIGHTMAP_OFF
o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
#ifndef LIGHTMAP_OFF
o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
//判断使否使用了光照映射,如果没有的话就计算该顶点的SH光照(一种快速就算光照的方法)
//把结果存储到 o.vlight中;
// SH /环境和顶点灯光
#ifdef LIGHTMAP_OFF
#if UNITY_SHOULD_SAMPLE_SH
float3 shlight = ShadeSH9 (float4(worldNormal,1.0));
o.vlight = shlight;
#else
o.vlight = 0.0;
#endif
//判断是否开启了逐顶点光照,如果是就计算最重要的4个逐顶点光照的光照结果,把结果叠加到o.vlight中。
#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_OFF
//最后,计算阴影坐标并传递给片元着色器
TRANSFER_SHADOW(o); //将阴影坐标传递给像素着色器
return o;
}
//在Pass的最后,Unity定义了真正的片元着色器。Unity首先利用插值后的结构体v2f_surf来初始化Input结构体中的变量:
// 片元着色器
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// 准备并解压缩数据
Input surfIN;
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 lightDir = _WorldSpaceLightPos0.xyz;
//随后声明一个SurfaceOutput结构体的变量,并对其中的表面属性进行了初始化,再调用了表面函数:
#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);
// 调用表面函数
surf (surfIN, o);
//在上面的代码中,Unity还使用#ifdef语句判断当前的编译语言类型是否是HLSL,,如果是就使用饿
//更严格的声明方式来声明SurfaceOutput结构体(因为DirectX平台往往有更加严格的语义要求)。
//当对各个表面属性进行初始化后,Unity调用了表面函数surf来填充这些表面属性。
//之后,Unity进行了真正的光照计算。首先,计算得到了光照衰减和世界空间下的法线方向
// 计算照明和阴影系数
UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
fixed4 c = 0;
fixed3 worldN;
worldN.x = dot(IN.tSpace0.xyz, o.Normal);
worldN.y = dot(IN.tSpace1.xyz, o.Normal);
worldN.z = dot(IN.tSpace2.xyz, o.Normal);
o.Normal = worldN;
//其中,变量c用于存储最终颜色的输出颜色,此时被初始化为0。随后,Unity判断是否
//关闭了光照映射,如果关闭了,就把逐顶点的光照结果叠加到输出颜色中:
#ifdef LIGHTMAP_OFF
c.rgb += o.Albedo * IN.vlight;
#endif // LIGHTMAP_OFF
//而如果需要使用光照映射,Unity就会使用之前计算的光照纹理采样坐标,对光照纹理进行采样并解码,得到光照纹理中的光照结果。
// lightmaps
#ifndef LIGHTMAP_OFF
#ifdef DIRLIGHTMAP_OFF
// single lightmap
fixed4 lmtex = UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy);
fixed3 lm = DecodeLightmap (lmtex);
#elif 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);
#elif DIRLIGHTMAP_SEPARATE
// directional with specular - no support
half4 lmtex = 0;
half3 lm = 0;
#endif // DIRLIGHTMAP_OFF
#endif // LIGHTMAP_OFF
//如果没有使用光照映射,意味着我们需要使用自定义的光照模型计算光照结果;
// 实时照明:呼叫照明功能
#ifdef LIGHTMAP_OFF
c += LightingCustomLambert (o, lightDir, atten);
#else
c.a = o.Alpha;
#endif
//而如果使用了光照映射的话,Unity会根据之前由光照纹理得到的结果得到颜色值,并叠加到输出
//颜色c中。如果还开启了动态光照映射,Unity还会计算动态光照纹理的想采样结果,同样把
//结果叠加到输出颜色c中。
#ifndef LIGHTMAP_OFF
//将光图与实时阴影相结合
#ifdef SHADOWS_SCREEN
#if defined(UNITY_NO_RGBM)
c.rgb += o.Albedo * min(lm, atten*2);
#else
c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
#endif
#else //阴影的屏幕
c.rgb += o.Albedo * lm;
#endif // 阴影的屏幕
#endif // LIGHTMAP_OFF
#ifndef DYNAMICLIGHTMAP_OFF
fixed4 dynlmtex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, IN.lmap.zw);
c.rgb += o.Albedo * DecodeRealtimeLightmap (dynlmtex);
#endif
//最后,Unity调用自定义的颜色修改函数,对输出颜色c进行最后的修改:
mycolor (surfIN, o, c);
UNITY_OPAQUE_ALPHA(c.a);
return c;
}
//在上面的代码中,Unity还使用了内置宏UNITY_OPAQUE_ALPHA(在UnityCG.cginc里被定义)
//来重置片元的透明通道。在默认情况下,所有不透明类型的表面器的透明通道都会被重置
//为1.0,而不管我们是否在光照函数中改变了它,如上所示。如果我们想要保留它的透明通道的话,
//可以在表面着色器的编译指令中添加keepalpha参数。
ENDCG
//至此,ForwardBase Pass就结束了。接下来的ForwardAdd Pass和上面的ForwardBase Pass基本类似。
//只是代码更加简单了,Unity去掉了对逐顶点光照和各种判断是否使用了光照映射的代码,因为
//这些额外的Pass不需要考虑这些。
//最后一个重要的Pass是ShadowCaster Pass。相比于之前的两个Pass,它的代码比较简单短小。它的生成原理很简单,
//就是通过调用自定义的顶点修改函数来保证计算阴影时使用的是和之前一致的顶点坐标。正如我们在
//11.3.3和15.1中看到的一样,这个自定义的阴影投影的Pass同样使用了内置的
//V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET和SHADOW_CASTER_FRAGMENT
//来计算阴影投射。
}
// ---- 前向渲染加亮灯通过:
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardAdd" }
ZWrite Off Blend One One
CGPROGRAM
// 编译指令
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_fwdadd
#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"
// Surface shader code generated based on:
// vertex modifier: 'myvert'
// writes to per-pixel normal: YES
// writes to emission: 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
// passes tangent-to-world matrix to pixel shader: YES
// reads from normal: no
// 2 texcoords actually used
// float2 _MainTex
// float2 _BumpMap
#define UNITY_PASS_FORWARDADD
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#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))
// 原始表面着色器代码片段:
#line 10 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif
// surf - which surface function.
// CustomLambert - which lighting model to use.
// vertex:myvert - use custom vertex modification function.
// finalcolor:mycolor - use custom final color modification function.
// addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling.
// exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path.
// nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information).
//#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
//#pragma target 3.0
fixed4 _ColorTint;
sampler2D _MainTex;
sampler2D _BumpMap;
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) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb;
o.Alpha = tex.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.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
color *= _ColorTint;
}
//vertex-to-fragment插值数据
struct v2f_surf {
float4 pos : SV_POSITION;
float4 pack0 : TEXCOORD0; // _MainTex _BumpMap
fixed3 tSpace0 : TEXCOORD1;
fixed3 tSpace1 : TEXCOORD2;
fixed3 tSpace2 : TEXCOORD3;
float3 worldPos : TEXCOORD4;
SHADOW_COORDS(5)
};
float4 _MainTex_ST;
float4 _BumpMap_ST;
// 顶点着色器
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
myvert (v);
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.pack0.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 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 = fixed3(worldTangent.x, worldBinormal.x, worldNormal.x);
o.tSpace1 = fixed3(worldTangent.y, worldBinormal.y, worldNormal.y);
o.tSpace2 = fixed3(worldTangent.z, worldBinormal.z, worldNormal.z);
o.worldPos = worldPos;
TRANSFER_SHADOW(o); // 将阴影坐标传递给像素着色器
return o;
}
//片元着色器
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// 准备并解压缩数据
Input surfIN;
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 = IN.worldPos;
#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);
//调用表面函数
surf (surfIN, o);
UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
fixed4 c = 0;
fixed3 worldN;
worldN.x = dot(IN.tSpace0.xyz, o.Normal);
worldN.y = dot(IN.tSpace1.xyz, o.Normal);
worldN.z = dot(IN.tSpace2.xyz, o.Normal);
o.Normal = worldN;
c += LightingCustomLambert (o, lightDir, atten);
c.a = 0.0;
mycolor (surfIN, o, c);
UNITY_OPAQUE_ALPHA(c.a);
return c;
}
ENDCG
}
// ---- 影子施法者通过:
Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On ZTest LEqual
CGPROGRAM
// 编译指令
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_shadowcaster
#pragma skip_variants FOG_LINEAR FOG_EXP FOG_EXP2
#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"
// Surface shader code generated based on:
// vertex modifier: 'myvert'
// writes to per-pixel normal: YES
// writes to emission: 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
// passes tangent-to-world matrix to pixel shader: YES
// reads from normal: no
// 0 texcoords actually used
#define UNITY_PASS_SHADOWCASTER
#include "UnityCG.cginc"
#include "Lighting.cginc"
#define INTERNAL_DATA
#define WorldReflectionVector(data,normal) data.worldRefl
#define WorldNormalVector(data,normal) normal
// 原始表面着色器代码片段:
#line 10 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif
// surf - which surface function.
// CustomLambert - which lighting model to use.
// vertex:myvert - use custom vertex modification function.
// finalcolor:mycolor - use custom final color modification function.
// addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling.
// exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path.
// nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information).
//#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa
//#pragma target 3.0
fixed4 _ColorTint;
sampler2D _MainTex;
sampler2D _BumpMap;
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) {
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = tex.rgb;
o.Alpha = tex.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.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
color *= _ColorTint;
}
// vertex-to-fragment插值数据
struct v2f_surf {
V2F_SHADOW_CASTER;
};
// 顶点着色器
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
myvert (v);
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
//片元着色器
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// 准备并解压缩数据
Input surfIN;
UNITY_INITIALIZE_OUTPUT(Input,surfIN);
surfIN.uv_MainTex.x = 1.0;
surfIN.uv_BumpMap.x = 1.0;
#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);
// 调用表面函数
surf (surfIN, o);
SHADOW_CASTER_FRAGMENT(IN)
}
ENDCG
}
// ----表面着色器生成代码的结束
#LINE 57
}
FallBack "Legacy Shaders/Diffuse"
}
17.6 Surface Shader的缺点
表面着色器然可以快速实现各种光照效果,但我们失去了对各种优化和各种特效实现的控制。因此,使用表面着色器往往会对性能造成一定的影响,而内置的shader,例如Diffuse、Bumped Specular等都是使用表面着色器编写的。尽管Unity提供了移动平台的相应版本,例如Mobile/Diffuse和Mobile/Bumped Specular等,但这些版本的Shader往往只是去掉了额外的逐像素Pass、不计算全局光照和其他一些光照计算上的优化。但要想进行更多深层的优化,表面着色器就不能满足我们的需求了。
除了性能比较差以外,表面着色器还无法完成一些自定义的渲染结果,例如10.2.2中透明玻璃的效果。表面着色器的这些缺点让很多人跟愿意使用自由的顶点/片元着色器来实现各种效果,尽管处理光照时这可能难度更大些。
PS:
·如果你需要和各种光源打交道,尤其是想要使用Unity中的全局光照的话,你可能更喜欢使用表面着色器,但要时刻小心他的性能。
·如果你需要处理的光源数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
·最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。