(轉)【Unity Shaders】Alpha Test和Alpha Blending


轉自:http://blog.csdn.net/candycat1992/article/details/41599167

 

寫在前面

 

關於alpha的問題一直是個比較容易摸不清頭腦的事情,尤其是涉及到半透明問題的時候,總是不知道為什么A就遮擋了B,而B明明在A前面。這篇文章就總結一下我現在的認識~

 

Alpha Test和Alpha Blending是兩種處理透明的方法。

 

Alpha Test采用一種很霸道極端的機制,只要一個像素的alpha不滿足條件,那么它就會被fragment shader舍棄,“我才不要你類!”。被舍棄的fragments不會對后面的各種Tests產生影響;否則,就會按正常方式寫入到緩存中,並進行正常的深度檢驗等等,也就是說,Alpha Test是不需要關閉ZWrite的。Alpha Test產生的效果也很極端,要么完全透明,即看不到,要么完全不透明。

 

Alpha Blending則是一種中庸的方式,它使用當前fragment的alpha作為混合因子,來混合之前寫入到緩存中顏色值。但Alpha Blending麻煩的一點就是它需要關閉ZWrite,並且要十分小心物體的渲染順序。如果不關閉ZWrite,那么在進行深度檢測的時候,它背后的物體本來是可以透過它被我們看到的,但由於深度檢測時大於它的深度就被剔除了,從而我們就看不到它后面的物體了。因此,我們需要保證物體的渲染順序是從后往前,並且關閉該半透明對象的ZWrite。

 

注意Alpha Blending只是關閉ZWrite,人家可沒有關閉ZTest哦!這意味着,在輸出一個Alpha Blending的fragment時,它還是會判斷和當前Color Buffer中的fragment的深度關閉,如果它比當前的fragment深度更遠,那么它就不會再做后續的混合操作;否則,它就會和當前的fragment進行混合,但是不會把自己的深度信息寫入Depth Buffer中。這是非常重要的,這一點決定了,即便一個不透明物體出現在一個透明物體的前面,不透明物體仍可以正常的遮擋住透明物體!也就說說,對於Alpha Blending來說,Depth Buffer是只讀的。

 

 

Surface Shader

 

在Unity的Surface Shader里實現上述兩種技術是非常簡單的,可以參見之前的文章——Alpha TestAlpha Blending。簡單總結一下就是,只要在#pragma里設置alphatest:_Cutoff或alpha指令即可。

 

但是,很多童鞋說在使用Alpha Blending的時候得不到正確的結果,那么很大可能就是Tags沒有設對,更具體一點,就是渲染隊列沒有設置正確。一般透明對象應該起碼設置為Tags { "Queue" = "Transparent" }。為什么要正確設置渲染隊列呢?如果沒有正確設置,那么很有可能透明物體后面的物體會出現在透明物體的前面。

 

但是,它們背后的原理是什么呢?這就要從它們生成的Vertex & Fragment Shader說起了。那么,請看下一節~

 

 

Vertex & Fragment Shader

 

我們先來說比較簡單的Alpha Test

 

在Vertex & Fragment Shader里,要實現它非常簡單。

 

一種方法是自己在shader中編寫代碼,只要使用類似下面的語句就可以了:

 

[plain]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. // alpha test  
  2. clip (o.Alpha - _Cutoff);  


clip函數非常簡單,就是檢查它的參數是否小於0。如果是,就調用discard舍棄該fragment;否則就放過它。

 

 

另一種方法是使用固定管線的Alphatest指令。具體可見官方文檔。使用Alphatest指令的方法選擇更多,我們不僅僅是判斷它小於_Cutoff時舍棄該fragment,還可以是判斷它是否大於、是否大於等於,等等。但原理是和第一種方法一樣,歸根到底都是要靠discard函數來舍棄那么不符合條件的fragments。

 

Alpha Blending略微復雜一點,因為它涉及到了ZWrite的一些問題。

 

首先,需要正確設置渲染隊列:

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. Tags { "Queue"="Transparent" }  



 

其次,需要關閉ZWrite(但其實指定了下面的混合函數后,背后就會關閉深度緩存):

 

