原文地址:Unity Shader-渲染隊(duì)列,ZTest,,ZWrite,,Early-Z簡介在渲染階段,引擎所做的工作是把所有場景中的對象按照一定的策略(順序)進(jìn)行渲染,。最早的是畫家算法,,顧名思義,就是像畫家畫畫一樣,,先畫后面的物體,,如果前面還有物體,,那么就用前面的物體把物體覆蓋掉,,不過這種方式由于排序是針對物體來排序的,而物體之間也可能有重疊,,所以效果并不好,。所以目前更加常用的方式是z-buffer算法,類似顏色緩沖區(qū)緩沖顏色,,z-buffer中存儲的是當(dāng)前的深度信息,,對于每個(gè)像素存儲一個(gè)深度值,這樣,,我們屏幕上顯示的每個(gè)像素點(diǎn)都會進(jìn)行深度排序,,就可以保證繪制的遮擋關(guān)系是正確的。而控制z-buffer就是通過ZTest,和ZWrite來進(jìn)行,。但是有時(shí)候需要更加精準(zhǔn)的控制不同類型的對象的渲染順序,,所以就有了渲染隊(duì)列。今天就來學(xué)習(xí)一下渲染隊(duì)列,,ZTest,,ZWrite的基本使用以及分析一下Unity為了Early-Z所做的一些優(yōu)化。 Unity中的幾種渲染隊(duì)列首先看一下Unity中的幾種內(nèi)置的渲染隊(duì)列,,按照渲染順序,,從先到后進(jìn)行排序,隊(duì)列數(shù)越小的,,越先渲染,,隊(duì)列數(shù)越大的,越后渲染,。 Background(1000) 最早被渲染的物體的隊(duì)列,。 Geometry (2000) 不透明物體的渲染隊(duì)列。大多數(shù)物體都應(yīng)該使用該隊(duì)列進(jìn)行渲染,,也是Unity Shader中默認(rèn)的渲染隊(duì)列,。 AlphaTest (2450) 有透明通道,需要進(jìn)行Alpha Test的物體的隊(duì)列,,比在Geomerty中更有效,。 Transparent(3000) 半透物體的渲染隊(duì)列。一般是不寫深度的物體,,Alpha Blend等的在該隊(duì)列渲染,。 Overlay (4000) 最后被渲染的物體的隊(duì)列,一般是覆蓋效果,,比如鏡頭光暈,,屏幕貼片之類的。 Unity中設(shè)置渲染隊(duì)列也很簡單,,我們不需要手動(dòng)創(chuàng)建,,也不需要寫任何腳本,只需要在shader中增加一個(gè)Tag就可以了,,當(dāng)然,,如果不加,那么就是默認(rèn)的渲染隊(duì)列Geometry,。比如我們需要我們的物體在Transparent這個(gè)渲染隊(duì)列中進(jìn)行渲染的話,,就可以這樣寫: 我們可以直接在shader的Inspector面板上看到shader的渲染隊(duì)列: 另外,我們在寫shader的時(shí)候還經(jīng)常有個(gè)Tag叫RenderType,,不過這個(gè)沒有Render Queue那么常用,,這里順便記錄一下: Opaque: 用于大多數(shù)著色器(法線著色器,、自發(fā)光著色器、反射著色器以及地形的著色器),。 Transparent:用于半透明著色器(透明著色器,、粒子著色器、字體著色器,、地形額外通道的著色器),。 TransparentCutout: 蒙皮透明著色器(Transparent Cutout,兩個(gè)通道的植被著色器),。 Background: 天空盒著色器,。 Overlay: GUITexture,鏡頭光暈,,屏幕閃光等效果使用的著色器,。 TreeOpaque: 地形引擎中的樹皮。 TreeTransparentCutout: 地形引擎中的樹葉,。 TreeBillboard: 地形引擎中的廣告牌樹,。 Grass: 地形引擎中的草。 GrassBillboard: 地形引擎何中的廣告牌草,。 相同渲染隊(duì)列中不透明物體的渲染順序拿出Unity,,創(chuàng)建三個(gè)立方體,都使用默認(rèn)的bump diffuse shader(渲染隊(duì)列相同),,分別給三個(gè)不同的材質(zhì)(相同材質(zhì)的小頂點(diǎn)數(shù)的物體引擎會動(dòng)態(tài)合批),,用Unity5帶的Frame Debug工具查看一下Draw Call。(Unity5真是好用得多了,,如果用4的話,,還得用NSight之類的抓幀) 可以看出,Unity中對于不透明的物體,,是采用了從前到后的渲染順序進(jìn)行渲染的,,這樣,不透明物體在進(jìn)行完vertex階段,,進(jìn)行Z Test,,然后就可以得到該物體最終是否在屏幕上可見了,如果前面渲染完的物體已經(jīng)寫好了深度,,深度測試失敗,,那么后面渲染的物體就直接不會再去進(jìn)行fragment階段,。(不過這里需要把三個(gè)物體之間的距離稍微拉開一些,,本人在測試時(shí)發(fā)現(xiàn),如果距離特別近,,就會出現(xiàn)渲染次序比較亂的情況,,因?yàn)槲覀儾恢繳nity內(nèi)部具體排序時(shí)是按照什么標(biāo)準(zhǔn)來判定的哪個(gè)物體離攝像機(jī)更近,,這里我也就不妄加猜測了) 相同渲染隊(duì)列中半透明物體的渲染順序透明物體的渲染一直是圖形學(xué)方面比較蛋疼的地方,對于透明物體的渲染,,就不能像渲染不透明物體那樣多快好省了,,因?yàn)橥该魑矬w不會寫深度,也就是說透明物體之間的穿插關(guān)系是沒有辦法判斷的,,所以半透明的物體在渲染的時(shí)候一般都是采用從后向前的方法進(jìn)行渲染,,由于透明物體多了,透明物體不寫深度,,那么透明物體之間就沒有所謂的可以通過深度測試來剔除的優(yōu)化,,每個(gè)透明物體都會走像素階段的渲染,會造成大量的Over Draw,。這也就是粒子特效特別耗費(fèi)性能的原因,。 我們實(shí)驗(yàn)一下Unity中渲染半透明物體的順序,還是上面的三個(gè)立方體,,我們把材質(zhì)的shader統(tǒng)一換成粒子最常用的Particle/Additive類型的shader,,再用Frame Debug工具查看一下渲染的順序: 半透明的物體渲染的順序是從后到前,不過由于半透相關(guān)的內(nèi)容比較復(fù)雜,,就先不在這篇文章中說了,,打算另起一篇。 自定義渲染隊(duì)列Unity支持我們自定義渲染隊(duì)列,,比如我們需要保證某種類型的對象需要在其他類型的對象渲染之后再渲染,,就可以通過自定義渲染隊(duì)列進(jìn)行渲染。而且超級方便,,我們只需要在寫shader的時(shí)候修改一下渲染隊(duì)列中的Tag即可,。比如我們希望我們的物體要在所有默認(rèn)的不透明物體渲染完之后渲染,那么我們就可以使用Tag{“Queue” = “Geometry+1”}就可以讓使用了這個(gè)shader的物體在這個(gè)隊(duì)列中進(jìn)行渲染,。 還是上面的三個(gè)立方體,,這次我們分別給三個(gè)不同的shader,并且渲染隊(duì)列不同,,通過上面的實(shí)驗(yàn)我們知道,,默認(rèn)情況下,不透明物體都是在Geometry這個(gè)隊(duì)列中進(jìn)行渲染的,,那么不透明的三個(gè)物體就會按照cube1,cube2,cube3進(jìn)行渲染,。這次我們希望將渲染的順序反過來,那么我們就可以讓cube1的渲染隊(duì)列最大,,cube3的渲染隊(duì)列最小,。貼出其中一個(gè)的shader: 其他的兩個(gè)shader類似,只是渲染隊(duì)列和輸出顏色不同,。 通過渲染隊(duì)列,,我們就可以自由地控制使用該shader的物體在什么時(shí)機(jī)渲染,。比如某個(gè)不透明物體的像素階段操作較費(fèi),我們就可以控制它的渲染隊(duì)列,,讓其渲染更靠后,,這樣可以通過其他不透明物體寫入的深度剔除該物體所占的一些像素。 PS:這里貌似發(fā)現(xiàn)了個(gè)問題,,我們在修改shader的時(shí)候一般不需要什么其他操作就可以直接看到修改后的變化,,但是本人改完渲染隊(duì)列后,有時(shí)候會出現(xiàn)從shader的文件上能看到渲染隊(duì)列的變化,,但是從渲染結(jié)果以及Frame Debug工具中并沒有看到渲染結(jié)果的變化,,重啟Unity也沒有起到作用,直到我把shader重新賦給材質(zhì)之后,,變化才起了效果...(猜測是個(gè)bug,,因?yàn)榭吹骄W(wǎng)上還有和我一樣的倒霉蛋被這個(gè)坑了,本人的版本是5.3.2,,害我差點(diǎn)懷疑昨天是不是喝了,,剛實(shí)驗(yàn)完的結(jié)果就完全不對了...) ZTest(深度測試)和ZWrite(深度寫入)上一個(gè)例子中,雖然渲染的順序反了過來,,但是物體之間的遮擋關(guān)系仍然是正確的,,這就是z-buffer的功勞,不論我們的渲染順序怎樣,,遮擋關(guān)系仍然能夠保持正確,。而我們對z-buffer的調(diào)用就是通過ZTest和ZWrite來實(shí)現(xiàn)的。 首先看一下ZTest,,ZTest即深度測試,,所謂測試,就是針對當(dāng)前對象在屏幕上(更準(zhǔn)確的說是frame buffer)對應(yīng)的像素點(diǎn),,將對象自身的深度值與當(dāng)前該像素點(diǎn)緩存的深度值進(jìn)行比較,,如果通過了,本對象在該像素點(diǎn)才會將顏色寫入顏色緩沖區(qū),,否則否則不會寫入顏色緩沖,。ZTest提供的狀態(tài)較多。ZTest Less(深度小于當(dāng)前緩存則通過,, ZTest Greater(深度大于當(dāng)前緩存則通過),,ZTest LEqual(深度小于等于當(dāng)前緩存則通過),ZTest GEqual(深度大于等于當(dāng)前緩存則通過),,ZTest Equal(深度等于當(dāng)前緩存則通過),,ZTest NotEqual(深度不等于當(dāng)前緩存則通過),ZTest Always(不論如何都通過)。注意,,ZTest Off等同于ZTest Always,,關(guān)閉深度測試等于完全通過,。 下面再看一下ZWrite,,ZWrite比較簡單,只有兩種狀態(tài),,ZWrite On(開啟深度寫入)和ZWrite Off(關(guān)閉深度寫入),。當(dāng)我們開啟深度寫入的時(shí)候,物體被渲染時(shí)針對物體在屏幕(更準(zhǔn)確地說是frame buffer)上每個(gè)像素的深度都寫入到深度緩沖區(qū),;反之,,如果是ZWrite Off,那么物體的深度就不會寫入深度緩沖區(qū),。但是,,物體是否會寫入深度,除了ZWrite這個(gè)狀態(tài)之外,,更重要的是需要深度測試通過,,也就是ZTest通過,如果ZTest都沒通過,,那么也就不會寫入深度了,。就好比默認(rèn)的渲染狀態(tài)是ZWrite On和ZTest LEqual,如果當(dāng)前深度測試失敗,,說明這個(gè)像素對應(yīng)的位置,,已經(jīng)有一個(gè)更靠前的東西占坑了,即使寫入了,,也沒有原來的更靠前,,那么也就沒有必要再去寫入深度了。所以上面的ZTest分為通過和不通過兩種情況,,ZWrite分為開啟和關(guān)閉兩種情況的話,,一共就是四種情況: 1.深度測試通過,深度寫入開啟:寫入深度緩沖區(qū),,寫入顏色緩沖區(qū),; 2.深度測試通過,深度寫入關(guān)閉:不寫深度緩沖區(qū),,寫入顏色緩沖區(qū),; 3.深度測試失敗,深度寫入開啟:不寫深度緩沖區(qū),,不寫顏色緩沖區(qū),; 4.深度測試失敗,深度寫入關(guān)閉:不寫深度緩沖區(qū),,不寫顏色緩沖區(qū),; Unity中默認(rèn)的狀態(tài)(寫shader時(shí)什么都不寫的狀態(tài))是ZTest LEqual和ZWrite On,,也就是說默認(rèn)是開啟深度寫入,并且深度小于等于當(dāng)前緩存中的深度就通過深度測試,,深度緩存中原始為無限大,,也就是說離攝像機(jī)越近的物體會更新深度緩存并且遮擋住后面的物體。如下圖所示,,前面的正方體會遮擋住后面的物體: 寫幾個(gè)簡單的小例子來看一下ZTest,,ZWrite以及Render Queue這幾個(gè)狀態(tài)對渲染結(jié)果的控制。 讓綠色的對象不被前面的立方體遮擋,,一種方式是關(guān)閉前面的藍(lán)色立方體深度寫入: 通過上面的實(shí)驗(yàn)結(jié)果,,我們知道,按照從前到后的渲染順序,,首先渲染藍(lán)色物體,,藍(lán)色物體深度測試通過,顏色寫入緩存,,但是關(guān)閉了深度寫入,,藍(lán)色部分的深度緩存值仍然是默認(rèn)的Max,后面渲染的綠色立方體,,進(jìn)行深度測試仍然會成功,,寫入顏色緩存,并且寫入了深度,,因此藍(lán)色立方體沒有起到遮擋的作用,。 另一種方式是讓綠色強(qiáng)制通過深度測試: 這個(gè)例子中其他立方體的shader使用默認(rèn)的渲染方式,綠色的將ZTest設(shè)置為Always,,也就是說不管怎樣,,深度測試都通過,將綠色立方體的顏色寫入緩存,,如果沒有其他覆蓋了,,那么最終的輸出就是綠色的了。 那么如果紅色的也開了ZTest Always會怎么樣,? 在紅色立方體也用了ZTest Always后,,紅色遮擋了綠色的部分顯示為了紅色。如果我們換一下渲染隊(duì)列,,讓綠色在紅色之前渲染,結(jié)果就又不一樣了: 更換了渲染隊(duì)列,,讓綠色的渲染隊(duì)列+1,,在默認(rèn)隊(duì)列Geometry之后渲染,最終重疊部分又變回了綠色??梢?,當(dāng)ZTest都通過時(shí),上一個(gè)寫入顏色緩存的會覆蓋上一個(gè),,也就是說最終輸出的是最后一個(gè)渲染的對象顏色,。 再看一下Greater相關(guān)的部分有什么作用,這次我們其他的都使用默認(rèn)的渲染狀態(tài),,綠色的立方體shader中ZTest設(shè)置為Greater: 這個(gè)效果就比較好玩了,,雖然我們發(fā)現(xiàn)在比較深度時(shí),,前面被藍(lán)色立方體遮擋的部分,,綠色的最終覆蓋了藍(lán)色,是想要的結(jié)果,,不過其他部分哪里去了呢,?簡單分析一下,渲染順序是從前到后,,也就是說藍(lán)色最先渲染,,默認(rèn)深度為Max,藍(lán)色立方體的深度滿足LEqual條件,,就寫入了深度緩存,,然后綠色開始渲染,重疊的部分的深度緩存是藍(lán)色立方體寫入的,,而綠色的深度值滿足大于藍(lán)色深度的條件,,所以深度測試通過,重疊部分顏色更新為綠色,;而與紅色立方體重合的部分,,紅色立方體最后渲染,與前面的部分進(jìn)行深度測試,,小于前面的部分,,深度測試失敗,重疊部分不會更新為紅色,,所以重疊部分最終為綠色,。而綠色立方體沒有與其他部分重合的地方為什么消失了呢?其實(shí)是因?yàn)榫G色立方體渲染時(shí),,除了藍(lán)色立方體渲染的地方是有深度信息的,,其他部分的深度信息都為Max,藍(lán)色部分用Greater進(jìn)行判斷,,肯定會失敗,,也就不會有顏色更新。 有一個(gè)好玩的效果其實(shí)就可以考ZTest Greater來實(shí)現(xiàn),就是游戲里面經(jīng)常出現(xiàn)的,,當(dāng)玩家被其他場景對象遮擋時(shí),,遮擋的部分會呈現(xiàn)出X-光的效果;其實(shí)是在渲染玩家時(shí),,增加了一個(gè)Pass,,默認(rèn)的Pass正常渲染,而增加的一個(gè)Pass就使用Greater進(jìn)行深度測試,,這樣,,當(dāng)玩家被其他部分遮擋時(shí),遮擋的部分才會顯示出來,,用一個(gè)描邊的效果渲染,,其他部分仍然使用原來的Pass即可。 Early-Z技術(shù)傳統(tǒng)的渲染管線中,,ZTest其實(shí)是在Blending階段,,這時(shí)候進(jìn)行深度測試,所有對象的像素著色器都會計(jì)算一遍,,沒有什么性能提升,,僅僅是為了得出正確的遮擋結(jié)果,會造成大量的無用計(jì)算,,因?yàn)槊總€(gè)像素點(diǎn)上肯定重疊了很多計(jì)算,。因此現(xiàn)代GPU中運(yùn)用了Early-Z的技術(shù),在Vertex階段和Fragment階段之間(光柵化之后,,fragment之前)進(jìn)行一次深度測試,,如果深度測試失敗,就不必進(jìn)行fragment階段的計(jì)算了,,因此在性能上會有很大的提升,。但是最終的ZTest仍然需要進(jìn)行,以保證最終的遮擋關(guān)系結(jié)果正確,。前面的一次主要是Z-Cull為了裁剪以達(dá)到優(yōu)化的目的,,后一次主要是Z-Check,為了檢查,,如下圖: Early-Z的實(shí)現(xiàn),,主要是通過一個(gè)Z-pre-pass實(shí)現(xiàn),簡單來說,,對于所有不透明的物體(透明的沒有用,,本身不會寫深度),首先用一個(gè)超級簡單的shader進(jìn)行渲染,,這個(gè)shader不寫顏色緩沖區(qū),,只寫深度緩沖區(qū),,第二個(gè)pass關(guān)閉深度寫入,開啟深度測試,,用正常的shader進(jìn)行渲染,。其實(shí)這種技術(shù),我們也可以借鑒,,在渲染透明物體時(shí),,因?yàn)殛P(guān)閉了深度寫入,有時(shí)候會有其他不透明的部分遮擋住透明的部分,,而我們其實(shí)不希望他們被遮擋,,僅僅希望被遮擋的物體半透,這時(shí)我們就可以用兩個(gè)pass來渲染,,第一個(gè)pass使用Color Mask屏蔽顏色寫入,,僅寫入深度,第二個(gè)pass正常渲染半透,,關(guān)閉深度寫入,。 如果我們先繪制后面的物體,再繪制前面的物體,,就會造成over draw,;而通過Early-Z技術(shù),我們就可以先繪制較近的物體,,再繪制較遠(yuǎn)的物體(僅限不透明物體),,這樣,通過先渲染前面的物體,,讓前面的物體先占坑,,就可以讓后面的物體深度測試失敗,進(jìn)而減少重復(fù)的fragment計(jì)算,,達(dá)到優(yōu)化的目的,。Unity中默認(rèn)應(yīng)該就是按照最近距離的面進(jìn)行繪制的,我們可以看一下Unity官方的文檔中顯示的: 從文檔給出的流程來看,,這個(gè)Depth-Test發(fā)生在Vertex階段和Fragment階段之間,,也就是上面所說的Early-Z優(yōu)化。 簡單總結(jié)一下Unity中的渲染順序:先渲染不透明物體,,順序是從前到后,;再渲染透明物體,順序是從后到前,。 Alpha Test(Discard)在移動(dòng)平臺消耗較大的原因從本人剛剛開始接觸渲染,,就開始聽說移動(dòng)平臺Alpha Test比較費(fèi),,當(dāng)時(shí)比較納悶,直接discard了為什么會費(fèi)呢,,應(yīng)該更省才對?。窟@個(gè)問題困擾了我好久,,今天來刨根問底一下,。還是跟我們上面講到的Early-Z優(yōu)化。正常情況下,,比如我們渲染一個(gè)面片,,不管是否是開啟深度寫入或者深度測試,這個(gè)面片的光柵化之后對應(yīng)的像素的深度值都可以在Early-Z(Z-Cull)的階段判斷出來了,;而如果開啟了Alpha Test(Discard)的時(shí)候,,discard這個(gè)操作是在fragment階段進(jìn)行的,也就是說這個(gè)面片光柵化之后對應(yīng)的像素是否可見,,是在fragment階段之后才知道的,,最終需要靠Z-Check進(jìn)行判斷這個(gè)像素點(diǎn)最終的顏色。其實(shí)想象一下也能夠知道,,如果我們開了Alpha Test并且還用Early-Z的話,,一塊本來應(yīng)該被剃掉的地方,就仍然寫進(jìn)了深度緩存,,這樣就會造成其他部分被一個(gè)完全沒東西的地方遮擋,,最終的渲染效果肯定就不對了。所以,,如果我們開啟了Alpha Test,,就不會進(jìn)行Early-Z,Z Test推遲到fragment之后進(jìn)行,,那么這個(gè)物體對應(yīng)的shader就會完全執(zhí)行vertex shader和fragment shader,,造成over draw。有一種方式是使用Alpha Blend代替Alpha Test,,雖然也很費(fèi),,但是至少Alpha Blend雖然不寫深度,但是深度測試是可以提前進(jìn)行的,,因?yàn)椴粫趂ragment階段再?zèng)Q定是否可見,,因?yàn)槎际强梢姷模皇峭该鞫缺容^低罷了,。不過這樣只是權(quán)宜之計(jì),,Alpha Blend并不能完全代替Alpha Test。 最后再附上兩篇參考文章 |
|