GPU Skin
轉自:http://geekfaner.com/unity/blog4_GPUSkin.html
GPU Skin這門技術在端游時代屬于標配,特別是MMO游戲,但是手游時代就要case by case了,因為手機的GPU資源還是很珍貴的(后處理之類的)。作為技術人員,這門手藝不能丟,所以還是說一下GPU Skin。下面需要先轉載三篇文章,介紹了兩種GPU Skin的原理以及Unity中GPU Skin的使用方法。,一篇來自陳嘉棟(慕容小匹夫),鏈接為:http://www.cnblogs.com/murongxiaopifu/p/7250772.html。一篇來自UWA,鏈接為:https://blog.uwa4d.com/archives/Sparkle_GPUSkinning.html。還有一篇來自Gad-騰訊游戲開發者平臺,鏈接為:http://www.sohu.com/a/127714410_466876。
利用GPU實現大規模動畫角色的渲染(轉載自陳嘉棟(慕容小匹夫),鏈接為:http://www.cnblogs.com/murongxiaopifu/p/7250772.html)
前言
我想很多開發游戲的小伙伴都希望自己的場景內能渲染越多物體越好,甚至是能同時渲染成千上萬個有自己動作的游戲角色就更好了。
但不幸的是,渲染和管理大量的游戲對象是以犧牲CPU和GPU性能為代價的,因為有太多Draw Call的問題,如果游戲對象有動畫的話還會涉及到cpu的蒙皮開銷,最后我們必須找到其他的解決方案。那么本文就來聊聊利用GPU實現角色的動畫效果,減少CPU端的蒙皮開銷;同時將渲染10,000個帶動畫的模型的Draw Call從10,000+減少到22個。(模型來自:RTS Mini Legion Footman Handpainted)
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了。
Vertex Shader和AnimMap
寫過shader的小伙伴可能很清楚,我們可以很方便的在vs中改變網格的頂點坐標。因此,一些簡單的動畫效果往往可以在vs中實現。例如飄揚的旗幟或者是波浪等等。
?
那么我們能否利用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。(Patrick:飄揚的旗幟是無法使用批處理合并DC的,因為在VS中需要使用到模型在模型空間的坐標,而通過批處理合并DC之后,就只剩下世界空間的坐標了,而這篇文章介紹的這種GPU Skin也無法使用批處理的,因為VS中使用到了SV_VertexID。而且如果使用GPU Instancing的話,可以通過SV_InstanceID使得每個物件不同步的播放動畫。還一個問題,就是有些GPU(DX9/11)在VS階段不支持tex2D運算,因為在VS階段Shader無法拿到lod信息,所以在這些GPU中的VS階段中訪問shader,需要用tex2Dlod函數)
效果不錯的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, uint iid : SV_InstanceID) {UNITY_SETUP_INSTANCE_ID(v);//讓shader的方法可以訪問到該instance ID... }?
使用GPU Instancing之后,我們渲染10,000個士兵的Draw Call就從10,000左右降低到20上下了。
當然,關于GPU Instancing的更多內容各位可以在文末的參考鏈接中找到。
顏色精度和頂點坐標
還記得之前我說過在利用貼圖的紋素的顏色屬性保存對應頂點的位置時需要考慮到的一個小問題嗎?
是的,那就是顏色的精度問題。
由于現在rgb分別代表了坐標的x、y、z,因此rgb的精度就要好好考慮了。例如rgba32,每個通道只有8位,也就是某一個方向上的位置只有256種可能性,這對位置來說是一個不好的限制。
那么有沒有解決方案呢?
當然還是有的。既然這是一個和顏色的精度相關的問題,那么最簡單的方案就是增加精度。例如在寫本文的時我的Demo就是采用的這種方式,我使用了RGBAHalf這種紋理格式,而它的精度是每個通道16bit。當然,移動平臺上渲染大量角色的需求往往對動畫的精確程度的要求沒有那么高,因此8bit的精度問題應該也不大。
完整的項目可以到這里到這里下載:Render-Crowd-Of-Animated-Characters
Patrick:上面這個方法非常酷炫,用空間換取時間,用紋理保存了所有頂點在關鍵幀的位置信息,這樣優點是連GPU Skin都不需要了。。GPU中不需要計算頂點位置,直接從紋理中讀取即可。缺點是紋理大小是個問題,粒子中用的是512個頂點(或者以內)的mesh,采樣了32幀(或者以內),使用RGBAHalf的格式,占內存大小是512*32*64/(8 * 1024) = 128KB,對于頂點數量數千的角色mesh,一個動作就超過1M,一個角色少數也有一二十個動作,那么一個角色就一二十M,即使不是同時出現的動作,內存不會同時占用,那么包體大小也是個問題(不過針對頂點數量少的還是不錯的,附上一個我從UE4.16源碼中扒出來的3D Max腳本VertexAnimationTools,用于將動作生成AnimMap)。所以下面要介紹的另外一個方案,是屬于比較規矩的GPU Skin,在VS中通過骨骼和模型在模型空間的坐標做GPU Skin。
GPU Skinning 加速骨骼動畫(轉載自UWA,鏈接為:https://blog.uwa4d.com/archives/Sparkle_GPUSkinning.html)
起因
我們知道,場景中有很多人物動畫模型的時候,性能會產生大量開銷。這些開銷除了 Draw Call 外,很大一部分來自于骨骼動畫。Unity 內置了 GPU Skinning 功能,但筆者測試下來并沒有對整體性能有任何提升,反而增加了不少。有很多種方法來減小骨骼動畫的開銷(Patrick:減小骨骼動畫的開銷?不知道這個作者是不是想減小Unity AnimationClip的大小,由于每一幀中不是所有的骨骼都會動,那么將關鍵幀中不需要使用的骨骼信息刪掉就可以減小這個大小。然后通過Unity提供的壓縮格式對AnimationClip進行保存,至于使用哪個壓縮格式,參考UWA文檔Unity加載模塊深度解析之動畫資源),每一種方法都有其利弊,都不是萬金油,這里介紹的方法同樣如此。其實本質還是由我們自己來實現 GPU Skinning,只是和 Unity 內置的 GPU Skinning 有所區別。
?
使用了 ShadowGun中的角色模型
開啟 Unity 內置的 GPU Skinning
從上圖中可以看到,Unity 調用到了OpenGL ES 的 Transform Feedback 接口,這個接口至少要在 OpenGL ES 3.0 中才有。筆者理解的 Transform Feedback,就是將大批的數據傳遞給 Vertex Shader,將 GPU 計算過后的結果通過一個 Buffer Object 返回到 CPU 中,CPU 再從 Buffer Object 讀取數據(或直接將 Buffer Object 傳遞給下一步),在隨后步驟中使用。顯然,在骨骼動畫中,TransformFeedback 負責骨骼變換,Unity 將變換后的結果拿來再進行 GPU 蒙皮操作。(Patrick:在VS中計算好了之后還用TFBO傳回給CPU干啥?只是為了再重新經過另外一個VS的時候,因為不需要使用模型空間坐標了,可以批處理合并DC?)
這次我們要動手實現的就是這個過程,但是不使用 Transform Feedback,因為要保證在 OpenGL ES 2.0 上也能良好運行,況且Unity引擎也沒有提供這么底層的接口。
大致的步驟如下:
- 將骨骼動畫數據序列化到自定義的數據結構中。這么做是因為這樣能完全擺脫 Animation 的束縛(Patrick:怎么擺脫?),并且可以做到 Optimize GameObjects(Unity 中一個功能(Patrick:在哪里?),在不丟失綁點的情況下將骨骼的層級結構 GameObjects 完全去掉,減少開銷);
- 在 CPU 中進行骨骼變換;
- 將骨骼變換的結果傳遞給 GPU,進行蒙皮。
很簡單的三大步驟,對于傳統的骨骼動畫來說沒有任何特殊情況,下面我會對其中的每一步展開說明,并將其中的細節描述清楚。
實現
提取骨骼動畫數據
Unity 中的 Animation 數據
這個步驟的目的就是將這些數據提取出來,存儲到自定義的數據結構中。代碼大致如下:
其中有兩個注意點。第一,要清楚 AnimationCurve 中提取出來的旋轉量是歐拉角還是四元數。這里我一開始就弄錯了,想當然認為是歐拉角,所以隨后計算得到的結果也就錯了。第二,用來旋轉的四元數,必須是單位四元數(模是1),否則你會得到 Unity 的一個報錯信息。(Patrick:這一塊比較復雜,需要實際操作一遍。。)
以上的代碼中,我將每一幀的數據以 30fps 的頻率直接采樣了出來,其實也可以不采樣出來,而是等需要的時候再從 AnimationCurve 中采樣,這樣會更平滑但是運行時的計算量也更多了。(Patrick:還沒想到應該如何做。。)
骨骼變換
骨骼變換是所有代碼的核心部分了,看似挺復雜,其實想清楚后代碼量是最少的:
簡單來說骨骼變換就是一個矩陣乘法,比如 bone0(簡寫為b0) 是 bone1(簡寫為b1)的父骨骼:
注意這里是矩陣左乘(從右往左讀),trs 是 Matrix4x4.TRS,也就是從 AnmationCurve 采樣到的數據。
Bindpose 的作用是將模型空間中的頂點坐標變換到骨骼空間中(是骨骼矩陣的逆矩陣),然后應用當前骨骼的變換,沿著層級關系一層層地變換下去。
蒙皮
蒙皮CPU部分的代碼如下:
??
由于骨骼數量固定為 24,所以圖中的 96 = 24 x 4(Patrick:Uniform有數量限制,所以骨骼也就有數量限制。)
GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = GL_MAX_FRAGMENT_UNIFORM_COMPONENTS + GL_MAX_UNIFORM_BLOCK_SIZE * GL_MAX_FRAGMENT_UNIFORM_BLOCKS / 4GL_MAX_COMBINED_UNIFORM_BLOCKS = 24GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = GL_MAX_VERTEX_UNIFORM_COMPONENTS + GL_MAX_UNIFORM_BLOCK_SIZE * GL_MAX_VERTEX_UNIFORM_BLOCKS / 4GL_MAX_FRAGMENT_UNIFORM_BLOCKS = 12GL_MAX_FRAGMENT_UNIFORM_COMPONENTS = 896GL_MAX_FRAGMENT_UNIFORM_VECTORS = 224(ES3.0)/16(ES2.0)GL_MAX_UNIFORM_BLOCK_SIZE = 16384GL_MAX_UNIFORM_BUFFER_BINDINGS = 24GL_MAX_VERTEX_UNIFORM_BLOCKS = 12GL_MAX_VERTEX_UNIFORM_COMPONENTS = 1024GL_MAX_VERTEX_UNIFORM_VECTORS = 256(ES3.0)/128(ES2.0)使用 SetMatrixArray 其實有點浪費了,因為對于一個 4x4 的矩陣(四個 float4 )來說,最后一維永遠是 (0, 0, 0, 1),所以可以使用 3x4 的矩陣(三個float4)代替,這樣就減少了數據傳遞的壓力。
現在所有的骨骼變換矩陣已經傳遞到 Shader 中了,就可以使用這些數據來進行蒙皮(變換頂點坐標)。
改進
此時所有角色的動作都是同步的。接下來進行改進,不再使用 uniform array 的方式來傳遞數據,而是將骨骼動畫數據存儲到紋理中,并加以一定的差異化,避免所有角色的動作完全同步的問題。(Patrick:這種方法有點類似上面兩個辦法的結合,又規避了第二種辦法骨骼有限制的問題,又規避了第一種辦法紋理過大的問題,第二種辦法紋理大小,24個骨骼32幀大小為:24*4*32*32/8/1024=12KB。但是在下面的shader中只看到了Time一個變量,所以應該每個物件沒什么太大差異性的,差異性主要還是靠GPU Instance,然而由于依然是使用Skin mesh,所以這個方法沒法用GPU Instance。)在運行的最開始,將所有幀的動畫數據存儲到紋理中,代碼如下:
Shader中的蒙皮代碼相應變為:
以上就是筆者實現 GPU Skinning 的細節。但沒有一種方法是完美的,作為能夠減少骨骼動畫開銷的備選方案之一,在恰當的情況下使用會大大地提高性能。
測試
為了進一步驗證該方案在移動設備上的可行性,UWA在真機上進行如下了實驗。
我們在一個空場景中放置一定數目的模型播放動畫,對 Mecanim 和 GPU Skinning 的運行效率進行對比。模型取自 ShadowGun,具有2600面片,24根骨骼。使用 Mecanim 時,模型使用 Generic 模式,并且使用 Optimize GameObject。在紅米Note2運行1000幀的數據如下:
FPS 變化
測試場景 CPU 耗時數據
上圖是GPU Skinning方案在場景中存在300個角色時的主線程 CPU 耗時數據。不同角色數目的平均每幀 CPU 耗時(主線程)如下:
從數據可以看出,不論從整體的 FPS還是主線程平均每幀的 CPU 耗時,GPU Skinning都表現出了更好的性能,從而可以讓寶貴的 CPU 耗時用于更多的游戲邏輯。
優點和局限性
該方法將CPU中的蒙皮工作轉移到 GPU 中進行,真機上的測試數據驗證了該方法能夠較大地提升多角色場景的運行效率。該方法具備以下優點:
- 極大地降低 MeshSkinning.Render 的CPU耗時,同時還可以去除對 Animator 組件的依賴(Patrick:只需要一個animation組件和一個skin mesh組件即可。),從而完全避免 MeshSkinning.Update 和 Animator.Update 的 CPU 占用;
- 通過紋理保存動畫數據,只需要少量內存開銷即可帶來巨大運行效率提升;
- 適用于大規模群體動畫模擬,如 MMO、RTS 等游戲類型。
當然,該方法在當前也存在如下局限性:
- 增加 GPU 運算負擔;
- 當前的 Shader 實現中使用了 tex2Dlod,該 API 在某些低端機型上可能存在適配問題;
- 目前還無法直接處理動畫事件、動畫融合等操作,需要研發團隊進行進一步開發。
Patrick:轉載了兩篇文章了,我就把我自己的GPU Skin也放出來吧,用的方法和第二種方法的類似(不用紋理版),每個頂點跟三根骨骼關聯,GPU Skin主要就是VS部分,那就把skinmesh.vs貼出來。下面轉載的是騰訊對Unity內置GPU Skin的分析。
Unity引擎源碼札記:GPUSkinning(轉載自Gad-騰訊游戲開發者平臺,鏈接為:http://www.sohu.com/a/127714410_466876)
Skinning設置
Unity支持GPU Skining,構建時開啟PlayerSetting.gpuSkinning 將允許符合條件平臺使用GPU Skinning (官方文檔:DX11, OpenGL ES 3.0 and Xbox 360 can do mesh skinning on the GPU)。
BuildSetting(IOS/Android)
Android下需要額外設置(如果選擇“Force Open GL ES 2.0”,在支持ES 3.0的設備上依然不會生效GPU Skinning,原因見下文源碼分析):
適用設備(IOS/Android)
對于手機平臺,IOS/Android,Opengl ES支持版本及設備如下:
OpenGL ES 2.0 Supported by:
- The Android platform since Android 2.0 through NDK and Android 2.2 through Java
- Apple iOS 5 or later in iPad, iPad Mini, iPhone 3GS or later, and iPod Touch 3rd generation or later
OpenGL ES 3.0 Supported by:
- Android since version 4.3, on devices with appropriate hardware and drivers, including:
Nexus 7 (2013)、 Nexus 4、 Nexus 5、 Nexus 10、 HTC Butterfly S、 HTC One/One Max、 LG G2、 LG G Pad 8.3、 Samsung Galaxy S4 (Snapdragon version)、 Samsung Galaxy S5、 Samsung Galaxy Note 3、 Samsung Galaxy Note 10.1 (2014 Edition)、 Sony Xperia M、 Sony Xperia Z/ZL、 Sony Xperia Z1、 Sony Xperia Z Ultra、 Sony Xperia Tablet Z - iOS since version 7, on devices including:
iPhone 5S、 iPad Air、 iPad mini with Retina display - Supported by some recent versions of these GPUs:
Adreno 300 and 400 series (Android, BlackBerry 10, Windows Phone 8, Windows RT)、 Mali T600 series onwards (Android, Linux, Windows 7)、 PowerVR Series6 (iOS, Linux)、 Vivante (Android, OS X 10.8.3, Windows 7)、 Nvidia (Android, Linux, Windows 7)、 Intel (Linux)
引擎源碼分析
SkinnedMeshRenderer::AwakeFromLoad會創建當前平臺的GPUSkinningInfo
GLES 2.0將返回NULL,因此,若在BuildSetting時Graphic level選擇“Froce Open GL 2.0”,GPUSkinningInfo為空,即使在支持 GLES 3.0的設備上,也不進行GPUSkinning.
SkinnedMeshRenderer::SkinMesh根據條件進行CPU 或 GPU Skinning計算,
?
而skin.memExport條件取決于:
- PlayerSetting.gpuSkinning是否開啟
- 當前初始創建的GPUSkinningInfo是否為NULL( 即m_MemExportInfo)
- 是否Cloth及是否具有skin信息、骨骼權重等
源碼小結
從源碼上看,如前文1)中的構建設置選項,構建時打開PlayerSetting.gpuSkinning (Andorid平臺Graphic level: Automatic )在GLES 2.0設備上應會自動選擇CPU Skinning,在GLES 3.0設備會使用GPU Skinning,實際情況需要進行目標機型的兼容性測試。
性能測試分析
使用不同數目的角色skinned mesh 測試CPU 負載情況:
測試數據(單機,不限幀,忽略流量,測試60s)
- 隨人數增加,兩者CPU負載趨一致,GPU Skinning比CPU Skinning內存稍低;
- 隨人數增加,GPU Skinning比CPU Skinning FPS高30%左右;
- 75人以下,GPU Skinning 比 CPU Skinning的CPU負載稍低,內存較低,FPS相近(此時非GPU瓶頸);
小結
是否使用GPUSkinning策略,也取決于CPU或GPU的負載情況。如果當前的CPU負載瓶頸,GPU較輕,可使用GPUSkinning;反之,則建議使用默認的CPUSkinning。
值得注意的是,在項目中開啟GPUSkinning后,部分機型會出現蒙皮錯誤,存在兼容性問題,但是使用CPUSkinning正常。因此,慎用Unity 4.6中的GPUSkinning(Unity 5沒做測試未知),需要進行更多的兼容性測試和驗證。(Patrick:總之Unity自帶的GPU Skin貌似還是有蠻多坑的,所以還是自己寫GPU Skin吧。)
總結
- 上一篇: 天涯明月刀各阶段功力提升性价比指南
- 下一篇: 图形学教程Lecture 2: Revi