如何在 UE4 移動端中實現 HZB?

Youwei發表於2020-09-29
Hierarchical Z-Buffering 分層 Z 緩衝(HZB)對遮擋剔除研究具有重要影響,是 GPU Driven Rendering Pipeline 的重要剔除手段。目前部分主流商業引擎可能因為某些原因導致該技術無法完全在 GPU 端工作,但依然是值得探討的。本文先介紹 HZB 的基本原理以及 UE4 在 PC 端的實現方式,然後介紹如何移植到移動端並分析其效能和帶來的價值,以及未來還可以做的工作。

HZB 的原理

一般來說,大多數基於 HZB 的遮擋剔除是這樣的工作的:

1. 使用一些遮擋器生成一個完整的分層 Z-金字塔。

如何在 UE4 移動端中實現 HZB?

z-pyramid 的最低階別是一個標準的 z-buffer。在所有其他層,每個 z 值都是上一層對應的 2×2 畫素中最遠的 z。

如何在 UE4 移動端中實現 HZB?

2. 要測試的物件是否被遮擋,可以將其包圍體投射到螢幕空間,並在 z-pyramid 中估計 mip 級別。將物件的包圍體投影到螢幕空間。最長的邊 l(畫素)用來計算 mip 等級λ。

如何在 UE4 移動端中實現 HZB?

邊長越長,選取的 mip 等級越高。

如何在 UE4 移動端中實現 HZB?

3. 根據選定的 mip 測試遮擋。如果結果不明確,可以繼續使用更細的 mip 級別進行測試。

這個選擇的原因是它使成本可預測——最多需要讀取和測試四個深度值。此外,這種測試可以被看作是“概率性的”,因為大物件比小物件更容易被看到,所以在這些情況下沒有理由讀取更多的深度值,即節省了頻寬,也增加了 Cache 命中。

UE4 的實現

UE4 只在 PC 端進行了實現,過程大致一樣,不同的是在構建層級 Z 緩衝上分為 ComputeShader 和 PixelShader 兩種方式,然後最終剔除工作主要在 CPU 端進行,意味著需要回讀 GPU 的測試結果。以下是 UE4 的工作流程:

使用 SceneDepth 作為資料來源構建層級 Z 緩衝。Mip0 為第一級,大小為 1024*512,總共構建 10 級。分為兩種方式,PixelShader 方式比較簡單,一次構建一級,總共執行十次。ComputeShader 則稍微複雜一點,利用 GroupMemoryBarrierWithGroupSync,每次同時構建 4 級,總共只要執行 3 次,便完成構建。

場景中的物體經過視錐剔除以後,剩下的會被收集起來,存放在一個陣列中,並且每個物體會儲存自己在陣列中的索引值。然後,建立 2 張 RGBA32 格式的貼圖,一張存放物體包圍盒的質心座標,一張存放物體包圍盒的大小。每次從陣列中取 64 個物體作為一組,儲存到貼圖的 64 個畫素區域中。

取樣第二步中的貼圖,獲取物體的質心座標和包圍盒大小,可以計算出物體包圍盒的八個頂點的世界位置,對這八個頂點進行投影,選取其中最近的 Z 值。根據投影后的矩形區域,選取最長邊長計算 mipmap 等級,然後在矩形區域內取樣該 mipmap 的 16 個畫素,選取其中最遠的 Z 值。如果包圍盒最近的 Z 值比它還小,則物體不可見。將結果儲存到一張格式為 RGBA8 的 RenderTarget 中,作為下一幀讀取。

當前幀讀取上一幀的貼圖資料到一個陣列中。每個物體將上一幀儲存的陣列索引到該資料進行查詢,查詢結果決定了該物體在當前幀的可見性。

以上步驟看出,UE4 的 HZB 實現流程沒有完全放在 GPU 端執行,在下一幀的時候需要回讀上一幀的結果,然後進行查詢以決定物體當前幀的可見性。另外,在第三步中,計算最遠 Z 值時,取樣了矩形區域內 16 個畫素,而不是 4 個畫素。

移動端的實現

移動端的 HZB 方案大部分可以與 PC 端共用一套邏輯實現,然後針對移動端效能點進行優化。移動端需要解決的第一個首要問題就是 SceneDepth 的獲取,因為它是構建層級 Z 緩衝的重要資料來源。

