基於Unity3D引擎的大地形載入研究

遊資網發表於2019-11-15
在之前的幾個月中,本人開始著手探索Unity3D引擎在主機PC平臺的一套比較高效的渲染和載入方式,到目前為止已經實現了基本的渲染方法,這裡先放上主要的實現參考,GDC 2017 Ghost Recon Wildlands Terrain Tools and Technology:

Ghost Recon Wildlands Terrain Tools and Technologywww.youtube.com

前排警告:這篇文章,非!常!長!而且因為技術點瑣碎,描述難免有些倫無語次!希望諸位看官見諒!

本篇文章我們將會從以下幾個方面講解在Unity3D中搭建一套以高階平臺為目標的,理論支援無限大的平坦大地形實現,為什麼要特別強調“平坦”呢,因為這裡實現的地形是指狹義的使用高度圖對平面進行置換的渲染方法,因此類似山洞,峭壁,樹木等等暫時不在這套系統的實現範圍內,在本文中也將不予討論。我們將分為以下5個部分講解這套大地形的原理和表現:

1.        基本渲染實現

2.        資源的載入和解除安裝

3.        地形著色與生產流程

4.        效能表現

5.        總結

基本渲染實現:

我們採用了曲面細分 + 高度圖的方法進行渲染。先來說曲面細分,每一片地形會經過DX11以後支援的Hull Domain Shader,經過數倍細分,分成一張三角形均勻排布的平面:

基於Unity3D引擎的大地形載入研究
x1, x32, x64 的細分

比起傳統的網格繪製,相同面數下曲面細分的效能表現非常優越,首先其光柵化效能比單獨的三角形光柵化要高數倍,在PC平臺進行壓力測試時,我們發現同屏細分幾千萬面,每幀的耗時也只有寥寥數毫秒的變動,這令我們感到十分驚喜,並直接決定使用Quad Plane + Tessellation代替傳統的GPU Instance方法。同時Tessellation最高支援64級細分,這樣每兩個三角形就能承擔起64 * 64解析度的高度置換圖,這也讓高度的精度選擇十分靈活,大大增大了製作的自由度。

在LOD方面,我們使用了四叉分割樹的方法進行計算:

基於Unity3D引擎的大地形載入研究

四叉分割的演算法是大學資料結構課裡比較基礎的部分,這裡就不再贅述。從圖上不難看出,地形資源的儲存趨近於一個金字塔型,與紋理貼圖中常用的MipMap非常相似,實際上兩者原理本來也是十分接近的,區別只是地形中每個“畫素”位置儲存的並不是顏色而是這一整塊地形的所有資訊,四叉分割樹的特性對每一部分的影響在我們本篇文章中都會有所涉獵。

目前整個地形用到的所有貼圖都是基於Virtual Texture的,我們在上一篇文章中已經講過Virtual Texture的實現:

MaxwellGeng:Virtual Texture Tools & Practices

高度圖也不例外,所以Shader中讀高度圖進行置換的方法十分簡單粗暴:

基於Unity3D引擎的大地形載入研究

地形使用的Fragment Shader中進行著色的部分也和普通Standard PBR Shader幾乎沒有任何區別,唯一的區別可以說就是把讀取普通貼圖換成了讀取Virtual Texture:

基於Unity3D引擎的大地形載入研究

因此可以看到,Shader在渲染中基本沒有做任何事情,Virtual Texture的生成才是渲染最主要的部分,Ghost Recon中是用這樣的辦法進行材質生成的:

首先將32套PBR材質貼圖存到Texture Array中,然後用一張8位的Mask Map進行選取取樣,取樣出的效果大概如此:

基於Unity3D引擎的大地形載入研究

進行手動Bilinear,使取樣效果變得平滑:

基於Unity3D引擎的大地形載入研究

因為Ghost Recon中並不是“同一塊”地形,或者說,不同區域的地形美術風格不一樣,而所有區域的材質數量加起來遠高於32套,因此他們還使用了一個Indirect Buffer做索引跳轉,相當於開了一個物件池實時的解除安裝掉那些在這個區域內不可能用到的材質,並載入這個區域內一定會用到的:

基於Unity3D引擎的大地形載入研究

這個實現實際上與遊戲邏輯比較耦合,這裡我們暫時不實現Indirect Buffer,這方面會在之後的材質流程部分實現。

