基於Unreal引擎的大地形載入研究
來自專欄 UWA:簡單優化、優化簡單
UWA從去年開始進入Unreal引擎的學習,並且從去年底開始發表了一系列關於Unreal引擎使用方面的技術文章。但是,今天的這篇文章與以往的功能介紹不太一樣,我們想通過一個實際的案例來讓你對Unreal引擎產生更加深入的了解。
由於吃雞遊戲的火爆以及精品MMO遊戲對於場景的需求越來越大,所以,我們這兩個月使用Unreal引擎對大地形的動態載入功能進行了研究。從中,我們希望可以搞清楚以下兩點:
- Unreal引擎對於大場景動態載入的製作方法;
- Unreal引擎在當今移動設備上的性能表現。
為了讓我們的地形展示看起來更加「漂亮」一點,我們放棄了自己手動製作大地形場景的想法(我們自己做的地形實在拿不出手),而是使用了之前在Unity引擎中使用過的大地形場景模型。除了最終的渲染效果,我們幾乎是1:1地在Unreal引擎還原了該場景環境,其在真機上的運行效果如視頻所示。
https://www.zhihu.com/video/978297950715326464
視頻中左邊是Unity引擎的渲染結果,右邊是Unreal引擎的渲染結果。
本文將主要介紹我們通過Unreal引擎製作大地形的過程中遇到的問題,希望可以對正在或即將使用Unreal引擎開發大地形功能的研發團隊有所幫助。本文主要包含四個部分:
- Unreal引擎的資源準備;
- Unreal引擎場景流式載入功能介紹;
- Unreal引擎大場景動態載入製作介紹;
- Unreal引擎在真機上的性能分析。
接下來,我們將分別介紹這四部分內容。
一、 Unreal引擎的資源準備
由於我們希望使用一個已有的案例資源來製作Unreal引擎的大地形載入,所以,我們首先需要解決的,就是如何將Unity引擎的資源轉換到Unreal引擎中進行使用。在資源轉換過程中,我們需要處理四種類型的資源:地形、模型、紋理和材質。接下來我們分別介紹這四種資源的轉換方式。
1. 地形資源
在原始的大場景動態載入案例中,我們使用的是Unity引擎的Terrain系統。但由於Unity與Unreal引擎中地形的數據格式不一樣,因此我們無法直接在Unreal引擎中使用Unity引擎的地形數據。因此,我們嘗試通過地形的高度圖來將Unity引擎Demo中的地形轉換到Unreal引擎中,其轉換方式如下圖所示:
我們首先對Unity引擎中的地形塊導出高度圖,然後將高度圖導入Unreal引擎重建出相同的地形。高度圖中的每一個像素記錄的實際是地形塊中每一個頂點在垂直方向上的高度。Unreal引擎中對於高度圖的導入非常便利,如下圖所示。在Unreal引擎高度圖導入界面,我們只需在New Landscape界面中選擇Import from File,並選中我們想要導入的高度圖,再點擊Import按鈕即可導入高度圖並重建地形。
但是,由於Unity與Unreal引擎的坐標系的不同,從Unity引擎中導出的高度圖直接放入Unreal引擎中會導致重建出的地形與原始Unity引擎中的地形相對X軸呈現鏡像的關係,如下圖所示:
上圖中左圖是Unity引擎坐標系和原始地形截圖,右圖是Unreal引擎坐標系和重建地形截圖。對於這種情況,我們可以做一個簡單的坐標變換來讓其看起來一致。但是,我們發現在Unity引擎中導出高度圖時勾選Flip Vertically選項,也可以達到同樣的效果,如下圖所示:
其中,左圖是Unity引擎原始地形,右圖是高度圖Flip後在Unreal引擎中重建出的地形。
在製作Unreal引擎大地形Demo時,我們不僅希望地形的形狀與Unity引擎Demo一致,並且希望地形參數設置也盡量與Unity保持一致。在Unreal引擎地形參數中,主要包含了三個參數:
- Component
- Section
- Quad
這三個參數從上至下是包含關係,即:一塊地形中可包含多個Component,一個Component可以包含多個Section,一個Section可以包含多個Quad。而Quad則是地形網格中最小的四邊形。接下來我們分別介紹這三個參數的設置。
1.1 Component
在Unreal引擎的地形中,Component實際上是裁剪、渲染以及碰撞檢測的基本單元。Unreal引擎在渲染地形時,一個Component要麼被整體裁剪掉,要麼被整體渲染出來並參與碰撞檢測的計算。而其數量則是通過New Landscape窗口中的Number of Components參數進行設置,如下圖所示:
其中,左圖顯示了New Landscape窗口設置界面,紅色方框顯示了Number of Components參數。該參數的設置可以任意選擇,例如:1x1,1xN或者NxN。而我們在設置該參數時主要是參考Unity引擎大地形Demo的設置。右圖顯示了Unity引擎中地形的截圖。在Unity引擎中物體裁剪是以GameObject為單位,而我們在製作地形時是以一塊子地形作為一個GameObject。因此,在Unity引擎中地形的裁剪是以一塊子地形為獨立的裁剪單元。為了與Unity引擎Demo的設置保持一致,我們將Unreal引擎的地形設置中Component設置為了1x1,即一塊子地形只包含一個Component。這樣就保證了Unreal引擎中地形的裁剪也是以一塊子地形為基本單元。
1.2 Section
在Unreal引擎的地形中,Section實際上是LOD的基本單元。它的數量可以通過Sections Per Component進行設置,如下圖所示:
其中,一個Component中可以包含1(1x1)個或者4(2x2)個Section。該參數的設置我們也是參考了Unity引擎中地形的LOD。右圖顯示了Unity引擎中的一塊地形網格截圖。其中,右下角1/4大小的地形是比較密的網格,而其他3/4是比較稀疏的網格。因此,Unity引擎Demo中一個子地形塊的LOD變化單元是1/4大小。所以,我們將Unreal引擎中的Section數量設置成2x2,即一個Component包含4個Section。這樣可以保證地形的LOD變化粒度與Unity引擎地形基本一致。
1.3 Quad
在Unreal引擎的地形中,Quad決定了最後生成的地形網格的頂點數。其數量可通過Section Size參數進行設置,如下圖所示:
其中,左圖是Unreal引擎地形參數設置界面,右圖是Unity引擎中地形解析度參數界面。由右圖可知,Unity引擎Demo中一塊地形的頂點數是65x65。而Unreal引擎地形的Section Size參數則不能任意設置,只有幾個選項可選擇。因此,我們選擇了最為接近的解析度,如左圖紅框中所示。最終,我們重建出的地形的頂點數為63x63,如左圖中藍色框所示。
但是,雖然解析度很接近,但問題還是出現了。由於我們在Unreal引擎中使用65x65解析度的高度圖重建出了63x63頂點數的地形,導致Unreal引擎在重建地形時對高度圖進行了重採樣。而重採樣的結果造成了我們將子地形塊進行拼接時,在相鄰地形塊之間出現了裂縫,如下圖所示:
為了解決這一問題,我們對高度圖進行了預處理。該預處理的目標有兩個:
- 將高度圖從65x65重採樣成63x63;
- 保持相鄰高度圖的公共邊的數值一致。
我們採用了如下的方法進行重採樣:
如上圖所示,左圖表示重採樣之前的高度圖,右圖表示重採樣之後的高度圖。在計算中間像素時,我們從原始高度圖中選取周圍4個臨近的像素進行插值,如上圖綠色框所示。在計算到邊緣像素時,我們僅僅使用邊緣上的兩個像素進行差值,如上圖紅色方框所示。由於原始高度圖中相鄰邊上像素的數值是一樣的,而差值的過程中又保證了相鄰邊的採樣點僅僅來自於邊緣像素,所以保持了重採樣後的高度圖中相鄰邊的像素值一致。通過這個過程,我們得到了63x63解析度的高度圖,並且保證了其相鄰邊上數值一致。因此,我們在Unreal引擎中重建出的地形就可拼接完好,如下圖所示:
其中,左圖是原始案例中地形的網格截圖,右圖是Unreal引擎中重建出的地形拼接完成後的網格截圖。
2. 模型資源
Unreal引擎中模型資源的導入非常便利,如下圖所示:
我們只需在Content Browser窗口下點擊Import按鈕,然後選擇需要導入的模型,並點擊導入設置窗口中的Import即可導入。Unreal引擎中支持各種不同格式的模型導入,例如:FBX,OBJ等。
3. 紋理資源
Unreal引擎中紋理資源的導入過程與模型資源類似。目前來說,Unreal引擎支持絕大多數的圖像格式,並且官方推薦使用的是PSD格式。
4. 材質資源
Unreal引擎中材質資源通常是使用Blueprint節點圖進行編輯的。因此,我們在Unreal引擎中創建出材質Blueprint,並添加相應的紋理以及設置好參數。然後,將材質設置到對應的模型中,如下圖所示:
其中,左圖顯示的是材質節點圖,右圖中紅色方框顯示的是將材質設置到相應模型中。
與模型材質不同的是,Unity引擎地形材質是通過將四張不同的紋理混合而成的。混合的參數則是通過一張Splat紋理中的RGBA四個通道分別指定,如下圖所示:
其中,左圖顯示了Unity引擎中四張混合紋理,右圖顯示了記錄混合參數的Splat紋理。我們利用Unity引擎的TerrainData.alphamapTextures[0]將其導出。然後,我們在Unreal引擎的材質Blueprint中導入這些紋理,並編輯節點圖進行混合,如下圖所示:
由此,我們準備好了Unreal引擎中所需要的資源,並重建出了地形。接下來,我們需要將Unreal場景中的模型進行擺放和復原。
由於Unity與Unreal引擎的坐標系不同,所以我們要對其進行轉換,如下圖所示:
我們將原始場景中的所有GameObject的Transform信息製作成配置文件,然後通過一定的轉換計算出Unreal引擎中Actor的坐標。由於Unity引擎中使用的長度單位是米,而Unreal引擎中使用的長度單位是厘米。因此,我們最終使用了如下的公式進行計算:
其中,左邊是位置信息的轉換,右邊是旋轉信息的轉換,下邊是縮放信息的轉換。通過轉換過的配置文件,我們即可在Unreal引擎中創建出與原始場景幾乎一致的場景。
二、Unreal引擎場景流式載入
在重建了場景之後,我們接下來需要考慮如何對其進行動態載入。Unreal引擎為開發者提供了便利的場景流式載入方案,即Streaming Level。在Unreal引擎中,場景關卡主要分為兩種:Persistent Level和Sub Level。Persistent Level在遊戲啟動時自動載入,並且會一直存在、不能被卸載。而Sub Level則可被動態載入和卸載。Streaming Level功能則使得我們能夠很方便地動態非同步載入、卸載Sub Level。在製作大地形動態載入時,我們只需將動態載入的內容分別放到不同的Sub Level中,就可利用Streaming Level進行非同步載入和卸載了。
Streaming Level中觸發Sub Level載入和卸載的方式有兩種:
- Level Streaming Volume;
- Scripted Level Streaming。
Level Streaming Volume方式是利用在場景中擺放一個立方體,當Player進入該立方體空間內時,觸發與該立方體綁定的Sub Level的載入。當Player離開該立方體時觸發與之綁定的Sub Level的卸載,如下圖所示:
其中,淺黃色的立方體方框即為Level Streaming Volume,深黃色矩形顯示的是與之綁定的Sub Level中的一塊地形。當玩家進出該淺黃色方框時,就會觸發該地形塊的載入和卸載。
Scripted Level Streaming則是提供了兩個API:Load Stream Level和Unload Stream Level,如下圖所示:
利用這兩個API可以在任何時刻對指定的Sub Level進行載入和卸載。
這兩種觸發方式各有優缺點。使用Level Streaming Volume進行觸發時,修改載入策略便利,但是觸發方式單一。使用Scripted Level Streaming進行載入時,觸發方式靈活,但是需要自己管理載入策略。
在使用Unreal引擎提供的Streaming Level功能製作大地形載入比較便利。我們的製作過程如下圖所示:
我們在場景中創建出一系列的Level Streaming Volume,然後將其與對應的Sub Level進行綁定。在運行時,引擎便會自動根據玩家位置載入、卸載Sub Level。
https://www.zhihu.com/video/978297372614393856
上述為通過Unreal引擎內置的Streaming Level功能來製作大場景的動態載入,同樣,也可以通過代碼來進行實現,邏輯與上述一致,其具體實現細節就不在這裡細表了。
三、移動端性能分析
通過以上兩章說明,相信大家已經知道如何通過Unreal引擎來實現一個大型場景的動態管理功能。在本章中,我們將就Unreal引擎的性能效率來進行檢測。對此,我們通過與之前在Unity引擎上的場景案例來進行真機性能對比,以期讓大家對Unreal引擎的性能效率能有一個更為直觀的了解。
在原始案例中,我們通過Unity引擎使用兩種方式來製作了大地形的動態載入功能。一種是自己控制任何一塊地形、任何一個地表物體(花、草、樹和石頭等)的流式載入和卸載;另一種是使用Unity引擎提供的Scene Manager功能進行實現,通過LoadSceneAsync和UnloadSceneAsync來進行地形塊的載入和卸載。我們的製作方式如下圖所示:
首先我們獲取玩家的位置信息,然後根據玩家位置確定當前距離玩家最近的九個場景,最後利用Scene Manager API進行場景的載入和卸載。自己控制流式載入實現的過程也是類似進行管理。
但在性能對比時,我們更多的是使用自己實現的流式載入版本來進行對比,因為這是我們高度優化過的版本,其資源動態載入更為可控,從而性能較之SceneManager版本稍好一些,如下圖所示。
在比較Unreal與Unity引擎製作相同大場景載入的性能時,我們對兩個引擎的渲染參數進行了相同的設置。在測試過程中,我們選用Unity引擎的版本是2017.2,選用Unreal引擎的版本是4.18。同時,我們設置渲染的管線都是Forward Path,都不開啟HDR和AA,Light Map都使用了Non-Directional。由於Unity引擎的Demo使用了飽和度顏色映射的圖像後處理,因此我們添加了Unreal引擎自帶的圖像後處理飽和度映射,如下圖所示:
其中,左圖是Unity引擎的圖像後處理設置界面,右圖是Unreal引擎的圖像後處理設置界面。我們分別從CPU性能、能耗和內存三個角度來進行分析。接下來我們分別進行介紹。
1. 性能比較
在性能比較中,我們比較了Unreal與Unity引擎Demo的FPS以及每幀耗時統計。在實驗中,我們分別對比了Unity與Unreal引擎的PBR材質,以及Unity引擎Legacy和Unreal引擎PBR簡化版的性能比較。這主要是因為在Unity 2017版本以後,地形系統的材質存在四種模式,如下圖所示。所以,我們選擇了Built In Standard和Built In Legacy Diffuse來進行測試。具體如下:在對比PBR材質時,Unity引擎中我們採用了Standard Shader進行渲染,Unreal引擎中我們使用了默認的參數設置。在對比Legacy和PBR簡化版時,Unity引擎中我們採用了Legacy Diffuse材質,Unreal引擎中我們去掉了部分默認的PBR效果,例如:使用Full Rough,以及去掉Decal Response等。接下來我們將分別介紹它們的數據結果。
1.1 Unity PBR vs Unreal PBR default FPS
我們在高、中、低不同機型上進行了測試,下圖分別為紅米2、小米5X和三星S6上的FPS統計圖:
其中,紅色線條表示Unreal引擎的幀率統計,藍色線條表示Unity引擎的幀率統計。從結果上看,在地形使用PBR材質時,Unreal引擎在紅米2設備上幀率在15~25 FPS區間,在小米5X設備上幀率在40~55 FPS區間,而在三星S6設備上,Unreal引擎基本上可以達到滿幀(60FPS)。同時,我們也可以看出,Unreal引擎在當前案例中的性能確實要領先於Unity引擎。
1.2 Unity Legacy vs Unreal PBR Simplified FPS
下圖分別顯示了紅米2、小米5X以及三星S6上Unity Legacy Diffuse與Unreal PBR
簡化版的FPS比較:
由上圖可知,在地形使用Legacy材質時,Unreal引擎和Unity引擎在移動設備上的性能均明顯提升。其中,Unreal版本在紅米2設備上幀率在30~45 FPS區間,在小米5X設備上幀率在45~60 FPS區間。同時,我們也可以看到,Unity版本雖然還有一些掉幀情況,但在高端設備上性能有了大幅提升。
除上述設備之外,我們一共在8台不同檔次的移動設備上進行了比較,其具體的幀率統計如下表所示。
由上圖可知,在目前的高中低檔設備中,Unreal版本(PBR簡化版)的運行效率都相當優秀。除了紅米Note2和魅族MX5兩款機型外,Unreal引擎在本測試用例中的性能均高於Unity引擎。
2. 能耗比較
在能耗測試中,我們採用Unity Legacy Diffuse與Unreal PBR簡化版材質進行渲染,並且將幀率固定在30FPS,然後使用高通Trepn工具統計實時功率。下圖分別顯示了紅米2、小米5X以及小米5S機型上的統計數據,單位為毫瓦(mW):
由上圖可知,在中、低端機型上Unreal引擎的能耗比Unity引擎稍高,而在高端機型(小米5S)上,Unreal引擎要比Unity引擎高出許多。
3. 內存比較
在比較內存的試驗中,我們採用Unity Legacy Diffuse與Unreal PBR簡化版材質進行渲染,然後統計Android平台的PSS內存。下圖顯示了紅米Note2機型上,Unity引擎自實現流式載入方式、SceneManager載入方式和Unreal引擎的內存統計,單位為MB:
由上圖可知,使用Unity引擎 Scene Manager載入方式(灰色)的PSS內存會在一開始階段隨時間逐步上升,最後穩定在一個固定值階段。這是因為Scene Manager載入方式在Unload場景時只是刪除了GameObject而沒有釋放相關的資源,所以其內存會越來越多,直到場景遍歷完為止。如果要釋放該部分資源,則需手動調用UnloadUnusedAssets介面才行。而在我們自實現的流式載入版本中,我們通過UnloadAsset API來手動控制相關資源的卸載,因此,可以做到在整個場景遍歷時,內存都維持在較低且平穩的程度上。而Unreal Streaming Level模式則會自動釋放資源,其資源卸載是由引擎自動管理,因此Unreal版本在整體運行過程中,PSS內存也較為一致。
下圖顯示了更多機型上的內存佔用。我們採用了Unity自實現流式載入方式與Unreal引擎比較,單位為MB:
由上圖可知,Unreal引擎的PSS內存佔用較之Unity引擎普遍較高。但其具體原因,我們暫時還無法給出解釋,關於Unreal引擎底層的內存使用分析,是我們接下來計劃重點研究的方向之一。
四、未來工作
以上則是我們近期對於Unreal引擎關於大地形動態載入的一些工作心得,相信讀到這裡的你,已經對開篇的兩個問題——Unreal引擎中大地形的動態載入製作方法和在移動設備上的性能效率有了充分的答案。接下來,我們會在以下幾個方面進行進一步的工作:
- 對Unreal引擎的內存使用進行深入研究,並對案例中的內存佔用進行優化;
- 對Unreal引擎的CPU開銷和能耗問題進行深入研究,並對案例中的性能進行優化;
- 將完善後的案例工程進行整理和開放,讓更多的遊戲開發者受益。
註:當我們第一次在真機上對Unreal的大地形版本進行測試時,完全被它的性能表現給震撼了。我們團隊一直專註在遊戲的性能優化方面,應該還算是有一定的技術積累,對於在何種設備上,何種複雜程度的遊戲能跑出多大的性能數值還算是有些預判的。但當我們在紅米2上運行我們自己做的Unreal版本時,心中的念頭只有一個——「Unbelievable」!它展現出來的性能效果大大出乎我們的意料。也許,這就是Unreal引擎這二十年來的技術底蘊,它讓我們對於Epic這個單詞產生了更加深刻的理解。
但是,需要鄭重說明的是,該篇文章雖然比較了Unreal引擎和Unity引擎在真機上的性能效率,但是這並不能說明兩個引擎哪一個更好。引擎之間的比較是多維度的,並且是無法從一個案例中得出結論的。因此,我們希望讀者不要產生誤解。在我們眼裡,二者都是當今世界上偉大的引擎。無論是Unity還是Unreal,亦或是其他引擎,只要是能推動中國遊戲進步的就都是好引擎!
最後,感謝Unreal中國團隊在我們案例製作和研究過程中提供的專業的技術支持!
推薦閱讀: