一口氣解決RenderQueue、Ztest、Zwrite、AlphaTest、AlphaBlend和Stencil

知道嗎,如果只是想要實現Xray效果的話,其實並不難。

實現上圖的效果,原理就是對角色畫兩次。第一次是被遮擋住的效果(半透明、單色),第二次是正常的效果(為了簡化這裡使用unlight只顯示貼圖)

這兩個pass最大的區別,在於使用不同的Ztest(深度測試)。但是這一次我決定不僅僅只寫關於Ztest的問題。反正我已經決定對抗懶癌晚期,那就乾脆一口氣把RenderQueue、Ztest、Zwrite、AlphaTest、AlphaBlend、StencilTest這些爛七八糟的東西都拎出來寫一遍,因為這些東西有很多地方都是相通的,一起說明白反而省些力氣。

不過說實話,這些東西確實是有點麻煩。我盡自己最大的努力去把這些東西說明白。但是鑒於個人能力實在有限,如果有哪裡說得不對或者不清楚,還請見諒。

如上圖,現在有三個多邊形分別是紅色盒子綠色盒子和藍色盒子,在鏡頭裡紅色的盒子在最前面(距離攝像機最近),所以蓋住了其他兩個顏色的盒子。

按照我們的生活常識,顯示最前面的紅盒子這樣的結果是再正常不過了。可是計算機並不存在所謂的「人類的常識」,它只依靠數學的方法去處理問題。而如何判斷誰在前誰在後,這個問題卻並非那麼簡單,並且很容易讓人陷入混亂。因為這牽扯到Ztest(深度測試)ZWrite(深度寫入或者叫深度緩存)和RenderQueue(渲染序列)。

如果是2D的話,只需要一個Zindex就可以確定Sprite之間的前後(覆蓋)關係。RenderQueue(渲染序列)和這個Zindex的概念很像,都是直截了當指定了一個渲染的順序。 關於RenderQueue可用的標籤,有:

Background:1000

Geometry:2000

AlphaTest:2450

Transparent:3000

Overlay:4000

(寫起來的樣子是這樣的:"Queue" = "TransParent")

數字越大的物體,其渲染順序就越靠後,就會遮住數字小的物體。從名字里也能看得出來,BackGround自然是那種最先渲染然後被所有東西覆蓋掉的東西(比如天空盒)。而像Overlay這樣的東西在絕大部分物體之後渲染,適合用來製作UI。

值得注意的是半透明物體(Transparent Objects)的渲染順序十分靠後。一般情況下是在所有非半透明物體渲染之後,再渲染半透明物體。至於其原因等稍後再說明。

除了使用默認的標籤之外,還可以更詳細指定渲染序列,寫起來大概是這樣的: "Queue" = "Geometry+1" 。這樣這個物體會在所有Geometry渲染之後再渲染,順序增加了一個「身位」。如果是"Queue" = "Geometry+5000" ,那可就是比Overlay還靠後,絕對是最最後渲染的東西,理論上覆蓋在一切東西之上。

聽起來似乎很簡單,好像我們已經拿到了一把萬能鑰匙,可以隨意控制那個小小3D世界裡的所有一切。然而進度條告訴你事情並沒這麼簡單(霧)。

因為顯卡既不允許你用這麼簡單粗暴的方式控制渲染結果,實際上你也沒法用簡單的Queue值來確定物體渲染的前後關係。

試想一個大場景里動輒成千上萬的物體,你如何去一個一個指定他們的RenderQueue?即便你真的這麼做了,一旦鏡頭轉個180°是不是就全錯了?更不要提每一幀都在變換位置的角色。就是神仙也不可能預知他們所處的位置到底應該是渲染序列的哪一個位置。

這一點和2D遊戲有著本質上的區別。在2D遊戲里指定ZIndex的做法在3D遊戲里肯定是走不通的。

所以在大多數情況下(除了製作UI和天空盒之外),這個RenderQueue並沒有什麼卵用。我用幾張圖來具體說明。