所以我們這裡使用Mask進行混合的實現如下,首先封裝一下Mask Texture的Bilinear Interpolation,畢竟Mask也要全圖連線,所以也是一張Virtual Texture:

基於Unity3D引擎的大地形載入研究

用計算出的權重進行混合:

基於Unity3D引擎的大地形載入研究

但是純線性的Bilinear會讓整個地形充滿醜陋的鋸齒感,這裡用一張噪波圖擾動一下即可,當然噪波圖怎麼生成或者噪波演算法怎麼寫屬於製作內容,在之後的部分中我們將慢慢展開。Compute Shader Kernel直接呼叫這幾個方法:

基於Unity3D引擎的大地形載入研究

混合效果:

基於Unity3D引擎的大地形載入研究

其實這裡可以看到邊緣的混合已經比較平滑而且不規則了,但是搭眼一看格子感依然非常嚴重,所以這個鍋可以說並不是取樣的鍋,而是材質的處理依然有問題,所以這個問題也會在之後的製作部分講解。

在有了基本的地形VT的載入渲染之後,我們需要讓地形變得可以互動。所謂互動就是有來有回:有來,指的是任何物體的Shader都可以使用絕對座標獲得Virtual Texture的相對座標,這樣在實現一些高階特性時會十分容易,比如沙石交融效果的示例圖:

基於Unity3D引擎的大地形載入研究

在這張示例圖中,很明顯石頭是一個模型,而這個模型的材質卻需要和地面完美融合,在傳統的地形系統中要想實現這套系統不僅非常麻煩,而且很可能造成額外消耗:如果使用Pixel Offset,就會破壞Early Z,在某些硬體上甚至可能導致效能驟降,如果使用二次疊加,那石頭的Shader可能會過於複雜,而且功能耦合十分嚴重。而對於Virtual Texture Terrain來說這基本可以說不是問題了,我們只需要將世界座標到UV座標的轉換變數作為Uniform傳遞進Shader:

基於Unity3D引擎的大地形載入研究

注意,這裡有個細節,因為考慮到地形非常龐大,因此浮點精度損失在所難免,我們就在C#指令碼中統一使用64位雙精度浮點數進行運算,並在傳入Shader時將整數部分和小數部分分開傳入,因此這裡多了一個floor frac疊加的策略,不僅如此,還有一個TerrainVTOffset變數同樣也是為了解決浮點精度問題,當攝像機距離世界中心非常遠(如5km以上),這時客戶端遊戲邏輯就可以觸發地形偏移,將整個地形向世界中心方向偏移,那麼浮點精度問題也就自然而然解決了。(當然,這種平移全圖的操作目前僅限單機遊戲考慮,涉及到網路部分超出本人能力範疇,不在此過多討論)。

這裡隨便擺個球測試一下混合效果,看起來混合上完全沒有問題,使用世界座標簡單粗暴的插值兩套貼圖,實現方法和使用二套UV讀貼圖混合並沒有本質區別,只是UV變成世界座標而已,從這裡也可以看出Virtual Texture強大的魯棒性:

基於Unity3D引擎的大地形載入研究

互動的“來”已經有了,接下來是“回”。除了能夠對外輸出圖片以外,地形系統還相容了上一篇文章中講到的貼花系統,之前也同樣講了Virtual Texture的貼花是基於正交攝像機投影的一套獨立的非常輕量的管線,相當於Deferred Shading的Geometry Pass,只不過輸出的目標不是螢幕而是VT而已。在地形中我們分了兩個Rendering Pass,一個負責Albedo等PBR貼圖的輸出,另一個負責Heightmap,這是因為PBR貼圖的解析度一般要比Heightmap要高,所以不可能通過單個MRT Pass輸出。我們先寫一個最簡單的置換輸出Shader,這個Shader的用途是讓地形隆起一塊:

基於Unity3D引擎的大地形載入研究

基於Unity3D引擎的大地形載入研究

Shader的實現十分簡單,畢竟只是個渲染測試,這裡直接上程式碼:

基於Unity3D引擎的大地形載入研究

