天天看點

Unity使用者手冊-Shader

Shader

GPU上的渲染程式。

渲染管線的流程:

幾何階段:

  • 頂點着色器(Vertex Shader):

主要工作是坐标變換,逐頂點光照和輸出後續階段所需的資料。

所有的幾何體,以頂點的形式,先經過頂點着色器,進行頂點的坐标轉換,把頂點坐标從模型空間轉換到齊次裁剪空間,正向渲染(Forward Render)會對頂點進行光照計算,得到頂點的光照。

VBO(Vertex Buffer Object):在顯存中申請一塊空間,存儲頂點的各類屬性資訊,在渲染時,直接從顯存的VBO中讀取頂點的屬性資訊,不需要從CPU傳輸資料,執行效率更高。

  • 裁剪處理(Clipping):

完全在視野内的圖元會繼續傳遞到下一個階段,完全在視野外的圖元不會繼續向下傳遞,因為他們不需要渲染。而部分在視野内需要進行一個處理,這就是裁剪。例如,一條線段的一個頂點在視野内,另一個頂點在視野外,那麼在視野外部的頂點應該使用一個新的頂點來代替,這個新的頂點位于這條線段和視野邊界的交點處。

  • 螢幕映射(Screen Mapping):

主要任務是把每個圖元的x和y坐标轉換到螢幕坐标系。

光栅化階段:

重要的目标就是計算每個片元覆寫了哪些像素,以及為這些像素計算他們的顔色。

  • 三角形設定。

上一階段輸出的是三角形網格的頂點,即我們得到的是三角形每條邊的兩個端點。但如果要得到整個三角形網格對像素的覆寫情況,就必須計算每條邊上的像素坐标。為了能夠計算邊界像素的坐标資訊,就需要得到三角形邊界的表示方式。這樣一個計算三角形網格表示資料的過程叫三角形設定。

  • 三角形周遊。

根據上一階段的計算結果來判斷一個三角形網格覆寫了哪些像素,被覆寫的像素,就會對應生成一個片元(fragment),并使用三角網格的3個頂點的頂點資訊對整個覆寫區域的像素進行插值,得到片元中的狀态。這一步輸出的就是一個片元序列。需要注意的是,一個片元并不是真正意義上的像素,而是包含了很多狀态的集合,這些狀态用于計算每個像素的最終顔色。包含了螢幕坐标,深度資訊,以及從幾何階段輸出的頂點資訊,例如法線、紋理坐标等。

  • 片元着色器(Fragment Shader):

為了在片元着色器中,進行紋理采樣,通常在頂點着色器階段輸出每個頂點對應的紋理坐标,然後經過光栅化階段對三角形網格的三個頂點對應的紋理坐标進行插值,得到其覆寫的片元的紋理坐标。

  • 逐片元操作,或是光栅化操作(Per-Fragment Operations):

1. 決定每個片元的可見性。例如模闆測試、深度測試等。

2. 如果一個片元通過了所有的測試,就需要把這個片元的顔色值和已經存儲在顔色緩沖區中的顔色進行混合。

模闆測試(Stencil Test):

如果開啟了模闆測試,GPU會首先讀取(使用讀取掩碼)模闆緩沖區中該片元位置的模版緩沖值(Stencil buffer),然後将該值和讀取到的參考值(Reference buffer)進行比較,比較函數由開發者指定,例如小于時舍棄該片元等。模闆測試通常用于限制渲染的區域。還有些進階用法,渲染陰影、輪廓渲染等。

深度測試(Depth Test):

如果開啟了深度測試,GPU将會把該片元的深度值與已經存在于深度緩沖區的深度值進行比較。比較函數由開發者指定。通常比較函數是小于等于的關系,才會讓它通過測試,即如果這個片元的深度值大于等于目前緩沖區的值,就會舍棄它。因為我們隻想顯示出離錄影機最近的物體,那些被其他物體遮擋的就不需要出現在螢幕上。如果通過了測試,開發者可以通過開啟/關閉深度寫入,指定是否要用這個片元的深度值覆寫原有的深度值。

混合(Blend):