如上圖,正常情況下這三個盒子都是"Queue" = "Geometry"。因為是「正常情況」,所以顯示的效果肯定是正確的(紅色的盒子擋住其他兩個,同時綠色盒子擋住藍色盒子)。但是打開FrameDebugger你會發現,渲染的順序是很混亂的。也許是因為做測試的時候改動過RenderQueue。現在莫名其妙的是先中間後兩邊。

關於非透明物體渲染的排序問題,我在這裡多說兩句。3D實時渲染性能消耗的兩個重要部分是CPU和GPU。如果想節省GPU的時間,就要在渲染之前計算一次渲染順序,這樣在Ztest之後就,被遮擋的部分就不會進入fragment shader;反之想要解放CPU的負擔,就不要對渲染物體進行排序(排序這個東西大家都懂的)。當然這樣會多次渲染被遮擋的像素。

在Unity3d文檔里,我找到了關於控制非透明物體渲染順序的API,其描述如下:

在我的印象當中以前是沒有Camera.opaqueSortMode這個東西的,估計是新版本後加入的(我的5.4.0版本已經比較老了)。大家可以根據自己遊戲性能的考慮去做優化。

如上圖,當我們強行讓綠色盒子的RenderQueue發生改變("Queue"="Geometry+1"),這樣綠色盒子的渲染序列變為最後渲染,然而實際的效果依然沒有改變,紅色盒子一如既往地蓋住了綠色盒子(哪怕紅色盒子是在綠色盒子之前就渲染出來的)。

RenderQueue之所以只決定了物體的渲染順序,卻沒能決定物體的渲染結果,是因為顯卡在渲染的時候,更多的是依靠深度測試(Ztest)來進行判斷。

Ztest的工作原理是這樣的(假設這3個盒子是屏幕上的3個像素點):

Step1:顯卡按照渲染順序先畫出了藍色盒子的像素(渲染的每一個步驟都可以在FrameDebugger里看到,真是方便)

在畫藍色盒子的像素的時候,除了RGB三個顏色的值以外,顯卡還會把這個像素與當前鏡頭的距離記錄下來(這裡記錄為z1)。與背景相比,藍盒子顯然距離鏡頭更近,即z1<∞。按照「默認」的做法(注意在這個例子里我一直強調是在「默認」的情況,或者「默認」的做法),畫出藍色的盒子,並且將攝像機在這個像素上的深度值替換為z1。

Step2:接下來按照渲染順序,開始渲染紅色的盒子。

當然紅色盒子也有一個深度值(記錄為z2)。這個時候顯卡會用z2和攝像機在當前像素的深度值z1進行比較,發現z2<z1(因為紅色盒子距離鏡頭比較近)。於是按照「默認」的做法畫出紅色的盒子,並且將攝像機當前像素值更新為z2。

Step3:接下來按照渲染順序,開始渲染綠色的盒子。

雖然這張圖和上一張很像,但是注意這個時候渲染的是被「神隱」的綠色盒子

當渲染綠色盒子的時候,情況就發生了變化。我們知道綠色盒子之所以最後渲染,是因為我們強行改變了綠色盒子的渲染順序("Queue" = "Geometry+1")。但是綠色盒子距離攝像機的距離是大於紅色盒子的。

所以當渲染綠色盒子的時候,其深度值(記錄為z3)必然會比當前像素的深度值z2大(z3>z2,和上一步完全相反的情況)。於是顯卡按照「默認」的做法,扔掉了綠色盒子的像素,並且保持當前像素值為z2。其結果就是看起來綠色盒子完全被紅色盒子遮擋住了(哪怕它是最後渲染出來的物體)。

這一套流程走下來我們不難看出,所謂「默認」的工作原理(注意我再次強調是「默認」),就是當一個物體像素的z值小於當前鏡頭在該位置像素的深度值時,畫出該物體的這個像素,並且將這個較小的z值更新為當前鏡頭在這個像素上的深度值。

反之,當一個物體的像素的z值大於當前鏡頭在該位置像素的深度值時,不畫出該物體的這個像素,並且保留攝像機在這個像素上的深度值。

