Unity Shader入門精要學習筆記 - 第17章 Unity的表面着色器探秘


轉自 馮樂樂的《Unity Shader 入門精要》

2010年的Unity 3 中,Surface Shader 出現了。

表面着色器的一個例子。

我們先做如下准備工作。

1)新建一個場景,去掉天空盒子

2)新建一個材質,新建一個Shader,賦給材質。

3)場景中創建一個膠囊體,上步材質賦給它

然后我們修改Shader代碼:

 

 
  1. Shader "Unity Shaders Book/Chapter 17/Bumped Diffuse" {  
  2.     Properties {  
  3.         _Color ("Main Color", Color) = (1,1,1,1)  
  4.         _MainTex ("Base (RGB)", 2D) = "white" {}  
  5.         _BumpMap ("Normalmap", 2D) = "bump" {}  
  6.     }  
  7.     SubShader {  
  8.         Tags { "RenderType"="Opaque" }  
  9.         LOD 300  
  10.           
  11.         CGPROGRAM  
  12.         #pragma surface surf Lambert  
  13.         #pragma target 3.0  
  14.   
  15.         sampler2D _MainTex;  
  16.         sampler2D _BumpMap;  
  17.         fixed4 _Color;  
  18.   
  19.         struct Input {  
  20.             float2 uv_MainTex;  
  21.             float2 uv_BumpMap;  
  22.         };  
  23.   
  24.         void surf (Input IN, inout SurfaceOutput o) {  
  25.             fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);  
  26.             o.Albedo = tex.rgb * _Color.rgb;  
  27.             o.Alpha = tex.a * _Color.a;  
  28.             o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));  
  29.         }  
  30.           
  31.         ENDCG  
  32.     }   
  33.       
  34.     FallBack "Legacy Shaders/Diffuse"  
  35. }  

我們在場景中添加一個點光源和聚光燈,效果如下圖所示。

 

從上面的例子可以看出,相比之前所學的頂點/片元着色器技術,表面着色器的代碼量很少。而且我們可以非常輕松地實現常見的光照效果,甚至不需要和任何光照變量打交道,Unity就幫我們處理好了每個光源的光照結果。

和頂點/片元着色器不同的是,表面着色器的CG代碼是直接而且也必須寫在SubShader中,Unity會在背后為我們生成多個Pass。當然,可以在SubShader一開始處使用Tags來設置該表面着色器使用的標簽。

一個表面着色器中最重要的部分是兩個結構體以及它的編譯指令。其中兩個結構體是表面着色器中不同函數之間信息的傳遞的橋梁,而編譯指令是我們和Unity溝通的重要手段。

 

編譯指令

編譯指令最重要的作用是指明該表面着色器使用的表面函數和光照函數,並設置一些可選參數。表面着色器的CG塊中的第一句代碼往往就是它的編譯指令。一般格式如下:

 

 
  1. #pragma surface surfaceFunction lightModel [optionalparams]  

其中,#pragma surface 用於指明該編譯指令是用於定義表面着色器的,在它的后面需要指明使用的表面函數和光照模型,同時,還可以使用一些可選參數來控制表面着色器的一些行為。

 

與之前遇到的頂點/片元抽象層不同,一個對象的表面屬性定義了它的反射率、光滑度、透明度等值。而編譯指令中的surfaceFunction 就用於定義這些表面屬性。surfaceFunction 通常就是名為surf 的函數(函數名可以是任意的)。它的函數格式是固定的:

 

 
  1. void surf (Input IN,inout SurfaceOutput o)  
  2. void surf (Input IN,inout SurfaceOutputStandard o)  
  3. void surf (Input IN,inout SurfaceOutputStandardSpecular o)  

其中,后兩個是Unity 5 中由於引入了基於物理的渲染而新添加的兩種結構體。SurfaceOutput、SurfaceOutputStandard 和 SurfaceOutputStandardSpecular 都是Unity 內置的結構體,它們需要配合不同的光照模型使用。

 

