對於剛學習Shader的開發人員來講,對於渲染隊列中ZTest和ZWrite可能一點都不清楚,爲了幫助你們開發,本篇文章就給初學Shader的朋友準備了渲染隊列學習,ZTest,ZWrite的基本使用以及分析一下Unity爲了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這個渲染隊列中進行渲染的話,就能夠這樣寫:
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:
Shader "Custom/RenderQueue1" {
SubShader
{
Tags { "RenderType"="Opaque" "Queue" = "Geometry+1"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(0,0,1,1);
}
ENDCG
}
}
//FallBack "Diffuse"
}
其餘的兩個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分爲開啓和關閉兩種狀況的話,一共就是四種狀況:
- 深度測試經過,深度寫入開啓:寫入深度緩衝區,寫入顏色緩衝區;
- 深度測試經過,深度寫入關閉:不寫深度緩衝區,寫入顏色緩衝區;
- 深度測試失敗,深度寫入開啓:不寫深度緩衝區,不寫顏色緩衝區;
- 深度測試失敗,深度寫入關閉:不寫深度緩衝區,不寫顏色緩衝區;
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性能的影響,簡單引用一下官方的連接以及一篇討論帖:架構
最後再附上兩篇參考文章
原文連接:http://blog.csdn.net/puppet_master https://blog.csdn.net/puppet_master/article/details/73478905