如何快速大量繪製遊戲物件?這個方法值得一試

遊資網發表於2019-10-14
有多種通過GPU實現骨骼動畫的例項化繪製方法,本文介紹的是其中的一種:將頂點資訊逐幀寫入紋理後,在頂點著色器中通過讀取動畫紋理,提取頂點位置並變換,最終實現角色動畫的方法。

本文將簡述其實現原理,並分享一個(完成了一半的)網格合併及例項化繪製工具。

如何提高繪製效率

當產生了“要將大量遊戲物件呈現給玩家”的需求時,我們就會碰到這樣一個問題:如何才能提高GPU的繪製效率?

如何快速大量繪製遊戲物件?這個方法值得一試
批量繪製較多的騎兵

通常情況下CPU對GPU發起的繪製命令,才是效能的瓶頸所在。CPU為繪製準備資料、視訊記憶體載入資料、為GPU設定渲染狀態等行為所花費的時間,通常比GPU繪製所花費的時間要多。這也就是為什麼我們經常會把DrawCall次數當成快速評判渲染效率的“KPI”。

反觀Unity提供的Static batching(靜態合批)和Dynamic batching(動態合批),也都是從減少CPU到GPU的呼叫次數為出發點,儘量一次傳送一個大的網格(一大堆頂點資料),以減少CPU和GPU的通訊次數,提高彼此的工作效率。

但是無論靜態還是動態合批,在大量遊戲物件繪製的需求面前,都不太合適。

靜態合批從名字上就知道不能用來繪製移動物體,而且其本身還會產生非常大的記憶體開銷(它需要額外的記憶體空間來儲存合併的網格);動態合批也有自己的問題,如頂點數量的限制、材質球限制、無法作用於蒙皮網格(SkinnedMeshRenderer)等,還會對CPU產生不小的壓力(因為它要不停地去動態計算併合併網格)。

例項化繪製

例項化繪製技術的出現,就是為了在不提高CPU負擔的基礎之上,解決CPU到GPU呼叫開銷大的問題。對於相同的物體(同一個網格),只需一次呼叫,GPU就會根據我們想要繪製的次數,啪啪啪一通畫,非常的高效。

但是簡單重複繪製一個物體多次(比如重複繪製1000次小兵),並沒有任何意義。為了能夠繪製出1000個不同的小兵,我們還需要提前為GPU準備一些額外的資料,比如1000個轉換矩陣(畫在不同的位置)、1000個混合色(呈現不同的顏色)等,最終在螢幕上呈現出千軍萬馬的畫面。

如果我們想要在遊戲世界上呈現非常多相同的、靜止不動的石頭,那到此為止就可以了。我們使用Unity提供的手動例項化繪製介面Graphics.DrawMeshInstanced,通過傳入同一個石頭的網格和每一個石頭的轉換矩陣,就可以實現需求(其實Unity也會自動為新增了MeshRenderer元件的單位嘗試使用例項化繪製以提高效率)。

但是對戰場中的小兵做這種簡單地操作就不太合適了。

這是因為小兵通常是採用骨骼動畫來實現動作的,而骨骼動畫對於蒙皮網格的驅動,是CPU即時計算出來的。每個小兵相同時刻的狀態可能都不同,也就是說相同網格同一時刻的頂點位置會有很大差別,因此無法直接進行例項化繪製。

既然CPU上即時計算的骨骼動畫無法進行例項化繪製,我們就不讓CPU計算,而讓這些計算髮生在GPU上,便可將問題解決。

它的原理很簡單

1、將骨骼動畫每一幀對網格各個頂點的變化結果存在一張紋理中,其中紋理的橫座標是頂點索引,縱座標是時間,而橫縱相交對應的值,是這一時刻該頂點在本地空間下的座標。

2、有了這張“頂點動畫紋理”,在頂點著色器中,我們就可以忽視傳入頂點著色器的頂點位置資訊;而以當前所處理的頂點索引為U,以動畫播放至此的時間刻度為V,從上一步的紋理座標中取樣。而取樣到的結果,就是當前這個頂點此時的位置。

3、接下來的步驟便與傳統繪製一樣,與MVP矩陣相乘做空間變換,傳入片段著色器中著色等...可以很容易的想象到,連續為網格上所有頂點設定不同時間下的空間位置,最終繪製到螢幕上時,就能呈現出動畫效果了。

一些相對重要的細節

1、用例項化ID來獲取差異例項單位的屬性

