Unity Shader-渲染隊列,ZTest,ZWrite,Early-Z


在渲染階段,引擎所做的工作是把所有場景中的對象按照一定的策略(順序)進行渲染。最早的是 畫家算法,顧名思義,就是像畫家畫畫一樣,先畫后面的物體,如果前面還有物體,那么就用前面的物體把物體覆蓋掉,不過這種方式由於排序是針對物體來排序的,而物體之間也可能有重疊,所以效果並不好。所以目前更加常用的方式是z-buffer算法,類似顏色緩沖區緩沖顏色,z-buffer中存儲的是當前的深度信息,對於每個像素存儲一個深度值,這樣,我們屏幕上顯示的每個像素點都會進行深度排序,就可以保證繪制的遮擋關系是正確的。而控制z-buffer就是通過ZTest,和ZWrite來進行。但是有時候需要更加精准的控制不同類型的對象的渲染順序,所以就有了渲染隊列。今天就來學習一下渲染隊列,ZTest,ZWrite的基本使用以及分析一下Unity為了Early-Z所做的一些優化。
 

Unity中的幾種渲染隊列

 
首先看一下Unity中的幾種內置的渲染隊列,按照渲染順序,從先到后進行排序,隊列數越小的,越先渲染,隊列數越大的,越后渲染。
 
Background(1000) 最早被渲染的物體的隊列。
Geometry   (2000) 不透明物體的渲染隊列。大多數物體都應該使用該隊列進行渲染,也是Unity Shader中默認的渲染隊列。
AlphaTest   (2450) 有透明通道,需要進行Alpha Test的物體的隊列,比在Geomerty中更有效。
Transparent(3000) 半透物體的渲染隊列。一般是不寫深度的物體,Alpha Blend等的在該隊列渲染。
Overlay      (4000) 最后被渲染的物體的隊列,一般是覆蓋效果,比如鏡頭光暈,屏幕貼片之類的。
 
Unity中設置渲染隊列也很簡單,我們不需要手動創建,也不需要寫任何腳本,只需要在shader中增加一個Tag就可以了,當然,如果不加,那么就是默認的渲染隊列Geometry。比如我們需要我們的物體在Transparent這個渲染隊列中進行渲染的話,就可以這樣寫:
[csharp]  view plain  copy
 
  1. Tags { "Queue" = "Transparent"}  
我們可以直接在shader的Inspector面板上看到shader的渲染隊列:
 
另外,我們在寫shader的時候還經常有個Tag叫RenderType,不過這個沒有Render Queue那么常用,這里順便記錄一下:
Opaque: 用於大多數着色器(法線着色器、自發光着色器、反射着色器以及地形的着色器)。
Transparent:用於半透明着色器(透明着色器、粒子着色器、字體着色器、地形額外通道的着色器)。
TransparentCutout: 蒙皮透明着色器(Transparent Cutout,兩個通道的植被着色器)。
Background: 天空盒着色器。
Overlay: GUITexture,鏡頭光暈,屏幕閃光等效果使用的着色器。
TreeOpaque: 地形引擎中的樹皮。
TreeTransparentCutout:  地形引擎中的樹葉。
TreeBillboard: 地形引擎中的廣告牌樹。
Grass: 地形引擎中的草。
GrassBillboard: 地形引擎何中的廣告牌草。
 

相同渲染隊列中不透明物體的渲染順序

 
拿出Unity,創建三個立方體,都使用默認的bump diffuse shader(渲染隊列相同),分別給三個不同的材質(相同材質的小頂點數的物體引擎會動態合批),用Unity5帶的Frame Debug工具查看一下Draw Call。(Unity5真是好用得多了,如果用4的話,還得用NSight之類的抓幀)
可以看出,Unity中對於不透明的物體,是采用了從前到后的渲染順序進行渲染的,這樣,不透明物體在進行完vertex階段,進行Z Test,然后就可以得到該物體最終是否在屏幕上可見了,如果前面渲染完的物體已經寫好了深度,深度測試失敗,那么后面渲染的物體就直接不會再去進行fragment階段。(不過這里需要把三個物體之間的距離稍微拉開一些,本人在測試時發現,如果距離特別近,就會出現渲染次序比較亂的情況,因為我們不知道Unity內部具體排序時是按照什么標准來判定的哪個物體離攝像機更近,這里我也就不妄加猜測了)
 

相同渲染隊列中半透明物體的渲染順序

 
透明物體的渲染一直是圖形學方面比較蛋疼的地方,對於透明物體的渲染,就不能像渲染不透明物體那樣多快好省了,因為透明物體不會寫深度,也就是說透明物體之間的穿插關系是沒有辦法判斷的,所以半透明的物體在渲染的時候一般都是采用從后向前的方法進行渲染,由於透明物體多了,透明物體不寫深度,那么透明物體之間就沒有所謂的可以通過深度測試來剔除的優化,每個透明物體都會走像素階段的渲染,會造成大量的Over Draw。這也就是粒子特效特別耗費性能的原因。
 