在表面函數中,會使用輸入結構體Input IN 來設置各種表面屬性,並把這些屬性存儲在輸出結構體SurfaceOutput、SurfaceOutputStandard 或 SurfaceOutputStandardSpecular 中,再傳遞給光照函數計算光照結果。

除了表面函數,我們還需要指定另一個非常重要的函數——光照函數。光照函數會使用表面函數中設置的各種表面屬性,來應該某些光照模型,進而模擬物體表面的光照效果。Unity 內置了基於物理的光照模型Standard 和 StandardSpecular ,以及簡單的非基於物理的光照模型函數Lambert 和 BlinnPhong。

當然,我們也可以自定義自己的光照函數。例如,可以使用下面的函數來定義用於前向渲染中的光照函數:

 

 
  1. //用於不依賴視角的光照模型,例如漫反射  
  2. half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);  
  3. //用於依賴視角的光照模型,例如高光反射  
  4. half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir,half atten);  

在編譯指令的最后,我們還可以設置一些可選參數。這些可選參數包含了很多非常有用的指令類型,例如,開啟/關閉透明度混合/測試,指明自定義的頂點和顏色修改函數,控制生成的代碼等。

 

1)自定義的修改函數。除了表面函數和光照模型外,表面着色器還可以支持其他兩種自定義的函數:頂點修改函數和最后的顏色修改函數。頂點修改函數允許我們自定義一些頂點屬性,例如,把頂點顏色傳遞給表面函數,或是修改頂點位置,實現某些頂點動畫等。最后的顏色修改函數則可以在顏色繪制到屏幕前,最后一次修改顏色值,例如實現自定義的霧效等。

2)陰影。我們可以通過一些指令來控制和陰影相關的代碼。例如,addshaow 參數會為表面着色器生成一個陰影投射的Pass。通常情況下,Unity可以直接在Fallback中找到通用的光照模型為ShadowCaster的Pass,從而將物體正確地渲染到深度和陰影紋理中。但對於一些進行了頂點動畫、透明度測試的物體,我們就需要對陰影的投射進行特殊處理,來為它們產生正確的陰影。fullforwardshadows 參數則可以在前向渲染路徑中支持所有光源類型的陰影。默認情況下,Unity只支持最重要的平行光的陰影效果。如果我們需要讓點光源或聚光燈在前向渲染中也可以由陰影,就可以添加這個參數。相反地,如果我們不想對使用這個Shader 的 物體進行任何陰影計算,就可以使用noshadow 參數來禁用陰影。

3)透明度混合和透明度測試,我們可以通過alpha 和 alphatest 指令來控制透明度混合和透明測試。例如alphatest:VariableName 指令會使用名為VarialName 的變量來剔除不滿足條件的片元。此時,我們還可能需要使用上面提到的addshadow參數來生成正確的陰影投射的Pass。

4)光照。一些指令可以控制光照對物體的影響,例如noambient 參數可以告訴Unity不要應該任何環境光照或光照探針。novertexlights 參數告訴Unity 不要應用任何逐頂點光照。noforwardadd會去掉前向渲染中的額外的Pass。也就是說,這個Shader只會支持一個逐像素的平行光,而其他的光源會按照逐頂點或SH的方法來計算光照影響。這個參數通常會用於移動平台版本的表面着色器中。還有一些用於控制光照烘焙、霧效模擬的參數,如nolightmap、nofog等。

5)控制代碼的生成。一些指令還可以控制由表面着色器自動生成的代碼,默認情況下,Unity 會為一個表面着色器生成相應的前向渲染路勁、延遲渲染路徑使用的Pass,這會導致生成的Shader 文件比較大。如果我們確定該表面着色器只會在某些渲染路徑中使用,就可以exclude_path:deferred、exclude_path:forward 和 exclude_path:prepass來告訴Unity不需要為某些渲染路徑生成代碼。

兩個結構體
Input結構體 包含了許多表面屬性的數據來源,因此,它會作為表面函數的輸入結構體。Input支持很多內置的變量名,通過這些變量名,我們告訴Unity需要使用的數據信息。下表給出了Input結構體中內置的變量。


