GPU Skinning 加速骨骼動畫

原文鏈接:GPU Skinning 加速骨骼動畫

這是侑虎科技第222篇原創文章,感謝作者程可汗供稿提供了優化思路以及相關案例。同時,UWA根據作者提供的案例在不同的移動設備上進行測試和對比,並總結成此文,希望對使用骨骼動畫的朋友有所借鑒。當然,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群:465082844)

相關Github:chengkehan/GPUSkinning

一、起因

我們知道,場景中有很多人物動畫模型的時候,性能會產生大量開銷。這些開銷除了 Draw Call 外,很大一部分來自於骨骼動畫。Unity 內置了 GPU Skinning 功能,但筆者測試下來並沒有對整體性能有任何提升,反而增加了不少。有很多種方法來減小骨骼動畫的開銷,每一種方法都有其利弊,都不是萬金油,這裡介紹的方法同樣如此。其實本質還是由我們自己來實現 GPU Skinning,只是和 Unity 內置的 GPU Skinning 有所區別。

使用了 ShadowGun中的角色模型

開啟 Unity 內置的 GPU Skinning

從上圖中可以看到,Unity 調用到了OpenGL ES 的 Transform Feedback 介面,這個介面至少要在 OpenGL ES 3.0 中才有。

這次我們要動手實現的就是這個過程,但是不使用 Transform Feedback,因為要保證在 OpenGL ES 2.0 上也能良好運行,況且Unity引擎也沒有提供這麼底層的介面。

大致的步驟如下:

  • 將骨骼動畫數據序列化到自定義的數據結構中。這麼做是因為這樣能完全擺脫 Animation 的束縛,並且可以做到 Optimize

    GameObjects(Unity 中一個功能,在不丟失綁點的情況下將骨骼的層級結構 GameObjects 完全去掉,減少開銷);
  • 在 CPU 中進行骨骼變換;
  • 將骨骼變換的結果傳遞給 GPU,進行蒙皮。

很簡單的三大步驟,對於傳統的骨骼動畫來說沒有任何特殊情況,下面我會對其中的每一步展開說明,並將其中的細節描述清楚。

二、實現

1)提取骨骼動畫數據

Unity 中的 Animation 數據

這個步驟的目的就是將這些數據提取出來,存儲到自定義的數據結構中。代碼大致如下:

其中有兩個注意點。第一,要清楚 AnimationCurve 中提取出來的旋轉量是歐拉角還是四元數。這裡我一開始就弄錯了,想當然認為是歐拉角,所以隨後計算得到的結果也就錯了。第二,用來旋轉的四元數,必須是單位四元數(模是1),否則你會得到 Unity 的一個報錯信息。

以上的代碼中,我將每一幀的數據以 30fps 的頻率直接採樣了出來,其實也可以不採樣出來,而是等需要的時候再從 AnimationCurve 中採樣,這樣會更平滑但是運行時的計算量也更多了。

2)骨骼變換

骨骼變換是所有代碼的核心部分了,看似挺複雜,其實想清楚後代碼量是最少的:

簡單來說骨骼變換就是一個矩陣乘法,比如 bone0(簡寫為b0) 是 bone1(簡寫為b1)的父骨骼:

注意這裡是矩陣左乘(從右往左讀),trs 是 Matrix4x4.TRS,也就是從 AnmationCurve 採樣到的數據。

Bindpose 的作用是將模型空間中的頂點坐標變換到骨骼空間中(是骨骼矩陣的逆矩陣),然後應用當前骨骼的變換,沿著層級關係一層層地變換下去。

3)蒙皮

蒙皮CPU部分的代碼如下:

由於骨骼數量固定為 24,所以圖中的 96 = 24 x 4

使用 SetMatrixArray 其實有點浪費了,因為對於一個 4x4 的矩陣(四個 float4 )來說,最後一維永遠是 (0, 0, 0, 1),所以可以使用 3x4 的矩陣(三個float4)代替,這樣就減少了數據傳遞的壓力。

現在所有的骨骼變換矩陣已經傳遞到 Shader 中了,就可以使用這些數據來進行蒙皮(變換頂點坐標)。

三、改進

此時所有角色的動作都是同步的。接下來進行改進,不再使用 uniform array 的方式來傳遞數據,而是將骨骼動畫數據存儲到紋理中,並加以一定的差異化,避免所有角色的動作完全同步的問題。在運行的最開始,將所有幀的動畫數據存儲到紋理中,代碼如下:

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 中進行,真機上的測試數據驗證了該方法能夠較大地提升多角色場景的運行效率。該方法具備以下優點:

  1. 極大地降低 MeshSkinning.Render 的CPU耗時,同時還可以去除對 Animator 組件的依賴,從而完全避免 MeshSkinning.Update 和 Animator.Update 的 CPU 佔用;
  2. 通過紋理保存動畫數據,只需要少量內存開銷即可帶來巨大運行效率提升;
  3. 適用於大規模群體動畫模擬,如 MMO、RTS 等遊戲類型。

當然,該方法在當前也存在如下局限性:

  1. 增加 GPU 運算負擔;
  2. 當前的 Shader 實現中使用了 tex2Dlod,該 API 在某些低端機型上可能存在適配問題;
  3. 目前還無法直接處理動畫事件、動畫融合等操作,需要研發團隊進行進一步開發。

文末,再次感謝程可汗的分享,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群:465082844)。

也歡迎大家來積极參与U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!


推薦閱讀:

為什麼近幾年NVIDIA高端顯卡越來越貴?
GPU 中的計算單元之間能不能通信?
高通和英偉達,誰能成為汽車處理器的贏家?
GPU編程的IO瓶頸如何解決?

TAG:图形处理器GPU | 骨骼 | Unity游戏引擎 |