在移動端上,一般來說,如果存在後處理材質需要訪問場景深度資訊,UE4 會將場景線性深度值儲存在 SceneColor 的 Alpha 通道。而對於透明材質,如果使用了 DepthFade 材質節點,則會通過移動裝置擴充套件 API 來直接提取 FrameBuffer 中的深度資訊。

那我們如果想在移動端上直接訪問深度紋理的話,需要怎麼做了?可以通過設定 r.Mobile.ForceDepthResolve 為 1 來始終保留移動端的深度資訊。強制深度解析,為裝置保留深度紋理。

移動端獲取深度紋理

如何在 UE4 移動端中實現 HZB?

獲取了深度紋理,就可以開始構建層級 Z 緩衝。考慮到移動裝置的相容性,這裡只使用了 PixelShader 方式,依然構建了 10 級 mipmap。為了保證深度值的精度,這裡將每個深度值編碼到 rgba8888 格式的貼圖中。

移動端構建層級 Z 緩衝

如何在 UE4 移動端中實現 HZB?

構建完層級 Z 緩衝以後,接下來就是進行遮擋測試。演算法沿用了 PC 端的方式,將結果儲存在貼圖中,下一幀回讀。移動端上回讀 GPU 貼圖資料,需要注意的是,UE4 會預設處理上下翻轉。因為這裡只是存放遮擋結果的資料貼圖,所以不需要做上下翻轉。

如何在 UE4 移動端中實現 HZB?

最後一步遮擋查詢,過程和 PC 端一樣,每個物體用上一幀的陣列索引去查詢自己當前幀的可見性。

優化讀取效能

移動端回讀 GPU 資料相當耗費效能,我們可以來看下 glReadPixels 分別在 oppo 手機型號為 r15 和 r17 上的測試結果:

r15 耗時 6~8ms

如何在 UE4 移動端中實現 HZB?

r17 耗時 16~20ms

如何在 UE4 移動端中實現 HZB?

直接呼叫 glReadPixels 相當慢,不過,好在目前大多數移動裝置的 opengles 已經達到 3.0 以上,所以,可以考慮使用 PBO 的方式進行優化,使用 glMapBufferRang 進行讀取。過程大致如下:

初始化 2 個 buffer。

buffer1 用於非同步 glReadPixels 讀取,buffer2 用於 glMapBufferRange 讀取。

下一幀交換 buffer,buffer1 用於 glMapBufferRange,buffer2 用於 glReadPixels。

如何在 UE4 移動端中實現 HZB?

再來看下,優化後的測試結果:

優化後,r15 耗時 0.9ms

如何在 UE4 移動端中實現 HZB?

優化後,r17 耗時 4~6ms

如何在 UE4 移動端中實現 HZB?

硬體遮擋查詢 和 HZB 在 oppo r15 上的效能對比

經過初步優化,我們來看下,硬體遮擋查詢和 HZB 各自在手機上的效能對比。測試分為靜態物體和動態物體。新建一個場景,場景內隨機生成 10000 個物體。在分別只開啟硬體查詢和只開啟 HZB 的情況下,對幀率和被遮擋物數量的影響。UE4 針對硬體查詢做了 Batch 優化,這樣可以大大降低硬體查詢帶來的 DC 開銷,不過 Batch 只對靜態物體有效。所以,需要分開測試。

動態物體:

如圖,只開啟了硬體查詢,因為動態物體無法 Batch,Occlusion queries 相當高,達到 987,而 draw call 數量達到了 1450。被遮擋物體為 579,可見物體為 411。幀率只有 18。由此可見,對於大量動態物體,硬體查詢本身帶來了巨大的 DC 開銷。

如何在 UE4 移動端中實現 HZB?

如何在 UE4 移動端中實現 HZB?

如圖,只開啟了 HZB,硬體查詢的 DC 開銷已經沒有了,被遮擋物體為 570,可見物體為 420。Draw call 為 467,幀率為 30。

如何在 UE4 移動端中實現 HZB?

如何在 UE4 移動端中實現 HZB?

