【Unity Shaders】初探Surface Shader背後的機制,shadersshader
轉載請註明出處: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是遠遠無法滿足你的,而且某種方面來說它會讓你變得越來越困惑。
困惑了怎麼辦呢?老老實實去學習主流的渲染語言吧~比如CG、GLSL、HLSL等。等學了一些上述內容後,再回過頭來看Surface Shader就會別有一番理解了。
說教了這麼多,本篇的主旨其實是分析下Surface Shader背後做的事情啦!也就是,分析Surface Shader到底是怎樣解析我們編寫的那些surf、LightingXXX等函數的,又是如何得到像素顏色的。那麼,開始吧!
流水線
首先,我們要明白Surface Shader支援哪些特性。詳情請見官網。
Surface Shader最重要的部分是
兩個結構體以及它的編譯指令。
兩個結構體
兩個結構體就是指struct Input和SurfaceOutput。其中Input結構體是允許我們自訂的。它可以包含一些紋理座標和其他提前定義的變數,例如view direction(float3 viewDir)、world space position(worldPos)、world space reflection vector(float3 worldRefl)等。這些變數只有在
真正使用的時候才會被計算產生。比如,在某些Pass裡產生而某些就產生。
另一個結構體是SurfaceOutput。我們無法自訂這個結構體內的變數。關於它最難理解的也就是每個變數的具體含義以及工作機制(對像素顏色的影響)。我們來看一下它的定義(在Lighting.cginc裡面):
struct SurfaceOutput { half3 Albedo; half3 Normal; half3 Emission; half Specular; half Gloss; half Alpha;};
上述結論是分析產生的程式碼所得,若有不對歡迎指出。大家碰到不懂的,也可以像這樣分析產生的程式碼,一般問題都可以理解啦~
編譯指令
編譯指令的一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
Surface Shader和CG其他部分一樣,代碼也是要寫在CGPROGRAM和ENDCG之間。但區別是,它必須寫在SubShader內部,而不能寫在Pass內部。Surface Shader自己會自動產生所需的各個Pass。由上面的編譯格式可以看出,surfaceFunction和lightModel是必須指定的,而且是可選部分。
surfaceFunction通常就是名為surf的函數(函數名可以任意),它的函數格式是固定的:
void surf (Input IN, inout SurfaceOutput o)
即Input是輸入,SurfaceOutput是輸出。
lightModel也是必須指定的。由於Unity內建了一些光照函數——Lambert(diffuse)和Blinn-Phong(specular),因此這裡在預設情況下會使用內建的Lambert模型。當然我們也可以自訂。
optionalparams包含了很多可用的指令類型,包括開啟、關閉一些狀態,設定產生的Pass類型,指定可選函數等。這裡,我們只關注可指定的函數,其他可去官網自行查看。除了上述的surfaceFuntion和lightModel,我們還可以自訂兩種函數:vertex:VertexFunction和finalcolor:ColorFunction。也就是說,Surface Shader允許我們自訂四種函數。
兩個結構體+四個函數——它們在整個的render pipeline中的流程如下:
從可以看出來,Surface Shader背後的”那些女人“就是vertex shader和fragment shader。除了VertexFunction外,另外兩個結構體和三個函數都是在fragment shader中扮演了一些角色。Surface Shader首先根據我們的代碼產生了很多Pass,用於forwardbase和forwardadd等,這不在本篇的討論範圍。而每個Pass的代碼是基於上述四個函數產生的。
以一個Pass的代碼為例,Surface Shader的產生過程簡述如下:
程式碼分析
我們以一個Surface Shader為例,分析它產生的程式碼。
Surface Shader如下:
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_OFFstruct 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_OFFstruct v2f_surf { float4 pos : SV_POSITION; float2 pack0 : TEXCOORD0; float4 cust_vertColor : TEXCOORD1; float2 lmap : TEXCOORD2; LIGHTING_COORDS(3,4)};#endif#ifndef LIGHTMAP_OFFfloat4 unity_LightmapST;#endif// 定義所需的紋理座標float4 _RampTex_ST;// vertex shaderv2f_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_OFFsampler2D unity_Lightmap;#ifndef DIRLIGHTMAP_OFFsampler2D unity_LightmapInd;#endif#endif// fragment shaderfixed4 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"}
其中比較重要的部分我都寫了注釋。
一些問題
回到我們一開始的那個問題:為什麼我的情境裡沒有燈光,但物體不是全黑的呢?這一切都是Fragment Shader中一些顏色疊加計算的結果。
我們仔細觀察Fragment Shader中計算顏色的部分。前面說過,它使用LightingXXX對顏色值進行初始化,但後面還進行了一系列顏色疊加計算。其中,在沒有使用LightMap的情況下,Unity還計算了vertex lights對顏色的影響,也就是下面這句話:
#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.0half3 ShadeSH9 (half4 normal){half3 x1, x2, x3;// Linear + constant polynomial termsx1.r = dot(unity_SHAr,normal);x1.g = dot(unity_SHAg,normal);x1.b = dot(unity_SHAb,normal);// 4 of the quadratic polynomialshalf4 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 polynomialfloat 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中調整)有關。如果把這個環境光線也調成黑色,那麼情境就真的全黑了。
呼呼呼,關於這些光源計算的部分就是一開始所說的那些讓寫shader變得很複雜的原因之一!如果你真的去看那些在UnityCG.cginc、AutoLight.cginc等檔案裡的關於指令的定義,可以發現Unity是根據定義的光照類型來處理不同的光照的。這部分還沒有搞明白,後面會繼續探究一下的!
上述內容純屬研究所得,如有錯誤歡迎指正~