圖形引擎實戰(zhàn):一種大世界巨量植被管理與渲染方案

Unity目前在大世界開發(fā)中提供的相關(guān)解決方案甚少,大部分開發(fā)團隊都需要根據(jù)自己項目需求重新定制一套大世界管理和渲染方案,在這之間,大世界中的巨量植被渲染是需要解決的要點之一。本文基于這一需求,結(jié)合筆者的項目經(jīng)驗,簡要介紹一種比較簡單的在大世界場景中管理巨量植被對象和繪制的方案,供大家參考。
1.核心思路
本文主要探討大世界中的植被管理,一般來說,能稱為“大世界”的場景,其面積基本都在十幾公里*十幾公里以上,在這種環(huán)境中,美術(shù)種植的植被數(shù)量甚至可能會達到上千萬單位的程度。因此在介紹本方案的核心思路之前,我們先總結(jié)一下如果直接使用Unity 默認的GameObject方式管理植被會有哪些問題。
場景GameObject至少包括Transform、MeshFilter、MeshRenderer三個組件,序列化屬性較多,通過這種方式序列化巨量的植被,對包體、運行內(nèi)存的影響非常大;
即使通過大世界流式加載的方式管理植被對象,在植被密度大的地方,也可能會有短時間內(nèi)需要同時實例化或卸載大批量植被的情況,導致幀率不穩(wěn);
在野外等植被密集的地方,同一時刻場景中可能存在數(shù)萬單位植被,Unity對這么大數(shù)量的GameObject的管理、視錐裁剪、Lod計算會占用大量的cpu時間,拉低幀率。
… …
鑒于上述問題,本文所討論的植被方案其主要的核心在于不使用GameObject實例化植被,而是自己管理植被相關(guān)數(shù)據(jù),并向GPU提交適當?shù)睦L制請求。該方案的核心思路為:
對大世界中的植被根據(jù)合適的范圍大小進行分塊,每一塊中的植被被分類合并單獨保存為序列化數(shù)據(jù),游戲中流式管理這些分塊數(shù)據(jù)對象的加載和卸載;
對于合適的硬件,植被系統(tǒng)使用ComputeShader對當前場景中存在的植被數(shù)據(jù)進行裁剪和Lod計算,然后繪制通過裁剪的植被數(shù)據(jù);
如果硬件不支持使用GPU剔除,則通過四叉樹結(jié)構(gòu)對植被分塊數(shù)據(jù)進行裁剪和Lod計算,然后繪制通過裁剪的植被數(shù)據(jù)。
2.分塊管理
2.1植被分塊
植被數(shù)據(jù)的分塊可以根據(jù)大世界加載范圍和視距確定一個合適的范圍大小,所有處于該范圍的植被數(shù)據(jù)被保存在同一個序列化文件中。例如,如下如所示,以200m*200m分塊,每一塊中囊括一定量的植被數(shù)據(jù),圖中的綠色Box代表分塊中一種序列化植被數(shù)據(jù)所形成的AABB包圍盒,其中有的包圍盒可能大于200m,是因為有的分塊中植被數(shù)量很少或者占空間很小,因此被合并到臨近的分塊數(shù)據(jù)中。

下圖是對保存的分塊植被數(shù)據(jù)進行預(yù)覽,其中每一根線都代表一顆植被。

2.2數(shù)據(jù)序列化
為了讓保存出的分塊植被數(shù)據(jù)盡可能小,本方案僅保存渲染植被必要的一些數(shù)據(jù),單位植被的數(shù)據(jù)結(jié)構(gòu)為:

其中,_colorR、_colorG、_colorB用來保存植被的附加顏色屬性。
對于分塊數(shù)據(jù)中的一種特定植被,通過VegtationPrefab定義,其中,VegtationLod記錄該植被的lod層級對應(yīng)的mesh、material等相關(guān)資源。


經(jīng)過保存的植被數(shù)據(jù)在編輯器Inspector中,如下圖所示:

一個植被數(shù)據(jù)綁定在一個確定位置和覆蓋范圍的GameObject上,與大世界中其他物體一樣被管理,在運行過程中,大世界加載系統(tǒng)根據(jù)需要加載位于主角附近的植被區(qū)塊數(shù)據(jù),然后植被系統(tǒng)使用這些數(shù)據(jù)進行必要的裁剪工作,并組織渲染。
2.3運行時數(shù)據(jù)
由于壓縮序列化文件體積的需要,本方案并沒有直接保存每一單位植被的Local2World矩陣,因此當植被數(shù)據(jù)被加載上來時,需要將保存的數(shù)據(jù)轉(zhuǎn)為Local2World矩陣,同時,為了保證向GPU傳遞的數(shù)據(jù)盡可能小,可以將顏色等附加信息編碼進Local2World矩陣的第四行中。