需要注意的是,我們並不需要自己計算上述的各個變量,而只需要在Input結構體中按上述名稱嚴格聲明這些變量即可,Unity會在背后為我們准備好這些數據,而我們只需要在表面函數中直接使用它們即可。一個例外的情況是,我們自定義了頂點修改函數,並需要向表面函數中傳遞一些自定義的數據。例如,為了自定義霧效,我們可能需要在頂點修改函數中根據頂點在視角空間下的位置信息計算霧效混合系數,這樣我們就可以在Input結構體中定義一個名為half fog 的變量,把計算結果存儲在該變量后進行輸出。

有了Input結構體來提供所需要的數據后,我們就可以據此計算各種表面屬性。因此,另一個結構體就是用於存儲這些表面屬性的結構體,即SurfaceOutput、SurfaceOutputStandard 和 SurfaceOutputStandardSpecular,它會作為表面函數的輸出,隨后會作為光照函數的輸入來進行各種光照計算。相比於Input結構體的自由性,這個結構體里面的變量是提前聲明好的,不可以增加也不會減少。SurfaceOutput 的聲明可以在Lighting.cginc文件中找到:

 

 
  1. struct SurfaceOutput {  
  2.     fixed3 Albedo;  
  3.     fixed3 Normal;  
  4.     fixed3 Emission;  
  5.     half Specular;  
  6.     fixed Gloss;  
  7.     fixed Alpha;  
  8. };  

而SurfaceOutputStandard 和 SurfaceOutputStandardSpecular 的聲明可以在UnityPBSLighting.cginc 中找到

 

 

 
  1. struct SurfaceOutputStandard  
  2. {  
  3.     fixed3 Albedo;      // base (diffuse or specular) color  
  4.     fixed3 Normal;      // tangent space normal, if written  
  5.     half3 Emission;  
  6.     half Metallic;      // 0=non-metal, 1=metal  
  7.     half Smoothness;    // 0=rough, 1=smooth  
  8.     half Occlusion;     // occlusion (default 1)  
  9.     fixed Alpha;        // alpha for transparencies  
  10. };  
  11. struct SurfaceOutputStandardSpecular  
  12. {  
  13.     fixed3 Albedo;      // diffuse color  
  14.     fixed3 Specular;    // specular color  
  15.     fixed3 Normal;      // tangent space normal, if written  
  16.     half3 Emission;  
  17.     half Smoothness;    // 0=rough, 1=smooth  
  18.     half Occlusion;     // occlusion (default 1)  
  19.     fixed Alpha;        // alpha for transparencies  
  20. };  

在一個表面着色器中,只需要選擇上述三者之一即可,這取決於我們選擇使用的光照模型。Unity內置的光照模型有兩種,一種是Unity5 之前的、簡單的、非基於物理的光照模型,包含了Lambert 和 BlinnPhong;另一種是Unity5 添加的、基於物理的光照模型,包括Standard 和 StandardSpecular ,這種模型會更加符合物理規律,但計算也會復雜很多。如果使用了非基於物理的光照模型,就使用SurfaceOutputStad,否則分別使用SurfaceOutputStandard 和SurfaceOutputStandardSpecular 。其中,SurfaceOutputStandard 結構體用於默認的金屬工作流程,對應了Standard 光照函數;而SurfaceOutputStandardSpecular 結構體用於高光工作流程,對應了StandardSpecular 光照函數。

在SurfaceOutput結構體中,部分表面屬性有:

1)fixed3 Albedo 對光源的反射率。通常由紋理采樣和顏色屬性的乘積計算而得。

2)fixed3 Normal 表面法線方向

3)fixed3 Emission 自發光。Unity 通常會在片元着色器最后輸出前,使用類似下面的語句進行簡單的顏色相加。

 

 
  1. c.rgb += o.Emission;  

4)half Specular 高光反射中的指數部分的系數,影響高光反射的計算。例如,如果使用了內置的BlinnPhong 光照函數,它會使用如下語句計算高光反射的強度:

 

 

 
  1. float spec = pow(nh,s.Specular*128.0)*s.Gloss;  