[plain]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. ZWrite Off  


然后,我們可以指定混合函數,類似下面這樣:

 

 

[plain]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. Blend SrcAlpha OneMinusSrcAlpha  


上述是最常見的混合函數因子,其他可以參見官方文檔

 

 

在使用Alpha Blending時,一定要格外小心由於它關閉了深度緩存而造成的種種問題。從Unity的這張圖可以看出:

 

深度檢驗是在Vertex Shader后面就進行的,因此在Fragment Shader階段,由於它關閉了深度緩存,所以像素的覆蓋與否完全取決於渲染的先后順序。

 

注意:評論里有童鞋說OpenGL Wiki中給出的順序圖明明是Culling和Depth Test在Fragment Shader的后面啊!怎么這里又跑到前面去了呢!有圖為證(來源OpenGL Wiki):

上圖中“Fragment Tests”就是做Depth Test的地方。這位童鞋看得很仔細啊。沒錯,從理論上來說,的確是要等到Fragment Shader完成后,再對所有fragments進行各種檢驗。但現代的GPU為了性能考慮,往往會做一個類似於“Early-Z”的東西。這個東西可以理解為在Fragment Shader之前就進行一個深度檢驗,從而剔除那些不可能被渲染到的像素,這些像素就不會再調用Fragment Shader進行處理,從而可以提高性能。而正常的FS后面的Depth Test一般情況下仍然是會做的,之前的這個“Early-Z”可以理解為是一個粗略地剔除。在一些情況下,GPU為了提高性能會做兩遍“Depth Test”(對於不同的GPU,第一次“Depth Test”的做法實現可能不一樣),但在某些情況下(比如進行了Alpha Test),那么FS之前的Depth Test就需要關閉,這個在后面會講到。

 

因此,Unity給出的圖,它里面的Depth Test應該是指Early-Z的結果,但在這張圖的后面還是會有正常的Depth Test。

 

以上內容不保證完全正確哦。

 

當然,有時我們可以混合使用這兩種技術,例如第一個pass里使用Alpha Test渲染實體部分,第二pass里對上一個pass里被剔除的fragment使用Alpha Blending進行柔和渲染。

 

 

為什么渲染隊列和渲染順序這么重要

 

當然,這里說的要正確設置渲染隊列是指Alpha Blending時的策略。之前說過,如果不關閉ZWrite,那么在進行深度檢測的時候,Alpha Blending背后的物體本來是可以透過它被我們看到的,但由於深度檢測時大於它的深度就被剔除了,從而我們就看不到它后面的物體了。因此,我們需要關閉該半透明對象的ZWrite。那么,和渲染隊列有什么關系呢?如果你的場景里有且只有這么一個物體,那么渲染隊列是不重要的。但一旦場景里有了其他不透明物體,問題就麻煩了。正如OpenGL Wiki里說的,“First - the bad news. REALLY bad news.”。。。關閉了深度緩存帶來了很多麻煩。“麻煩大了你!”

 

有兩篇文章我覺得大家可以看看:一篇是OpenGL Wiki,一篇是MSDN上的一篇博客。我這里來簡單說一下為什么關閉深度緩存會出現這么多麻煩事。

 

我們首先來理解,為什么一個物體會看起來是“半透明”的。在OpenGL中,這是通過Blending技術實現的。我們都知道一個叫“Color Buffer”的東西,這個可以理解成我們會在屏幕上看到的各種顏色。對於不透明物體來說,經過Fragment Shader處理后的fragment會和當前在Color Buffer中的fragment進行深度比較,結果要么覆蓋它要么就被舍棄。但對於半透明物體,由於它關閉了深度緩存,因此不會進行深度比較,而是通過混合系數和當前Color Buffer中的顏色進行混合,使物體看起來好像透過它看到了其他物體。

 

我們來考慮下面這種情況(來源MSDN上的一篇博客):

