将渲染流程劃分為表面着色器、光照模型和光照着色器這樣的層面。
其中,表面着色器定義了模型表面的反射率、法線和高光等,光照模型選擇是使用蘭伯特還是Blinn-Phong等模型。而光照着色器負責計算光照衰減、陰影等。
表面着色器實際上就是在頂點/片元着色器之上又添加了一層抽象。
表面着色器的一個例子
使用表面着色器來實作一個使用了法線紋理的漫反射效果。
Shader "Custom/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"
}
表面着色器代碼很少,非常輕松地實作了常見地光照模型,甚至不需要和任何光照變量打交道,Unity就幫我們處理好了每個光源的光照結果。
表面着色器例子:表面着色器
和頂點/片元着色器需要包含到一個特定的Pass塊不同,表面着色器的CG代碼是直接而且也必須寫在SubShader塊中,Unity會在背後為我們生成多個Pass。
也可以在SubShader一開始處使用Tags來設定該表面着色器使用的标簽。
使用了LOD指令設定了該表面着色器的LOD值。然後使用CGPROGRAM和ENDCG定義了表面着色器的具體代碼。
一個表面着色器最重要的部分是兩個結構體以及它的編譯指令。其中,兩個結構體是表面着色器中不同函數之間資訊傳遞的橋梁,而編譯指令是我們和Unity溝通的重要手段。
編譯指令
編譯指令最重要的作用是指明該表面着色器使用的表面函數和光照函數,并設定一些可選參數。
表面着色器的CG塊的第一句往往就是它的編譯指令。格式一般如下:

其中#pragma surface用于指明該編譯指令是用于定義表面着色器的,在它的後面需要指明使用它的表面函數和光照模型,同時還可以使用一些可選參數來控制着色器的一些行為。
表面函數
void surf(Input IN, inout SurfaceOutput o)
void surf(Input IN, inout SurfaceOutputStandard o)
void surf(Input IN, inout SurfaceOutputStandardSpecular o)
在表面函數中,會使用輸入結構體Input IN來設定各種表面屬性,并把這些屬性存儲在輸出結構體
SurfaceOutput 、SurfaceOutputStandard、SurfaceOutputStandardSpecular中,再傳遞給光照函數計算光照結果。
光照函數
光照函數會使用表面函數中設定的各種表面屬性,來應用某些光照模型,進而模拟物體表面的光照效果。Unity内置了基于實體的光照模型函數Standard和StandardSpecular(在UnityPBSLighting.cginc檔案中被定義),以及簡單的光照模型函數Lambert和BlinnPhong(在Lighting.cginc檔案中被定義)。
當然可以使用下面的函數來定義用于前向渲染中的光照函數:
//用于不依賴視角的光照模型,例如漫反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);
//用于依賴視角的光照模型,例如高光反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
Unity手冊的表面着色器中的自定義光照模型一文中找到更全面的自定義光照模型的介紹。
其他可選參數
可以在Unity官方手冊的編寫表面着色器一文中找到更加詳細的參數和設定說明。
- 自定義的修改函數。頂點修改函數,允許我們自定義一些頂點屬性,例如,把頂點顔色傳遞給表面函數,或是修改頂點位置,實作某些頂點動畫等。顔色修改函數,可以在顔色繪制到螢幕前,最後一次修改顔色值,例如實作自定義的霧效等。
- 陰影。
- 透明度混合和透明度測試。
- 光照。
- 控制代碼的生成。如果确定該表面着色器隻會在某些渲染路徑中使用,可以使用參數來告訴Unity不需要為某些渲染路徑生成代碼。
兩個結構體
一個表面着色器需要兩個結構體:表面函數的輸入結構體Input,以及存儲了表面屬性的結構體SurfaceOutput(SurfaceOutputStandard和SurfaceOutputStandardSpecular)。
資料來源:Input結構體
它會作為表面函數的輸入結構體(頂點修改函數的輸出結構體)。
Input支援的内置變量名,uv_MainTex和uv_BumpMap(主紋理和法線紋理的采樣坐标)。
表面屬性:SurfaceOutput結構體
它的聲明可以在Lighting.cginc中找到:
而SurfaceOutputStandard和SurfaceOutputStandardSpecular的聲明可以在UnityPBSLighting.cginc中找到:
SurfaceOutputStandard結構體用于預設的金屬工作流程。
SurfaceOutputStandardSpecular結構體用于高光工作流程。
SurfaceOutput結構體中的表面屬性:
- Albedo:對光源的反射率。通常由紋理采樣和顔色屬性的乘積計算得到。
- Normal: 表面的法線方向。
- Emission:自發光。
- Specular:高光反射中的指數部分的系數,影響高光反射的計算。
- Gloss:高光反射中的強度系數。一般在包含了高光反射的光照模型中使用。
- Alpha:透明通道。如果開啟了透明度的話,會使用該值進行顔色混合。
Unity背後做了什麼
Unity在背後會根據表面着色器生成了一個包含了很多Pass的頂點/片元着色器。
以Unity生成的LightMode為ForwardBase的pass為例,它的渲染計算流水線如下。
Unity對該Pass的自動生成過程如下。
(1)直接将表面着色器中的CGPROGRAM和ENDCG之間的代碼複制過來。
(2)分析上述代碼,生成頂點着色器的輸出——v2f_surf結構體,用于在頂點着色器和片元着色器之間進行資料傳遞。
(3)接着生成頂點着色器。
(4)生成片元着色器。
表面着色器執行個體分析
對模型進行膨脹效果。
Shader "Custom/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
// 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;
}
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}
頂點修改函數,使用頂點法線對頂點位置進行膨脹;
表面函數使用主紋理設定了表面屬性中的反射率,并使用法線紋理設定了表面法線方向;
光照函數實作了簡單的蘭伯特反射光照模型;
在最後的顔色修改函數中,簡單地使用了顔色參數對輸出顔色進行調整。
當使用“Show generated code”後,可以看到Unity生成地頂點/片元着色器。
一共生成了3個Pass,它們地LightMode分别是ForwardBase、ForwardAdd和ShadowCaster。
ForwardBase Pass
(1)指明了編譯指令:
// ---- 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都是自動生成的。
(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結構體中的一些變量。
(4)在表面着色器中編寫的CG代碼複制過來,作為Pass的一部分,以便後續調用。
(5)Unity定義了頂點着色器到片元着色器的插值結構體(即頂點着色器的輸出結構體)v2f_surf。
// 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
};
// 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
(6)随後,定義了真正的頂點着色器。
// 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);
......
return o;
}
(7)在Pass的最後,Unity定義了真正的片元着色器。
// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
UNITY_SETUP_INSTANCE_ID(IN);
// prepare and unpack data
Input surfIN;
......
surfIN.uv_MainTex = IN.pack0.xy;
surfIN.uv_BumpMap = IN.pack0.zw;
......
// 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
......
// realtime lighting: call lighting function
#ifndef LIGHTMAP_ON
c += LightingCustomLambert (o, lightDir, atten);
#else
c.a = o.Alpha;
#endif
mycolor (surfIN, o, c);
UNITY_OPAQUE_ALPHA(c.a);
return c;
}
Surface Shader的缺點
表面着色器雖然可以快速地實作各種光照效果,但失去了對各種優化和各種特效實作的控制。是以,使用表面着色器往往由一些性能上的影響。
除了性能比較差以外,表面着色器還無法完成一些自定義的渲染效果。