首先,呼叫SRP 提供的API,渲染所有RenderQueue中的TerrainDisplacement Pass,並且設定當前高度圖為RenderTarget,這裡要凸起而不是挖坑,所以混合方式標記為BlendOP Max,當然這也可以不用,因為為了考慮到可能有更加智慧的混合方式,這裡對貼圖做了Double Buffer,也就是說Frag Shader可以一邊讀一邊寫,也就上方_VirtualHeightmap和_OffsetIndex這兩個變數的用途。最後,用傳入的比例,從世界座標反推出高度圖的數值並輸出,最後輸出結果就可以直接對映到地形的高度圖上。

但是,這種貼花置換的方法簡單暴力,也十分突兀,因此我們非常需要讓貼花的混合變得科學柔和一些。這裡提供一種最常用的材質混合方法:高度混合。這裡熟悉貼圖製作軟體如Substance Designer的美術同學應該會比較明白,高度混合是做這類地面混合時最常用的方法之一,即上邊的蓋住下邊的,上下兩種材質在結合處給予一定的混合比例,譬如埋在沙土裡的磚,就可以製作出沙土和磚兩種材質,並通過兩者的高度進行混合,效果往往會比較理想。

當然,具體怎麼應用也同樣會在製作部分講解,這裡只講原理,基本原理是使用Alpha Blend,並讀取Virtual Texture本身的高度,以此計算出頂點高度和地形高度的差,用這個差當做混合權重輸出到Alpha通道中,即可達成目標。

首先是獲取混合權重部分:

基於Unity3D引擎的大地形載入研究

這一部分比較容易理解,是用世界座標作為統一的座標系,這樣在編輯階段比較友好,然後計算兩者的差並用材質中的混合權重相乘。

最終使用高度差計算出的權重,將會被用於混合PBR部分,以及高度本身:

基於Unity3D引擎的大地形載入研究
混合PBR貼圖

基於Unity3D引擎的大地形載入研究
混合高度圖

經過高度混合的貼花就不像之前那樣,平地立起一個蛋非常突兀,相反會變得平滑不少,在交界處也有了柔和的混合:

基於Unity3D引擎的大地形載入研究

到此,渲染部分的框架基本搭完,接下來需要一套合適合理的資源管理系統,畢竟我們本篇文章的題目是“載入研究”,因此渲染還是需要在資源管理系統的加持下才能工作。

資源的載入和解除安裝:

對於開放世界遊戲中的大地形來說,最突出的資源管理的矛盾無非是兩點:從硬碟載入的遲緩和有限的記憶體容量的矛盾,分散式生產開發和要求統一的資料結構。

第一點的矛盾在於開發者必須制定好一套合理的流式非同步載入方法,走到哪,載入到哪,並且載入的過程需要潤物細無聲,對主執行緒的影響最好微乎其微,這是比較難解決的一點。第二點就是整個地形必須要保證資料結構統一併且強關聯才可以做到無縫,但是開發過程很大概率是多個小分隊一支隊伍負責一小部分,這就要求多個團隊分佈開發的資源需要能夠無成本合併。

這裡我們為了解決這兩個問題,使用了這樣的開發思想:整個地形以一個完整的四叉樹格式進行儲存和讀取,其中最大的可渲染地形塊永遠是1km,LOD每高一級邊長縮為1/2倍,體積縮為1/4倍,而相對解析度也會翻倍。

舉個例子,整個地形8KM,那麼最初的等級就是8KM,接下來是4km和2km,這三個等級都完全不會被用來渲染或者做什麼事情,它們只是為了方便更高等級的LOD遍歷而存在的根節點,當然也沒什麼需要儲存的資訊。

接下來到了下一級,也就是1km,這時候已經開始渲染了,這時我們需要載入負責Virtual Texture著色的Mask map,按照Ghost Recon的規格,Mask Map解析度是 2pixel / m,那麼這一張圖的大小就是2048 * 2048,當然,由於Mask也和地形一樣屬於無縫的圖,所以我們同樣需要把這張貼圖一步到位載入到Virtual Texture中,這自然不必多說。在完成整體的Mask Map載入後,開始載入高度圖,高度圖是每個地塊256x256解析度(當然這個標準隨時可以改)。由於高度圖是單通道而且不容許任何精度損失,因此我們把所有高度圖都打包成完整的二進位制,儲存在二進位制檔案中,而這個儲存有整個8KM地形高度資訊的二進位制檔案格式就是:

LOD0: 8 * 8 * 256x256

