0x00 前言
我想很多開發游戲的小伙伴都希望自己的場景內能渲染越多物體越好,甚至是能同時渲染成千上萬個有自己動作的游戲角色就更好了。
但不幸的是,渲染和管理大量的游戲對象是以犧牲CPU和GPU性能為代價的,因為有太多Draw Call的問題,如果游戲對象有動畫的話還會涉及到cpu的蒙皮開銷,最后我們必須找到其他的解決方案。那么本文就來聊聊利用GPU實現角色的動畫效果,減少CPU端的蒙皮開銷;同時將渲染10,000個帶動畫的模型的Draw Call從10,000+減少到22個。(模型來自:RTS Mini Legion Footman Handpainted)
0x01 Animator和SkinnedMeshRender的問題
正常情況下,大家都會使用Animator來管理角色的動畫,而角色也必須使用SkinnedMeshRender來進行渲染。
例如在我的測試場景中,默認情況下渲染10,000個帶動作的士兵模型,可以看到此時的各個性能指標十分糟糕:CPU 320+ms,DrawCall:8700+。
因此,可以發現如果要渲染的動畫角色數量很大時主要會有以下兩個巨大的開銷:
-
CPU在處理動畫時的開銷。
-
每個角色一個Draw Call造成的開銷。
CPU的這兩大開銷限制了我們使用傳統方式渲染大規模角色的可能性。因此一些替代方案——例如廣告牌技術——被應用在這種情況下。但是實事求是的說,在這種情境下廣告牌技術的實現效果並不好。
那么有沒有可能讓我們使用很少的開銷就渲染出大規模的動畫角色呢?
其實我們只需要回過頭看看造成開銷很大的原因,解決方案已經藏在問題之中了。
首先,主要瓶頸之一是角色動畫的處理都集中在CPU端。因此一個簡單的想法就是我們能否將這部分的開銷轉移到GPU上呢?因為GPU的運算能力可是它的強項。
其次,瓶頸之二是CPU和GPU之間的Draw Call問題,如果利用批處理(包括Static Batching和Dynamic Batching)或是從Unity5.4之后引入的GPU Instancing就可以解決這個問題。但是,不幸的是這兩種技術都不支持動畫角色的SkinnedMeshRender。
那么解決方案就呼之欲出了,那就是將動畫相關的內容從CPU轉移到GPU,同時由於CPU不需要再處理動畫的邏輯了,因此CPU不僅省去了這部分的開銷而且SkinnedMeshRender也可以替換成一般的Mesh Render,我們就可以很開心的使用GPU Instancing來減少Draw Call了。
0x02 Vertex Shader和AnimMap
寫過shader的小伙伴可能很清楚,我們可以很方便的在vs中改變網格的頂點坐標。因此,一些簡單的動畫效果往往可以在vs中實現。例如飄揚的旗幟或者是波浪等等。
(來源於bing搜索)
那么我們能否利用vs設置頂點坐標的方式來展現我們的角色動畫呢?
答案當然是可行。只不過和飄揚的旗幟那種簡單的效果不同,這次我們不僅僅利用幾個簡單的vs的屬性來實現動畫效果,而是將角色的動畫信息烘焙成一張貼圖供vs使用。
簡單來說,我們按照固定的頻率對角色動畫取樣並記錄取樣點時刻角色網格上各個頂點的位置信息,並利用貼圖的紋素的顏色屬性(Color(float r, float g, float b, float a))保存對應頂點的位置(Vector3(float x, float y, float z))。當然利用顏色屬性保存頂點的位置信息時需要考慮到一個小問題,在下文我會再說。
這樣該貼圖就記錄了整個動畫時間內角色網格頂點在各個取樣點時刻的位置,這個貼圖我把它稱為AnimMap。
一個AnimMap的結構就是下圖這樣的:
在實際工程中,AnimMap是這個樣子的。水平方向記錄網格各個頂點的位置,垂直方向是時間信息。
上圖是將角色的Animator或Animation去掉,將SkinnedMeshRender更換為一般的Mesh Render,只使用AnimMap利用vs來隨時間修改頂點坐標實現的動畫效果。
到這里我們就完成了將動畫效果的實現從CPU轉移到GPU運算的目的,可以看到在CPU的開銷統計中已經沒有了動畫相關的內容。但是在渲染的統計中,Draw Call並沒有減少,此時渲染8個角色的場景內仍然有10個Draw Call的開銷。因此下一步我們就來利用GPU Instancing技術減少Draw Call。
0x03 效果不錯的GPU Instancing
除了使用批處理,提高圖形性能的另一個好辦法是使用GPU Instancing(批處理可以合並不同的mesh,而GPU Instancing主要是針對同一個mesh來的)。
GPU Instancing的最大優勢是可以減少內存使用和CPU開銷。當使用GPU Instancing時,不需要打開批處理,GPU Instancing的目的是一個網格可以與一系列附加參數一起被推送到GPU。要利用GPU Instancing,則必須使用相同的材質,並傳遞額外的參數到着色器,如顏色,浮點數等。
不過GPU Instancing是不支持SkinnedMeshRender的,也就是正常情況下我們帶動畫的角色是無法使用GPU Instancing來減少Draw Call的,所以我們必須先完成上一小節的目標,將動畫邏輯從CPU轉移到GPU后就可以只使用Mesh Render而放棄SkinnedMeshRender了。
很多build-in的shader默認是有開啟GPU Instancing的選項的,但是我們利用AnimMap實現角色動畫效果的shader顯然不是build-in,因此需要我們自己開啟GPU Instancing的功能。
#pragma multi_compile_instancing//告訴Unity生成一個開啟instancing功能的shader variant
...
struct appdata
{
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID//用來給該頂點定義一個instance ID
}
v2f vert(appdata v, uint vid : SV_VertexID)
{
UNITY_SETUP_INSTANCE_ID(v);//讓shader的方法可以訪問到該instance ID
...
}
使用GPU Instancing之后,我們渲染10,000個士兵的Draw Call就從10,000左右降低到20上下了。
當然,關於GPU Instancing的更多內容各位可以在文末的參考鏈接中找到。
0x04 顏色精度和頂點坐標
還記得之前我說過在利用貼圖的紋素的顏色屬性保存對應頂點的位置時需要考慮到的一個小問題嗎?
是的,那就是顏色的精度問題。
由於現在rgb分別代表了坐標的x、y、z,因此rgb的精度就要好好考慮了。例如rgba32,每個通道只有8位,也就是某一個方向上的位置只有256種可能性,這對位置來說是一個不好的限制。
那么有沒有解決方案呢?
當然還是有的。既然這是一個和顏色的精度相關的問題,那么最簡單的方案就是增加精度。例如在寫本文的時我的Demo就是采用的這種方式,我使用了RGBAHalf這種紋理格式,而它的精度是每個通道16bit。當然,移動平台上渲染大量角色的需求往往對動畫的精確程度的要求沒有那么高,因此8bit的精度問題應該也不大。
完整的項目可以到這里到這里下載:
Render-Crowd-Of-Animated-Characters
ref:
【1】GPUInstancing
【2】How to take advantage of textures in the vertex shader
【3】GPU Gems 3 Chapter 2. Animated Crowd Rendering
【4】題圖來自《全面戰爭:阿提拉》
各位如果覺得有趣的話,歡迎點個贊。
-EOF-
最后打個廣告,歡迎支持我的書《Unity 3D腳本編程》
歡迎大家關注我的公眾號慕容的游戲編程:chenjd01