對于不透明物體,關閉混合操作,這樣片元着色器計算得到的顔色值就可以直接覆寫掉已經存在于顔色緩沖區的顔色值。對于半透明物體,需要使用混合操作,讓物體看起來是透明的。開啟混合功能,GPU會把片元着色器計算得到的顔色值和已經存在于顔色緩沖區的顔色值,進行混合。

預設情況下OpenGL使用幀緩沖區作為渲染的目的地。幀緩沖區(FrameBuffer),包括深度緩沖區(depth),模闆緩沖區(stencil)和顔色緩沖區(color buffers)。顔色緩沖區是必不可少的,其他緩沖區可以存在可以不存在。

最後,為了避免我們看到正在進行光栅化的圖元,GPU會使用雙重緩沖的政策。對場景的渲染是放在幕後的,當場景被渲染到後置緩沖中,GPU就會交換後置緩沖區和前置緩沖區中的内容,而前置緩沖區是之前顯示在螢幕上的圖像。由此,保證我們看到的圖像總是連續的。

透明度測試(Alpha Test ):

隻要一個片元的透明度不滿足條件(小于某一個門檻值),那麼這個片元就會被舍棄。

為什麼在移動平台上,透明度測試會影響遊戲性能?

為了減少OverDraw(一個像素被渲染多次),PowerVR(ios)使用基于瓦片的延遲渲染(Tiled-based Deferred Rendering, TBDR)架構,把所有的渲染圖像裝入一個個瓦片(tile)中,再由硬體找到可見的片元,而隻有這些可見的片元才會執行片元着色器。另一些基于瓦片的GPU架構,如Adreno(高通)和Mali(ARM)會使用Early-Z或相似的技術進行一個低精度的深度測試,來剔除那些不需要渲染的片元。

由于在clip函數中使用了discard,改變了片元是否會被渲染的結果。是以,隻有執行完了所有的片元着色器後,GPU才能知道哪些片元會被真正渲染到螢幕上。這樣原先可以減少OverDraw的優化就都無效了。這種時候,使用透明度混合的性能往往比使用透明度測試更好。