LOD1: 16 * 16 * 256x256

LOD2: 32 * 32 * 256x256

……

LODX: 2^(3 + X)^2 * 256x256

這樣當我需要獲得某一級的某一個位置的地塊的時候,計算的方法就是這樣:

1.        使用LODLevel M算出當前Level的總體偏移O:

基於Unity3D引擎的大地形載入研究

2. 將座標降維並累加到偏移,獲得位元組位置P:

基於Unity3D引擎的大地形載入研究

這個演算法是不是看起來非常熟悉呢?沒錯,這就是Mipmap的演算法,只不過這裡把每個畫素換成了一整張圖,相當於一個4D Texture。

縱觀這套演算法,可以看到我們基本考慮到了提到的兩個需要解決的矛盾。首先是流式載入,載入Mask Map的時間只需要寥寥幾幀就可以完成,畢竟一張圖也只有2048 * 2048 * 8 bit,也就是4M大小,而這張圖將會負責整個1km地形的基本材質,價效比極高。高度圖的載入效率也非常高,使用純二進位制檔案載入,對主執行緒的唯一影響就是在提交渲染之前把分執行緒的資料提交到顯示卡這個過程需要同步,這個過程用時在0.1ms以內,除此之外並沒有任何多餘消耗,可以說就是純粹的從硬碟讀取的過程,沒有什麼中間商賺差價。

同樣這種方法對於解決團隊分佈協作問題也比較容易。在開發時先從DCC製作出一整塊1km地形的最高階LOD的高度置換圖,當然,由於解析度很高因此肯定不可能是一張圖,我們可以讓DCC給每張圖命名為HeightMap_X_Y,其中X和Y表示其所針對的地形塊,在Editor中使用工具把每張圖序列化到指定為位置,原理則是使用上方的公式計算偏移並使用FileStream輸出即可,我們目前也有一些測試性的小工具,這種實現十分簡單沒必要詳細展開講解。

到此我們就有了一個1km的高精度的地形高度儲存在硬碟裡,接下來就像普通的計算MipMap的方法一樣,把上下左右4張貼圖分別貼到一張2倍解析度的RT,並降取樣到原解析度,回讀到記憶體中並寫入到上一級LOD。

這個運算過程只需要幾秒鐘就可以完成,即使需要手動在DCC中刷高度圖,也完全可以刷完後直接在引擎中執行並看到效果,可以說這就解決了第二種問題。

從這張巨集觀俯檢視就能看出這套資源管理的載入效果,第一張是材質的分佈,這裡故意用了4張完全不同演算法的隨機噪波圖,因此中間有明顯的分裂感。第二張則是每塊地形的UV分佈:

基於Unity3D引擎的大地形載入研究

基於Unity3D引擎的大地形載入研究

接下來是貼花的載入。既然貼花是普通的Prefab,GameObject,那麼載入上就不需要像上邊這樣大費周章開發一套二進位制方法了,而是使用引擎提供的資源載入即可。我們使用Addressable提供的API進行非同步載入可以非常方便。

Addressable除了提供Load Unload,還提供了InstantiateAsync這種API,它可以自動進行非同步分幀的例項化操作,十分方便,載入時只需要在協程中呼叫即可,這一點還是要吹一下Unity官方提供的這個Package的,當然它的不足之處也不是沒有,它使用了引用計數的方法標記例項化資源,這就意味著靠這個API例項化出的GameObject最終不可以用普通的Destroy來銷燬,而是必須使用這套系統提供的API,在開發專案時必須嚴格注意這種容易留坑的部分。

貼花部分的載入邏輯和地形四叉樹這種載入是完全不同完全獨立的兩套系統,貼花的載入我們稱之為“保守式載入”,所謂保守式與四叉樹這種激進式是對應關係。對於地形高度圖和材質遮罩,我們的載入思路是,從高階到低階LOD採用降取樣的方法,讓高階LOD從視訊記憶體中“當場去世”,馬上被回收掉。而從低階到高階LOD時也是類似的方法,在載入之前沒有任何快取,直接從硬碟中載入並替換到低階LOD的位置,低階LOD也瞬間被幹掉。這種方法稱之為“激進式載入”,因為載入過程前沒有任何緩衝,載入之後也沒有任何快取,手起刀落眼都不眨一下眼睛也不幹。之所以對於地形要激進,原因無非有二:資源不具有複用性、快取成本遠高於就地載入。資源不具有複用性是因為玩家在場景中的移動軌跡不確定,載入高階LOD時上一級LOD很大概率在相當一段時間內不會被用到,而且即使重新載入可能也只需要數毫秒,相比於佔用寶貴稀缺的記憶體空間,重新載入是更聰明的方法。