結論:對於大量動態物體查詢的場景,從被遮擋物體數量和可見物體數量兩個資料指標來看,硬體查詢和 HZB 不相上下。效能上,HZB 優勢明顯。

靜態物體:

如圖,只開啟硬體查詢,因為靜態物體的原因,硬體查詢發揮了 Batch 的優勢,Occlusion queries 只有 58,draw call 只有 511,幀率達到 35。

如何在 UE4 移動端中實現 HZB?

如何在 UE4 移動端中實現 HZB?

如圖,只開啟了 HZB,硬體查詢 Batch 帶來的 DC 也沒有了,所以 draw call 略有下降。幀率為 33。

如何在 UE4 移動端中實現 HZB?

如何在 UE4 移動端中實現 HZB?

結論:對於大量靜態物體查詢的場景,HZB 仍適用,效能與硬體查詢 Batch 相當。

硬體遮擋查詢 和 HZB 在 oppo r17 上的效能對比

同樣分別測試動態物體和靜態物體,r17 和 r15 表現了完全的不一樣的結果,之前在優化讀取時也發現,r17 依然有 4~6ms 的開銷,這是為什麼了?這跟 r15 和 r17 在硬體上不同有關。

R15 硬體引數

如何在 UE4 移動端中實現 HZB?

R17 硬體引數

如何在 UE4 移動端中實現 HZB?

另外,值得一提的是,UE4 針對高通裝置,做了硬體查詢的上限限制。最大 510 次查詢,而其他裝置預設是最大 4000 次查詢。

如何在 UE4 移動端中實現 HZB?

如何在 UE4 移動端中實現 HZB?

這導致了 R17 在大量動態物體場景下,即便沒有做 Batch,也只有不超過 250 的 Occlusion queries 數量。這並不是什麼優化,而是 UE4 直接放棄了超過該數量的硬體查詢,再加上依然存在的回讀耗時,這樣使得 HZB 的優勢就不那麼明顯了。

如圖,R17 上,開啟硬體查詢,關閉 HZB。

如何在 UE4 移動端中實現 HZB?

如圖,R17 上,關閉硬體查詢,開啟 HZB。

如何在 UE4 移動端中實現 HZB?

結論:基於以上原因,在大量動態物體和靜態物體場景下,HZB 在 R17 上都表現不佳。

其他效能開銷

移動端除了回讀耗時以外,在 HZB 構建以及遮擋測試階段,效能消耗也不能忽視,特別是取樣 16 次貼圖的操作。如下分別是在 R15 和 R17 上的測試結果:

R15 在構建和測試階段分別耗時 2.6ms 和 1.6ms。回讀耗時 0.9ms

如何在 UE4 移動端中實現 HZB?

R17 在構建和測試階段分別耗時 0.48ms 和 0.41ms。回讀耗時 5.2ms

如何在 UE4 移動端中實現 HZB?

通過對比發現,R15 回讀快,而構建慢。R17 回讀慢,而構建快。不同的硬體架構帶來的效能差異很大,關於移動硬體分析已經不屬於本文探討範疇了,在這裡就不展開講了。

總結

關於 UE 硬體查詢的 Batch 結論:

動態物體不會做 Batch,全部一個一個去查詢,帶來巨大的 DC 開銷。

靜態物體在被遮擋的情況下會做 batch 查詢,DC 顯著減少。

高通手機最大查詢次數為 510,其他為 4000,而實際推薦最佳查詢次數是 250,2000(分別除了 2)。

移動端 HZB 結論:

大量動態物體查詢,HZB 適用於非高通移動裝置上。

大量靜態物體查詢,HZB 仍適用於非高通移動裝置上,效能與硬體查詢 Batch 相當。

針對移動端未來可以做的優化方案:

將物體資料由質心座標+包圍盒範圍改為質心座標+包圍球半徑,可節省一張 RGBA32 貼圖。

將 16 次取樣改為取樣包圍球表面最近點和包圍盒四個頂點,可減少 11 次取樣。甚至更保守點,直接將包圍球面最近點作為取樣點進行比較。

高通裝置可以考慮使用 vulkan 圖形 API 進行資料回讀。

高通裝置在構建 HZB 的時候可以考慮使用 ComputeShader。


來源:GWB-騰訊創意遊戲合作計劃

相關文章