【Unity Shaders】Mobile Shader Adjustment—— 什麼是高效的Shader

Kaitiren發表於2016-03-01

目錄(?)[+]

本系列主要參考《Unity Shaders and Effects Cookbook》一書(感謝原書作者),同時會加上一點個人理解或擴充。

這裡是本書所有的插圖。這裡是本書所需的程式碼和資源(當然你也可以從官網下載)。

========================================== 分割線 ==========================================



寫在前面


之前學習的各種Shader時,我們從沒有考慮在所有平臺下的可用性。Unity是一個強大的跨平臺遊戲引擎,但這也決定了在編寫程式碼時我們需要考慮更多的平臺因素。對於Shader而言,如果沒有進行相應的優化,很有可能無法執行在移動平臺等對效能限制較高的平臺上。我們需要理解一些關鍵的因素來優化我們的Shader,以提高遊戲效能而又能儘可能保持取得同樣的視覺效果。


尤其是如果你的目標平臺包括Android系統,那麼就一定要小心中國各種山寨機的大浪一下把你拍在沙灘上的後果。。。所以,如果你從來沒有為你的Shader考慮過這些情況,那麼,且用且小心吧。。。


這一章中,我們會學習三節內容:什麼是一個高效的Shader,怎樣對Shader進行效能分析,為移動平臺優化我們的Shader。


那麼,什麼是一個高效的Shader呢?這是個有點複雜的問題,它涉及到了很多因素。例如,和你使用的變數個數及其所佔記憶體,Shader使用的紋理個數有關等等。還有可能,你的Shade雖然工作良好,但我們實際商可以使用一半數目的變數就可以取得相同的效果。我們將在本節中發掘這樣的一些技巧,並向你說明它們是如何組合起來讓我們的Shader更快更高效的,而又可以各種平臺上取得同樣高質量的視覺效果。



準備工作


我們將首先使用一個最常見的Shader之一:Bumped Diffuse Shader。也就是應用了法線貼圖的Shader。


  1. 建立一個新的場景和一個球體,新增一個平行光。
  2. 建立一個新的Shader和Material,可以命名為OptimizedShader001。
  3. 把Shader賦給Material,把Material賦給球體。
  4. 最後,使用下列程式碼修改Shader。


  1. Shader "Custom/OptimizedShader001" {  
  2.     Properties {  
  3.         _MainTex ("Base (RGB)", 2D) = "white" {}  
  4.         _NormalMap ("Normal Map", 2D) = "bump" {}  
  5.     }  
  6.     SubShader {  
  7.         Tags { "RenderType"="Opaque" }  
  8.         LOD 200  
  9.           
  10.         CGPROGRAM  
  11.         #pragma surface surf SimpleLambert  
  12.   
  13.         sampler2D _MainTex;  
  14.         sampler2D _NormalMap;  
  15.   
  16.         struct Input {  
  17.             float2 uv_MainTex;  
  18.             float2 uv_NormalMap;  
  19.         };  
  20.   
  21.         inline float4 LightingSimpleLambert (SurfaceOutput s, float3 lightDir, float atten)  
  22.         {  
  23.             float diff = max (0, dot (s.Normal, lightDir));  
  24.               
  25.             float4 c;  
  26.             c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);  
  27.             c.a = s.Alpha;  
  28.             return c;  
  29.         }  
  30.           
  31.         void surf (Input IN, inout SurfaceOutput o)   
  32.         {  
  33.             float4 c = tex2D (_MainTex, IN.uv_MainTex);  
  34.               
  35.             o.Albedo = c.rgb;  
  36.             o.Alpha = c.a;  
  37.             o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));  
  38.         }  
  39.         ENDCG  
  40.     }   
  41.     FallBack "Diffuse"  
  42. }  

簡單的光照函式裡面進行了簡單的漫反射處理,surf函式裡則改變了模型的法線。


最後,你得到的效果大概是這樣的:





實現


下面,我們來一步步優化這個Shader。


首先,我們需要優化變數型別,以便它們儘可能少地佔用記憶體:

  1. 修改Input結構。之前,我們的UV座標都是儲存在了float2型別的變數中,現在我們將它們改為half2
    1. struct Input {  
    2.     half2 uv_MainTex;  
    3.     half2 uv_NormalMap;  
    4. };  

  2. 接下來是光照函式。同樣,將其中float家族的變數改成對應的fixed型別變數:
    1. inline fixed4 LightingSimpleLambert (SurfaceOutput s, fixed3 lightDir, fixed atten)  
    2. {  
    3.     fixed diff = max (0, dot (s.Normal, lightDir));  
    4.       
    5.     fixed4 c;  
    6.     c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);  
    7.     c.a = s.Alpha;  
    8.     return c;  
    9. }  

  3. 最後,修改surf函式中的變數型別。同樣使用fixed型別變數:
    1. void surf (Input IN, inout SurfaceOutput o)   
    2. {  
    3.     fixed4 c = tex2D (_MainTex, IN.uv_MainTex);  
    4.       
    5.     o.Albedo = c.rgb;  
    6.     o.Alpha = c.a;  
    7.     o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));  
    8. }  