由於我們的最終目標是繪製多個不同動畫狀態的單位,因此從動畫紋理中,用於取樣資訊的時間刻度值,是根據例項化ID,從儲存例項化屬性的資料塊中獲取到的,這樣就可以實現每個例項化單位的動畫播放進度的差異。

2、合併多個不同的網格

手動呼叫例項化繪製介面時,只能傳入一個網格。而我們平時使用的遊戲物件,通常是由若干個蒙皮網格和若干個普通網格組成。比如一個騎兵模型:士兵和馬匹分別是兩個蒙皮網格;而士兵手持的武器通常是一個普通網格,以方便後期做武器替換。

如何快速大量繪製遊戲物件?這個方法值得一試
一個遊戲物件可能會由兩種、多個網格組合而成

因此我們會在編輯器模式下,將整個物件包含的網格合併成一個網格,並將這個網格儲存成資源,以便後面呼叫繪製命令時作為實參傳入。

如何快速大量繪製遊戲物件?這個方法值得一試
合併成為一個網格

3、多貼圖時處理UV

此外,有些模型上不同的網格還對應了不同的貼圖,比如網格Mesh_0,使用了貼圖Texture_0,網格Mesh_1使用了貼圖Texture_1,由於網格進行了合併,如果針對合併後的網格使用同一張貼圖,便會出現錯誤。

如何快速大量繪製遊戲物件?這個方法值得一試
胯下戰馬錯誤的顏色取樣

針對這種情況我們要在合併時做特殊處理,一種處理方式是合併多張貼圖,如將Texture_0與Texture_1合併,然後偏移原本Mesh_1的uv座標,但是這要求兩張貼圖都不能太大,否則無法合併到一張貼圖中;另一種方法是仍然保留兩張貼圖Texture_0和Texture_1,但是對Mesh_0和Mesh_1的uv2做特殊處理,如使用uv2的x儲存兩張貼圖的Lerp值。這樣片段著色器中對兩張貼圖的取樣結果做二次計算後,就可以得到正確的顏色了。

如何快速大量繪製遊戲物件?這個方法值得一試
為戰士和戰馬分別替換貼圖

4、動畫的混合

通過紋理實現的動畫也可以實現簡單的混合效果,它是通過在頂點著色器中對多個動畫紋理進行取樣,然後根據一個混合比例,對多個位置資訊進行計算以實現的。

如何快速大量繪製遊戲物件?這個方法值得一試
根據速度一維向量進行的Locomotion狀態混合

5、脫離了Renderer的渲染

由於是直接呼叫了Graphics.DrawMeshInstanced進行的繪製,因此並沒有GameObject被建立出來,減少了物件的建立數量,一定程度上也減少了記憶體及CPU的開銷;但是需要自己在loop中組織資料的更新及渲染的更新。

如何快速大量繪製遊戲物件?這個方法值得一試
脫離了GameObject+Renderer的繪製

使用動畫紋理的優缺點

優點

1、易於理解、易於實現;

2、CPU的計算(合併網格、記錄動畫資訊)發生在編輯器階段,遊戲執行時CPU沒有額外的開銷;

3、可以實現例項化繪製,充分發揮GPU的繪製效率。

缺點

1、記錄頂點動畫的紋理大小,一方面取決於模型的頂點數量,另一方面取決於動畫的長度,如果頂點數量過多,或動畫過長,生成的紋理就會很大,對視訊記憶體的佔用量也會上升;

2、實現動畫混合,需要從多個動畫紋理中取樣並進行計算,取樣次數多;

3、無法使用動畫狀態機控制動作;

4、動作資訊在儲存時會受儲存格式的精度影響,因此讀取出來的動畫可能不夠精確;

5、無法實現骨骼動畫中的IK(反向動力學)等。

雖然有不少缺點,但是如果你的目的是大批量繪製環境裝飾(樹、草、石頭)或細節要求不高的雜魚小兵、路人,它都是你實現目的優秀手段,值得你去使用它。

最後

最後,分享一個沒有寫完的網格合併及例項化繪製工具,可以實現上述簡單的功能。

如何快速大量繪製遊戲物件?這個方法值得一試
通過工具生成動畫資原始檔

如何快速大量繪製遊戲物件?這個方法值得一試
簡單的動畫播放

如何快速大量繪製遊戲物件?這個方法值得一試
大批攜帶動畫角色的例項化繪製

工具及Demo下載地址:https://github.com/elsong823/AnimationBaker

來源:偶爾學學Unity

相關文章