//透明度測試使用AlphaTest隊列,在SubShader中設定
Tags{ "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}

clip(texColor.a - _CutOff);
//内部實作相當于
//if((texColor.a - _CutOff) < 0)
//{
// discard;
//}           

clip函數:

Cg提供了clip函數,如果給定的參數的任何一個分量為負數,就會舍棄目前像素的輸出顔色。

如果傳入alpha分量小于_CutOff,就會舍棄目前像素的輸出顔色,用來進行透明度測試。

透明度混合(Alpha Blending):

使用目前片元的透明度作為混合因子,與已經存儲在顔色緩沖中的顔色值進行混合,得到新的顔色。透明度混合要非常注意物體的渲染順序,需要關閉深度寫入。

為什麼透明度混合需要關閉深度寫入?

如果不關閉深度寫入,一個半透明物體後的物體本來是可以被我們看見的,但由于深度測試時,判斷該半透明物體具體錄影機更近,導緻後面的物體将被剔除,我們也就無法透過半透明物體看到後面的物體了。

可程式設計Shader:Vertex Shader

傳入的是頂點在模型空間的位置、光照、顔色等資訊,傳出的是頂點在剪裁空間的位置和顔色等資訊。

可程式設計Shader:Fragment Shader

在像素處理器中,傳入的是已經光栅化的像素在剪裁空間的位置,顔色等資訊,傳出的是像素被着色後的頂點顔色。

Shader與Material的關系

Material是對Shader的包裝,也可以說是Shader的預制。

OpenGL或DirectX

圖像應用程式設計接口,這些接口用于渲染二維或三維的圖形。這些接口架起了上層應用程式和GPU的溝通橋梁。我們的程式運作在CPU上,應用程式通過調用OpenGL或DirectX的圖形接口,将需要渲染的資料,例如頂點資料、紋理資料、材質參數等存儲在顯存中的特定區域。随後,可以通過調用圖形程式設計接口發出渲染指令(DrawCall),指令将會被顯示卡驅動翻譯成GPU可以運作的代碼,進行真正的繪制。

實體渲染 & Standard Shader

實體渲染是遵循光傳播的實體學模拟,更真實,尤其适合自然界中的材質模拟。

原理:基于物體表面的微表面理論,物體表面有很多微表面組成,反射光照由與視線一緻的微表面的直接反射(高光)和其他微表面互相之間的漫反射組成。

材質和Unity Shader的橋梁:Properties

Properties {

_MyColor ("Some Color", Color) = (1,1,1,1)

_MyVector ("Some Vector", Vector) = (0,0,0,0)

_MyNum("Num", Int) = 1

_MyRange("Range", Range(1, 11)) = 5

_MyFloat ("My float", Float) = 0.5

_MyTexture ("Texture", 2D) = "white" {}

_MyCubemap ("Cubemap", Cube) = "white" {}

_My3D("3D", 3D) = "black"{}

}           

以_MyColor("Some Color", Color) = (1,1,1,1)為例,_MyColor為Shader中變量名,"Some Color"為編輯器中顯示的屬性名,最後一個Color為類型,(1,1,1,1)為Color的值。

SubShader

SubShader{

// 可選的标簽

Tags { “RenderType”= “Opaque” }

// 可選的狀态

Cull Off

Pass{

}

// Other Pass

}           
  • 可以有多個SubShader,但至少有一個。Unity會掃描所有的SubShader,然後選擇第一個能夠在目标平台上運作的SubShader。如果都不支援,調用FallBack指定的Unity Shader。
  • SubShader中定義了一系列的Pass以及可選的狀态([RenderSetup])和标簽([Tags])設定。每個Pass定義了一次完整的渲染流程,但如果Pass的數目過多,就會造成渲染性能下降。至少有一個Pass。

狀态設定

Cull Back | Front | Off 設定剔除模式:剔除背面 | 正面 | 關閉剔除

ZTest Less | Greater | LEqual | GEqual | Equal | NotEqual | Always 開啟深度測試,設定深度測試的比較函數

ZWrite On | Off 開啟 | 關閉深度寫入

Blend SrcFactor DstFactor 開啟并設定混合模式,生成的顔色乘以SrcFactor。螢幕上已有的顔色乘以DstFactor,兩者相加。           

在SubShader中,設定渲染狀态,将會應用到所有的Pass。如果不想這樣,可以在Pass語義塊中單獨進行狀态設定。

參數對應聲明及類型說明

Unity中的屬性

_MyColor ("Some Color", Color) = (1,1,1,1)

_MyVector ("Some Vector", Vector) = (0,0,0,0)

_MyNum("Num", Int) = 1

_MyRange("Range", Range(1, 11)) = 5

_MyFloat ("My float", Float) = 0.5

_MyTexture ("Texture", 2D) = "white" {}

_MyCubemap ("Cubemap", Cube) = "white" {}

_My3D("3D", 3D) = "black"{}           

那麼在cg程式裡面應該再次聲明這些參數,才能在cg程式裡面使用

fixed4 _MyColor;

float4 _MyVector;

float _MyNum;

float _MyRange;

float _MyFloat;

sampler2D _MyTexture;

float4 _MyTexture_ST;

float4 _MyTexture_TexelSize;

samplerCUBE _MyCubemap;

sampler3D _My3D;           

float 32位存儲

half 16位存儲,-6萬 ~ +6萬

fixed 11位存儲,-2 ~ +2

是以一般情況下,顔色的值在[0, 1]之間,用fixed4來存儲。

float3x3 3*3的矩陣,類型中是字母x

與其他屬性不同的是,我們需要為紋理類型的屬性聲明一個float4類型的變量_MainTexture_ST。在Unity中,我們需要使用紋理名_ST的方式來聲明某個紋理的屬性。ST是縮放(Scale)和平移(Translation)的縮寫。_MainTexture_ST.xy存儲的是縮放值,而_MainTexture_ST.zw存儲的是偏移值,分别對應着材質面闆中,Tiling的x和y值,Offset的x和y值。

_MyTexture_TexelSize為紋理的每個紋素的大小,可以用來計算各個相鄰區域内的紋理進行采樣。比如紋理大小為(512*512),紋素大小為 (1 / 512)。

慎用分支和循環語句

慎用if-else、for和while語句,GPU在最壞的情況下,花在一個分支語句的時間相當于運作了所有的分支語句的時間。盡量避免使用流程控制語句,因為它們會降低GPU的并行處理操作。

不要除以0,在不同平台,結果往往不可預測。解決方法是,對于除數可能為0的情況,使用if語句判斷除數是否為0。

如果不可避免的使用分支語句來進行運算,一些建議:

  • 分支判斷語句中使用的條件變量最好是常數
  • 每個分支中包含的操作指令數盡可能的少
  • 分支的嵌套層數盡可能的少

盡量把放在片元着色器中的計算放到頂點着色器中

光照模型

光照模型就是一個公式,使用這個公式來計算在某個點的光照效果

标準光照模型

在标準光照模型中,我們把進入錄影機的光分為四個部分:

  • 自發光(Emissive)
  • 高光發射(Specular)
  • 漫反射(Diffuse,蘭伯特光照模型)
  • 環境光(Ambient,間接光照,光線通常會在多個物體之間反射)

蘭伯特光照(Lambert)模型

在平面某點漫反射光的光照強度,與反射光的法線向量和入射光線的夾角餘弦值成正比。

Diffuse = 入射光線的顔色和強度 * 反射系數 * max(0, cos夾角(入射光和法線的夾角))。

Cg提供了saturate函數,可以截取[0 , 1]之間的值。normal和lightDirection需要進行歸一化normalize處理,友善使用點積的方式獲得cos值。

Diffuse = 入射光線的顔色和強度 * 反射系數 * saturate( dot(normal,lightDirection))。

半蘭伯特模型

在光照無法到達的區域,模型外觀通常是全黑的,沒有任何明暗變化。Value在開發《半條命》時,提出了一種改善技術,叫做半蘭伯特模型。

Diffuse = 入射光線的顔色和強度 * 反射系數 * (dot(normal,lightDirection)* 0.5 + 0.5)。

Phong高光反射模型

Specular = 入射光線的顔色和強度 * 高光反射系數 * max(0, 視角方向點積反射方向)^_Gloss(高光區域的大小)

Cg提供了reflect函數,計算反射方向。reflect(i , n) i,入射方向;n,法線方向

reflectDir反射方向,viewDir視角方向,需要進行歸一化normalize處理。

fixed3 reflectDir = normalize(reflect(-worldLightDirection, worldNormal));

Specular = 入射光線的顔色和強度 * 高光反射系數 * pow(saturate(dot(reflectDir, viewDir)), _Gloss(高光區域的大小))

BlinnPhong高光反射模型

BlinnPhong光照模型沒有使用反射方向,引入一個新的矢量,通過對視角方向和光照方向相加後再歸一化得到的

fixed3 worldLightDirection = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

fixed3 halfDir = normalize(worldLightDirection + viewDir);

Specular = 入射光線的顔色和強度 * 高光反射系數 * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

性能優化

盡量采用Mobile檔案夾下的Shader。基于實體渲染的Standard Shader,在移動平台性能開銷太大。

CPU優化

使用批處理技術減少DrawCall數目。批處理技術原理是減少每幀需要的DrawCall數目,即每次調用DrawCall時盡可能的處理多個物體。

動态批處理

每一幀把可以進行批處理的模型網格進行合批,再把合并好的模型資料傳遞給GPU,然後使用同一種材質對其渲染。經過動态批處理的物體仍然可以移動,這是因為每幀Unity都會重新合并一次網格。

動态批處理條件限制:

(1)進行批處理的網格頂點屬性規模要小于900,如果Shader有三個屬性,那麼頂點數目不能超過300個。

(2)多Pass的Shader會中斷批處理。在前向渲染中,我們有時需要使用額外的Pass來為模型添加更多的光照效果,這樣一來,模型就不會被動态批處理了。

靜态批處理:

在運作開始的階段,把需要進行靜态批處理的模型合并到一個新的網格結構中,這意味着模型不能再運作時被移動。往往需要占用更多的記憶體來存儲合并後的網格結構。

無論是動态批處理還是靜态批處理,都要求模型之間需要共享同一個材質。如果兩個材質之間隻是使用的紋理不同,可以把這些紋理合并到一張更大的紋理中,這張更大的紋理叫做圖集(atlas)。

GPU優化

減少需要的頂點數目

(1)優化模型,盡可能的減少三角形的面數,移除不必要的硬邊及紋理銜接,避免邊界平滑和紋理分離。

邊界平滑(smoothing splits,一個頂點可能會對應多個法線資訊或切線資訊,在Unity導入模型時,有一個Smoothing Angles(光滑組)的設定,當Smoothing Angles的值為0時,就沒有共用的頂點,拆分出更多新的頂點,可以展示更多細節。當這個值越來越大,共用頂點越多,細節就更少一些。)

紋理分離(uv splits,一個頂點可能有多個紋理坐标。面與面之間使用的一些相同頂點,在不同面上,同一個頂點的紋理坐标可能并不相同 ,GPU會把這個頂點拆分成多個具有不同紋理坐标的頂點)。

(2)使用模型的LOD技術

LOD允許當對象逐漸遠離錄影機時,減少模型上的面片數量,進而提高性能。

(3)使用遮擋剔除技術

消除在其他物體後面看不到的物體,也就不會渲染這個看不到的頂點,進而提高性能。注意:在移動平台,遮擋剔除開銷太大,不建議使用。

(4)Camera.layerCullDistances

相機跟每一層的剔除距離。比如,在視野中有很多npc,可以把npc設定到npc層,并在代碼中為npc層設定較小的layerCullDistances剔除距離,這樣就可以隻渲染npc層剔除距離内的npc,減少性能開銷。

(5)注意錄影機的FOV(Field of View),影響可視物體的數量

(6)Camera視錐體越小越好,注意遠裁減面的距離

減少需要處理的片元數目

(1)控制繪制順序,由于深度測試的存在,如果我們可以保證物體都是從前往後繪制,那麼就可以很大程度上減少OverDraw,這是因為在後面繪制的物體由于無法通過深度測試,就不會在進行後面的渲染處理。

(2)在移動平台,渲染透明物體,Alpha混合性能比Alpha測試更好

(3)慎用實時光照

使用光照烘焙技術,把光照提前烘焙到一張光照紋理中(lightmap),在運作時根據紋理坐标得到光照結果。

減少計算複雜度

(1)使用Shader的LOD技術

Shader的LOD技術可以控制使用的Shader等級。原理是隻有Shader的LOD值小于某個設定值,這個Shader才會被使用。

在某些情況下,我們可能需要去掉一些使用複雜計算的Shader渲染。這時,我們可以使用Shader.maximumLOD或Shader.globalMaximumLOD來設定允許的最大LOD值。

(2)代碼方面的優化

  • 盡可能使用低精度的浮點值進行計算。
  • 使用插值寄存器把資料從頂點着色器傳遞給下一個階段時,應該使用盡可能少的插值變量。
  • 盡量不要使用全屏的螢幕後處理效果,如果真的需要使用,盡量使用低精度計算,高精度計算可以使用查找表(LUT)或者轉移到頂點着色器中進行處理。把多個特效合并到一個shader中。使用縮放思想,在高性能的平台,使用更高的分辨率,開啟螢幕後處理效果,在低性能平台保證遊戲正常運作即可。
  • 盡可能不要使用分支或循環語句。
  • 盡可能避免使用類似sin、tan、pow、log等較為複雜的數學計算,請考慮使用查找紋理(lookup texture, LUT)作為複雜數學計算的替代方法。

節省記憶體帶寬

  • 減少紋理大小,針對不同平台,采用壓縮紋理來減少紋理大小,可以加快加載速度,減少記憶體占用,顯著提高渲染性能。
  • 利用mipmap,始終為3D場景中使用的紋理啟用mipmap,但此規則例外的是:UI元素或2D遊戲中
  • 利用分辨率縮放,Screen.SetResolution