而對於貼花,我們則使用保守的載入策略,即大量快取,比如在2km之外就開始非同步載入這一個區塊內可能用到的所有的貼花,當離開這個區域時幹掉一些基本在其他部分不可能用到的貼花,並且保留一些可能用到的貼花的例項。與地形高度圖不同,貼花具有很強的複用性,像小石子小土屑這些可能一張圖只需要256x256解析度,而一張2048x2048的Atlas就可以容納64張貼圖,同時,不同的模型也可以共用同一套貼圖,比如都是土塊,形狀大不相同,但貼圖複用,無論從視覺效果,還是生產效率,都是最優的做法。

因此我們可以從1km的LOD0或512m的LOD1開始載入所有貼花,並且對貼花使用LayerMask進行分級,對於遠處和近處的地面使用不同的Culling Layer,進而實現精度的差異,這裡在配置檔案中設定不同LOD等級的Layer:

基於Unity3D引擎的大地形載入研究

這裡再拿剛剛用過的那個球來演示一下:

這裡首先把這個滷蛋一樣的貼花存成Prefab並標記為Addressable:

基於Unity3D引擎的大地形載入研究

基於Unity3D引擎的大地形載入研究

這裡在完成製作時直接把整個區塊內所有的貼花GameObject全部打包到同一個Prefab中即可,在接近區域時直接對一整個Prefab進行非同步例項化,不需要擔心傳統Instantiate函式的卡頓問題,堪稱一鍵傻瓜式操作。

這種保守的載入方法好處和壞處都十分明顯,好處是其載入幾乎完全不拖累地形的載入,因為可以認為當地形需要被載入時,貼花就已經在原地等著了。當然壞處也很明顯:記憶體佔用肯定比激進的載入解除安裝方式更高,同時遊戲開始時可能存在讀條時間更長的情況,這需要考慮專案的實際需求進行權衡。

地形著色與生產流程:

在第一部分渲染部分中完成了引擎邏輯的實現,在本章節中我們將完善著色部分和生產流程對接部分。

首先迫切需要解決的就是法線顯示的正確性問題。在載入高度圖時,此時法線依然是平面的法線,也就是(0,1,0),相當於在計演算法線是並沒有考慮到TBN Matrix,所以看起來整個是平的。法線的標準非常重要,因為法線在出了問題後很可能一時半會不容易發現,比如某個軸反了等等。曾經筆者的一個朋友就搞混了DX和OpenGL標準的法線標準,結果慘遭無情嘲諷。這是生產過程中絕對是不允許的。

基於Unity3D引擎的大地形載入研究

因為地形是依靠高度圖置換的,因此其朝向可以認為永遠朝上,所以我們就可以認為預設的Tangent永遠是(1,0,0),這樣對高度進行求導,獲取到置換後的Normal,計算置換後的Normal與Tangent的叉積,就獲得了置換後的Binormal,再利用置換後的Binormal和Normal獲取到置換後的Tangent,程式碼如下:

基於Unity3D引擎的大地形載入研究

這樣,法線的座標轉換放在了貼花計算之前,之後貼花輸出的法線將直接提供世界座標的法線,這樣就可以保證材質的法線和貼花的法線標準統一,正確的法線座標下可以看到有很明顯的凹凸感了:

基於Unity3D引擎的大地形載入研究

標註統一成功後我們就要開始著手材質的實現,毫無疑問使用貼圖記錄材質索引的方法會導致邊緣處完全沒有任何過渡,唯一的過渡就是Bilinear Interpolation提供的那點微薄的貢獻,這時肯定有美術同學問“為什麼不使用傳統的Blend Mask”,首先一張Mask圖片最多隻能提供4個通道,而我們一塊地形很有可能用十幾種材質,那麼Mask就會嚴重限制材質發揮,其次,在混合時效果也有比較嚴重的問題,在TGDC上 Trace Yang 老師也提到過這一點,一個綠草材質和一個黃沙材質混合,會得到一個黃色草地,而顯然黃色草地這種材質是並不存在的,也就是說使用Mask Blend進行材質混合的表現,是具有不可控性的。