在修改了變數型別後,我們現在來利用Unity內建的光照函式變數,以便我們可以控制Shader是如何處理光源的。為此,我們可以很大程度上減少Shader處理的光源個數。修改#pragma宣告:

  1. CGPROGRAM  
  2. #pragma surface surf SimpleLambert noforwardadd  

現在,我們可以使用共享UV座標來繼續優化Shader。為此,我們使用_MainTex的UV座標代替_NormalMap的UV在UnpackNormal()中的查詢作用,並移除Input結構中的uv_NormalMap:

  1. o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));  

  1. struct Input {  
  2.     half2 uv_MainTex;  
  3. };  

最後,我們告訴Unity,這個Shader只工作在特定的渲染器上

  1. CGPROGRAM  
  2. #pragma surface surf SimpleLambert exclude_path:prepass noforwardadd  

最後優化前後效果如下(左前右後):

    


可以看出,我們肉眼幾乎看不出任何差別,但是我們已經減少了這個Shader被繪製到螢幕上所花費的時間。我們將在下一節中利用Unity的視覺化工具來分析這種減少程度的大小。但在這裡,我們關注的是,使用了更少的資料來得到相同的渲染效果。在建立我們自己的Shader的時候,也要一直記住這個思想!


解釋


上面一共提到了4種優化方式:優化變數型別,共享UV座標,減少處理的光源個數,讓Shader只工作在特定的渲染器上。下面,我們來更深入地理解這些技術是如何工作的,最後再學習其他一些技巧。


優化變數型別


首先,我們來看一下在我們宣告變數時每個變數儲存的資料大小。由於在宣告變數時,我們往往有多個選擇(float,half,fixed),我們需要來看一下這些型別的特點:

  • float:高精度浮點值,通常是32位,也是三者中最慢的一個。它對應的還有float2,float3和float4。

  • half:中精度浮點值。通常是16位,範圍是-60000至+60000,它適合儲存UV座標,顏色值等,比float型別快很多。它對應的還有half2,half3,和half4。

  • fixed:低精度浮點值。通常是11位,範圍是-2.0至+2.0,精度為1/256。這是三者中最小的一個,可以用於光照計算顏色等。它對應的值有fixed2,fixed3和fixed4。

官網對型別的選擇,給出了下面的建議:
  • 儘可能使用低精度變數。
  • 對於顏色值和單位長度的向量,使用fixed。
  • 對於其他型別,如果範圍和精度合適的話,使用half;其他情況使用float。


減少處理的光源個數


從上可以看出,這一步優化是通過在#pragma語句中宣告noforwardadd值來實現的。這主要是告訴Unity,使用這種Shader的物件,只接受一個單一的平行光光源作為逐畫素光源,其他的光源都使用內建的球諧函式處理後作為逐頂點的光源。當我們在場景中放置了另一個光源時,這種策略會很明顯,因為我們的Shader使用一個法線貼圖進行逐畫素的操作。

這樣做當然很好,但是如果我們需要不止一個平行光,而且想要控制哪一個是用於該逐畫素計算的主光源,又該怎麼辦呢?這就需要Unity皮膚中的一個設定啦!如果你仔細觀察,就會法線每一個光源都有一個Render Mode下拉選單。當你點選它時,會出現AutoImportant, 和Not Important三種選項。通過選擇Important,你可以告訴Unity這個光源更需要被當成一個逐畫素光源,而非一個逐頂點光源。如果設定為Auto,那麼就由Unity自己做決定啦!


懵了是不是。。。為了說明上述意思,我們來做個試驗!在場景裡放置另一個點光源,然後移除Shader中的Main Texture。第一次,開啟平行光,關閉點光源(左圖);第二次關閉平行光,開啟點光源(右圖)。你可以發現第二個點光源並不會影響我們的法線貼圖(只是照亮了模型,也就是它只是逐頂點處理),只有第一個平行光才會影響。


   


這裡的優化,是由於我們把其他所有光源當成了頂點光源,而在計算畫素顏色時只計算一個主平行光作為畫素光源。



共享UV座標


這步優化很簡單,僅僅使用了Main Texture的UV座標來代替法線貼圖的UV座標,這樣實際上減少了內部提取法線貼圖UV座標的程式碼。這種方法可以很好地簡化我們的程式碼。


只工作在特定渲染器上


最後,我們在語句中宣告瞭,以便告訴Unity,這個Shader不會再接受來自延遲渲染中的其他任何自定義的光照。這意味著,我們僅可以在正向渲染(forward render)中有效地使用這個Shader,這是在主攝像機的設定中設定的。


幫助連結:正向渲染延遲渲染


寫在最後


其他的優化策略還有很多。我們之前學過如何把多個灰度圖打包到一個RGBA貼圖中,以及如何使用一張貼圖來模擬光照效果。由於這些眾多的技術,因此問如何優化Shader是一個很模糊的問題。但是,瞭解這些技術使得我們可以根據不同的Shader和平臺採用合適的技術,來得到一個具有穩定幀率的Shader。

相關文章