上圖中PackColor32函數(shù)的目的是將Color32顏色信息使用float保存。
在shader中,利用上述矩陣,組織渲染必要的其他數(shù)據(jù)。


同時,為了方便運行時合并多個植被數(shù)據(jù)中的同種類植被,這里也使用各級lod屬性在運行時的實例ID組合來作為VegtationPrefab唯一性標志,前文RuntimeInit函數(shù)中m_runtimeMapID就是做這個事情的。
3.GPU驅(qū)動裁剪與渲染
在實機運行起來時,需要根據(jù)設(shè)備情況選擇使用GPU(ComputeShader)還是CPU來驅(qū)動剪裁。

3.1運行時合并數(shù)據(jù)
如果使用ComputeShader進行植被裁剪,已經(jīng)加載到場景的植被數(shù)據(jù)會根據(jù)上文所述唯一的m_runtimeMapID合并到VegtationPrefabRuntime類中。VegtationPrefabRuntime類代表運行時的一種植被類型,裁剪和渲染相關(guān)的具體工作都在該類中進行處理。

3.2裁剪與LOD計算
無論使用ComputeShader還是CPU對植被數(shù)據(jù)進行裁剪,都需要通過當前幀視錐信息計算6個裁剪平面。

除此之外,還需要向ComputeShader傳遞一些其他的必要裁剪屬性,例如渲染距離,以及HiZ屬性等。

一切都準備好之后,就可以對所有種類的植被進行裁剪計算了。


上面的代碼中,會先對植被數(shù)據(jù)進行一次包圍盒和視錐的相交計算,如果無相交,則不需要處理。然后,調(diào)用ComputerShader進行裁剪計算,并對通過裁剪的植被單元填充到ComputeBufferType.Append類型的ComputeBuffer中,然后使用DrawMeshInstancedIndirect API對通過裁剪的植被進行繪制。

在ComputerShader中每一個植被的矩陣數(shù)據(jù)要經(jīng)歷這樣的計算過程:
根據(jù)植被到攝像機的距離確定要使用的包圍盒參數(shù)和目標Buffer
使用裁剪平面數(shù)據(jù)判斷植被是否處于視野中

3.使用HiZ判斷植被是否被其他物體遮擋

4.若前三者都通過則填充到對應(yīng)的目標Buffer中,供Instancing繪制使用。
經(jīng)過上述視錐裁剪、HiZ剔除、以及LOD計算,可以大大降低最終被渲染的植被數(shù)量。如下圖所示,每一幀只渲染處于視錐體之中,并且沒有被攝像機前的物體遮擋的植被單元。

CPU驅(qū)動裁剪與繪制
在不能使用ComputShader進行裁剪的情況下,最方便的方式是將每一分塊的植被數(shù)據(jù)構(gòu)造四叉樹結(jié)構(gòu),每一個四叉樹節(jié)點保存處于節(jié)點中植被數(shù)據(jù)所形成的AABB包圍盒,運行中每幀對四叉樹使用GeometryUtility.TestPlanesAABB API不斷向下進行裁剪計算,對處于視野中的葉節(jié)點根據(jù)包圍盒中心到攝像機距離確定應(yīng)該使用哪一級Lod進行渲染,最終根據(jù)植被種類合并到對應(yīng)的VegtationPrefabRuntime_CpuCull實例中進行渲染。由于Graphics.DrawMeshInstanced API有一次繪制1023個單位的限制,因此這里使用RenderBatch類專門進行處理。

由于四叉樹的構(gòu)建有較大的性能消耗,因此最好將四叉樹跟植被數(shù)據(jù)一樣提前生成好,并保存在導出的植被序列化文件中(VegtationPrefab類中的m_treeRoot)。下圖中每一個綠色框為離線生成好的四叉樹的葉節(jié)點,其中的數(shù)字為葉節(jié)點中包含的植被單元數(shù)量。


這種方式相比ComputeShader,最終渲染的植被數(shù)量會稍微多一點,同時,植被顯隱和切換是一塊一塊的,因此視覺效果和性能都會稍差一點,但是兼容性和穩(wěn)定性更好。
歡迎加入我們!
感興趣的同學可以投遞簡歷至:CYouEngine@cyou-inc.com