5) fixed Gloss 高光反射中的強度系數。一般在包含了高光反射的光照模型里使用

 

6) fixed Alpha 透明通道

 

Unity 背后做了什么

我們之前說過,Unity在背后會根據表面着色器生成一個包含了很多Pass的頂點/片元着色器。這些Pass有些是為了針對不同的渲染路徑,例如,默認情況下Unity 會為前向渲染路徑生成LightMode 為 ForwardBase 和 ForwardAdd 的Pass,為Unity 5 之前的延遲渲染路徑生成LightMode 為PrePassBase 和 PrePassFinal 的Pass,為Unity5之后的延遲渲染路徑生成LightMode 為 Deferred 的Pass。還有一些Pass 是用於產生額外的信息。例如,為了給光照映射和動態全局光照提取表面信息,Unity 會生成一個LightMode 為 Meta 的Pass。有些表面着色器由於修改了頂點位置,因此,我們可以利用adddshaow 編譯指令為它生成相應的LightMode 為 ShadowCaster 的陰影投射Pass。這些Pass 的生成都是基於我們再表面着色器中的編譯指令和自定義的函數,這是由規律可循的。Unity 提供了一個功能,讓我們可以對表面着色器自動生成的代碼一探究竟:在每個編譯完成的表面着色器的面板上,有一個“Show generated code” 按鈕,如下圖所示。我們只需要單擊一下就可以看到Unity為這個表面着色器生成的所有頂點/片元着色器。


通過查看這些代碼,我們就可以了解到Unity到底是如何根據表面着色器生成各個的Pass的。以Unity生成的LightMode 為ForwardBase 的Pass為例,它的渲染流水線如下圖所示。

Unity對該Pass的自動生成過程大致如下:

1)直接將表面着色器中CGPROGRAM 和 ENDCG 之間的代碼復制過來,這些代碼包括了我們對Input 結構體、表面函數、光照函數等變量和函數的定義。這些函數和變量會在之后的處理過程中被當成正常的結構體和函數進行調用。

2)Unity會分析上述代碼,並據此生成頂點着色器的輸出——v2f_surf 結構體,用於在頂點着色器和片元着色器之間進行數據傳遞。Unity會分析我們再定義函數中所使用的變量,例如,紋理坐標、視角方向、反射方向等。如果需要,它就會在v2f_surf 中生成相應的變量。而且,即便有時我們在Input 中定義了某些變量,但Unity在分析代碼時發現我們並沒有使用這些變量,那么這些變量實際上是不會在v2f_surf 中生成的。也就是說,Unity 做了一些優化。v2f_surf 中還包含了一些其他需要的變量,例如陰影紋理坐標、光照紋理坐標、逐頂點光照等。

3)接着,生成頂點着色器。如果我們自定義了頂點修改函數,Unity會首先調用頂點修改函數來修改頂點數據,或填充自定義的Input結構體中的變量。然后,Unity 會分析頂點修改函數中修改的數據,在需要時通過Input 結構體將修改結果存儲到v2f_surf 相應的變量中。然后計算v2f_surf 中其他生成的變量值。這主要包括了頂點位置、紋理坐標、法線方向、逐頂點光照、光趙文麗的采樣坐標等。當然,我們可以通過編譯指令來控制某些變量是否需要計算。最后將v2f_surf傳遞給接下來的片元着色器。

4)生成片元着色器。使用v2f_surf 中對應變量填充Input結構體,例如,紋理坐標、視角方向等。然后調用我們自定義的表面函數填充SurfaceOutput結構體。接着調用光照函數得到初始的顏色值。如果使用的是內置的Lambert 或 blinnPhong 光照函數,Unity 還會計算動態全局光照,並添加到光照模型的計算中。再而進行其他顏色的疊加。例如,沒有使用光照烘焙,還會添加逐頂點光照的影響。最后,如果自定義了最后的顏色修改函數,Unity就會調用它進行最后的顏色修改。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM