Unity大場景資料載入及優化方案
前段時間,有幾個虛擬模擬公司跟我請教關於大地形的載入優化問題,它們使用的引擎都是自己研發的,引擎對於開發者來說,大同小異,它們的基本構造是一樣的,關鍵是在於解決問題的方法,正是基於這個前提寫了這個課程,希望給讀者提供一些解決問題的思路。
目前,大地形動態載入已經成為當前遊戲開發或者虛擬模擬領域必須要解決的問題,尤其在虛擬軍事模擬領域,由於要涉及到大兵團虛擬演練作戰,這對模擬真實性要求比較高,需要根據真實地形資料將其在終端硬體裝置上繪製出來,或者是通過美術人員利用建模工具製作大地形,比較流行的做法是利用航拍拍攝城市或者山區地形生成高度圖,利用現有的技術按照一定的比例還原城市和山區地形原貌,生成的大地形大部分是上千公里區域,面對這麼龐大的資料,由於硬體的限制,需要程式做對地形的載入做一些優化操作,以解決執行效率問題。
該課程主要是提供了大地形動態資料的載入及優化方案,因為在虛擬模擬以及遊戲開發中都會涉及到海量資料的載入,由於記憶體的限制,不可能直接將大地形一次性的載入到記憶體中,即使硬體滿足了需求,除了地形還有其他道具的載入,這樣會對記憶體帶來嚴重的負載問題。解決大地形載入方案:首先想到的做法是將大地形進行分塊載入,單單的分塊載入並不能完全解決實際問題,比如在飛行模擬中,由於角色飛行速度快,會導致載入地形塊不完整的情況出現,當然也會出現程式執行卡頓情況,還有一種情況,角色在場景中做移動操作,如果角色在塊與塊邊界附近不停的旋轉視角或者是來回移動,這樣也會導致地形頻繁的載入解除安裝,同樣會出現地形載入問題,體驗非常不好。另外,大地形上面的建築物非常密集,它們也會隨地形載入到記憶體中,這些建築物也要進行分塊以及遮擋處理的;除了地形和建築物模型資料,貼圖的載入和渲染也是需要解決的問題。大地形以及建築物二者都會涉及到大量貼圖的載入,貼圖的載入也會吃掉大量記憶體的,如何把這麼多貼圖載入到記憶體中?以上種種問題,該課程給出瞭解決問題的答案。下面我們把本篇課程涉及的主要技術給讀者介紹一下,後續章節會詳細給讀者講解,下面先從大地形資料載入方案說起。
大地形資料載入方案
大地形載入考慮到現有的記憶體機制, 不可能一次性將其加入到記憶體中,這個問題是顯而易見的,其實在遊戲開發中經常遇到,比如我們常見的進度條,載入進度條的目的就是等待程式載入場景,進度條只是一個蒙板遮罩而已。大地形的載入,別無他法,只能用分塊,這個是大方向,因此作為程式來說,要做的事情是如何分塊?塊的大小是多少?這些具體的問題我們要根據需求劃分,比如飛行模擬器塊大小可能就要大一些,因為俯瞰的視角比較大,場景漫遊塊可以小一些等等,下面我們就以遊戲的經典之作——魔獸世界地形載入方案為例給讀者先介紹一下它的實現原理,魔獸世界這款遊戲實現的就是無縫地圖的拼接,所以非常具有參考價值,先看下圖所示:
魔獸世界是如何實現無限地圖的?其實它也是很多的場景塊拼接而成的,我們通過編輯器分析魔獸世界的地形塊的大小劃分,魔獸世界場景我們稱為MapWorld是由一系列MapTile組成,這些MapTile的大小是1600/3 ≈ 533.33m,而每個MapTile又是由 16x16 個MapChunk組成,由此可以計算出每個MapChunk≈33.33m。再就是每個MapChunk又由9x9+8x8個地形頂點高度,法線,若干貼圖層(一般為4層)組成的地表紋理。魔獸世界地形的大小,在這裡我們就不討論了,但它劃分塊的思想我們是可以借鑑的。
繼續分析魔獸世界的分塊方法:它們是根據矩陣的方式進行劃分的,在XZ平面上進行的,每個塊都會包含一定的資訊資料的,比如:在XZ(3,3)位置的MapTile,每個MapTile都包含了該tile內使用的貼圖、模型例項等等。所謂模型例項也就是我們的道具,可以理解成相同模型在tile內不同擺放位置、大小、角度的資訊,它們都是被儲存在二進位制檔案中的,為了節省檔案尺寸,模式例項是通過index模型方式儲存的,同頂點索引類似,在每個MapTile裡面還有貼圖資訊比如貼圖的名字和UV資訊等等。本篇課程的分塊思維方式跟魔獸世界的類似,會在後面的章節中詳細介紹,塊分好了以後,下面就是實現原理了。
實現原理:在任何時刻,程式總是儲存著玩家所在的及其周圍的3x3個MapTile,隨著玩家的移動,這些MapTile會被動態更新,新的MapTile被載入以替換被解除安裝的舊MapTile。為了提高排程效率,魔獸引入了Cache機制,Cache中儲存著最多16個MapTile資料。需要載入新的MapTile時,首先會在Cache中查詢;解除安裝的舊MapTile也不會被立刻刪除,而是儲存在Cache中以備再次呼叫。由於一段時間內玩家的活動範圍通常不會有太大變化,這一Cache策略在應用中表現的非常出色,這是無縫地圖的基本原理。地形的動態載入解除安裝我們會使用多執行緒去實現,我們會整兩個執行緒:一個執行緒專門用於載入地形,另一個執行緒專門用於解除安裝或隱藏地形MapTile。讓我們再來回憶一下游戲的經典之作,遊戲場景效果如下所示:
本篇課程實現的方法可以使用兩種方式處理塊的載入顯示問題,一種是利用物件池的方式,預先載入分塊地形,根據視距進行檢測判斷顯示那些地塊以及隱藏那些地塊,在這裡並不刪掉它們。這樣只需要一個執行緒就可以。另一種方式是利用多執行緒,起一個執行緒專門用於移除解除安裝不在視線範圍內的地塊,這樣可以提升效率,下面介紹使用多執行緒的載入方案。多執行緒實現大地形載入方案
多執行緒在PC端遊戲中使用的比較多,比如可以起一個執行緒專門進行資源的載入,遊戲伺服器中同樣也會有多執行緒的使用,下面給讀者介紹多執行緒實現方案,多執行緒處理問題就是把所有的載入邏輯放到了新的程序中,和主執行緒做一些程序間的通訊,接受主執行緒的載入建議,做按需載入,也會自主做一些提前預載入,放進分配的記憶體,就跟魔獸世界的處理方式一樣,通過程序間的記憶體共享機制,把載入的地形資料,共享給主程序使用。主遊戲程序,永遠只要維護一個很小的記憶體即可,大量的記憶體資料,都在另一個程序中處理。這樣就可以優化大地形塊的載入,實現方式如下所示:
首先主執行緒會先載入九塊地形,主執行緒只負責維護這九塊地形,無論角色怎麼移動,角色所在的整個區域永遠是九塊地形,如上圖所示的,這九塊可以直接使用主執行緒載入到記憶體中,剩下的16塊我們通過另一個執行緒將其放到快取中,角色的位置是在已經載入好的九塊地形中間,也就是在A所在的位置。隨著角色的移動,會有新的地形塊加入進來,同時現有的地形塊會被置換出去,這樣一直顯示九塊地形,被置換的地形並不會馬上解除安裝掉,會根據角色移動情況做預判,它會等主執行緒通知,按照一定的規則進行解除安裝地塊和載入地塊。其實這種實現方式就是我們通常所說的雙快取-多執行緒技術。實現的效果如下所示:
地形分塊載入完事了後,下面就要考慮地形上面的紋理貼圖問題了,地形的貼圖資源也會佔用大的記憶體,下面介紹如何載入海量貼圖資料。
大地形海量圖片的載入方案
大地形中的場景圖片非常多,地形中的貼圖至少會有四層,這麼多貼圖我們在載入時需要考慮的,我們分塊時也需要考慮這些因素,另外場景中使用的LightMap烘培也是要考慮的問題,為了緩解記憶體壓力,我們事先會將不同塊中的地形材質以及建築物材質進行打包,先介紹如何分塊載入場景貼圖?它實現方式如下所示:
該思路就是將場景中的貼圖根據我們劃分的塊打成不同的圖集,當然也可以將兩個塊中的貼圖打成一個圖集,圖集大小對於PC端來說,最大是4096,在移動端最大是2048。這個也是為了避免記憶體頻繁的載入解除安裝會導致很多記憶體碎片,不利於後面大記憶體的分配。在打圖集之前我們需要做點事情就是需要將地形塊中的紋理貼圖與我們的打包圖集之間建立一一對應關係,方便對號入座。因為我們打包的圖集跟實際地形之間不會有任何關係,要確立二者之間的對應關係我們需要在它們中間再整一張索引檔案表格,它是連線圖集與實際地形紋理的橋樑,通過我們建的索引檔案,我們可以找到實際地形中紋理與圖集紋理之間的對應關係,我們建的索引表格是要載入到記憶體中的,而我們的圖集是根據載入任務後期才加到記憶體中的,這就要求我們的索引檔案儘可能的少,因為它們是常駐記憶體的,除了海量圖片的載入,我們還需要處理密集建築的載入。- 密集建築的載入方案
密集的建築載入,大家試想一下,如果把場景中所有的建築一次性載入到記憶體中,記憶體瞬間就會佔滿,幀數瞬間下降,這也是為什麼大家在遊戲場景中移動時,遇到密集的建築就會卡頓一下的原因。以前處理方式是使用LOD處理,被遮擋的物體使用簡模,這樣也會加大記憶體的負載效果,如果角色一直在建築物之間來回穿梭,這樣不同LOD模型就需要來回切換,對記憶體也是一個負擔,效果不理想。這些問題對於程式設計師面來說必須解決的問題,如何解決呢?很多人想到了合併大Mesh,這種方法行不通的,大網格並不適合做裁剪操作,試想一下,我們合併的網格,如果攝像機只看其一小部分,因為它們是一個整體這樣就需要把他們一起載入到記憶體中,而實際上我們並不需要這麼多模型資料,在合併網格時,在這裡也給讀者一個建議,儘量把靠的很近的模型進行合併,避免上述問題發生。其實最有效解決方案還是劃分塊,這個劃分塊可以利用地形劃分的思想進行,它是與地形塊緊密相關的,每個地形塊中的建築物跟隨地形塊一起載入。如果塊中的建築非常密集,這種方法還不能夠完全解決,還需要進一步的處理,就是要加入OC遮擋演算法結合LOD演算法,這樣就可以完全解決我們當前的問題了,這也是本篇課程
要講解的方法,再進一步的優化方法是可以將OC遮擋演算法和LOD演算法放到GPU中計算,這樣效率還會提升,在Siggraph2015發表了一篇文章GPU-Driven Rendering Pipelines,它的思想就是使用GPU進行遮擋裁剪處理,主要分兩個階段,使用的是DX12圖形API,如下圖所示:
它的思想就是第一步先做一個初略的遮擋裁剪列表,而後在此基礎上再根據視線距離或者射線檢測做進一步的細化裁剪操作,這個思想跟我們的碰撞檢測演算法類似,引擎中碰撞檢測演算法也是基於這個原理實現的,給讀者介紹一下:實際可用的碰撞檢測演算法,一般要分2個階段:
第一階段,broad phase 快速找出潛在的碰撞物體對列表,不在這個列表裡的是絕對沒可能碰撞的。broad phase確定了一批需要進一步檢查的物體對。
第二階段,narrow phase 準確找出發生碰撞的物體對列表。因為上一個階段的部分物體對實際上是沒有碰撞的,需要在這個階段剔除。
broad phase其中有一個簡單演算法叫sweep and prune(SAP),本質上是利用了排序演算法。第一步是初始化排序列表,列表中的元素是包圍盒,可以用任意排序演算法完成,例如快排;之後的排序就不是用快排了,而是用氣泡排序,為什麼用氣泡排序更好呢?是因為一個預設的前提:物體的運動有時間相關性(temporal coherence),即當前幀和下一幀的位置是相近的,所以在氣泡排序過程中,發生的位置交換預期都很靠近。
其實演算法中有很多類似的地方,這裡我們也要互相借鑑它們解決問題的思想用於解決我們的問題。筆者以前做的是端遊,端遊中很多優化思想同樣適用於移動端,移動端跟PC端比,就是一臺配置比較低的電腦而已。接著我們的遮擋裁剪繼續給讀者介紹,論文作者也做了一個效率測試,以250’000物體,1G的網格為例,測試效果如下所示:
是不是很酷啊!在專案開發中完全可以用它解決問題,下面我們再談談使用GPU去優化我們的大地形場景。
GPU大地形渲染優化解決方案
我們的大地形首先會有自己的地表貼圖,常用的地表貼圖是四張紋理融合,最多可以有八張貼圖融合,地形紋理渲染會涉及到LOD演算法,遠處的地形網格可以簡化一些,對應的貼圖也是最低的,這就是MipMap的使用。另外肯定有草有花以及其他大量相同的物件渲染,先說說草和花的繪製,他們在遊戲中會非常的多,常用的做法是引擎提供的面片或者是十字交叉,或者三張圖片交叉,然後將帶有Alpha通道的貼圖對映在上面,如下圖所示效果:
CPU繪製這些草或者花在PC端是可以的,因為現在的電腦都是多核的,在手機端就會影響到效率問題了。使用CPU繪製,DrawCall會非常的多,而且草或者花還需要擺動,計算量很大的,這嚴重影響了執行效率,CPU有難,GPU可以幫忙,我們可以將草或者花的繪製放到GPU中執行,效果如下所示:
這些草的繪製都是在GPU中進行的,遊戲中花的繪製原理跟草的繪製是類似的。它們的製作並不是美術建模的草或者花而是程式自定義生成Mesh,然後在Mesh上面加上貼圖進行渲染處理的,從而降低DrawCall,GPU使用的是幾何著色器,DX10圖形API使用的就是幾何著色器。對於其他相同物件的繪製,我們可以使用GPU Instancing優化處理,除了處理批量物體外,我們還會使用GPU進行材質渲染以及處理角色動畫蒙皮等等。如下圖模型材質渲染效果所示:
除了這些渲染外,我們還需要針對場景的後處理渲染,Bloom,Blur,SSAO,HDR等等常用後處理渲染。下面給讀者展示一個體積光渲染,效果如下所示:
圖片來源:https://zhuanlan.zhihu.com/p/39754801
這樣渲染的效果讓場景更加逼真形象,GPU能幫助我們處理很多事情,這裡就不一一列舉了。- 總結
本篇課程主要目的是解決大地形資料載入和密集建築物載入優化問題,本篇課程主要會從以上幾方面結合著案例給讀者進行講解,希望通過本篇課程的學習能夠給讀者提供一些解決問題的思路,做到舉一反三。不同的專案需求是不同的,但是遇到的問題都差不多,隨著硬體的提升,演算法的改進,在大地形的效率執行方面還會有質的提升。