【GDC2023乾貨分享】移動平臺上的軟光柵遮擋剔除方案
低效能手機也能跑得動?《明日之後》手遊中大量的建築物、車輛、石頭等遮擋物,如何能被快速計算,以低功耗完成遮擋剔除的?
在GDC2023的核心會場(Core concept)中,網易遊戲的資深引擎程式Tao介紹了《明日之後》所使用的創新高效能軟光柵遮擋剔除方案(SOC)。藉助SOC方案,在低端手機平臺(如 iPhone 6s)上,遊戲每幀僅需要約1.5ms的消耗,能夠完美適配動態場景,美術可以放心創造複雜精巧的場景而不用擔心效能問題!
以下是演講實錄(根據現場英文演講內容翻譯整理):
本次演講將跟大家分享我們是如何在《明日之後》專案中,實現移動平臺的高效能軟光柵遮擋剔除方案的。演講主要分為 3 個部分:輕量級軟光柵遮擋剔除方案、最佳化剔除管線和離線生成遮擋網格。
首先介紹下背景資訊。《明日之後》是一款開放世界多人TPS手遊。玩家要在末日世界中生存,探索廢棄荒蕪的城市、危險重重的森林,山脈和海洋,還要對抗怪物,收集資源以及建造營地。
先介紹下演講中涉及的術語。
一、最佳化動機
由於《明日之後》是款手遊,遮擋剔除可能會對移動平臺的效能產生較大的影響。在常規場景中,60%以上的物件可以被剔除,意味著這些物體不用渲染,從而幫助我們節省大量的時間。
然而,要設計針對移動平臺的遮擋剔除解決方案,需要滿足很多條件。
首先要滿足的是高剔除率,以及無錯誤遮擋——這意味著沒有可見物件被錯誤剔除。
該解決方案必須支援靜態物件和動態物件,而且佔用的包體較小。
另外,也是非常關鍵的一點是,它得跑的很快。對於計算能力較弱的移動平臺,這個要求非常的有挑戰性。
這裡有一個表格,包含常見的遮擋剔除解決方案,潛在可視集合 (Potential Visible Set, 以下簡稱 PVS )、硬體遮擋查詢、以及軟體遮擋剔除,簡稱SOC。
可以看到,PVS不支援動態物件渲染,而硬體遮擋查詢則導致出現了錯誤遮擋。
SOC 方案滿足了我們的大部分要求,但是它的結果嚴重依賴於遮擋網格的質量,而且它在移動平臺上執行太慢了。但我們最終還是選擇使用 SOC 解決方案,但是需要做一些努力來解決這些問題。
我們的目標是,儘可能提升 SOC 解決方案的速度,所以我們必須針對移動平臺最佳化演算法,同時使用高質量的遮擋網格。
我們的方案
這裡就要提到我們的高效軟體遮擋剔除解決方案。
該方案由 3 個部分組成,一個用於剔除的輕量級soc 演算法,即圖表中的綠色部分;一個用於組織資料的最佳化管線,即黃色部分,以及用於生成高質量遮擋網格的離線生成器,即藍色部分。
在接下來的演講部分,我會逐一解釋以上三個部分。
二、剔除演算法(Occlusion Mesh Generator)
讓我們從第一部分開始,剔除演算法。
該演算法有 2 個目標。先是對選定的遮擋物光柵化,計算出深度值,儲存在深度緩衝區中,
接著使用這個深度緩衝區來做深度測試,判斷被遮擋物的可見性。
2.1 傳統SOC方案和輕量級SOC方案對比
傳統SOC方案
作為對比,我簡單介紹一下傳統的“帶遮罩的軟體遮擋剔除方案”(Masked Software Occlusion Culling,MSOC),它是Intel在2016年提出的解決方案,也是現在PC和主機上的主流SOC實現方案。
該演算法針對支援 AVX 的 CPU 進行了最佳化,具有分層深度緩衝區。它會收集遮擋物的三角形資訊(wedge),然後使用“邊緣填充光柵化演算法”來檢查並標記給定遮擋物三角形的覆蓋畫素, 再然後使用’啟發式丟棄'演算法,來更新深度緩衝區資訊,從而得到新的深度值。
接著計算被遮擋物的螢幕矩形區域,對深度緩衝區進行遮擋查詢。
輕量級SOC方案
接下來,再看看我們的輕量級SOC 軟體遮擋剔除演算法。藍色標記的步驟是我們與 MSOC 不同的地方。可以看到框架非常相似,但是在細節上進行了最佳化。
我們的演算法也使用 SIMD 指令進行了最佳化。在移動平臺上,SIMD 通常用Arm Neon技術實現。我們沿用了分層深度緩衝區的想法,但做了一些小的調整。
在對遮擋物光柵化部分,我們在準備階段上進行了很大的改動:不再使用背面剔除(BackfaceCulling),最佳化了頂點排序,並且在楔形資訊收集階段增加了一個額外的預排序步驟。準備之後,我們仍然使用邊緣填充光柵化進行覆蓋檢測,但不再使用“啟發式丟棄”過程,深度資訊更新則回落到使用簡單的寫入方式。
在對被遮擋物進行可見性查詢,我們在查詢之前新增了一個“Early Out(儘早退出迴圈或函式)”步驟。我將在後面的演講內容觀眾詳細介紹我們的演算法,重點介紹我們創新的部分。
2.2 深度緩衝區和深度值儲存
深度緩衝區結構
我們使用解析度很低的深度緩衝區,比如 256x160。如圖所示,整個緩衝區被分成 4 個 bin。每個 bin 進一步被劃分為80 個圖塊。 每個圖塊又分為 4 個子圖塊。圖塊由 8*4 的畫素塊組成, 每個圖塊共享一個深度值。
最後,每個圖塊包含 128 個畫素,對應一個 m128i;4 個深度值對應一個 m128,可以看成是4個float32。
儲存深度值
在儲存深度值方面,我們也有一個小技巧。
我們注意到深度值僅用於 Z 測試,所以真正重要的是相對大小。因此,我們使用反向Z (Reversed-Z) 投影來提高深度緩衝區精度。這也是常見的做法。
透過進一步修改投影矩陣,將遠平面投影設定為0,可以更方便地設定深度緩衝區。最終的投影矩陣如下圖所示。引數'c'可以是任何正實數。可以看到矩陣中只有4個非零項,比原始矩陣少1 個非零項,也就意味著投影速度加快了。實際上,如果你使用SIMD (Single Instruction Multiple Data,即單指令流多資料流)會快得多。
2.3 楔形資訊收集階段(Wedge Collecting Phase)
接下來是處理過程。在對遮擋物進行處理時,首先會經過楔形收集階段。我們使用上文中所示的修改後的投影矩陣,將遮擋物投影到楔形板中。
對於每個三角形, 我們只儲存 1 個深度值,即只儲存最遠、最保守(即最嚴格)的深度值。然後,當所有遮擋物都完成投影時,按照深度值對楔形內的物體進行排序。
我們為什麼要進行這種排序呢?
與 MSOC 中的啟發式搜尋方法類似,都是為了確保後續深度緩衝區的深度值更新。
使用排序而不是“啟發式丟棄”法,是因為我們考慮到了兩個因素:準確性和速度。
排序的準確性
排序的結果準確性更高。
“啟發式丟棄”,顧名思義, 是一種啟發式演算法,結果不穩定。在下圖中,我們可以看到使用“啟發式丟棄”法 時,沿著陰影邊界的一些畫素丟失了,而使用排序時,結果則顯示正常。不要低估這個小問題。由於我們使用的是極低的解析度,這可能會極大的影響剔除率。
排序的渲染速度
而渲染速度則取決於需要處理的遮擋物三角形數量。
從上圖中可以看出,三角形面數較多時,“啟發式丟棄法”(橙線)效率更高,三角形面數較少時,則排序法速度更快。在我們的解決方案中,輸入的三角形面數非常少,甚至低於 4000。因此,排序法是更為合理的方案。
2.4 準備階段(Preparation Phase)
接下來是準備階段。
計算資訊
首先,我們來計算資訊,方便後續進行邊緣填充演算法。
需要計算的資訊包括螢幕的楔形邊界框以及每條邊的斜率等。所有輸入的三角形都被視為雙面,並且按批次打包,透過 SIMD 指令批次進行處理。
為了有效地從打包的SIMD資料中找到三角形的指定邊,我們需要將資訊打包在一些指定的圖案中。
實際上,我們會將三角形頂點排列成 2 個指定的圖案,如下圖所示。因此,我們透過對頂點進行排序來形成三角形。
排序有 3 個目標:
相比MSOC,我們需要完成的目標更多(“將頂點設定為逆時針方向”,這是一個額外的需求),但我們透過最終使用比 MSOC更少的指令數完成了所有的目標,只用到了 22 條SIMD 指令。這其中的關鍵點在於,要意識到這個額外的目標事實上使我們放開了手腳,因為不必保留原始頂點順序。
程式碼如下所示。我們透過簡單比較找到頂點v0,計算出與其相鄰的兩個點v1和v2的向量,對這兩個向量進行叉積運算,最後將v1和v2按照順時針的方向排序。
2.5 光柵化階段(Rasterization Phase)
接下來是光柵化階段,也是最終的處理階段。在這個階段,我們將螢幕的楔形光柵化,儲存到深度緩衝區中。其中包括兩個步驟:覆蓋檢測和深度更新。
與 MSOC 一樣,我們在覆蓋檢測部分使用了“邊緣填充光柵化”演算法。主要原理是是為每一行找到最左邊和最右邊的畫素,接著填充之間所有的畫素。
接著我們要更新深度緩衝區中的深度值。由於前面的排序,深度值的更新非常簡單,即當子圖塊被完全覆蓋的時候,記錄當前的深度值。
現在我們已經完成了遮擋物的部分,獲得了深度緩衝區,接著再處理被遮擋物。
在這部分中,我們將被遮擋物的 AABB 投影到螢幕空間中,執行簡單的碰撞測試,得到早期剔除結果,再透過深度測試查詢可見性。
這個方法很簡單——也就意味著很快:如果投影面積太小,則被遮擋物將不可見。如果離相機過近,那麼被遮擋物將是可見的。實際上,我們可以跳過大約 50% 的被遮擋物查詢。
這是我們的輕量級軟體遮擋剔除演算法的效能資料。在對遮擋物光柵化時,每個三角形只需花費 0.4 微秒。 對遮擋物進行可見性查詢,每個被遮擋物花費 0.56 微秒。 還可以看出,“Early Out”有效地降低了可見性查詢的成本,繞過了幾乎 50% 的被遮擋物。
三、最佳化剔除管線(Optimized Culling Pipeline)
接下來到剔除管線的部分。
剔除管線需要組合多個剔除模組,以及所有相關資料的組織,包括遮擋物和被遮擋物,並輸出報告結果。
下圖是一個典型的剔除管線實現方法。
因為過於簡單明瞭,幾乎很少人關注它在電腦和主機平臺上的應用情況。
然而在移動平臺上,我們發現管線本身的消耗很大。在舊版本的引擎中,渲染5000 個被遮擋物需要 1.5ms 的時間,留給 SOC 的空間很小。因此,我們需要加速渲染管線。
3.1 降低快取丟失率
3.1.1 陣列遍歷
首先,我們考慮如何有效地判斷被遮擋物。
有兩個選擇:直接使用普通的陣列來管理場景中的物體,或者,空間加速結構。
陣列
如果我們選擇使用陣列,策略非常簡單:遍歷所有被遮擋物來進行判斷。這個場景中有5個遮擋物,每次Cache Fetch可以儲存3個物體到快取,則可以看到,需要進行5次可見性查詢和2次Cache Fetch。
空間加速結構
而如果使用空間加速結構,例如 邊界體層次結構 (Bounding Volume Hierarchy (BHV))或 八叉樹(Octree)等方法,就能減少查詢次數。
如下圖,我們新增 2個父級包圍體積( Parent Bounding Volume(PBV)),V1和V2,一旦被判斷為不可見,比如V2,它的所有子級包圍體積,C,D,E,都可被設為不可見,無需進一步測試。因此,我們可以減少 1 次可見性查詢。但此時,我們訪問記憶體的方式,相比陣列來說,更加的隨機。在這種情況下,我們需要3次Cache Fetch,比使用陣列多一次。
那麼你可能會好奇,如果將陣列中的子項和父項放在一起,是否可以減少快取預取的效能消耗呢?實際上,該方案下記憶體訪問將是線性的,確實可以減少消耗。但是此時維護加速結構的開銷會顯著增加,考慮到被遮擋物移動的情況下,會需要我們去做更多的工作來維持結構。
兩者對比
結合我們的實際情況,在我們的解決方案中,剔除查詢的消耗較低,而且遊戲中的被遮擋物數量不是那麼大,因此跳過的查詢次數,無法抵消快取未命中的消耗,更不用說維護加速結構的額外成本了。所以,使用陣列顯然是更好的選擇。
由於快取行(cache line) 的大小通常是固定的。資料結構越小,Cache Fetch次數越少。所以,我們希望最小化被遮擋區的資料大小。我們在資料結構中僅儲存了相關的資料,並使用盡可能低的資料精度。此外,我們使用位域來最小化布林值和列舉值 。
透過對資料進行組織和最佳化,我們獲得了約0.3毫秒的時間效能提升。
3.1.2 函式呼叫
下一個要解決的問題是函式呼叫時出現的快取未命中。
由於虛擬函式呼叫導致快取未命中(cache miss)時,會產生顯著消耗。
我們先來看看這些快取未命中是如何發生的。當呼叫一個普通函式時,CPU 處理器需要載入該函式的指令。如果指令沒有儲存在指令快取中,就會發生快取未命中。
呼叫虛擬函式時,處理器首先需要載入虛表(vtable)指標,然後載入vtable指標引用的函式地址。而載入地址時,有可能會發生快取未命中。因此最糟糕的情況就是在呼叫函式時,多了兩次快取未命中。
此外,使用虛擬函式時,CPU很難預測下一條指令,導致指令執行效率降低。
因此,我們決定刪掉所有的虛擬函式呼叫,簡化操作,解決快取未命中問題。
這個修改本身並不複雜,但我也有一些小小的建議,來使你更快的完成這類修改:你可以將資料和指令分開存放,這樣可以方便地訪問資料,也可以透過函式實現物件導向的指令部分;其次,還可以使用不同的陣列來儲存不同型別的遮擋物資訊,並呼叫特定的通知函式。
透過上述方式,我們獲得了 0.5ms 的效能提升。
3.1.3 可見性過濾
下一個問題是可見性過濾器。
什麼是可見性過濾器呢?在一個渲染管線中,一個被遮擋物需要經過多次檢測,才能判斷為可見。這個過程是多個過濾器的級聯,包括邏輯設定、視體剔除,以及軟體演算法執行遮擋剔除等等。
這個過濾器有兩種常見的實現方式。第一種方法是標記可見性。已標記為不可見的遮擋物將跳過後續測試。第二種方法是使用一個額外的陣列來儲存過濾後的遮擋物。
第一種方法的缺點是不可見的遮擋物佔用了快取行。而第二個方法則會導致不必要的資料複製消耗。
那麼哪個方案更好呢?取決於實際情況。
在我們的專案中,大約 80% 的物件會被視體剔除過濾掉,因此複製操作並不頻繁。在這種情況下,我們發現使用“過濾”方法效率更高,可節約0.05ms。
3.2 不要光柵化低遮擋的三角形
讓我們根據上面這張圖來分析可見部分。上圖中,你可以看到,這個遮擋物,不僅隱藏了被遮擋物,還隱藏了一部分自身——我們稱之為自遮擋(Self Occlusion)。此外,平行於觀察方向的部分幾乎沒有遮擋效力。
對場景來說,左邊的遮擋物和右邊的遮擋物具有相同的遮擋效果,但右邊的面數顯著低於左邊,因此對左邊這個遮擋物進行完整的光柵化,算對算力的浪費。
為了解決這個問題,我們將遮擋網格分成多個部分,只光柵化有效部分,也就是我們說的“可見部分”。
可見部分資訊是離線渲染的。我們先給定一個遮擋網格,根據三角形的法線和位置對三角形進行聚類,將網格分成幾個部分。然後我們從不同的角度拍攝快照,並計算每個部分的可見尺寸大小。
在這張圖中,有一個部分只是一個線段,或者說是平面。
綠線和紅線表示不同的觀察方向。
0表示在這個檢視中方向,完全不可見,可能是被其他部分遮擋了。
這裡的 1 是指投影后,這個部分在視線中佔1平方米大小。這裡的單位是平方米。
透過彙總所有資訊,每個部分都可以用一個視錐體來描述其可見範圍。假設我將閾值設為1,視錐體的大小和形狀應該能夠包含所有可見尺寸等於或大於1平方米的物體。所以我們得到如圖所示的錐體。
在執行時,會使用視錐體來進行可見性測試。如果在執行時,視角方向在可見錐體之外,那麼被觀察物體的可見尺寸一定小於某個閾值,這部分將被視為不可見。
而我們只對那些可見的部分進行光柵化——使用我們的輕量級軟體遮擋剔除演算法。在實踐中,大約 50% 的遮擋三角形可以跳過光柵化過程,並且不會對渲染效果產生嚴重影響。效能資料顯示如下,可以看到需要光柵化的三角形數量從1539降到了874。
3.3 多執行緒渲染
接下來要解決的是如何在移動平臺上使用多執行緒。
目前,我們的大多數手機都是採用 ARMbig.LITTLE 架構,是一種異構處理器架構。該架構結合了兩種不同的處理器核心,一種是高效能的大核,另一種是效能弱但耗能低的小核心。由於大核心已經被邏輯執行緒、渲染執行緒佔用,所以作業執行緒不得不跑在小核心上,速度慢很多,另外執行緒排程的消耗也很大。
因此,我們必須謹慎選擇傳送給作業執行緒的任務,該任務在工作執行緒上執行的時間與主執行緒上的任務執行時間相匹配,並且足夠大,以抵消排程消耗。
最後我們發現“可見截面選取”和“楔形收集階段”這兩個任務完美地滿足了傳送給作業執行緒需求,它們可以和視錐遮擋同時執行。這麼做可以減少0.25ms 的時間。
這裡展示了剔除管線的資料。可以看到經過最佳化後,渲染時間得到了大幅減少,特別是在iPhone6s上,時間成本從 1.5ms 減少到大約了0.4毫秒。
四、用於生成高質量遮擋網格的離線生成器(High-quality Occlusion Mesh Generator)
有了一個高效的剔除管線後,接下來我們要為其提供高質量的遮擋體。因此需要打造用於生成高質量遮擋網格的離線生成器。
為什麼遮擋網格如此重要?
首先,為了在低端手機上,為了實現在 2ms內完成整個剔除渲染過程,我們需要將遮擋網格的三角形面數設定為4000,這是一個極低的面數要求。與此同時,還要保證遮擋網格是高質量的。否則,即使只是輕微的網格裂縫和偏差,也會導致剔除率急劇下降到35%,偶爾還會出現錯誤遮擋。
想象一下,常見場景下,畫面可能有 100 萬個三角形,而被遮擋物的三角形數量可能達到 10 萬個。然而,我們只需要渲染4000個三角形,可以實現同樣的遮擋效果。
聽起來似乎是不可能完成的任務,但是我們做到了。
先簡要介紹一下我們的網格生成器。
我們首先透過體素化和填充來對模型進行平滑處理,接著從中過濾出有效的聚類,再將每個聚類中的資料或物件轉換為三角形網格來進行渲染。
由於體素化模型不適合表示斜坡或曲面,我們將原始遮擋物按法向方向切割成子模型,再分別進行處理,最後再合併所有子模型的結果。
4.1 平滑過程(Smoothing)
在生成高質量的簡化遮擋網格的過程中,我們遇到了很多挑戰。在這其中,最難的一點,就是如何去平滑一個3D Mesh。我們嘗試了很多方法,最終找出了這個解決方案。
平滑Mesh
如下圖所示,我們的想法是儘可能多地填充體素化模型,得到平滑的Mesh,接著進行提取。
而問題是,如何在確保進行平滑操作後,還不會產生錯誤遮擋。
我們需要用數學公式來描述“無錯誤遮擋”的約束條件。因此,我們提出了可視函式的概念。
可視函式
接下來是演算法部分,我儘量講的簡單點,避免大家睡著:)。
可見性函式的定義很簡單,是指從一個任意位置,沿著一條任意方向,去觀察一個任意物體,如果物體可見,那麼函式結果為 True,反之為 False。
那麼我們可以這樣描述“無錯誤遮擋”約束條件:對於任意被遮擋物,在任何位置或任意視線方向,如果在使用簡化遮擋網格時,可見性函式為假,則在使用原始網格時,可見性函式也應該為假。如果函式為真,那麼說明該物體可以被看到,但如果使用簡化網格看不到它,說明物體被錯誤遮擋了。
這個約束條件仍然很難使用,因為“任意”的條件太多了,所以我們決定透過近似來簡化這個條件。
簡化“任意位置”
首先,我們簡化“任意位置”, 將任意位置替換為“near 和 far”,其中 near 表示AABB網格的擴充套件,而“far”表示無窮大。範圍如下:
緊接著,對於“任意方向”,我們將其簡化為幾個指定的觀察方向,在實踐中,我們選擇 3 條軸和其中線:
最後,我們還要定義“任意被遮擋物”,這相對複雜一些。我們提出了“重要體素”的概念:重要體素是那些已經存在了靜態物體的地方,以及動態物體中心部分可能出現的地方。然後,我們將約束條件中的“任意被遮擋物”替換為“任意重要體素”。
下圖具象說明了什麼是重要體素。圖中有一個預設的動態物件和一個遮擋物。我們讓動態物體在遮擋物內部徘徊,而紅色的體素就是中心部分可能出現的地方,也就是重要體素。
我們將約束條件被簡化為:對於任意重要體素,在 near 和 far 的位置,沿任何指定的方向觀察,如使用簡化遮擋網格時,可見性函式為假,則使用原始網格時,可見性函式也必須為假。
透過約束(Constraint)平滑Mesh
有了約束(Constraint)後,我們的平滑過程就很清晰了。如流程圖所示,我們需要遍歷所有空的體素,嘗試進行填充,並檢查約束是否仍然成立。
但是這個遍歷過程很耗時,所以在實踐中,我們使用啟發式的方法,再透過約束來判斷可填充的體素。
這個方法的思路是找到 2 個彼此不可見的重要體素,則它們之間的體素一定是可以安全填充位置,因為它們不會干擾重要的體素。這種啟發式方法要快得多,結果也可以接受。
以下圖為例,Voxel SA 和 SB 位於重要體素 IA 和 IB 之間,而 IA 和 IB 看不到對方。所以SA 和 SB 是安全體素。
這樣一來平滑過程就變得簡單。在設定可見性函式約束後,我們得出所有安全體素,進行填充,並得到填充的體素網格。
4.2 篩選聚類(Filtering)
接著,我們需要提取它的有效遮擋部分。我們首先沿著其中一個軸對體素網格進行切片。然後對於每個切片,我們將相連的體素劃分為簇,計算簇與簇之間的遮擋關係。然後我們選擇遮擋關係高於給定閾值的簇。
理論上講,一個簇的遮擋關係可以透過下面的公式來計算得到,但是這個過程很慢。因此我們最終沒有使用這個方案,這就裡不再詳細說明這個公式。
我們同樣提出了一種啟發式方法。我們首先找到滿足這樣條件的重要體素:當它被原始模型包圍時,可見性函式的返回值為假,當它被所有當前選擇的簇包圍時,其可見性函式的返回值為真。接著標記可以遮擋這些重要體素的簇,將標記的數量作為遮擋資訊的近似值。
在實踐中,這種啟發式方法與基準真相( ground truth) 相比,計算速度快10 倍,並且結果也很棒。
4.3 轉化為三角形(Triangulating)
而選中簇之後,接下來就是將它們轉換成三角形。我們使用了常見的做法。
首先,我們先提取出邊界資訊,並將孔洞與邊界相連線,從而將每個簇轉換為邊緣迴圈。
然後,我們透過經典的道格拉斯-普克演算法(Ramer-Douglas-Peucker)演算法沿邊緣曲線迴圈簡化。
之後,我們使用經典的切耳法(Ear Clipping)演算法對多邊形進行三角剖分,從而得到結果。
對於方形遮擋物,這樣處理後的結果已經可以接受。但是我們還有許多斜面和曲面的遮擋物。
解決這個問題的思路很簡單。我們從原始模型中分離出斜坡和曲面,形成一個獨立的子模型。用前面的提到方法簡化它們,接著將所有部分的結果結合起來,並將它們之間的間隙焊接起來,得到最終的遮擋網格。
現在我們得到最終的結果,就是一個簡化的高質量遮擋網格,如下面的案例。
這座教堂有 75240個三角形,而簡化的遮擋網格使用了不到900個三角形。你可以看到凹凸不平的牆壁被平滑了,塔尖被自動移除,因為塔尖基本不具備遮擋效果。
而這裡的圓形體育場,原來有大約 10k 個三角形,簡化後只剩下 900 個三角形。可以看到到生成器對於曲面非常有效。
我們的生成器生成的最終簡化遮擋網格大約是原始三角形數量的5%,並且不會產生任何錯誤遮擋效果。
總結
下表顯示了從高階到低端的不同機型的效能資料,可以看到我們在2ms 內剔除了 65% 的物件。
以Iphone 6s為例,我們的 SOC 演算法可以在1ms 內減少 65% 的繪製呼叫,而不會發生任何錯誤遮擋。對於整個剔除解決方案,時間是1.5ms,總剔除率約為 91%。
針對移動平臺的高效軟體遮擋剔除解決方案,需要綜合考慮各個方面。如:
這就是我們在《明日之後》中所實現的解決方案,這不是一項簡單的任務。沒有我們出色的開發團隊,就無法實現這套解決方案。感謝大家傾聽!
在GDC2023的核心會場(Core concept)中,網易遊戲的資深引擎程式Tao介紹了《明日之後》所使用的創新高效能軟光柵遮擋剔除方案(SOC)。藉助SOC方案,在低端手機平臺(如 iPhone 6s)上,遊戲每幀僅需要約1.5ms的消耗,能夠完美適配動態場景,美術可以放心創造複雜精巧的場景而不用擔心效能問題!
以下是演講實錄(根據現場英文演講內容翻譯整理):
本次演講將跟大家分享我們是如何在《明日之後》專案中,實現移動平臺的高效能軟光柵遮擋剔除方案的。演講主要分為 3 個部分:輕量級軟光柵遮擋剔除方案、最佳化剔除管線和離線生成遮擋網格。
首先介紹下背景資訊。《明日之後》是一款開放世界多人TPS手遊。玩家要在末日世界中生存,探索廢棄荒蕪的城市、危險重重的森林,山脈和海洋,還要對抗怪物,收集資源以及建造營地。
先介紹下演講中涉及的術語。
- 遮擋剔除(Occlusion Culling)是指當一個物體被其他物體遮擋時,不對其進行渲染。
- 遮擋物(Occluder)是一些選定的大型物體,可以遮擋視線中的其他物體,就像這裡的帳篷。
- 被遮擋物(Occludee)是場景中的所有可以被遮擋物遮擋的物體。
- 剔除率(Culling rate )是一種用於評估剔除演算法有效性的指標,剔除率 = 剔除掉的物體或面片數 / 總的物體或面片數。
- 錯誤遮擋(False Occlusion)指由於剔除演算法的誤判,導致可見物體被錯誤遮擋的情況。這種情況下,剔除演算法錯誤地認為某些物體或面片是不可見的,從而導致它們可能突然消失。
一、最佳化動機
由於《明日之後》是款手遊,遮擋剔除可能會對移動平臺的效能產生較大的影響。在常規場景中,60%以上的物件可以被剔除,意味著這些物體不用渲染,從而幫助我們節省大量的時間。
然而,要設計針對移動平臺的遮擋剔除解決方案,需要滿足很多條件。
首先要滿足的是高剔除率,以及無錯誤遮擋——這意味著沒有可見物件被錯誤剔除。
該解決方案必須支援靜態物件和動態物件,而且佔用的包體較小。
另外,也是非常關鍵的一點是,它得跑的很快。對於計算能力較弱的移動平臺,這個要求非常的有挑戰性。
這裡有一個表格,包含常見的遮擋剔除解決方案,潛在可視集合 (Potential Visible Set, 以下簡稱 PVS )、硬體遮擋查詢、以及軟體遮擋剔除,簡稱SOC。
可以看到,PVS不支援動態物件渲染,而硬體遮擋查詢則導致出現了錯誤遮擋。
SOC 方案滿足了我們的大部分要求,但是它的結果嚴重依賴於遮擋網格的質量,而且它在移動平臺上執行太慢了。但我們最終還是選擇使用 SOC 解決方案,但是需要做一些努力來解決這些問題。
我們的目標是,儘可能提升 SOC 解決方案的速度,所以我們必須針對移動平臺最佳化演算法,同時使用高質量的遮擋網格。
我們的方案
這裡就要提到我們的高效軟體遮擋剔除解決方案。
該方案由 3 個部分組成,一個用於剔除的輕量級soc 演算法,即圖表中的綠色部分;一個用於組織資料的最佳化管線,即黃色部分,以及用於生成高質量遮擋網格的離線生成器,即藍色部分。
在接下來的演講部分,我會逐一解釋以上三個部分。
二、剔除演算法(Occlusion Mesh Generator)
讓我們從第一部分開始,剔除演算法。
該演算法有 2 個目標。先是對選定的遮擋物光柵化,計算出深度值,儲存在深度緩衝區中,
接著使用這個深度緩衝區來做深度測試,判斷被遮擋物的可見性。
2.1 傳統SOC方案和輕量級SOC方案對比
傳統SOC方案
作為對比,我簡單介紹一下傳統的“帶遮罩的軟體遮擋剔除方案”(Masked Software Occlusion Culling,MSOC),它是Intel在2016年提出的解決方案,也是現在PC和主機上的主流SOC實現方案。
傳統軟體遮擋剔除方案(MSOC)流程圖
該演算法針對支援 AVX 的 CPU 進行了最佳化,具有分層深度緩衝區。它會收集遮擋物的三角形資訊(wedge),然後使用“邊緣填充光柵化演算法”來檢查並標記給定遮擋物三角形的覆蓋畫素, 再然後使用’啟發式丟棄'演算法,來更新深度緩衝區資訊,從而得到新的深度值。
接著計算被遮擋物的螢幕矩形區域,對深度緩衝區進行遮擋查詢。
輕量級SOC方案
接下來,再看看我們的輕量級SOC 軟體遮擋剔除演算法。藍色標記的步驟是我們與 MSOC 不同的地方。可以看到框架非常相似,但是在細節上進行了最佳化。
我們的輕量級SOC 軟體遮擋剔除演算法流程圖
我們的演算法也使用 SIMD 指令進行了最佳化。在移動平臺上,SIMD 通常用Arm Neon技術實現。我們沿用了分層深度緩衝區的想法,但做了一些小的調整。
在對遮擋物光柵化部分,我們在準備階段上進行了很大的改動:不再使用背面剔除(BackfaceCulling),最佳化了頂點排序,並且在楔形資訊收集階段增加了一個額外的預排序步驟。準備之後,我們仍然使用邊緣填充光柵化進行覆蓋檢測,但不再使用“啟發式丟棄”過程,深度資訊更新則回落到使用簡單的寫入方式。
在對被遮擋物進行可見性查詢,我們在查詢之前新增了一個“Early Out(儘早退出迴圈或函式)”步驟。我將在後面的演講內容觀眾詳細介紹我們的演算法,重點介紹我們創新的部分。
2.2 深度緩衝區和深度值儲存
深度緩衝區結構
我們使用解析度很低的深度緩衝區,比如 256x160。如圖所示,整個緩衝區被分成 4 個 bin。每個 bin 進一步被劃分為80 個圖塊。 每個圖塊又分為 4 個子圖塊。圖塊由 8*4 的畫素塊組成, 每個圖塊共享一個深度值。
最後,每個圖塊包含 128 個畫素,對應一個 m128i;4 個深度值對應一個 m128,可以看成是4個float32。
深度緩衝區結構
儲存深度值
在儲存深度值方面,我們也有一個小技巧。
我們注意到深度值僅用於 Z 測試,所以真正重要的是相對大小。因此,我們使用反向Z (Reversed-Z) 投影來提高深度緩衝區精度。這也是常見的做法。
透過進一步修改投影矩陣,將遠平面投影設定為0,可以更方便地設定深度緩衝區。最終的投影矩陣如下圖所示。引數'c'可以是任何正實數。可以看到矩陣中只有4個非零項,比原始矩陣少1 個非零項,也就意味著投影速度加快了。實際上,如果你使用SIMD (Single Instruction Multiple Data,即單指令流多資料流)會快得多。
投影矩陣
2.3 楔形資訊收集階段(Wedge Collecting Phase)
接下來是處理過程。在對遮擋物進行處理時,首先會經過楔形收集階段。我們使用上文中所示的修改後的投影矩陣,將遮擋物投影到楔形板中。
對於每個三角形, 我們只儲存 1 個深度值,即只儲存最遠、最保守(即最嚴格)的深度值。然後,當所有遮擋物都完成投影時,按照深度值對楔形內的物體進行排序。
我們為什麼要進行這種排序呢?
與 MSOC 中的啟發式搜尋方法類似,都是為了確保後續深度緩衝區的深度值更新。
使用排序而不是“啟發式丟棄”法,是因為我們考慮到了兩個因素:準確性和速度。
排序的準確性
排序的結果準確性更高。
“啟發式丟棄”,顧名思義, 是一種啟發式演算法,結果不穩定。在下圖中,我們可以看到使用“啟發式丟棄”法 時,沿著陰影邊界的一些畫素丟失了,而使用排序時,結果則顯示正常。不要低估這個小問題。由於我們使用的是極低的解析度,這可能會極大的影響剔除率。
“啟發式丟棄”時邊緣畫素的丟失
排序的渲染速度
而渲染速度則取決於需要處理的遮擋物三角形數量。
從上圖中可以看出,三角形面數較多時,“啟發式丟棄法”(橙線)效率更高,三角形面數較少時,則排序法速度更快。在我們的解決方案中,輸入的三角形面數非常少,甚至低於 4000。因此,排序法是更為合理的方案。
2.4 準備階段(Preparation Phase)
接下來是準備階段。
計算資訊
首先,我們來計算資訊,方便後續進行邊緣填充演算法。
需要計算的資訊包括螢幕的楔形邊界框以及每條邊的斜率等。所有輸入的三角形都被視為雙面,並且按批次打包,透過 SIMD 指令批次進行處理。
為了有效地從打包的SIMD資料中找到三角形的指定邊,我們需要將資訊打包在一些指定的圖案中。
實際上,我們會將三角形頂點排列成 2 個指定的圖案,如下圖所示。因此,我們透過對頂點進行排序來形成三角形。
排序有 3 個目標:
- 逆時針
- V0 的 y 值最小
- 識別出哪個頂點的 y 值居中
相比MSOC,我們需要完成的目標更多(“將頂點設定為逆時針方向”,這是一個額外的需求),但我們透過最終使用比 MSOC更少的指令數完成了所有的目標,只用到了 22 條SIMD 指令。這其中的關鍵點在於,要意識到這個額外的目標事實上使我們放開了手腳,因為不必保留原始頂點順序。
程式碼如下所示。我們透過簡單比較找到頂點v0,計算出與其相鄰的兩個點v1和v2的向量,對這兩個向量進行叉積運算,最後將v1和v2按照順時針的方向排序。
2.5 光柵化階段(Rasterization Phase)
接下來是光柵化階段,也是最終的處理階段。在這個階段,我們將螢幕的楔形光柵化,儲存到深度緩衝區中。其中包括兩個步驟:覆蓋檢測和深度更新。
與 MSOC 一樣,我們在覆蓋檢測部分使用了“邊緣填充光柵化”演算法。主要原理是是為每一行找到最左邊和最右邊的畫素,接著填充之間所有的畫素。
接著我們要更新深度緩衝區中的深度值。由於前面的排序,深度值的更新非常簡單,即當子圖塊被完全覆蓋的時候,記錄當前的深度值。
現在我們已經完成了遮擋物的部分,獲得了深度緩衝區,接著再處理被遮擋物。
在這部分中,我們將被遮擋物的 AABB 投影到螢幕空間中,執行簡單的碰撞測試,得到早期剔除結果,再透過深度測試查詢可見性。
這個方法很簡單——也就意味著很快:如果投影面積太小,則被遮擋物將不可見。如果離相機過近,那麼被遮擋物將是可見的。實際上,我們可以跳過大約 50% 的被遮擋物查詢。
這是我們的輕量級軟體遮擋剔除演算法的效能資料。在對遮擋物光柵化時,每個三角形只需花費 0.4 微秒。 對遮擋物進行可見性查詢,每個被遮擋物花費 0.56 微秒。 還可以看出,“Early Out”有效地降低了可見性查詢的成本,繞過了幾乎 50% 的被遮擋物。
三、最佳化剔除管線(Optimized Culling Pipeline)
接下來到剔除管線的部分。
剔除管線需要組合多個剔除模組,以及所有相關資料的組織,包括遮擋物和被遮擋物,並輸出報告結果。
下圖是一個典型的剔除管線實現方法。
因為過於簡單明瞭,幾乎很少人關注它在電腦和主機平臺上的應用情況。
然而在移動平臺上,我們發現管線本身的消耗很大。在舊版本的引擎中,渲染5000 個被遮擋物需要 1.5ms 的時間,留給 SOC 的空間很小。因此,我們需要加速渲染管線。
3.1 降低快取丟失率
3.1.1 陣列遍歷
首先,我們考慮如何有效地判斷被遮擋物。
有兩個選擇:直接使用普通的陣列來管理場景中的物體,或者,空間加速結構。
陣列
如果我們選擇使用陣列,策略非常簡單:遍歷所有被遮擋物來進行判斷。這個場景中有5個遮擋物,每次Cache Fetch可以儲存3個物體到快取,則可以看到,需要進行5次可見性查詢和2次Cache Fetch。
空間加速結構
而如果使用空間加速結構,例如 邊界體層次結構 (Bounding Volume Hierarchy (BHV))或 八叉樹(Octree)等方法,就能減少查詢次數。
如下圖,我們新增 2個父級包圍體積( Parent Bounding Volume(PBV)),V1和V2,一旦被判斷為不可見,比如V2,它的所有子級包圍體積,C,D,E,都可被設為不可見,無需進一步測試。因此,我們可以減少 1 次可見性查詢。但此時,我們訪問記憶體的方式,相比陣列來說,更加的隨機。在這種情況下,我們需要3次Cache Fetch,比使用陣列多一次。
那麼你可能會好奇,如果將陣列中的子項和父項放在一起,是否可以減少快取預取的效能消耗呢?實際上,該方案下記憶體訪問將是線性的,確實可以減少消耗。但是此時維護加速結構的開銷會顯著增加,考慮到被遮擋物移動的情況下,會需要我們去做更多的工作來維持結構。
兩者對比
結合我們的實際情況,在我們的解決方案中,剔除查詢的消耗較低,而且遊戲中的被遮擋物數量不是那麼大,因此跳過的查詢次數,無法抵消快取未命中的消耗,更不用說維護加速結構的額外成本了。所以,使用陣列顯然是更好的選擇。
由於快取行(cache line) 的大小通常是固定的。資料結構越小,Cache Fetch次數越少。所以,我們希望最小化被遮擋區的資料大小。我們在資料結構中僅儲存了相關的資料,並使用盡可能低的資料精度。此外,我們使用位域來最小化布林值和列舉值 。
透過對資料進行組織和最佳化,我們獲得了約0.3毫秒的時間效能提升。
3.1.2 函式呼叫
下一個要解決的問題是函式呼叫時出現的快取未命中。
由於虛擬函式呼叫導致快取未命中(cache miss)時,會產生顯著消耗。
我們先來看看這些快取未命中是如何發生的。當呼叫一個普通函式時,CPU 處理器需要載入該函式的指令。如果指令沒有儲存在指令快取中,就會發生快取未命中。
呼叫虛擬函式時,處理器首先需要載入虛表(vtable)指標,然後載入vtable指標引用的函式地址。而載入地址時,有可能會發生快取未命中。因此最糟糕的情況就是在呼叫函式時,多了兩次快取未命中。
此外,使用虛擬函式時,CPU很難預測下一條指令,導致指令執行效率降低。
因此,我們決定刪掉所有的虛擬函式呼叫,簡化操作,解決快取未命中問題。
這個修改本身並不複雜,但我也有一些小小的建議,來使你更快的完成這類修改:你可以將資料和指令分開存放,這樣可以方便地訪問資料,也可以透過函式實現物件導向的指令部分;其次,還可以使用不同的陣列來儲存不同型別的遮擋物資訊,並呼叫特定的通知函式。
透過上述方式,我們獲得了 0.5ms 的效能提升。
3.1.3 可見性過濾
下一個問題是可見性過濾器。
什麼是可見性過濾器呢?在一個渲染管線中,一個被遮擋物需要經過多次檢測,才能判斷為可見。這個過程是多個過濾器的級聯,包括邏輯設定、視體剔除,以及軟體演算法執行遮擋剔除等等。
這個過濾器有兩種常見的實現方式。第一種方法是標記可見性。已標記為不可見的遮擋物將跳過後續測試。第二種方法是使用一個額外的陣列來儲存過濾後的遮擋物。
第一種方法的缺點是不可見的遮擋物佔用了快取行。而第二個方法則會導致不必要的資料複製消耗。
那麼哪個方案更好呢?取決於實際情況。
在我們的專案中,大約 80% 的物件會被視體剔除過濾掉,因此複製操作並不頻繁。在這種情況下,我們發現使用“過濾”方法效率更高,可節約0.05ms。
3.2 不要光柵化低遮擋的三角形
讓我們根據上面這張圖來分析可見部分。上圖中,你可以看到,這個遮擋物,不僅隱藏了被遮擋物,還隱藏了一部分自身——我們稱之為自遮擋(Self Occlusion)。此外,平行於觀察方向的部分幾乎沒有遮擋效力。
對場景來說,左邊的遮擋物和右邊的遮擋物具有相同的遮擋效果,但右邊的面數顯著低於左邊,因此對左邊這個遮擋物進行完整的光柵化,算對算力的浪費。
為了解決這個問題,我們將遮擋網格分成多個部分,只光柵化有效部分,也就是我們說的“可見部分”。
可見部分資訊是離線渲染的。我們先給定一個遮擋網格,根據三角形的法線和位置對三角形進行聚類,將網格分成幾個部分。然後我們從不同的角度拍攝快照,並計算每個部分的可見尺寸大小。
在這張圖中,有一個部分只是一個線段,或者說是平面。
綠線和紅線表示不同的觀察方向。
0表示在這個檢視中方向,完全不可見,可能是被其他部分遮擋了。
這裡的 1 是指投影后,這個部分在視線中佔1平方米大小。這裡的單位是平方米。
透過彙總所有資訊,每個部分都可以用一個視錐體來描述其可見範圍。假設我將閾值設為1,視錐體的大小和形狀應該能夠包含所有可見尺寸等於或大於1平方米的物體。所以我們得到如圖所示的錐體。
在執行時,會使用視錐體來進行可見性測試。如果在執行時,視角方向在可見錐體之外,那麼被觀察物體的可見尺寸一定小於某個閾值,這部分將被視為不可見。
而我們只對那些可見的部分進行光柵化——使用我們的輕量級軟體遮擋剔除演算法。在實踐中,大約 50% 的遮擋三角形可以跳過光柵化過程,並且不會對渲染效果產生嚴重影響。效能資料顯示如下,可以看到需要光柵化的三角形數量從1539降到了874。
3.3 多執行緒渲染
接下來要解決的是如何在移動平臺上使用多執行緒。
目前,我們的大多數手機都是採用 ARMbig.LITTLE 架構,是一種異構處理器架構。該架構結合了兩種不同的處理器核心,一種是高效能的大核,另一種是效能弱但耗能低的小核心。由於大核心已經被邏輯執行緒、渲染執行緒佔用,所以作業執行緒不得不跑在小核心上,速度慢很多,另外執行緒排程的消耗也很大。
因此,我們必須謹慎選擇傳送給作業執行緒的任務,該任務在工作執行緒上執行的時間與主執行緒上的任務執行時間相匹配,並且足夠大,以抵消排程消耗。
最後我們發現“可見截面選取”和“楔形收集階段”這兩個任務完美地滿足了傳送給作業執行緒需求,它們可以和視錐遮擋同時執行。這麼做可以減少0.25ms 的時間。
這裡展示了剔除管線的資料。可以看到經過最佳化後,渲染時間得到了大幅減少,特別是在iPhone6s上,時間成本從 1.5ms 減少到大約了0.4毫秒。
四、用於生成高質量遮擋網格的離線生成器(High-quality Occlusion Mesh Generator)
有了一個高效的剔除管線後,接下來我們要為其提供高質量的遮擋體。因此需要打造用於生成高質量遮擋網格的離線生成器。
為什麼遮擋網格如此重要?
首先,為了在低端手機上,為了實現在 2ms內完成整個剔除渲染過程,我們需要將遮擋網格的三角形面數設定為4000,這是一個極低的面數要求。與此同時,還要保證遮擋網格是高質量的。否則,即使只是輕微的網格裂縫和偏差,也會導致剔除率急劇下降到35%,偶爾還會出現錯誤遮擋。
想象一下,常見場景下,畫面可能有 100 萬個三角形,而被遮擋物的三角形數量可能達到 10 萬個。然而,我們只需要渲染4000個三角形,可以實現同樣的遮擋效果。
聽起來似乎是不可能完成的任務,但是我們做到了。
先簡要介紹一下我們的網格生成器。
我們首先透過體素化和填充來對模型進行平滑處理,接著從中過濾出有效的聚類,再將每個聚類中的資料或物件轉換為三角形網格來進行渲染。
由於體素化模型不適合表示斜坡或曲面,我們將原始遮擋物按法向方向切割成子模型,再分別進行處理,最後再合併所有子模型的結果。
4.1 平滑過程(Smoothing)
在生成高質量的簡化遮擋網格的過程中,我們遇到了很多挑戰。在這其中,最難的一點,就是如何去平滑一個3D Mesh。我們嘗試了很多方法,最終找出了這個解決方案。
平滑Mesh
如下圖所示,我們的想法是儘可能多地填充體素化模型,得到平滑的Mesh,接著進行提取。
而問題是,如何在確保進行平滑操作後,還不會產生錯誤遮擋。
我們需要用數學公式來描述“無錯誤遮擋”的約束條件。因此,我們提出了可視函式的概念。
可視函式
接下來是演算法部分,我儘量講的簡單點,避免大家睡著:)。
可見性函式的定義很簡單,是指從一個任意位置,沿著一條任意方向,去觀察一個任意物體,如果物體可見,那麼函式結果為 True,反之為 False。
那麼我們可以這樣描述“無錯誤遮擋”約束條件:對於任意被遮擋物,在任何位置或任意視線方向,如果在使用簡化遮擋網格時,可見性函式為假,則在使用原始網格時,可見性函式也應該為假。如果函式為真,那麼說明該物體可以被看到,但如果使用簡化網格看不到它,說明物體被錯誤遮擋了。
這個約束條件仍然很難使用,因為“任意”的條件太多了,所以我們決定透過近似來簡化這個條件。
簡化“任意位置”
首先,我們簡化“任意位置”, 將任意位置替換為“near 和 far”,其中 near 表示AABB網格的擴充套件,而“far”表示無窮大。範圍如下:
緊接著,對於“任意方向”,我們將其簡化為幾個指定的觀察方向,在實踐中,我們選擇 3 條軸和其中線:
最後,我們還要定義“任意被遮擋物”,這相對複雜一些。我們提出了“重要體素”的概念:重要體素是那些已經存在了靜態物體的地方,以及動態物體中心部分可能出現的地方。然後,我們將約束條件中的“任意被遮擋物”替換為“任意重要體素”。
下圖具象說明了什麼是重要體素。圖中有一個預設的動態物件和一個遮擋物。我們讓動態物體在遮擋物內部徘徊,而紅色的體素就是中心部分可能出現的地方,也就是重要體素。
我們將約束條件被簡化為:對於任意重要體素,在 near 和 far 的位置,沿任何指定的方向觀察,如使用簡化遮擋網格時,可見性函式為假,則使用原始網格時,可見性函式也必須為假。
透過約束(Constraint)平滑Mesh
有了約束(Constraint)後,我們的平滑過程就很清晰了。如流程圖所示,我們需要遍歷所有空的體素,嘗試進行填充,並檢查約束是否仍然成立。
但是這個遍歷過程很耗時,所以在實踐中,我們使用啟發式的方法,再透過約束來判斷可填充的體素。
這個方法的思路是找到 2 個彼此不可見的重要體素,則它們之間的體素一定是可以安全填充位置,因為它們不會干擾重要的體素。這種啟發式方法要快得多,結果也可以接受。
以下圖為例,Voxel SA 和 SB 位於重要體素 IA 和 IB 之間,而 IA 和 IB 看不到對方。所以SA 和 SB 是安全體素。
這樣一來平滑過程就變得簡單。在設定可見性函式約束後,我們得出所有安全體素,進行填充,並得到填充的體素網格。
4.2 篩選聚類(Filtering)
接著,我們需要提取它的有效遮擋部分。我們首先沿著其中一個軸對體素網格進行切片。然後對於每個切片,我們將相連的體素劃分為簇,計算簇與簇之間的遮擋關係。然後我們選擇遮擋關係高於給定閾值的簇。
理論上講,一個簇的遮擋關係可以透過下面的公式來計算得到,但是這個過程很慢。因此我們最終沒有使用這個方案,這就裡不再詳細說明這個公式。
我們同樣提出了一種啟發式方法。我們首先找到滿足這樣條件的重要體素:當它被原始模型包圍時,可見性函式的返回值為假,當它被所有當前選擇的簇包圍時,其可見性函式的返回值為真。接著標記可以遮擋這些重要體素的簇,將標記的數量作為遮擋資訊的近似值。
在實踐中,這種啟發式方法與基準真相( ground truth) 相比,計算速度快10 倍,並且結果也很棒。
4.3 轉化為三角形(Triangulating)
而選中簇之後,接下來就是將它們轉換成三角形。我們使用了常見的做法。
首先,我們先提取出邊界資訊,並將孔洞與邊界相連線,從而將每個簇轉換為邊緣迴圈。
然後,我們透過經典的道格拉斯-普克演算法(Ramer-Douglas-Peucker)演算法沿邊緣曲線迴圈簡化。
之後,我們使用經典的切耳法(Ear Clipping)演算法對多邊形進行三角剖分,從而得到結果。
對於方形遮擋物,這樣處理後的結果已經可以接受。但是我們還有許多斜面和曲面的遮擋物。
解決這個問題的思路很簡單。我們從原始模型中分離出斜坡和曲面,形成一個獨立的子模型。用前面的提到方法簡化它們,接著將所有部分的結果結合起來,並將它們之間的間隙焊接起來,得到最終的遮擋網格。
現在我們得到最終的結果,就是一個簡化的高質量遮擋網格,如下面的案例。
這座教堂有 75240個三角形,而簡化的遮擋網格使用了不到900個三角形。你可以看到凹凸不平的牆壁被平滑了,塔尖被自動移除,因為塔尖基本不具備遮擋效果。
而這裡的圓形體育場,原來有大約 10k 個三角形,簡化後只剩下 900 個三角形。可以看到到生成器對於曲面非常有效。
我們的生成器生成的最終簡化遮擋網格大約是原始三角形數量的5%,並且不會產生任何錯誤遮擋效果。
總結
下表顯示了從高階到低端的不同機型的效能資料,可以看到我們在2ms 內剔除了 65% 的物件。
以Iphone 6s為例,我們的 SOC 演算法可以在1ms 內減少 65% 的繪製呼叫,而不會發生任何錯誤遮擋。對於整個剔除解決方案,時間是1.5ms,總剔除率約為 91%。
針對移動平臺的高效軟體遮擋剔除解決方案,需要綜合考慮各個方面。如:
- 使用輕量級 SOC 演算法來適配移動平臺。
- 構建快取友好和多執行緒執行的剔除管線。
- 用於高質量的遮擋網格的生成器。生成器可以基於可見性函式來製作。
這就是我們在《明日之後》中所實現的解決方案,這不是一項簡單的任務。沒有我們出色的開發團隊,就無法實現這套解決方案。感謝大家傾聽!
相關文章
- 【GDC2023乾貨分享】開源軟體在引擎開發中的幫助
- powerpoint: 遮擋文字
- 【GDC2023乾貨分享】創新的使用者獲取模式——直播引流探索模式
- scrollIntoView與鍵盤遮擋View
- 雲上的移動效能測試平臺
- flutter dialog中軟鍵盤遮擋解決衝突Flutter
- 移動跨平臺技術方案總結
- Cypress 踩坑記 - DOM 遮擋
- 直播平臺開發乾貨分享——標準直播及快、慢直播的特性
- 從 React Native 到 Flutter,移動跨平臺方案的真相React NativeFlutter
- 自動曝光在移動平臺上的實現方案——以《使命召喚手遊》為例
- 直播平臺原始碼,關於彈出框中輸入框被遮擋問題解決原始碼
- AI客服上線 乾貨 乾貨 全是乾貨!AI
- 淺談 2018 移動端跨平臺開發方案
- 解決虛擬按鍵遮擋popupWindow
- 關於直播平臺開發中流媒體傳輸,重點乾貨分享
- 乾貨分享丨攜程國際業務動態實時標籤處理平臺實踐
- 乾貨 | 蘇寧影片雲跨平臺播放器開發方案詳解播放器
- 【乾貨分享】可能是東半球最全的.NETCore跨平臺微服務學習資源NetCore微服務
- 迷你臺-最好的線上體育分享平臺
- 光纖KVM方案乾貨,秒懂視覺化KVM坐席協作視覺化
- 手自一體化的移動雲測試平臺建設方案
- 最火移動端跨平臺方案盤點:ReactNative、weex、FlutterReactFlutter
- 移動跨平臺方案對比:WEEX、React Native、Flutter和PWAReact NativeFlutter
- 乾貨分享——連結被微信停止訪問的解決方案
- 乾貨 | 移動安全管理多場景全解析
- 軟光柵-uraster程式碼閱讀(入門極品)AST
- 乾貨分享|GBase 8a叢集雙活容災方案
- 上乾貨!大廠面試走心經驗分享!面試
- Esfog_UnityShader教程_遮擋描邊(原理篇)Unity
- MySql乾貨分享之索引MySql索引
- 告訴你製作直播平臺都需要什麼硬體和軟體的乾貨文
- 乾貨分享:開啟PWM調光之門,一起來做呼吸燈
- 乾貨分享:Air780E軟體指南:字串處理AI字串
- 【乾貨分享】軟體Bug和缺陷有什麼區別?
- 乾貨 | 愛奇藝全鏈路自動化監控平臺的探索與實踐
- 乾貨分享!JAVA診斷工具Arthas在Rainbond上實踐~JavaAI
- 乾貨分享 | PCB測試點的用途