說起來實在是拗口,也不知道各位是否能看明白。反正我是儘力了。如果非要打個比方來說,我想和當初學C語言的時候進行數字排序的做法差不多。不知道各位同學是不是看起來很懷念呢?

#include<stdio.h> nvoid main() n{ nint a[15],i,j,temp; nprintf("請輸入十五個整數:"); nfor(i=0;i<15;i++) n{nscanf("%d",a[i]);n}nnfor(i=0;i<15;i++) n{nfor(j=i+1;j<15;j++) n{nif(a[i]>a[j])n{ntemp=a[j]na[j]=a[i]na[i]=temp;n}n}n}nfor(i=0;i<15;i++) nprintf("排序後的數是:%d",a[i]); n}n

而這個工作流程,就是所謂的Ztest+Zwrite。

比較新舊z值的大小,就是Ztest;之後更新攝像機每一個像素的z值,就是Zwrite。Ztest影響的是當前物體的顯示;Zwrite影響的是之後渲染物體的顯示。

可以看出來如果不進行Zwrite更新鏡頭的z值,那麼Ztest的時候就會出現不正常的結果(完全不知道前面渲染出來的物體的深度,只能完全依賴RenderQueue);而Zwrite是否更新攝像機在當前像素上的z值,根據兩個條件:

一是要看是否允許進行Zwrite(默認是Zwrite On。當然很多時候我們會手動關掉Zwrite,即Zwrite Off);二是要看Ztest是否通過,只有通過了ZWrite才會更新新的z值。

請務必注意這裡:z值是否更新並不在於物體在該像素上的z值比攝像機在該像素上的z值小。而在於是否通過Ztest。只不過在默認的情況下,通過Ztest的條件是小於等於。如果Ztest的條件改變,那麼Zwrite寫入的新值就未必比原來的值小(關於Ztest的條件馬上就會提到)。

Zwrite的概念相對簡單,無非就是根據條件,對一個變數進行反覆地賦值。比較有意思的Ztest。在三個盒子的例子里,我一直都在強調「默認」兩個字。那麼默認是什麼呢,就是Zwrite On + Ztest On。Zwrite就兩種情況(On或者Off)。而對於Ztest來說,條件就要豐富得多得多。Ztest的條件總共有如下幾種:

Less (當物體的這個像素的Z值小於當前攝像機在這個像素上的Z值,則通過Ztest)

LEqual(條件變為小於等於)

Greater(條件變為大於)

GEqual(條件變為大於等於)

Equal(條件變為相等)

NotEqual(條件變為不相等)

Always(Ztest永遠通過)

Never(Ztest永遠不通過)

Off(等同於 ZTest Always)

On(等同於ZTest LEqual)

ZTest LEqual也就是上面一直提到的「Ztest默認工作的原理」。當不寫明Ztest的處理方式的時候,ZTest的通過條件LEqual。因此我們就總能看到距離攝像機近的物體(Z值小)蓋住了距離攝像機遠(Z值大)的物體,這樣「理所當然」的效果。

有意思的是當我們相要搞些事情的時候,就可以利用ZTest那些非默認的選項。當物體被遮擋住的時候(即Ztest Greater),原本是看不見的。但是Xray的效果不就正是要看見原本看不見的東西么?

所以Xray效果的第一個pass。我使用以下的「黑科技」:

Blend SrcAlpha OneMinusSrcAlpha說明我們要用alpha blend的方式進行渲染(關於Alpha Blend後面會提到)。Ztest Greater意味著我就是要處理z值大於攝像機z值的情況(只有在別的物體後面z值才會比較大,也就是說只有實際上被別的物體擋住的時候,才會用這種方式渲染)。同時關掉Zwrite。

關閉Zwrite是比較重要的一步,開著Zwrite會把錯誤的z值(比較大的z值)更新上去。正如前面特彆強調的,Zwrite的條件之一是通過Ztest。這一次Ztest的條件是Greater,所以通過Ztest以後z值是比原來大的,更新上去以後會對其他物體的深度判斷造成影響,關於這一點我們馬上舉例說明。