我們實驗一下Unity中渲染半透明物體的順序,還是上面的三個立方體,我們把材質的shader統一換成粒子最常用的Particle/Additive類型的shader,再用Frame Debug工具查看一下渲染的順序:
半透明的物體渲染的順序是從后到前,不過由於半透相關的內容比較復雜,就先不在這篇文章中說了,打算另起一篇。
 

自定義渲染隊列

 
Unity支持我們自定義渲染隊列,比如我們需要保證某種類型的對象需要在其他類型的對象渲染之后再渲染,就可以通過自定義渲染隊列進行渲染。而且超級方便,我們只需要在寫shader的時候修改一下渲染隊列中的Tag即可。比如我們希望我們的物體要在所有默認的不透明物體渲染完之后渲染,那么我們就可以使用Tag{“Queue” = “Geometry+1”}就可以讓使用了這個shader的物體在這個隊列中進行渲染。
 
還是上面的三個立方體,這次我們分別給三個不同的shader,並且渲染隊列不同,通過上面的實驗我們知道,默認情況下,不透明物體都是在Geometry這個隊列中進行渲染的,那么不透明的三個物體就會按照cube1,cube2,cube3進行渲染。這次我們希望將渲染的順序反過來,那么我們就可以讓cube1的渲染隊列最大,cube3的渲染隊列最小。貼出其中一個的shader:
[csharp]  view plain  copy
 
  1. Shader "Custom/RenderQueue1" {  
  2.   
  3.     SubShader  
  4.     {  
  5.         Tags { "RenderType"="Opaque" "Queue" = "Geometry+1"}  
  6.       
  7.         Pass  
  8.         {  
  9.             CGPROGRAM  
  10.             #pragma vertex vert  
  11.             #pragma fragment frag  
  12.             #include "UnityCG.cginc"  
  13.             struct v2f  
  14.             {  
  15.                 float4 pos : SV_POSITION;  
  16.             };  
  17.   
  18.             v2f vert(appdata_base v)  
  19.             {  
  20.                 v2f o;  
  21.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  22.                 return o;  
  23.             }  
  24.   
  25.             fixed4 frag(v2f i) : SV_Target  
  26.             {  
  27.                 return fixed4(0,0,1,1);  
  28.             }  
  29.             ENDCG  
  30.         }  
  31.     }  
  32.     //FallBack "Diffuse"  
  33. }  
其他的兩個shader類似,只是渲染隊列和輸出顏色不同。
通過渲染隊列,我們就可以自由地控制使用該shader的物體在什么時機渲染。比如某個不透明物體的像素階段操作較費,我們就可以控制它的渲染隊列,讓其渲染更靠后,這樣可以通過其他不透明物體寫入的深度剔除該物體所占的一些像素。
 
PS:這里貌似發現了個問題,我們在修改shader的時候一般不需要什么其他操作就可以直接看到修改后的變化,但是本人改完渲染隊列后,有時候會出現從shader的文件上能看到渲染隊列的變化,但是從渲染結果以及Frame Debug工具中並沒有看到渲染結果的變化,重啟Unity也沒有起到作用,直到我把shader重新賦給材質之后,變化才起了效果...(猜測是個bug,因為看到網上還有和我一樣的倒霉蛋被這個坑了,本人的版本是5.3.2,害我差點懷疑昨天是不是喝了,剛實驗完的結果就完全不對了...)
 

ZTest(深度測試)和ZWrite(深度寫入) 

 
上一個例子中,雖然渲染的順序反了過來,但是物體之間的遮擋關系仍然是正確的,這就是z-buffer的功勞,不論我們的渲染順序怎樣,遮擋關系仍然能夠保持正確。而我們對z-buffer的調用就是通過ZTest和ZWrite來實現的。
 
