【Unity Shaders】初探Surface Shader背後的機制,shadersshader

來源:互聯網
上載者:User

【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;};

  • 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是必須指定的,而且是可選部分。
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是根據定義的光照類型來處理不同的光照的。這部分還沒有搞明白,後面會繼續探究一下的!



上述內容純屬研究所得,如有錯誤歡迎指正~




聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.