當前位置: 華文問答 > 數位

OpenGL繪制半透明物體最簡單的辦法是什麽?

2020-03-27數位

你以為圖形程式設計師這麽好當的啊。。。

上古時期有一個叫做 畫家演算法 [1] 的東西,本質上很簡單:從遠向近地繪制物體,這樣遮擋關系就是正確的了,當然有了z-buffer也就沒人再用了, 反倒是為了充分利用硬體的early-z特性,會從近向遠排序物體(這點在移動硬體上也是一樣的,不管是TBR還是TBDR)。 不過同樣的道理也適用於半透明的混合,因為混合操作本身就是和順序相關的,所以最簡單的當然是對場景物體按照從後往前的順序排序然後依次繪制。

這個辦法不能完美解決問題,因為我們排序的時候是按照物體的包圍盒去排序的,對於一些相交的物體來說,它們的三角面的前後順序未必和物體的前後順序一致,甚至遇到穿插的模型,兩個三角面內的像素也可能前後關系不一致,比如下圖這樣:

左邊正確,右邊錯誤,排序無法解決

由此才引出你提到的OIT,即次序無關的半透明,早期最經典的方案叫做 depth peeling [2] ,這個演算法的過程有點像剝皮,它主要依賴固定管線的depth test,每「一層」半透明圖層需要一張render target,對於一個固定像素來說,我們需要知道的是從視點(可認為是depth為0的位置)到不透明像素(不透明像素的深度,如果沒有就是1)之間到底有幾個半透明圖層,我們生成半透明圖層的深度順序是從前往後的,也就是我們先找到 最前面 的半透明圖層,然後 找到最前面這一層和不透明層之間夾著的次前的半透明圖層 ,以此類推,比如我們有n個半透明圖層,那寫出來的虛擬碼大概是這樣的(由於是虛擬碼,我們也不太去強調ping-pong buffer之類的最佳化,直觀為主):

/** CPU side **/ SetDepthWrite(true); // 開啟深度寫入和比較 SetDepthTest(true, Less); SetDepthBuffer(BackFaceDepthBuffer); drawPrimitives(OpaqueMeshList); for i = 1...n // 設定輸出的RT SetColorBuffer(ColorTextures[i]); SetDepthBuffer(FrontFaceDepthBuffers[i]); SetTexture(FrontFaceDepthBuffers[i - 1]); // 把上一層已經得到的深度傳入shader SetTexture(BackFaceDepthBuffer); drawPrimitives(TransparentMeshList); BlendColor(ColorTextures) /** GPU side, pixel shader **/ Texture2D LastLayerDepth; Texture2D BackLayerDepth; float4 Color: SV_Target0; void main() { Color = CalculateColor(); float CurDepth = CalculateDepth(); float LastDepth = Sample(LastLayerDepth, uv); // 讀取上一個半透明層當前位置的深度,對於第一層,可以傳入一個全黑的Texture clip(CurDepth- LastDepth); // 根據這個深度剔除掉已經找到的半透明層像素 float BackDepth = Sample(BackLayerDepth, uv); clip(BackDepth - CurDepth); }

其實這個演算法寫出來非常簡單,但是根據虛擬碼也能看出來一個比較明顯的問題:同一個半透明物體draw call次數比較多,頻寬消耗也比較大,後來在這個演算法的基礎之上又演化出了 Dual Depth Peeling [3] ,這裏就不展開描述了。

depth peeling逐層繪制的示意圖

另有一類基於半調對映思路做的隨機半透明混合 [4] ,基本思路是「 如果我想吃牛奶味的餅乾可我恰好只有普通餅乾和牛奶,那只要吃一口餅乾喝一口牛奶,然後放在嘴裏嚼一嚼混在一起就有那味兒了 」。但說到底,牛奶還是牛奶,餅乾還是餅乾,只是在一個大的尺度上看,好像它們混在了一起。比較典型的就是screen door transparency [5] ,對於每一個位置上,它只有一個sample,也就是說並沒有真正地做alpha blend,但他附近有一些待混合的其他sample,於是當你遠處來看,或者剛好是個近視眼的時候,那一塊看起來就好像是一個經過半透明混合的結果了,這類方法也有它的好處:每個像素都有完整的深度和顏色資訊,並且不依賴alpha blend。

除了Screen door transparency,後來的hashed alpha testing [6] 也是類似的思路:

在Compute Shader和UAV(Unorder Access View)誕生之後,由於我們可以自由讀寫一塊GPU Buffer上的任意位置,在GPU中構造一些復雜的數據結構就成為了可能性, per pixel linked list 就是其中一個。這個結構說白了就是我們在CPU中用的連結串列,連結串列裏面可以存的東西有很多種,比如某個位置的所有半透明像素的顏色的值(用來做半透明渲染) [7] ,又或者影響某個像素的所有光源的索引(用來做光照渲染) [8] 。當年AMD有一個變形金剛的半透明渲染DEMO,用的就是這個技術。

PPLL固然效果完美,但是占用視訊記憶體大(要預分配足夠的連結串列空間,如果是做光照那就是N*GBufferSize),頻寬高仍然是非常大的限制,所以實際上也沒有大規模流行起來(對於半透明渲染來說這個幾乎無解)。

總結來說,從後向前排序,盡量在設計上規避半透明的問題還是常用的方案,後續有時間我會在專欄另寫一篇更詳細的OIT技術綜述。另外還有比較簡單的方法,即先繪制背面的半透明,再繪制正面的半透明,對於只有兩層半透明(車窗)來說也勉強夠用 [9]

參考

  1. ^ 畫家演算法 https://zh.wikipedia.org/wiki/画家算法
  2. ^ https://my.eng.utah.edu/~cs5610/handouts/order_independent_transparency.pdf
  3. ^https://developer.download.nvidia.cn/SDK/10/opengl/src/dual_depth_peeling/doc/DualDepthPeeling.pdf
  4. ^https://luebke.us/publications/StochTransp-slides.pdf
  5. ^https://digitalrune.github.io/DigitalRune-Documentation/html/fa431d48-b457-4c70-a590-d44b0840ab1e.htm
  6. ^https://developer.download.nvidia.cn/assets/gameworks/downloads/regular/GDC17/RealTimeRenderingAdvances_HashedAlphaTesting_GDC2017_FINAL.pdf?rGDx5SUsI9mG0yPZRBiihKCRjHycAbgCE5nAB5ySwOZHJ44NfD5ADaNES5N_AvxdkAwye7_nS2Dx0dx4iBO4jC1sHOWysiDULZnK8tQw7pohpCKbl7oBjb_LVz5KsnGVaYsEUyxxe03fJHYs_j-1DP8o6jlpa-b-X3AlegjhaL9NPv9R6C-n6Bqp8utdd1AOW8BLFhZFcsKTGXyHQOiRdmHEzq1_Q
  7. ^ https://www.slideshare.net/hgruen/oit-and-indirect-illumination-using-dx11-linked-lists
  8. ^http://advances.realtimerendering.com/s2014/insomniac/Light Linked List.pptx
  9. ^http://www.klayge.org/2011/11/29/klayge-4-0中deferred-rendering的改进(三):透明的烦恼/