其中,A物體是半透明的,而B是一個不透明物體。如果我們先渲染B,再渲染A,那么B首先會寫入Color Buffer和Depth Buffer。渲染A的時候,A首先和Depth Buffer中的B進行比較,“誒我在你前面呢~”,然后就會和Color Buffer中B的顏色進行正確的混合。但是,如果我們先渲染A再渲染B,A首先寫入Color Buffer,但不會寫入Depth Buffer。注意此時的Color Buffer中沒有任何顏色,因此A沒有進行顏色混合就寫入了Color Buffer。等到渲染B的時候,B會做正常的深度檢驗,它發現“咦,深度緩存中還沒有人誒,那我就放心地寫入Color Buffer啦~”,結果也就是B會覆蓋A的顏色。從視覺上,看起來就是B出現在了A的前面(雖然A未被B覆蓋的部分看起來的確是透明了)。

 

這個例子說明,在A關閉了深度緩存的時候,渲染的順序是多么重要!一種最簡單的方法,也是Unity采用的方法,就是保證所有的不透明物體都會在半透明物體之前被渲染。而這就是通過Tags { "Queue" = "Transparent" }來保證的。Unity的Queue標簽決定了這個對象的渲染隊列。對於不透明物體來說,它的"Queue"="Geometry",而對於半透明物體,它的 "Queue"="Transparent"。而Geometry隊列中的對象總是會在Transparent之前被渲染,這就保證了所有的不透明物體都會在半透明物體之前被渲染。因此,如果你沒有正確設置這個值,那么很有可能就會出現上面例子中的情況:后面的B反而在A的前面。

 

上面這種方法即簡單又有效,但對於一些復雜情況下卻還是會出現問題。例如,我們需要渲染前后兩個半透明物體。還是上面的圖,這次A和B都是半透明物體。因為A和B都不會寫入深度緩存,因此結果完全取決於它們的繪制順序。如果我們先渲染B再渲染A,那么B正常寫入Color Buffer,而A會和Color Buffer中的B進行混合,結果正確。但是,如果我們先渲染A再渲染B,那么A寫入Color Buffer,隨后B會和Color Buffer中的A進行混合,這樣看起來就好像B在A的前面,結果錯誤。

 

一個方法就是保證從后往前渲染所有的半透明物體。這也是Unity的做法,Unity文檔中是這樣說的:

Geometry render queue optimizes the drawing order of the objects for best performance. All other render queues sort objects by distance, starting rendering from the furthest ones and ending with the closest ones.

 

也就是說,對於Geometry隊列渲染順序是Unity內部進行優化我們無法得知。但所有其他的隊列(包括了Transparent)都是對物體的距離進行排序,然后按從遠到近的順序進行渲染。

 

那么你會說,好了吧,這樣總沒事了吧。But but,還是有問題哦。如果你仔細想想的話,“對物體的距離進行排序”,什么是物體的距離呢?你會說,就是距離攝像機的Z值遠近嘛,真煩人!但是,由於我們的排序是基於整個物體的,而不是像Depth Test那樣是逐像素排序的。這意味着,排序結果是,要么物體A全部在B前面渲染,要么A全部在B后面渲染,但很多時候真實的物體是互相交叉的。我們可以考慮如下情況(來源OpenGL Wiki):

 

 

你覺得它們之前的相對排序結果是什么呢?答案是按照之前的整個物體進行排序是永遠不會得到正確的結果的,除非我們把每個物體按遮擋分為兩個部分。

 

看到這里你會說,分成兩個部分總好了吧!沒問題了吧!But but。。。沒錯,就是有這么多But。即便我們保證不會有這樣循環遮擋的物體,還是會有問題。我們再考慮下面的情況(來源OpenGL Wiki):

這里的問題是,“如何排序?”我們知道,一個物體的網格結構往往是占據了空間中的某一塊區域,也就是說,這個網格上每一個點的深度值可能都是不一樣的,我們使用哪個深度值來作為整個物體的深度來和其他物體進行排序呢?網格中點?最遠的點?最近的點?不幸的是,哪個都不對。例如上面這個圖,如果使用網格中點的深度值進行排序,那么B在C的前面,但實際上B有一部分被C遮擋了。同理,用最遠點和最近點也無法保證結果正確。Unity中的方法是使用網格的中心點來進行半透明物體的排序。這就意味着,在某些情況下半透明物體之前一定會出現錯誤的遮擋關系。比如這位仁兄就遇到了這樣的問題。如果出現這種問題,那么解決方法也是分割網格。其實,任何這種A有一部分在B上面,而B有一部分在A上面的問題,如果又關閉了ZWrite的話,大概就只有分割網格的方法了。

 

