(先回答一下先前文章評論區的問題,首先本人是興趣使然的獨立開發者,普通學生,這一系列文章是全部開源的,並非為哪家大廠的項目開發,而且我也不認為目前國內哪個大廠可能會使用到這種大地形繪製技術,所以大家就當看個樂吧)

在上一篇文章中,我們已經實現了針對GPU Driven Rendering Pipeline的Cluster Scene Streaming load,然而在上一篇中卻並沒有提供出一套像樣的資源管理方案,那麼在本文中我們將介紹一套基於Unity Editor與.Net Framework的資源非同步載入,序列化反序列化的方案。

因為目前Unity並沒有理想的二進位載入方法,要麼非同步容易出現堵塞,要麼GC極高造成卡頓,因此我們將直接使用.Net Framework提供的載入方法在非同步線程載入二進位模型和貼圖文件。是的,這種方法聽起來非常的「反人類」,完全與Unity自帶的人性化資源管理背道而馳。然而現在我們的GPU Driven RP的渲染方法可以認為本身就是反人類的存在,強行拆分所有靜態模型,模型不再存在「物體」的概念,並且要求所有模型都使用統一的材質,最後所有模型還要暴力的塞到顯存中,等待渲染調用。因此,我們必須在引擎中通過成熟的工具鏈保證引擎既對開發設計人員友好,又不破壞GPU Driven RP的。

其實這篇文章拖了非常久,原因在於為了實現合理的貼圖材質載入和項目管理,本人也是嘗試許許多多的方案,最先考慮的是早期主機上使用的Virtual Texture方法,然而這個方法也是首先被斃掉了,並不是說這個方案不用了,在之後的平坦地形繪製中可能依然會使用到,只是在建築物中需要額外的CPU剔除和顯存回讀操作,之前的文章中介紹過,目前家用PC的主板IO速度非常令人窒息,所以實時的回讀操作基本是不被允許的。然而現在顯存容量卻基本達到了一個夠用的標準,6G-8G是一個比較普遍的理想的容量,部分旗艦級別的顯卡可能在10G到12G左右,大多數情況下顯存容量是絕對夠用的。同時,我們渲染時的貼圖量是不太可能太多的,因為這套管線依然是專註於繪製材質比較簡單重複量比較大的部分,比如岩石,地上的石子,一些背景房屋等等靜態部分,而遊戲中一些可能有互動的部分則依然依靠傳統的渲染管線來代替。

再來說項目管理方面,按照之前文章講過的,在開放大地形的搭建中,我們將會把場景拆分成Sub Scene/Sub Level,可能每個sub level只有幾百萬面,以此來保證流式載入的精確性,因此在本文中我們將會對Sub Level進行統一的管理,保證項目管理的效率。

正如之前所說,傳統的貼圖載入方法是無法滿足需求的,我們將使用二進位手動非同步讀取的方法完成載入。.Net提供給我們的API BinaryReader已經足夠使用了,BinaryReader支持使用相對路徑,這一點非常不錯,在打包時基本上只需要把儲存有項目二進位數據的文件夾複製到打包後的文件夾中即可,比如在現在的項目里這個文件夾被命名為BinaryData,那麼我們直接暴力讀取Assets/BinaryData/即可,在發布時也可以直接把該文件夾複製到發布的文件夾里,配合可視化的序列化工具,對項目管理非常友好。

首先,我們需要整理場景中的材質信息,把這套材質信息儲存到Scriptable Object中,因為材質信息相對輕量級,所以Scriptable Object足可以應付,數據結構大概是這樣的:

先從最上方往下看,ClusterProperties將儲存所有場景的信息的索引,往下走Name和ClusterCount儲存著場景的名稱和cluster數量,載入方式在之前文章中已經講到了,下方的Properties則是所有材質的屬性信息,如_Color等都會被工具自動收集並存儲到這裡,那麼材質使用的貼圖呢,貼圖則都儲存在下方的TexPaths中,包括_MainTex, _BumpMap這些,都已GUID的形式儲存起來,這樣在載入時可以直接使用guid當文件名去讀取,也可以使用guid查重防止重複載入,Unity自帶的AssetDatabase基本可以保證guid的唯一性和可靠性。

有了這樣的數據結構,接下來就是在非同步載入中實現對位,之前提到過,這裡使用gpu driven rp繪製的部分貼圖重複率比較高,所以我們也提供了一個比較笨拙但是勉強能用的方案來處理貼圖的載入,也就是使用RenderTextureArray來儲存貼圖。在把貼圖以guid的形式全部序列化成二進位信息之後,傳遞給StructuredBuffer,再使用Shader逐像素的寫入到RenderTexture中,經過實際測試,這樣的方法效率要遠高於CopyTexture。

在經過非同步線程讀取二進位,主線程傳顯存,並使用字典去重,防止出現多餘的載入,這時需要用到的所有的貼圖都進入了同一個TexArray里,那麼這時該如何使用這個貼圖呢,在Shader中我們是這樣實現的:

為了對團隊內其他TA更加友好,在編寫Shader時採用了傳統Surface Shader的封裝方法,這樣輸入輸出可以做到和光照運算解耦合。傳入的uv自不必說,index是來自appdata,也就是我們手動傳入的cluster信息,這個在之前的非同步線程中已經對接到屬性了,因此這個index是直接指向屬性的,這裡讀取PropertiesBuffer,獲得屬性。

屬性的定義如下:

比較值得一提的是這裡的textureIndex,textureIndex使用了一個int3的數據類型,其中包含的3個索引分別是Albedo,BumpMap和Specular Map,如果索引的值為-1,則表示當前材質並沒有使用該貼圖,否則必須強迫所有材質的貼圖都非空,會對美術工作造成一定的困擾,這種情況我們是不想的。

這樣,貼圖載入的過程可以大概如下:

  1. 非同步讀取硬碟二進位數據
  2. 根據GUID獲得貼圖索引(若貼圖已經存在,則跳過第5,6步)
  3. 將貼圖索引寫入PropertyValue
  4. 將PropertyValue寫入全局統一的StructuredBuffer
  5. 非同步載入貼圖數據(若貼圖GUID已經存在,則跳過這一步)
  6. 將數據寫入到顯存中(若貼圖GUID已經存在,則跳過這一步)
  7. 結束協程,完成載入

用UML Sequency Diagram表現一下,大概是這樣的:

當然這一篇文章只能屬於整個資源載入的序章,因為這方面太過龐大,對工具鏈依賴很重,所以目前真的只停留在可以畫個材質球上(笑),在接下來的幾篇文章中,我們將嘗試使管線與一些現有的工程模式進行融合,以便使GPU Driven RP更快的投入到生產中。


推薦閱讀:
相关文章