轉自:http://blog.csdn.net/candycat1992/article/details/39994049
寫在前面
一直以來,Unity Surface Shader背后的機制一直是初學者為之困惑的地方。Unity Surface Shader在Unity 3.0的時候被開放給公眾使用,其宣傳手段也是號稱讓所有人都可以輕松地寫shader。但由於資料缺乏,很多人知其然不知其所以然,無法理解Unity Surface Shader在背后為我們做了哪些事情。
前幾天一直被問到一個問題,為什么我的場景里沒有燈光,但物體不是全黑的呢?為什么我把Light的顏色調成黑色,物體還是有一些默認顏色呢?這些問題其實都是因為那些物體使用了Surface Shader的緣故。因此,了解Surface Shader背后的機制是非常重要滴~
雖然Surface Shader一直是一個神秘的存在,但其實Unity給了我們揭開她面紗的方式:查看它生成的CG代碼。大家應該都知道,所謂的Surface Shader實際上是封裝了CG語言,隱藏了很多光照處理的細節,它的設計初衷是為了讓用戶僅僅使用一些指令(#pragma)就可以完成很多事情,並且封裝了很多常用的光照模型和函數,例如Lambert、Blinn-Phong等。而查看Surface Shader生成的代碼也很簡單:在每個編譯完成的Surface Shader的面板上,都有個“Show generated code”的按鈕,像下面這樣:

點開后,就可以查看啦~面板上還表明了很多其他的有用信息。而這些方便的功能實際上是Unity 4.5發布出來的。詳情可見這篇博文。
使用Surface Shader,很多時候,我們只需要告訴shader,“嘿,使用這些紋理去填充顏色,法線貼圖去填充法線,使用Lambert光照模型,其他的不要來煩我!!!”我們不需要考慮是使用forward還是deferred rendering,有多少光源類型、怎樣處理這些類型,每個pass需要處理多少個光源!!!(人們總會rant寫一個shader是多么的麻煩。。。)So!Unity說,不要急,放着我來~
上面的情景當然對於小白是比較簡單的方式,Surface Shader可以讓初學者快速實現很多常見的shader,例如漫反射、高光反射、法線貼圖等,這些常見的效果也都不錯。而對應面就是,由於隱藏了很多細節,如果想要自定義一些比較復雜或特殊的效果,使用Surface Shader就無法達到了(或者非常麻煩)。在學了一段時間的Surface Shader后,我認為:
- 如果你從來沒有學習過怎樣編寫shader,而又想寫一些常見的、比較簡單的shader,那僅學習Surface Shader是一個不錯的選擇。
- 如果你向往那些高品質的游戲畫面,那么Surface Shader是遠遠無法滿足你的,而且某種方面來說它會讓你變得越來越困惑。
流水線
兩個結構體
- struct SurfaceOutput {
- half3 Albedo;
- half3 Normal;
- half3 Emission;
- half Specular;
- half Gloss;
- half Alpha;
- };
- Albedo:我們通常理解的對光源的反射率。它是通過在Fragment Shader中計算顏色疊加時,和一些變量(如vertex lights)相乘后,疊加到最后的顏色上的。
- Normal:即其對應的法線方向。只要是受法線影響的計算都會受到影響。
- Emission:自發光。會在Fragment 最后輸出前(調用final函數前,如果定義了的話),使用下面的語句進行簡單的顏色疊加:
- c.rgb += o.Emission;
- Specular:高光反射中的指數部分的系數。影響一些高光反射的計算。按目前的理解,也就是在光照模型里會使用到(如果你沒有在光照函數等函數——包括Unity內置的光照函數,中使用它,這個變量就算設置了也沒用)。有時候,你只在surf函數里設置了它,但也會影響最后的結果。這是因為,你可能使用了Unity內置的光照模型,如BlinnPhong,它會使用如下語句計算高光反射的強度(在Lighting.cginc里):
- float spec = pow (nh, s.Specular*128.0) * s.Gloss;
- Gloss:高光反射中的強度系數。和上面的Specular類似,一般在光照模型里使用。
- Alpha:通常理解的透明通道。在Fragment Shader中會直接使用下列方式賦值(如果開啟了透明通道的話):
- c.a = o.Alpha;
編譯指令
- #pragma surface surfaceFunction lightModel [optionalparams]
Surface Shader和CG其他部分一樣,代碼也是要寫在CGPROGRAM和ENDCG之間。但區別是,它必須寫在SubShader內部,而不能寫在Pass內部。Surface Shader自己會自動生成所需的各個Pass。由上面的編譯格式可以看出,surfaceFunction和lightModel是必須指定的,而且是可選部分。
- void surf (Input IN, inout SurfaceOutput o)
即Input是輸入,SurfaceOutput是輸出。
- 直接將CGPROGRAM和ENDCG之間的代碼復制過來(其實還是更改了一些編譯指令),這些代碼包括了我們對Input、surfaceFuntion、LightingXXX等變量和函數的定義。這些函數和變量會在之后的處理過程中當成普通的結構體和函數進行調用,就和在C++中我們會在main函數中調用某些函數一樣;
- 分析上述代碼,生成v2f_surf結構,用於在Vertex Shader和Fragment Shader之間進行數據傳遞。Unity會分析我們在四個自定義函數中所使用的變量,例如紋理坐標等。如果需要,它會在v2f_surf中生成相應的變量。而且,即便有時我們在Input中定義了某些變量(如某些紋理坐標),但Unity在分析后續代碼時發現我們並沒有使用這些變量,那么這些變量實際上是不會在v2f_surf中生成的。這也就是說,Unity做了一些優化動作。
- 生成Vertex Shader。
* 如果我們自定義了VertexFunction,Unity會在這里首先調用VertexFunction修改頂點數據;然后分析VertexFunction修改的數據,最后通過Input結構體將修改結果存儲到v2f_surf中。
* 計算v2f_surf中其他默認的變量值。這主要包括了pos、紋理坐標、normal(如果沒有使用LightMap)、vlight(如果沒有使用LightMap)、lmap(如果使用LightMap)等。
* 最后,通過內置的TRANSFER_VERTEX_TO_FRAGMENT指令將v2f_surf傳遞給下面的Fragment Shader。 - 生成Fragment Shader。
* 使用v2f_surf中的對應變量填充Input結構,例如一些紋理坐標等。
* 調用surfFuntion填充SurfaceOutput結構。
* 調用LightingXXX函數得到初始的顏色值。
* 進行其他的顏色疊加。如果沒有啟用LightMap,這里會使用SurfaceOutput.Albedo和v2f_surf.vlight的乘積和原顏色值進行疊加;否則會進行一些更復雜的顏色疊加。
* 最后,如果自定了final函數,則調用它進行最后額顏色修改。
代碼分析
- Shader "Custom/BasicDiffuse" {
- Properties {
- _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
- _AmbientColor ("Ambient Color", Color) = (1,1,1,1)
- _MySliderValue ("This is a Slider", Range(0,10)) = 2.5
- _RampTex ("Ramp Texture", 2D) = "white"{}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "RenderType"="Opaque" }
- LOD 200
- CGPROGRAM
- #pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
- #pragma debug
- float4 _EmissiveColor;
- float4 _AmbientColor;
- float _MySliderValue;
- sampler2D _RampTex;
- struct Input
- {
- float2 uv_RampTex;
- float4 vertColor;
- };
- void vert(inout appdata_full v, out Input o)
- {
- o.vertColor = v.color;
- }
- void surf (Input IN, inout SurfaceOutput o)
- {
- float4 c;
- c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
- o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).rgb;
- o.Alpha = c.a;
- }
- inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
- {
- float difLight = max(0, dot (s.Normal, lightDir));
- float hLambert = difLight * 0.5 + 0.5;
- float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;
- float4 col;
- col.rgb = s.Albedo * _LightColor0.rgb * (ramp) * atten;
- col.a = s.Alpha;
- return col;
- }
- void final(Input IN, SurfaceOutput o, inout fixed4 color) {
- color = color * 0.5 + 0.5;
- }
- ENDCG
- }
- FallBack "Diffuse"
- }
它包含了全部四個函數,以及一些比較常見的運算。為了只關注一個Pass,我添加了noforwardadd指令。它所得到的渲染結果不重要(事實上我只是在BasicDiffuse上瞎改了一些。。。)
- Shader "Custom/BasicDiffuse_Gen" {
- Properties {
- _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
- _AmbientColor ("Ambient Color", Color) = (1,1,1,1)
- _MySliderValue ("This is a Slider", Range(0,10)) = 2.5
- _RampTex ("Ramp Texture", 2D) = "white"{}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "RenderType"="Opaque" }
- LOD 200
- // ------------------------------------------------------------
- // Surface shader code generated out of a CGPROGRAM block:
- // ---- forward rendering base pass:
- Pass {
- Name "FORWARD"
- Tags { "LightMode" = "ForwardBase" }
- CGPROGRAM
- // compile directives
- #pragma vertex vert_surf
- #pragma fragment frag_surf
- #pragma multi_compile_fwdbase nodirlightmap
- #include "HLSLSupport.cginc"
- #include "UnityShaderVariables.cginc"
- #define UNITY_PASS_FORWARDBASE
- #include "UnityCG.cginc"
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- #define INTERNAL_DATA
- #define WorldReflectionVector(data,normal) data.worldRefl
- #define WorldNormalVector(data,normal) normal
- // Original surface shader snippet:
- #line 11 ""
- #ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
- #endif
- //#pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
- #pragma debug
- float4 _EmissiveColor;
- float4 _AmbientColor;
- float _MySliderValue;
- sampler2D _RampTex;
- struct Input
- {
- float2 uv_RampTex;
- float4 vertColor;
- };
- void vert(inout appdata_full v, out Input o)
- {
- o.vertColor = v.color;
- }
- void surf (Input IN, inout SurfaceOutput o)
- {
- float4 c;
- c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
- o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).rgb;
- o.Alpha = c.a;
- }
- inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
- {
- float difLight = max(0, dot (s.Normal, lightDir));
- float hLambert = difLight * 0.5 + 0.5;
- float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;
- float4 col;
- col.rgb = s.Albedo * _LightColor0.rgb * (ramp);
- col.a = s.Alpha;
- return col;
- }
- void final(Input IN, SurfaceOutput o, inout fixed4 color) {
- color = color * 0.5 + 0.5;
- }
- // vertex-to-fragment interpolation data
- #ifdef LIGHTMAP_OFF
- struct v2f_surf {
- float4 pos : SV_POSITION;
- float2 pack0 : TEXCOORD0;
- float4 cust_vertColor : TEXCOORD1;
- fixed3 normal : TEXCOORD2;
- fixed3 vlight : TEXCOORD3;
- // LIGHTING_COORDS在AutoLight.cginc里定義
- // 本質上就是一個#define指令
- // e.g.
- // #define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
- // #define SHADOW_COORDS(idx1) float3 _ShadowCoord : TEXCOORD##idx1;
- LIGHTING_COORDS(4,5)
- };
- #endif
- #ifndef LIGHTMAP_OFF
- struct v2f_surf {
- float4 pos : SV_POSITION;
- float2 pack0 : TEXCOORD0;
- float4 cust_vertColor : TEXCOORD1;
- float2 lmap : TEXCOORD2;
- LIGHTING_COORDS(3,4)
- };
- #endif
- #ifndef LIGHTMAP_OFF
- float4 unity_LightmapST;
- #endif
- // 定義所需的紋理坐標
- float4 _RampTex_ST;
- // vertex shader
- v2f_surf vert_surf (appdata_full v) {
- v2f_surf o;
- // 使用自定義的vert函數填充Input結構
- Input customInputData;
- vert (v, customInputData);
- // 再賦值給真正所需的v2f_surf結構
- o.cust_vertColor = customInputData.vertColor;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- // 將頂點的紋理坐標轉換到紋理對應坐標
- o.pack0.xy = TRANSFORM_TEX(v.texcoord, _RampTex);
- #ifndef LIGHTMAP_OFF
- // 如果啟用了LightMap,則計算對應的LightMap坐標
- o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
- #endif
- // 計算世界坐標系中法線的方向
- // SCALED_NORMAL在UnityCG.cginc里定義
- // 本質上就是一個#define指令
- // #define SCALED_NORMAL (v.normal * unity_Scale.w)
- float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
- // 如果沒有開啟LightMap,
- // 頂點法線方向就是worldN
- #ifdef LIGHTMAP_OFF
- o.normal = worldN;
- #endif
- // SH/ambient and vertex lights
- #ifdef LIGHTMAP_OFF
- // 如果沒有開啟LightMap,
- // vertex lights就是球面調和函數的結果
- // 球面調和函數ShadeSH9在UnityCG.cginc里定義
- float3 shlight = ShadeSH9 (float4(worldN,1.0));
- o.vlight = shlight;
- // unity_4LightPosX0等變量在UnityShaderVariables.cginc里定義
- #ifdef VERTEXLIGHT_ON
- float3 worldPos = mul(_Object2World, v.vertex).xyz;
- 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, worldN );
- #endif // VERTEXLIGHT_ON
- #endif // LIGHTMAP_OFF
- // pass lighting information to pixel shader
- // TRANSFER_VERTEX_TO_FRAGMENT在AutoLight.cginc里定義,
- // 本質上就是一個#define指令
- // 用於轉換v2f_surf中的_LightCoord和_ShadowCoord
- TRANSFER_VERTEX_TO_FRAGMENT(o);
- return o;
- }
- #ifndef LIGHTMAP_OFF
- sampler2D unity_Lightmap;
- #ifndef DIRLIGHTMAP_OFF
- sampler2D unity_LightmapInd;
- #endif
- #endif
- // fragment shader
- fixed4 frag_surf (v2f_surf IN) : SV_Target {
- // prepare and unpack data
- #ifdef UNITY_COMPILER_HLSL
- Input surfIN = (Input)0;
- #else
- Input surfIN;
- #endif
- // 使用v2f_surf中的變量給Input中的紋理坐標進行賦值
- surfIN.uv_RampTex = IN.pack0.xy;
- surfIN.vertColor = IN.cust_vertColor;
- #ifdef UNITY_COMPILER_HLSL
- SurfaceOutput o = (SurfaceOutput)0;
- #else
- SurfaceOutput o;
- #endif
- // 初始化SurfaceOutput結構
- o.Albedo = 0.0;
- o.Emission = 0.0;
- o.Specular = 0.0;
- o.Alpha = 0.0;
- o.Gloss = 0.0;
- #ifdef LIGHTMAP_OFF
- o.Normal = IN.normal;
- #endif
- // call surface function
- // 調用自定義的surf函數填充SurfaceOutput結構
- surf (surfIN, o);
- // compute lighting & shadowing factor
- // LIGHT_ATTENUATION在AutoLight.cginc里定義,
- // 本質上就是一個#define指令
- // 用於計算光衰減
- fixed atten = LIGHT_ATTENUATION(IN);
- fixed4 c = 0;
- // realtime lighting: call lighting function
- #ifdef LIGHTMAP_OFF
- // 如果沒有開啟LightMap,
- // 調用自定義的LightXXX函數,
- // 使用填充好的SurfaceOutput等變量作為參數,
- // 得到初始的像素值
- c = LightingBasicDiffuse (o, _WorldSpaceLightPos0.xyz, atten);
- #endif // LIGHTMAP_OFF || DIRLIGHTMAP_OFF
- #ifdef LIGHTMAP_OFF
- // 如果沒有開啟LightMap,
- // 向像素疊加vertex light的光照顏色
- c.rgb += o.Albedo * IN.vlight;
- #endif // LIGHTMAP_OFF
- // lightmaps:
- #ifndef LIGHTMAP_OFF
- // 計算LightMap,這部分不懂
- #ifndef DIRLIGHTMAP_OFF
- // directional lightmaps
- fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
- fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);
- half3 lm = LightingLambert_DirLightmap(o, lmtex, lmIndTex, 0).rgb;
- #else // !DIRLIGHTMAP_OFF
- // single lightmap
- fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
- fixed3 lm = DecodeLightmap (lmtex);
- #endif // !DIRLIGHTMAP_OFF
- // combine lightmaps with realtime shadows
- #ifdef SHADOWS_SCREEN
- #if (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)) && defined(SHADER_API_MOBILE)
- 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
- // 給Alpha通道賦值
- c.a = o.Alpha;
- #endif // LIGHTMAP_OFF
- // 調用自定義的final函數,
- // 對像素值進行最后的更改
- final (surfIN, o, c);
- return c;
- }
- ENDCG
- }
- // ---- end of surface shader generated code
- #LINE 57
- }
- FallBack "Diffuse"
- }
其中比較重要的部分我都寫了注釋。
一些問題
- #ifdef LIGHTMAP_OFF
- // 如果沒有開啟LightMap,
- // 向像素疊加vertex light的光照顏色
- c.rgb += o.Albedo * IN.vlight;
- #endif // LIGHTMAP_OFF
而IN.vlight是在Vertex Shader中計算的:
- // 如果沒有開啟LightMap,
- // vertex lights就是球面調和函數的結果
- // 球面調和函數ShadeSH9在UnityCG.cginc里定義
- float3 shlight = ShadeSH9 (float4(worldN,1.0));
- o.vlight = shlight;
我們可以去查看ShadeSH9函數的實現:
- // normal should be normalized, w=1.0
- half3 ShadeSH9 (half4 normal)
- {
- half3 x1, x2, x3;
- // Linear + constant polynomial terms
- x1.r = dot(unity_SHAr,normal);
- x1.g = dot(unity_SHAg,normal);
- x1.b = dot(unity_SHAb,normal);
- // 4 of the quadratic polynomials
- half4 vB = normal.xyzz * normal.yzzx;
- x2.r = dot(unity_SHBr,vB);
- x2.g = dot(unity_SHBg,vB);
- x2.b = dot(unity_SHBb,vB);
- // Final quadratic polynomial
- float vC = normal.x*normal.x - normal.y*normal.y;
- x3 = unity_SHC.rgb * vC;
- return x1 + x2 + x3;
- }
它是一個球面調和函數,但unity_SHAr這些變量具體是什么我還不清楚。。。如果有人知道麻煩告訴我一下,不勝感激~但是,這些變量是和Unity使用了一個全局環境光(你可以在Edit->RenderSettings->Ambient Light中調整)有關。如果把這個環境光也調成黑色,那么場景就真的全黑了。