你會說,分割網格好麻煩的,而且萬一動了動模型,就又要重新分割,就沒有其他方法了嗎?當然有,其他方法也有些缺點,這意味着我們要做權衡。上述所有問題都是由於關閉了ZWrite的后果(你現在知道它有多可怕了吧)。那么,我們開啟它不就好啦~之前提到的Alpha Test就沒有關閉ZWrite,因此,我們可以使用Alpha Test來替代,但缺點是不會得到半透明那種平滑的邊界(而且在移動平台上還有性能下降的后果)。還有一種方法就是開啟ZWrite。Unity文檔中給出了這樣一個例子,就是先使用一個Pass來渲染得到深度信息,再使用Alpha Blending進行透明渲染,由於Alpha Blending不寫入Depth Buffer,它會根據上一個Pass的結果來決定是否進行混合。但這種方法的缺點是,僅僅是看起來像透明物體,但它不會透出后面物體的顏色。

 

由此,我們來總結下半透明物體渲染的注意事項:

 

  • 對於不透明物體和半透明物體之間的渲染關系,Unity通過Queue來保證渲染順序的正確性,因此,我們必須正確設置半透明物體的 "Queue"="Transparent"。
  • 對於半透明物體和半透明物體之間的渲染關系,Unity通過使用網格中心點進行排序+從后往前渲染,來盡可能保證渲染順序的正確性。但對於部分遮擋的物體,還是會產生不正確的遮擋效果。因此我們要么分割網格,要么使用Alpha Test或者開啟ZWrite來替代。

 

 

 

性能

 

Unity的官方文檔中,有兩個地方提到了它們的性能問題——一個是編寫Shaders時的性能提示,一個是優化圖像性能。微微地感覺這兩個頁面有很多重復,未來某一天可能會合成一個頁面。。。

 

它們是這么寫的:

 

      Fixed function AlphaTest or it’s programmable equivalent, clip(), has different performance characteristics on different platforms:

  • Generally it’s a small advantage to use it to cull out totally transparent pixels on most platforms.
  • However, on PowerVR GPUs found in iOS and some Android devices, alpha testing is expensive. Do not try to use it as “performance optimization” there, it will be slower.

以及

     Keep in mind that alpha test (discard) operation will make your fragments slower.

 

總結一下,就是使用Alpha Test看似更簡單,但其實在大多數平台上,相比與Alpha Blending,只有一點小小的性能提升。但是!!!在iOS和某些Android設備上,由於它們使用了PowerVR GPUs,因此Alpha Test的性能消耗反而會更大。因此,一個忠告就是,盡可能使用Alpha Blending,而不要使用Alpha Test

 

我們會覺得很奇怪,沒有關閉深度緩存,不需要計算混合顏色,僅僅調用來discard舍棄fragment不是非常簡單的事嗎?為什么在移動平台上反而效率更低呢?有句話叫,“簡單粗暴”,可以用在這里。性能下降的原因就是它太粗暴了!(我亂說的你不要當真。。。)

 

好啦,言歸正傳~原因呢,就是之前提到的兩遍檢驗。由於我經驗有限,只能依靠強大的谷歌來找答案。我找到了這里這里。總結一下,就是PowerVR GPUs使用了一種叫做“Deferred Tile-Based-Rendering”的技術。這種技術里有一個優化階段,就是為了減少overdraw它會在調用fragment shader前判斷哪些Tile是會被真正渲染的。也就說我們之前說的在FS之前做的“Depth Test”。但是,由於Alpha Test在fragment shader里使用了clip函數改變了fragment是否被渲染的結果,因此,GPUs就無法使用上述的優化策略了。也就是說,只要在完成了所有的fragment shader處理后,GPUs才知道哪些fragments會被真正渲染到屏幕上,這樣,原先那些可以減少overdraw的優化就都無效了。


免責聲明!

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



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