一. 寫在之前
目前正在製作一個3D體素類的項目,大量的模型帶有大量面數的模型在Unity中直接跑不動,FPS在15以下,考慮到一個場景中不僅有一個模型需要顯示,想要提高遊戲流暢度,必須對這些模型的網格面數進行深度優化,至少優化到5000面以下。先在網上調研了很多的資料,發現了一個確實很實用,但是不那麼容易理解的優化算法----貪心網格規劃算法(GreedyMesh,翻譯過來的)。
二. Meshing
《我的世界》這種現象級遊戲,最爲創新的點之一就是遊戲使用大量的多邊形渲染空間體積。使用多邊形的主要挑戰是弄清楚如何有效地將體素轉換爲多邊形。此過程稱爲網格劃分。本文章會提供三種方法劃分網格。在此之前,我們首先需要明白什麼是“網格規劃”,什麼纔是好的網格。
多邊形的渲染大致分爲兩部分:1.)圖元裝配(Primitive Assembly),以及2.)掃描線填充(也稱爲多邊形填充)。通常情況下,沒有任何方法可以更改需要繪製的像素/片段的數量;因此,出於渲染的目的,我們可以將其視爲固定成本,這樣一來,原始裝配操作的成本嚴格地取決於網格中面和頂點的數量。而降低成本的唯一方法是減少網格中多邊形的總數,因此我們對Minecraft網格可以得出下面的結論:網格數量越小越好。
當然,上面的分析還遠遠不夠,雖然更新網格塊的頻率不高,然而一旦需要更新網格塊的時候,我們還是希望能快速的更新完,用戶是不願意看到形狀更新要等上兩幀或以上,所以我們可以得出評估網格算法好壞的第二個標準:計算網格的耗時不能過高。
對於以上兩個標準,有一個好的解決方案:對於一個很複雜的網格,我們可以先不對其進行規劃,而是標記,然後在另外一個線程中的某個時間將其進行網格化算法處理,一旦處理成功,就可以將那個複雜的低質量網格替換成這個規劃後的網格。在理論上完美的解決了上述的問題。
網格規劃的一些算法:
上述的兩個標準中,其中第一個標準最爲重要,因此,我們將網格規劃算法的重點放在其輸出的多邊形上的質量上。
1. 笨辦法(StupidMethod)
當然這是最笨的也是最直接的辦法,體素與體素依次迭代,併爲每個實體體素生成一個6面立方體。如下圖所示:
該方法是線形複雜度,同樣的,使用的四邊形數量爲總體素數量的6倍,如果是 8*8*8的網格塊,那麼總的四邊形數 8*8*8*6 = 3072,這樣的結果明顯是很不合理的,大量的四邊形是重複冗餘的。嚴格來說這不算個方法,就是就是所有的體素壘起來,不做任何網格優化的結果,顯然不符合我們的要求。
2. 剔除法(Culling)
該方法是剔除了網格內部沒有用的表面,只留下表面的四邊形,如下圖所示:
很明顯,該算法的網格規劃最後生成8 * 8 * 6 = 384的四邊形數,是原來數量的1/8,但從本質上來說,這兩個方法是一樣的。
3. 貪心算法
該算法將相鄰的四邊形合併到更大的區域中,以減小几何的總大小。例如,我們可以嘗試通過將每邊的所有面融合在一起來對上一個立方體進行網格劃分:
這樣得到的網格只有6個四邊形(在這種情況下,實際上也是最佳算法!)顯然,這種算法的計算結果又是第二種算法的1/64。該算法有兩個創新點,首先,僅考慮爲某些2D橫截面生成四邊形網格的問題就足夠了。也就是說,我們可以在每個方向上一次掃過網格體,然後分別對每個橫截面進行網格劃分:
這樣可以將3D問題簡化爲2D。第二步是如何對每個2D切片進行網格劃分的問題。這(即最少的四邊形)是貪婪算法最核心也最難的地方。爲了容易理解,我們換一種方式表述這個問題,將在所有可能的四邊形的集合上定義某種類型的總階,然後選擇該集合的最小元素作爲網格。爲此,我們將首先在每個四邊形上定義一個順序,然後將其擴展到所有網格的集合上的順序。一種簡單的排序兩個四邊形的方法是將它們從上到下,從左到右排序,然後按它們的長度進行比較。更具體地,使用元組(x,y,w,h)表示一個四邊形,其中(x,y)是左上角,(w,h)是四邊形的寬度/高度,我們可以通過以下方式表示四邊形:
所謂二維四邊形網格,是指將某些集合S劃分爲四邊形Qi的集合。現在,在給定(x0,y0,w0,h0),(x1,y1,w1,h1)的情況下,我們定義這些四邊形的總階數:
或者,用僞代碼的方式,我們可以這樣寫:
接下來我們要做的是,將四邊形上的排序擴展爲網格上的排序,一種非常簡單的方式是,給定兩個四邊形的排序序列(q0,q1,...),(p0,p1,...),這樣qi \ leq q {i + 1}和pi \ leq p {i + 1},我們可以只需按字典順序比較它們即可。同樣,此新排序實際上是總大小,這意味着它有一個最小的元素。在貪婪網格劃分中,以這個最小元素作爲初始網格,這就是貪婪網格劃分算法。如下圖,是這些字典最小的網格之一的示例:
你可能會問,這種新順序找到最少的元素總是會產生更好的四邊形網格。例如,不能保證此新順序中的最少元素也是具有最少四邊形的網格。考慮使用以下方法對T形網格進行四邊形網格規劃時的情況:
此外,我們可以說,貪婪網格規劃算法中的每個四邊形始終完全覆蓋該區域周邊上的至少一個邊緣,這始終是正確的。因此,我們可以證明以下幾點:貪婪網格中的四邊形數量嚴格小於該區域的邊緣中的邊數量。
這意味着在最壞的情況下,我們應該期望貪婪網格的大小與邊的數量大致成比例。通過啓發式/尺寸分析,可以合理地估計出貪婪網格通常比球形幾何形狀(其中n是球體的半徑)小的網格大約小O(\ sqrt(n))倍。但是我們可以更加精確地瞭解貪婪網格的實際質量:貪婪網格規劃後的四邊形不超過最佳網格的8倍。
這就是最佳因素!爲了證明這一點,我們需要先引入一些術語。如果順時針繞網格旋轉時向右轉,我們將在該區域的外圍上稱爲頂點,否則,將其稱爲凹入。這是一張圖片,凸頂點爲紅色,凹頂點爲綠色:
關於這些數字有一個明顯的規律:對於任何在周長上具有V+凸頂點和V-凹頂點的簡單連接區域,V +V_ = 4。
推導:具有E邊周長的任何已連接屬G區域至少需要個四邊形進行網格劃分。
最後,代碼實現。(後面附有源碼)
由於計算量較大,使用Unity的ThreadedJob,具體構造如下:
每一個線程是一個網格生成器 GreedyMeshGenerator,本算法需要三個網格生成器,前後方向、左右方向、上下方向分別切片之後,對這三個方向的切片結果進行計算,是算法邏輯真正實現的地方。
- 初始化網格生成器:首先過濾到該切片上,那些不在邊緣上的、被其他的網格片遮擋住的點,然後根據這個點的顏色,將其分類存儲到一個字典中,字典的key是顏色,value是相同顏色的所有點集合List。後期的所有計算都是圍繞這個字典進行。
- 對每個方向上的切片進行網格規劃計算,以水平方向切片爲例,假設水平方向是xy平面,逐點判斷能否作爲邊緣點成爲新網格的頂點。具體詳見代碼。這裏需要注意的是,水平方向上,判斷某點的標準是垂直水平面方向上,上下各1單位的點是否落在整個網格的內部還是外部。
- 符合所有上述條件的頂點會添加到新的網格中,該網格在迭代中不斷的擴大。
引用文章來源:https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/
如果有問題,歡迎郵箱聯繫,共同學習探討。感謝
email:[email protected]