我們的方法則是構建多個複用同一套或同幾套貼圖的材質,在生產材質時,每一套材質都具有Albedo, Normal, Smoothness, Metallic, Occlusion, Height這4個屬性,其中Height,或者稱之為Displacement,與Smoothness, Metallic, Occlusion一起放置在同一張R8G8B8A8_UNorm格式的Render Texture中,8位的精度對於材質混合來說絕對夠了。那麼之後的混合原理也非常簡單,就是簡單的計算兩套貼圖的Height的差,並按照一定的比例進行混合,而傳入到GPU中的每個材質的資料則是:

第一套貼圖索引,Int32

第二套貼圖索引,Int32

混合比例,Float32

高度偏移,Float32

原理很簡單,但是必須要有一個合適的工具來完成這個事,畢竟調整材質這種事情需要所見即所得,這裡拿出兩套材質:

基於Unity3D引擎的大地形載入研究

基於Unity3D引擎的大地形載入研究

在工具中允許開發者將多套貼圖和材質快取,實時調整材質屬性並直接看到材質表現:

基於Unity3D引擎的大地形載入研究

使用這樣的方法,我們可以按照索引順序從上到下讓材質依次從以泥土為重到以草地為重,構建多套材質,使得相鄰的索引總是相似的材質,並且混合比重按照索引的序號進行單方面步進,這樣比起傳統的Mask方法好處在於兩套貼圖的混合效果永遠在人的控制範圍內,並且具有很高的複用性。

這裡我們整個地形不用任何其他貼圖,就單純用這兩套貼圖來做個實驗,當然要多定製幾套混合材質:

基於Unity3D引擎的大地形載入研究

混合的結果:

基於Unity3D引擎的大地形載入研究

基於Unity3D引擎的大地形載入研究

可以看到,即使在只有兩套貼圖重複感非常嚴重的情況下,邊緣的混合也顯得非常自然,因為整個混合過程都在工具的控制範圍內,而Index map的生成這裡是直接使用了一張很普通的FBM Noise Map,沒有做任何特殊處理,這意味著在接下來的材質生產過程中,材質製作的時間複雜度是O(logN)而非傳統的O(n),只要一次性儲存好所有的材質屬性,之後每一塊地形都只需要提供一個大致的Indexmap,並對邊緣進行均勻取樣模糊等程式化操作,即可實現圖中的柔和混合效果,似乎無論從執行效能,開發效率還是表現效果看,基於Virtual Texture的材質索引混合方式都是要優於傳統的Mask map的方法的。

前邊同樣提到過Mask map是放置在Virtual Texture中的,這就意味著我們可以很輕鬆的實現實時繪製,因為VT本身儲存容器就是Texture Array,只需要從字典中查詢到對應的地形塊所使用的Texture Element,即可直接設其為Render Target,完成繪製,這對於未來的PCG方向非常有幫助,對美術操作也比較友好,比如這裡我們寫一個簡單的筆刷,還是上方這個例子,土地部分是1,草地部分是0, 中間穿插數個高度混合插值的材質:

基於Unity3D引擎的大地形載入研究

直接兩種材質混合極為生硬,所以我們就多調整一下筆刷,在邊緣處多刷幾種混合材質,讓表現自然起來:

基於Unity3D引擎的大地形載入研究
紅箭頭處其實在手刷時有明顯交界,但是已經幾乎看不出來

在確定了材質生產的問題後,我們還要確定另一個問題,那就是地形的精度和效能的取捨問題。Virtual Texture在大大解放了地形材質複雜度問題時,同時也帶來了沉重的視訊記憶體壓力,所有地塊使用的貼圖均為不重複貼圖,且更新極為頻繁壓縮運算成本很高已是客觀事實,所以我們在控制地形LOD距離和大小切分時,必須慎之又慎,才能平衡視訊記憶體的佔用,渲染的消耗和效果的表現,這不僅僅是美術的問題也是策劃的問題,畢竟是與遊戲表現和邏輯息息相關的。這裡我們還是啟用UV Map的方式,並且將地形高度設定為0,能夠更清楚的看清地塊的分佈:

基於Unity3D引擎的大地形載入研究

在測試場景中,最高階LOD的地塊邊長為8m,而整個地形長2048m,1024pixel/8m對於大多數高階平臺遊戲來說是可以接受的,然而這樣的設定視訊記憶體佔用卻高的可怕,單純載入攝像機周圍一圈的地塊,物件池內最高貼圖佔用可以達到160張,單純視訊記憶體佔用就已經高達1.55G!按照中端PC顯示卡8G配置,這已經很難令人接受了。我們的優化思路有兩點,首先,對Virtual Texture能提供的貼圖數量限制死,並且在四叉分割時進行判定,如果當前池內已經沒有了足夠的貼圖,就放棄LOD細分,這樣即使池內容量不夠,地形還是會正常工作,只不過表現的精度會有所損失,這種做法增強了功能的魯棒性。其次是從攝像機的角度下手,對於大多數遊戲來說,很難出現一直在不停的旋轉的情況,因此對攝像機背後的地塊“偷偷摸摸”的降低LOD等級,並在攝像機轉過去(或者將要轉過去)的時候再使用高標準LOD進行載入和渲染,這種方法在GDC的演講中也多有提到,可以說是一種無奈的妥協之舉,畢竟即使是高階平臺容量方面也依舊是不容小覷的短板。

在測試中,我們假設玩家站在地上,以腳的位置為地形的Y軸座標,使用攝像機朝向方向和攝像機座標到地塊位置座標的方向點積,判斷攝像機是否朝向地塊,對身後的地塊採用0.33倍的LOD距離,也就是說本來該載入30m的地塊現在只會載入10m就回退到上一級了,到此我們已經將總貼圖佔用控制在了128-133塊,而精度則完全沒變。這一部分我們是這樣考慮的,我們計算地形的LOD等級時,使用的是攝像機到地形邊緣的最近距離,而非到中心距離,所以實際0.3倍左右距離並不會導致LOD差異很大這樣的問題出現,因而這樣的優化和平衡是可以接受的,唯一的缺陷就是快速移動+轉頭時能感到一瞬間(3-5幀)的載入遲滯,這一點屬實無奈,在進行這樣的權衡時非常需要策劃和程式進行大量的儒雅隨和的溝(si)通(bi)。

效能表現:

效能測試中,我們使用了一臺搭載有公版GTX1070的測試機,使用的規格如下:

地形總大小/總可視大小:2048m * 2048m

Material Index Map: 2048 * 2048 / 1km^2

HeightMap: 256x256 / 8m^2

因為使用了簡單粗暴的四叉分割,地形可視距離有限,因此即使理論可以無限大,實際肉眼能看到的距離也在2km左右,所以“如果再大點效能會不會差”這樣的可能性從原理上是不會發生的。

我們對效能的測試結果為:

四叉樹計算:<0.1ms

所有載入:< 1.1ms

繪製與提交:< 0.2ms

載入整個過程是耗時最多的,但是上方表現的也只是測試中載入耗時的峰值,實際大多數幀中載入任務並不會很繁重,載入任務也如前半部分文章講的採用了均勻的分幀和多執行緒手段基本杜絕了主執行緒效能栓塞,我們認為這樣的效能表現是合格的。

實際對於載入來說,更高的消耗往往還是由上面講到的各類基於Virtual Texture材質貼花,置換貼花,在渲染繪製方面,之後文章中可能會涉及到的草地,灌木,樹木等植被渲染,峭壁石塊渲染,這些特性同樣會給效能帶來不小的壓力,因此我們的效能測試環節將會持續跟隨到每一部分。

總結:

一句話總結我們整套地形的技術特點就是:完全基於Virtual Texture和四叉分割樹,使用預製作的材質屬性和材質索引貼圖進行實時載入和著色的高度置換大地形。這套實現方法較為新穎,在技術上比起一些老的渲染方式也有一些提升和增強,配合GPU Driven Rendering Pipeline,可以大大提高開放世界場景渲染的複雜程度。當然,Ubisoft分享的地形系統還有許多其他的技術熱點,這些在之後的文章中將會逐步推出,本篇則專注講解了基本的地形載入部分,其他部分並未太多涉及,還請諸位諒解。


作者:MaxwellGeng
專欄地址:https://zhuanlan.zhihu.com/p/85417843

相關文章