第一個pass效果如下:

我們看到,Z值比較小的像素(即未被遮擋住的像素),反而因為沒有處理Ztest Lequal的Pass而無法顯示出來。

接下來就是第二個Pass。我們使用新的Ztest條件:

其實這就是剛才我們一直所說的「默認情況」。換句話是其實Zwrite On 和 ZTest LEqual完全可以不用寫。效果如下:

那麼問題來了,如果我們在第一個pass中打開Zwrite會出現什麼結果呢?

第一個Pass打開Zwrite的效果如下:

無論是否被遮擋,人物都會顯示成Pass2的效果(而且還有明顯得錯誤)。

我們利用剛才獲知的原理來分析一下。在Pass1通過Ztest之後,因為打開了Zwrite,所以將角色在Pass1階段渲染出來的像素的深度值寫入到屏幕當前的深度值。注意這個深度值是大於牆的像素的深度值的,但是依然被寫進鏡頭的深度當中。

當來到Pass2時,Ztest的條件是LEqual(小於等於)。因為當前攝像機中該像素的深度值就是角色身上像素的深度(因為上一步通過Zwrite已經寫入)。所以完全符Equal(相等)的條件。於是Pass2的像素成功通過ZTest並被畫出來,Pass1畫出的像素自然就被Pass2覆蓋掉了。

有興趣的朋友也可以在Pass2中試一試,當Ztest的條件是Less的時候會出現什麼效果。這裡就不一一舉例了。

以上是關於ZTest、Zwrite和RenderQueue三個容易產生混亂的概念。下面又是一個類似的概念:Stencil(模板)。

Stencil和深度一樣,是寫進buffer里的一個數值(Z buffer和Stencil Buffer這兩個詞你應該聽過很多次了)。

Stencil的工作原理和Ztest+Zwrite很相似,但是靈活性更高一些。關於Stencil的一些具體例子和講解,網上有很多。我這裡的重點就不放在實際例子上,而是關於模板和深度這兩個東西在用法和原理上的異同。

關於Ztest+Zwrite我已經提到過很多次了,最簡單的理解就是「比較」+「寫入」。如果你真的對其原理理解得非常好,那麼搞定Stencil就沒有任何問題。

在Unity3D裡面並不存在「Stencil Test」和「Stencil Write」這兩個字眼兒。Stencil就是一個過程,同時包含了「比較」和「寫入」兩個步驟。

Stencil的完整語法:

stencil{

Ref referenceValue

ReadMask readMask

WriteMask writeMask

Comp comparisonFunction

Pass stencilOperation

Fail stencilOperation

ZFail stencilOperation

}

具體詳盡的用法寫起來太麻煩(我實在是怕麻煩怕得要死),我就稍微總結一下:總的來說你只要關注RefCompPass三個關鍵詞。再稍微複雜一點兒的情況,你可能需要用到FailZfail。最後在需要更複雜的判斷的時候,你也許會需要用到那兩個Mask。

我們再回顧一下Ztest+Zwrite的原理。獲取Z值->測試(比較)Z值->寫入新的Z值(如果通過測試)。

我們假定Stencil也有一個值叫Ref值。那麼Stencil的用法也實在是看著眼熟:獲取Ref值->測試(比較)Ref值->寫入新的Ref值(如果通過測試)。

說到底這倆玩意兒的區別,就是在第一步,獲取當前物體在這個像素上的這個變數。

Z值是根據像素到攝像機的距離算出來的,不會因為你的個人意願而改變;S值是你可以隨便填的(是的隨便填,想寫幾就寫幾,範圍0-255)。

這樣一來Stencil可以幫助你突破Ztest所帶來的限制,用更靈(jian)活(dan)便(cu)捷(bao)的方式來控制渲染效果。

Ref就是寫入這個像素的Ref值,正如我之前提到的想寫幾就寫幾完全看心情(所以我一直都認為叫Stencil Buffer模板緩衝實在是有點唬人的感覺。改成「看哪個數字順眼就用哪個數字比大小」更貼切一些)。