首先看一下ZTest,ZTest即深度測試,所謂測試,就是針對當前對象在屏幕上(更准確的說是frame buffer)對應的像素點,將對象自身的深度值與當前該像素點緩存的深度值進行比較,如果通過了,本對象在該像素點才會將顏色寫入顏色緩沖區,否則否則不會寫入顏色緩沖。ZTest提供的狀態較多。ZTest Less(深度小於當前緩存則通過, ZTest Greater(深度大於當前緩存則通過),ZTest LEqual(深度小於等於當前緩存則通過),ZTest GEqual(深度大於等於當前緩存則通過),ZTest Equal(深度等於當前緩存則通過),ZTest NotEqual(深度不等於當前緩存則通過),ZTest Always(不論如何都通過)。注意,ZTest Off等同於ZTest Always,關閉深度測試等於完全通過。
 
下面再看一下ZWrite,ZWrite比較簡單,只有兩種狀態,ZWrite On(開啟深度寫入)和ZWrite Off(關閉深度寫入)。當我們開啟深度寫入的時候,物體被渲染時針對物體在屏幕(更准確地說是frame buffer)上每個像素的深度都寫入到深度緩沖區;反之,如果是ZWrite Off,那么物體的深度就不會寫入深度緩沖區。但是,物體是否會寫入深度,除了ZWrite這個狀態之外,更重要的是需要深度測試通過,也就是ZTest通過,如果ZTest都沒通過,那么也就不會寫入深度了。就好比默認的渲染狀態是ZWrite On和ZTest LEqual,如果當前深度測試失敗,說明這個像素對應的位置,已經有一個更靠前的東西占坑了,即使寫入了,也沒有原來的更靠前,那么也就沒有必要再去寫入深度了。所以上面的ZTest分為通過和不通過兩種情況,ZWrite分為開啟和關閉兩種情況的話,一共就是四種情況:
 
1.深度測試通過,深度寫入開啟:寫入深度緩沖區,寫入顏色緩沖區;
2.深度測試通過,深度寫入關閉:不寫深度緩沖區,寫入顏色緩沖區;
3.深度測試失敗,深度寫入開啟:不寫深度緩沖區,不寫顏色緩沖區;
4.深度測試失敗,深度寫入關閉:不寫深度緩沖區,不寫顏色緩沖區;
 
Unity中默認的狀態(寫shader時什么都不寫的狀態)是ZTest LEqual和ZWrite On,也就是說默認是開啟深度寫入,並且深度小於等於當前緩存中的深度就通過深度測試,深度緩存中原始為無限大,也就是說離攝像機越近的物體會更新深度緩存並且遮擋住后面的物體。如下圖所示,前面的正方體會遮擋住后面的物體:
 
寫幾個簡單的小例子來看一下ZTest,ZWrite以及Render Queue這幾個狀態對渲染結果的控制。
 
讓綠色的對象不被前面的立方體遮擋,一種方式是關閉前面的藍色立方體深度寫入:
通過上面的實驗結果,我們知道,按照從前到后的渲染順序,首先渲染藍色物體,藍色物體深度測試通過,顏色寫入緩存,但是關閉了深度寫入,藍色部分的深度緩存值仍然是默認的Max,后面渲染的綠色立方體,進行深度測試仍然會成功,寫入顏色緩存,並且寫入了深度,因此藍色立方體沒有起到遮擋的作用。
另一種方式是讓綠色強制通過深度測試:
這個例子中其他立方體的shader使用默認的渲染方式,綠色的將ZTest設置為Always,也就是說不管怎樣,深度測試都通過,將綠色立方體的顏色寫入緩存,如果沒有其他覆蓋了,那么最終的輸出就是綠色的了。
 
那么如果紅色的也開了ZTest Always會怎么樣?
在紅色立方體也用了ZTest Always后,紅色遮擋了綠色的部分顯示為了紅色。如果我們換一下渲染隊列,讓綠色在紅色之前渲染,結果就又不一樣了:
更換了渲染隊列,讓綠色的渲染隊列+1,在默認隊列Geometry之后渲染,最終重疊部分又變回了綠色。可見,當ZTest都通過時,上一個寫入顏色緩存的會覆蓋上一個,也就是說最終輸出的是最后一個渲染的對象顏色。
 
再看一下Greater相關的部分有什么作用,這次我們其他的都使用默認的渲染狀態,綠色的立方體shader中ZTest設置為Greater:
這個效果就比較好玩了,雖然我們發現在比較深度時,前面被藍色立方體遮擋的部分,綠色的最終覆蓋了藍色,是想要的結果,不過其他部分哪里去了呢?簡單分析一下,渲染順序是從前到后,也就是說藍色最先渲染,默認深度為Max,藍色立方體的深度滿足LEqual條件,就寫入了深度緩存,然后綠色開始渲染,重疊的部分的深度緩存是藍色立方體寫入的,而綠色的深度值滿足大於藍色深度的條件,所以深度測試通過,重疊部分顏色更新為綠色;而與紅色立方體重合的部分,紅色立方體最后渲染,與前面的部分進行深度測試,小於前面的部分,深度測試失敗,重疊部分不會更新為紅色,所以重疊部分最終為綠色。而綠色立方體沒有與其他部分重合的地方為什么消失了呢?其實是因為綠色立方體渲染時,除了藍色立方體渲染的地方是有深度信息的,其他部分的深度信息都為Max,藍色部分用Greater進行判斷,肯定會失敗,也就不會有顏色更新。
有一個好玩的效果其實就可以考ZTest Greater來實現,就是游戲里面經常出現的,當玩家被其他場景對象遮擋時,遮擋的部分會呈現出X-光的效果;其實是在渲染玩家時,增加了一個Pass,默認的Pass正常渲染,而增加的一個Pass就使用Greater進行深度測試,這樣,當玩家被其他部分遮擋時,遮擋的部分才會顯示出來,用一個描邊的效果渲染,其他部分仍然使用原來的Pass即可。
 

Early-Z技術

 
傳統的渲染管線中,ZTest其實是在Blending階段,這時候進行深度測試,所有對象的像素着色器都會計算一遍,沒有什么性能提升,僅僅是為了得出正確的遮擋結果,會造成大量的無用計算,因為每個像素點上肯定重疊了很多計算。因此現代GPU中運用了Early-Z的技術,在Vertex階段和Fragment階段之間(光柵化之后,fragment之前)進行一次深度測試,如果深度測試失敗,就不必進行fragment階段的計算了,因此在性能上會有很大的提升。但是最終的ZTest仍然需要進行,以保證最終的遮擋關系結果正確。前面的一次主要是Z-Cull為了裁剪以達到優化的目的,后一次主要是Z-Check,為了檢查,如下圖:
Early-Z的實現,主要是通過一個Z-pre-pass實現,簡單來說,對於所有不透明的物體(透明的沒有用,本身不會寫深度),首先用一個超級簡單的shader進行渲染,這個shader不寫顏色緩沖區,只寫深度緩沖區,第二個pass關閉深度寫入,開啟深度測試,用正常的shader進行渲染。其實這種技術,我們也可以借鑒,在渲染透明物體時,因為關閉了深度寫入,有時候會有其他不透明的部分遮擋住透明的部分,而我們其實不希望他們被遮擋,僅僅希望被遮擋的物體半透,這時我們就可以用兩個pass來渲染,第一個pass使用Color Mask屏蔽顏色寫入,僅寫入深度,第二個pass正常渲染半透,關閉深度寫入。
 
關於Early-Z技術可以參考ATI的論文 Applications of Explicit Early-Z Culling以及 PPT,還有一篇Intel的 文章
 

Unity渲染順序總結

 
如果我們先繪制后面的物體,再繪制前面的物體,就會造成over draw;而通過Early-Z技術,我們就可以先繪制較近的物體,再繪制較遠的物體(僅限不透明物體),這樣,通過先渲染前面的物體,讓前面的物體先占坑,就可以讓后面的物體深度測試失敗,進而減少重復的fragment計算,達到優化的目的。Unity中默認應該就是按照最近距離的面進行繪制的,我們可以看一下Unity官方的文檔中顯示的:
從文檔給出的流程來看,這個Depth-Test發生在Vertex階段和Fragment階段之間,也就是上面所說的Early-Z優化。
簡單總結一下Unity中的渲染順序:先渲染不透明物體,順序是從前到后;再渲染透明物體,順序是從后到前。
 

Alpha Test(Discard)在移動平台消耗較大的原因

 
從本人剛剛開始接觸渲染,就開始聽說移動平台Alpha Test比較費,當時比較納悶,直接discard了為什么會費呢,應該更省才對啊?這個問題困擾了我好久,今天來刨根問底一下。還是跟我們上面講到的Early-Z優化。正常情況下,比如我們渲染一個面片,不管是否是開啟深度寫入或者深度測試,這個面片的光柵化之后對應的像素的深度值都可以在Early-Z(Z-Cull)的階段判斷出來了;而如果開啟了Alpha Test(Discard)的時候,discard這個操作是在fragment階段進行的,也就是說這個面片光柵化之后對應的像素是否可見,是在fragment階段之后才知道的,最終需要靠Z-Check進行判斷這個像素點最終的顏色。其實想象一下也能夠知道,如果我們開了Alpha Test並且還用Early-Z的話,一塊本來應該被剃掉的地方,就仍然寫進了深度緩存,這樣就會造成其他部分被一個完全沒東西的地方遮擋,最終的渲染效果肯定就不對了。所以,如果我們開啟了Alpha Test,就不會進行Early-Z,Z Test推遲到fragment之后進行,那么這個物體對應的shader就會完全執行vertex shader和fragment shader,造成over draw。有一種方式是使用Alpha Blend代替Alpha Test,雖然也很費,但是至少Alpha Blend雖然不寫深度,但是深度測試是可以提前進行的,因為不會在fragment階段再決定是否可見,因為都是可見的,只是透明度比較低罷了。不過這樣只是權宜之計,Alpha Blend並不能完全代替Alpha Test。
 
關於Alpha Test對於Power VR架構的GPU性能的影響,簡單引用一下官方的 鏈接以及一篇 討論帖
 


免責聲明!

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



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