Com是進行Test的條件,當你看到一大堆LessLEqualGreaterGEqualEqualNotEqualAlwaysNever這樣的字眼兒,是不是感到非常的眼熟?這一步比較的過程和Ztest完全一樣。

Pass和Zwrite簡直就是一個媽生出的倆個孩兒。區別就是這個小哥比他兄弟花樣兒多點。Zwrite無非就是寫入或者不寫入(On or Off)。Pass甚至還可以控制如何寫入(雖然大多數情況下可能用不到)。

Pass支持的條件一覽,其中Keep類似於Zwrite里的Off;Replace類似於ZWrite里的On(此圖來自互聯網)。

舉個栗子,如下圖所示,現在有一面牆和一個茶壺,按照與鏡頭的位置關係,牆體遮擋住茶壺的下半部分。

如果我們想要做一個如Flash里的Mask Layer效果。就可以使用Stencil來做。

Stencil n{n //攝像機在當前像素的默認stencil值是0n //設置當前物體當前像素的參考值為100n Ref 100 n //永遠通過stencil測試n //這個shader的唯一目的就是在這個物體所佔的像素上寫入stencil值100n Comp Alwaysn //通過後(因為Comp Always所以必然會通過),將當前stencil值更新為ref的值(100)n Pass replacen //這樣牆所佔有的像素的Stencil值就被確定下來了n //如果有多個牆,也可以用Comp GEqual或者Comp LEqualn //來找一個最大/最小的stencil值作為當前像素的stencil值n}nZwrite Off n

注意牆的深度緩衝要關掉,否則茶壺在做Ztest的時候會因為遮擋關係而被棄掉像素。

接著是茶壺的shader:

Stencil n{nt//像素默認的stencil值應該是0nt//設置茶壺在當前像素的參考值為90ntRef 90nt//因為之前牆的ref值100已經寫入到攝像機里,所以當前像素的ref值已經是100n //因為茶壺的Ref值(90)小於當前攝像機在該位置的像素的ref值100,測試通過ntComp LEqualnt//通過後,不更新ref值ntPass keepnt//這樣牆在該像素上的Stencil值(100)依然是攝像機在當前像素上的人ref值n} n

茶壺被透明牆遮擋住的部分,因為其Stencil值通過測試,所以被顯示了出來。

當然這裡存在一個潛在的問題。試想如果這兩個非透明物體在渲染的時候,順序並不是先畫牆再畫茶壺。其結果就會因為牆的ref值沒有提前更新好,而造成了茶壺在比較的ref值的時候出現我們不期望的結果。所以說,雖然我們並沒有太多注意過非透明物體的渲染順序。但是這東西確實會在各種意想不到的地方,造成莫名其妙的顯示錯誤。

最後就是Alpah Test 和 Alpha Blend。看到XXXTest是不是第一反應又是Test + Write這種東西。然後又是一堆Lequal、Gequal這些亂七八糟的條件。

好消息是這個世界上並不存在「Alpha Write」這種東西,並且Alpha Test也遠沒有之前那兩個Test那麼複雜;壞消息是你需要多了解一個新的概念——Alpha Blend,一個既麻煩又特別容易出問題的玩意兒。

首先一句話解決Alpha Test。與其他的Test概念相通的是:Alpha Test的運作原理也是當條件成立時,畫出該像素,否則拋棄該像素。但是它的特點是無需(也無法)同鏡頭中同一個位置的其他像素值進行比較(自然更加無法進行寫入)。

相對而言,其他的Test還需要跟別的東西比較一下,Alpha Test並不存在這個過程,它只和自己本身存在的變數進行比較,是一個非常自閉的過程。

因為AlpahTest有以上的特性,所以在Unity的shader里並沒有Alpha Test OnOff這樣的關鍵字。Alpha Test可用的函數只有兩個,一個是clip一個是discard。clip(x)函數的變數x必須小於0才會通過測試。比如說簡單粗暴的clip(-1)就把所有像素都幹掉了;而用if(){discard;}可以使用任意條件觸發。相對而言discard比較靈活,但是要用到if讓我很不爽。這兩個函數的具體用法大家可自行百度(好吧是我懶得貼)。

一般做漸變消失的時候,會用到clipdiscard。比如下圖

把不斷變化的時間值傳入shader,來不斷減小clip()函數的變數,就能做出如上的效果。當然這個效果還可以進一步改進,因為和本文無關所以就不展開了。需要注意的是在移動平台上,Alpha Test的消耗較大,屬於能不用就不用的東西(就像if、for這些東西能不用盡量別用)。

如果你非要搞明白為什麼簡單粗暴的alpha test反而消耗大,就自己去查關於PowerVR GPUs、Deferred Tile-Based-Rendering、Early-Z等等這些知識點,對於我一個懶人來說搬運這些東西簡直跟要了我的命沒什麼區別。

Alpha Test是一個非黑即白的過程。通過或者不通過,畫出或者拋棄,簡單粗暴一目了然。當然我們大多數時候並不喜歡如此粗暴的處理,畢竟人不是機器,凡事還需要溫柔一點。所以我們更多的時候用的是Alpha Blend而非Alpha Test。

Alpha Blend即透明混合。我們之前提到的所有Test方式,不是你蓋住我就是我蓋住你,總之沒有任何「和諧共處」的可能性。而Alpha Blend提供了這種可能性。根據Blend的方式不同,該物體在這個像素的rgb值會和其他物體在這個像素上的rgb進行混合。

Alpha Blend的效果,在一般意義上這就是我們理解的「半透明」。

我們之前曾經提到過,半透明的物體(也就是需要用Alpha Blend方式渲染的物體)一般來說渲染序列比較靠後(通常我們用"Queue" = "Transparent")。道理很簡單,你要和別的像素混合,那麼必須要有其他像素已經畫出來才行。如果透明物體被提前渲染出來,而當時還不存在後面要跟它混合的像素,自然就會出現錯誤。

所以難怪只有Overlay這種做UI的物體,渲染順序會排在Transparent之後——畢竟UI是不需要和場景中的半透物體進行混合。

如圖所示,當半透明物體(紅色方形粒子)沒有被指定渲染順序為Transparent的時候,在混合天空盒的時候發生了明顯錯誤。紅圈是渲染粒子的部分,黑圈是渲染天空盒的部分。很明顯在渲染粒子的時候,並沒有渲染出來天空盒,所以也就沒有混合(Blend)操作時可以用來混合的顏色。

當半透明的渲染順序被正確指定為Transparent的時候,渲染天空盒發生在渲染粒子之前,也就是在畫粒子的時候天空盒的像素就已經存在了。這樣粒子就有了可以進行混合操作的顏色,因此半透明粒子與天空盒的混合效果正確。(說實話我很奇怪為什麼Unity默認的天空盒渲染順序居然不是BackGround,也許他們有他們自己的考慮吧。)

注意在談關於Alpha Blend的時候,幾乎每一個細節都和RenderQueue息息相關。這和之前的Ztest完全不同。其區別在於Ztest只關心誰蓋住了誰,一旦被蓋住就不再在意被蓋住的像素到底是個什麼樣;然而Alpha Blend卻需要關注任何一個畫在當前位置的像素顏色,只有獲得這些顏色的全部信息,才可能進行正確的混合。這也是為什麼Alpha Blend的消耗很大(因為所有在該像素上的物體都要進入fragment shader進行繪製),而且常常會引發各種非常棘手的問題。

在寫Unity Shader的時候,Alpha Blend有兩個非常重要的語句:Zwrite Off和Blend的方式。

一般情況下我們渲染半透明物體的時候,都是Zwrite Off。

為什麼一定是Zwrite Off?我們最開始說,只有打開Zwrite,才有可能進行「正確有效」的Ztest,否則所有關閉Zwrite的物體,其渲染將完全依賴於RenderQueue。

但是對於透明物體之間來說(注意是透明物體之間,而不是透明和非透明物體之間),我們需要的恰恰是不要進行有效的Ztest——因為我們的初衷就是不能讓「正確」的遮擋關係產生作用。試想如果透明物體之間因為Ztest判定了「正確」的遮擋關係,而造成部分像素被顯卡丟棄不畫,又怎麼可能產生之後混合的過程呢?

而一旦放棄Zwrite。透明物體之間的Ztest其實都是統統通過的,換言之任何一個半透明物體的像素在與其他半透明物體的像素進行Ztest的時候,將不會被認為是需要棄掉不畫的像素(我再次強調,因為RenderQueue的關係,所有談到的東西都僅限於半透明物體之間)。

來看這張圖,注意粒子後面的角色和牆不一樣,這個角色與粒子相同也是個半透明的物體。當Zwrite On的時候,整個渲染過程是先畫了方塊形的粒子(Draw Dynamic),再畫的綠色的角色(那三個Draw Mesh)。當開始繪製角色的時候顯卡做了Ztest,其判定這個角色被粒子遮擋住,所以像素並沒有畫出來。

當Zwrite Off以後,注意這個時候依然是先畫出粒子再畫出角色,在角色做Ztest的時候,被判定並沒有被粒子遮擋(因為粒子的深度信息並沒有寫入,角色像素的Z值小於等於當前攝像機在當前像素上的Z值),所以角色的像素被繪製出來,並且與粒子的顏色進行了正常的混合。

你可能會問為什麼牆不會被擋住,因為牆是"Queue" = "Geometry",作為一個渲染序列靠前的物體,在畫粒子的時候其像素就已經存在了。

根據上面的實例,我總結一下關於顯卡的工作機制。顯卡只能確定當下的像素是否可以繪製以及如何繪製。其結果可能是1、棄掉這個像素不畫。2、這個像素會覆蓋掉之前的像素。3、如果是Alpha Blend就和之前的像素進行混合。但是注意無論如何渲染的過程都不可能影響之前的已經被畫出來的像素——顯卡也許會拋棄當前的像素不畫,但是絕不可能讓之前畫出來的像素消失掉。這個規則非常重要,請務必理解。

所以說對於Alpha Blend來說,RenderQueue非常的重要。已經畫出來的像素只能被混合卻不能被消除。所以基本上出問題的一定是透明物體和透明物體之間,因為他們的RenderQueue是相同的。先渲染的永遠存在,而後渲染的卻有可能被拋棄。

當然ZTest Off也許會解決這種因為遮擋而造成的不畫像素的問題,但是相信我你絕對不會這麼去做,因為會引發更多的麻煩(因為沒了Ztest,就是非透明物體也不能正確覆蓋住透明物體了)。

因為存在著如此「危險」的規則(之後的渲染不能改變之前的渲染),渲染的先後順序就絕對不可能是完全隨機的。和非透明物體的渲染順序控制類似,Unity也提供了控制透明物體排序的機制。

因為透明物體之間的排序比較重要,所以我稍微多說兩句。按照Unity3D的默認做法,在對透明物體在渲染之前的排序,是根據多邊形中心點與攝像機的遠近來比較的。比較之後顯卡會從後向前對透明物體依次進行渲染。所以絕大多數情況下你看到的粒子特效,其前後遮擋關係還是沒什麼大問題的。

但是這麼做又會引出一個新的問題——當半透明物體交叉在一起的時候,這種判斷方式幾乎沒有任何幫助。所以當一個複雜的多邊形(例如有很多部件的角色)在使用Alpha Blend的時候,經常會出現顯示效果錯誤,也是因為這種原因。

所以從優化的角度來講,我們一直希望盡量少用或者不用Alpha Blend,但是現在的遊戲幾乎到處都充斥著Alpha Blend的物體。好在現在的處理器性能比之過去實在是強了太多,這些問題似乎也漸漸地不再成為遊戲開發的限制。

那麼之所以我還要特意寫出來,是希望大家能知道關於Alpha Blend消耗的來龍去脈。畢竟無論處理器的性能如何發展,我們做遊戲還是要以能省一點兒是一點兒的態度去摳這些細節。

單以上面的例子而言,如果你對之前的講解理解深入的話,應該知道除了關閉Zwrite這一個辦法之外,也可以用指定RenderQueue的方式強行讓角色先繪製出來(或者讓粒子後繪製)。這種強行改變(指定)RenderQueue也能解決兩個半透明物體遮擋的問題。但是正如我之前所說的,強行指定RenderQueue是一種極其不被推薦的做法。還是那句話,如果這個時候鏡頭轉動了180°(即物體和物體之間的前後關係完全反轉),強行指定RenderQueue就會造成更嚴重的渲染錯誤。

如上圖,在沒有關閉Zwrite的前提下, 改變粒子的渲染順序("Queue" = "Transparent+1" )。這樣綠色的半透明角色就在粒子之前被渲染出來,紅色的粒子也就有了可以進行混合的對象。

Zwrite Off雖然已經成為Alpha Blend的「標配」,但是不能進行Zwrite其實是很麻煩的。如果你認為上一個效果沒毛病就萬事大吉,那可就大錯特錯了。來看下圖:

大多數時候我們當然希望第一張圖的效果(打開Zwrite,遮擋住原本應該被遮擋的壺把)而非第二張圖的效果(關閉Zwirte,這樣該物體的任何一個像素都不會改變攝像機在該像素上的深度,就會出現無法遮擋住問題)。

很顯然,在不打開Zwrite的前提下,是不可能做出第一張圖的效果的。但是正如我們之前所提到的,透明物體如果不是Zwrite Off,又會引發半透明物體之間因為遮擋而無法混合的問題。這真是一個讓人頭疼的麻煩。

以下是官方一個例子的原理(實在搜不到了只好自己動手,慘),是目前解決半透明問題比較常規的做法。首先做一個pass進行Zwrite,然後在第二個pass里關閉Zwrite,其他不變。可以做出一個完全是剪影的半透明效果。如下圖右面的茶壺。

ColorMask是指定輸出通道,這裡讓第一個pass完全不輸出任何東西,僅僅只是寫入深度。這樣一來茶壺就像是個非透明物體一樣在屏幕上改變了當前像素的深度值。第二個pass正常繪製,在其Ztest的時候比較的就是剛剛自己留在屏幕上的Z值。這樣一個完美的剪影就做出來了。

這裡說點題外話。一直以來我都以為把Tag放到Pass里是可行的,直到寫本文的時候我才發現只有將Tag放在Pass外面才會真正起作用。那就意味著多pass之間來回切換Tag是不可能的(或許是我理解上有問題,畢竟我剛剛才發現)。

最後要說的是混合方式。如果你用PhotoShop的話,應該對圖層混合的模式並不陌生,而Blend方式其實也是一樣的概念。所以關於Blend的方式,我就不過多展開了,相關資料網上很多有興趣可以自行百度。

一般來說正常的Blend方式是:

Blend SrcAlpha OneMinusSrcAlpha

這個語法翻譯成中文,大意是這個像素的顏色乘以這個像素的alpha值(SrcAlpha) + 這個像素背後的顏色 * (1 - 這個像素的alpha值)(OneMinusSrcAlpha)。

比如一個紅色的像素(1,0,0,0.7),期身後的顏色是藍色(0,0,1,1)。那麼在攝像機里,這個像素最終的顏色就應該是(0.7,0,0.3,0.7)((1,0,0) * 0.7 + (0,0,1)* (1 - 0.7))。如果再出現一個半透明的物體,那就繼續用這個步驟計算。

這是「正常」的方式,得到的效果是我們習慣的「默認」的效果。那麼非「正常」的效果呢?半透的混合方式還有如下幾種。作為比較特殊的混合方式,所有這些方式你都可以在PS的圖層混合里找到相同的效果。

懶人直接搬運,此圖來自互聯網


推薦閱讀:

關於Unity動畫系統優化,你可能遇到這些問題
一個關於渲染管線中坐標變換的簡單實例
Unity3D插件開發教程(四):獲取地址組件
從零開始學基於ARKit的Unity3d遊戲開發系列16

TAG:shader | Unity游戏引擎 |