這家公司太牛逼了,要重新造輪子!骨骼動畫

最近迷上的一首好歌,周深的《大魚》,分享給大家!



01

造輪子播放骨骼動畫


今天要介紹的是骨骼動畫的基本原理和一些常用優化手段,本文也不涉及任何骨骼動畫API的使用, 只涉及底層原理機制。


在寫這篇文章的時候,爲了保證講的內容並不是紙上談兵,我專門花時間造了個輪子: spine-player


輪子代碼在這裏:


https://github.com/laomoi/spine-player


這次造的輪子還是跟以前一樣,不使用任何渲染引擎(Unity,Cocos), 使用Typescript編寫骨骼動畫的核心代碼, 渲染層通過自己封裝webgl的接口進行渲染。


Spine骨骼動畫是2D的骨骼動畫軟件,   本文裏提到的骨骼動畫原理, 不僅僅適用於像Spine這樣的2D骨骼,也同樣適用於3D骨骼, 主要的區別僅僅是頂點座標多了個z維度, mesh頂點數量多出好多倍而已。


02

爲什麼我們需要骨骼動畫


先看下面這個動畫:



如果不使用骨骼動畫,那麼美術同學當然也可以通過mesh deform(網格變形)來做出這個動畫, 但是一旦人的動作需要調整,所有的mesh都要調整一遍, 我估計美術同學會罵街。


而且這種不用骨骼做出來的動畫, 在存儲動畫數據的時候,每個頂點都有關鍵幀的信息需要存儲,導致存儲量巨大


所以骨骼動畫是應運而生, 它把這樣的動畫拆解成2部分:


骨骼動畫 = 骨架動畫(Rigging) + 蒙皮(Skinning)


怎麼理解這個意思呢?


我們先假設角色內部是由一個骨架構成的,頭髮,皮膚這些就是蒙在對應的骨骼上面, 當骨骼動起來的時候, 蒙在上面的皮也會對應動起來。


像圖中這個妹子的骨骼可能就只有20多根, 美術調動作只需要調整這20多根骨骼的位置就可以了,存儲的也是這20根骨骼的動畫信息,需要存儲的信息量大大減少


而對於某些局部的細節, 比如乳搖這樣的效果, 可以通過多定義幾根骨骼,用權重蒙皮來實現效果, 也可以用最原始的非骨骼方法,直接對這個局部mesh做deform動畫。


所以, 骨骼動畫和 非骨骼的mesh 變形動畫都是美術需要的,2者其實是可以互相結合的。而Spine軟件也支持這2種做法。



下面這個哥布林是Spine官方提供的demo, 圖片中的效果是使用本文造的輪子代碼播放出來的動畫效果:

可以看到造的輪子實現了以下功能:

骨骼動畫+權重蒙皮+mesh deform + 動態網格替換(眼睛部分)+ 多種插值方式


如果想對骨骼動畫的實現原理有進一步理解,建議把倉庫clone下來看代碼,  因爲只實現了spine的核心功能,更容易看懂。



03

骨骼動畫的拆解


我們先看一下像下圖這樣的動畫是怎麼拆解成骨骼動畫的:


首先我們在非動畫狀態(也就是setup pos下)下進行骨架的創建和蒙皮的綁定

下圖是我們創建好的骨架:

在骨架上蒙好皮之後是這樣的:

接着我們讓骨架子動起來,會看到對應的蒙皮也會一起動:

下面我們針對骨架動畫 和 蒙皮實現分別進行深入闡述。


04

骨架動畫(Rigging)


1.構成骨架的骨骼之間存在父子關係, 父骨骼動起來的時候會帶動子骨骼運動




2.單根骨骼內部是使用關鍵幀進行插值的

下面看2個骨骼的關鍵幀動畫例子

這個hip是骨盆骨骼,它的屬性關鍵幀只包含了移動,也就是隨着動畫播放,它的xy會發生變化,變化的值使用關鍵幀之間插值得來。


這個torsor是軀幹骨骼, 它的屬性關鍵幀只包含了旋轉(變形暫且忽略),但是因爲它的父骨骼是hip骨骼,也就是隨着動畫播放,它不僅在做旋轉,也在跟着父骨骼做上下移動。


3.不同的關鍵幀插值方式

關鍵幀插值主要包括線性插值和貝塞爾曲線插值


1)先來看最簡單的插值線性插值

線性插值的公式很簡單, 開始值是P1, 結束值是P2, 中間某幀的值P, 假設P1到P2變化使用了1秒時間,中間該幀處於t秒(0<=t<=1)

那麼P = P0 (1-t)+ P1*t

P的值可以是rotation, scale, xy 等


2)接着是三階貝塞爾曲線插值

曲線插值是應用比較廣的插值方式,它不像線性插值在開始結束的地方比較生硬, 三階的貝塞爾曲線多了2個控制點,很方便美術去控制曲線的形狀


我們假設2個控制點的座標是 (c1, c2), (c3, c4),

那麼P = 貝塞爾插值(P1, P2, t, c1, c2, c3, c4)


這個求值函數應該怎麼實現呢?在實際實現中常用的方法是用多根首尾相接的折線來模擬這根曲線, 我們先判斷t落在哪根折線上, 在線段內部做線性插值即可。

如上圖所示,我們把0到1分別均分成10個值,


然後根據三階貝塞爾曲線公式:



把T=0.1,0.2,0.3....0.9代入公式, 即可求得圖中這9個紅點的座標


然後判斷t落在哪2個點座標之間, 接着在這2紅點之間線段內進行線性插值即可


具體代碼可以參考倉庫中的 SpineBezierUtils.ts


5. 骨架動畫的計算目標


我們計算骨架動畫,最終的目的, 是要算出在某個時刻, 所有骨骼在根節點下的變換矩陣, 我們暫且稱之爲 世界變換矩陣


計算方法如下

1)對所有的骨骼, 根據當前時間, 和動畫關鍵幀, 插值得到屬性(xy, rotation等)

2)計算所有骨骼的本地變換矩陣(local transformation)

3)從根骨骼開始, 從上到下計算所有骨骼在骨骼根節點空間上的變換矩陣(world transformation)


第1步裏需要計算插值, 前面已經講到了,不再冗述。

第2步需要計算所有骨骼的本地變換矩陣, 這個其實不難, 一般骨骼的屬性有 xy, rotation, scale, shear(變形),

我們用一個4x4的矩陣來存儲這個變換信息,

通常2D裏只會使用到 a, b, c, d, x, y 這些項

如果 旋轉角度A + 斜切角度(shearX, shearY) + 縮放(scaleX, scaleY) + 平移(x, y)

我們可以得到最終的本地變換矩陣:

第3步, 我們需要從根骨骼開始計算每根骨骼的最終世界變換矩陣

假設從上到下4根骨骼: 根->爺->父->我

依次計算如下:(w表示世界矩陣, l表示本地矩陣)

計算順序很重要,所以一開始需要對骨骼做好排序,然後依次計算即可。

到這裏爲止,我們已經得到了某根骨骼最終的世界變換矩陣, 接下來進入蒙皮計算。


05

蒙皮(Skinning)


1. 蒙皮是什麼?

mesh的頂點跟某根骨骼綁定, 可以想象成mesh的父節點就是這根骨骼


我們假設mesh中某個頂點相對這根骨骼的座標是x1, y1, 那麼這個頂點在骨骼空間裏的最終座標爲:


2. 蒙皮的過程

我們先來考慮最簡單的蒙皮, 也就是mesh只綁定在一根骨骼上的情況

如圖所示, 我們有2根骨骼, 上面蒙了一層mesh, 每個頂點只綁定了一根骨骼,我們假設2根骨骼接縫處這個綠色的頂點綁定的是左邊1號紅色骨骼。

那麼當骨骼轉動如下的時候:

根據蒙皮的計算, 綠色這個頂點是跟着骨骼1走的, 所以會得到如下的效果:

這種情況下看上去就不太自然,關節處感覺凸出了一個角。


所以一般來說,在這種關節處,頂點一般會綁定多根骨骼,

像圖中這種情況我們可以讓綠色頂點綁定左右2根骨骼,並分別給與50%的權重W。

然後我們使用LBS(linear blend skinning)的方法,來計算這個綠色頂點的最終位置


計算方法如下

首先我們假設綠色頂點如果只綁定了骨骼1, 它的實際座標會是P1,

然後我們假設綠色頂點如果只綁定了骨骼2, 它的實際座標會是P2,

然後我們把2個座標進行線性混合, P = P1w1 + p2w2,   (w1+w2=1)

P就是最終的座標, 如下圖所示

橙色點座標就是最終混合後的座標。


爲了讓這個關節處更平滑,如果我們在這個關節處左右兩邊再增加一些頂點,靠左邊的頂點,給與骨骼1的權重大一些,

靠右邊的頂點,給與骨骼2的權重大一些,最後我們混合完所有頂點座標後,就能得到如下比較平滑的效果:

可以看到這種情況下關節處會比較平滑不再那麼生硬。


3. 蒙皮的計算公式

假設某個頂點綁定了 2根骨骼

Bone1:  x1, y1, w1 

Bone2:  x2, y2, w2


下圖是哥布林手上拿的木棍的mesh的蒙皮設置:

圖中該點綁定了上下兩根骨骼, 權重大約是一半一半, 我們把骨骼的動作幅度故意調大一點可以看到骨骼動起來後,該點座標的變化情況:

可以看到該點由於權重混合的原因,位置始終介於2根骨骼的中間位置。



06

更多細節


1. 網格變形(Mesh Deform)

前面提到過,對於局部的mesh, 美術可以自由調整mesh的頂點位置,從而實現一些非骨骼動畫,

這種mesh頂點通過本身的xy關鍵幀插值產生的動畫叫 mesh deform

在上圖中哥布林的頭部的耳朵部分,主要使用了這種mesh動畫來實現。


這種mesh deform公式也很簡單,先對mesh頂點在時間軸根據關鍵幀做插值計算,  算出的值再代入蒙皮公式:



公式跟以前是一樣的,只是x1, y1, x2, y2是根據動畫時間做動態計算。


2.動作融合(Cross Fading)

當骨骼正在播放某個動作到一半時,如果需要切換到另外一個動作, 如果硬切, 就會產生生硬的效果

通常我們可以使用動作融合的方式,讓2個動作在某個短時間內做混合,最終過渡到第2個動作


07

可能的優化手段


經過上面的講解,我們可以總結一下, 骨骼動畫播放的全過程,其實就是:


Rigging (+Deform) + Skinning + Rendering


這裏面每一個步驟都可能有優化的空間,每種優化的手段都是根據性能瓶頸原因和優化目的來做的。


1.Rigging階段的優化

我們發現骨架動畫這塊, 有3步計算, 骨骼越多計算量越大, 對於那些循環播放的骨骼動畫,有很多的計算總是重複在計算。

所以我們其實可以在骨骼動畫初始化的時候,就把每一幀,每一個骨骼的世界變換矩陣先計算好存起來,在後面使用到的時候, 直接取相應矩陣就可以了。

這其實也是一種空間換時間的方法。

如果我們把這個計算再提前到程序運行之前,那就有點類似於離線烘焙動畫了(baking animation)

計算完的世界矩陣,我們可以放到內存裏,也可以序列化到磁盤上。

如果在下一步優化裏需要使用到gpu skinning, 我們甚至可以把這個世界矩陣序列化到一個臨時的紋理上, 然後傳入着色器。


2.Skinning階段的優化

如果我們發現模型的mesh頂點數太多,做skinning消耗了太多CPU時, 我們可以考慮對這個模型做GPU Skinning

我們先把相關所有骨骼的動畫數據先烘焙成一張紋理,當做uniform傳入頂點着色器,

然後把每個頂點相關的骨骼索引,權重, 相對xy信息,當做頂點數據的屬性傳入, 這樣就可以在頂點着色器裏做skinning.


具體思路大概如下:


1)動畫烘焙成紋理

我們假設動畫一共30幀,骨骼一共4根,每根骨骼在每一幀有一個世界矩陣要存儲, 而這個矩陣,在2D裏,最下面2行都是常數

如下圖:

我們把上面2行一共8個數值存儲到上下2個相鄰像素裏即可(32位紋理)

30幀,4根骨骼, 可以輸出寬度30, 高度8的紋理圖片。


然後大家發現單通道是1個字節時,存儲矩陣的浮點數可能存在精度丟失問題, 我們可以考慮把紋理改成 單通道4字節的紋理, 又或者是用4倍的像素數量來存儲這個矩陣(但是這樣會造成頂點着色器採樣次數過多的情況)。


2)頂點着色器做skinning

假設有4根骨骼,  b1, b2, b3, b4

每根骨骼的蒙皮需要 bone索引, x, y , w

我們可以使用4x4的矩陣來存儲這16個數值,當做頂點屬性,直接傳入頂點着色器就可以了。


頂點着色器VS拿到了上面baking出來的骨骼數據,又知道了蒙皮信息,那在VS裏做蒙皮計算就只是幾個乘法加法而已。


3.Rendering階段的優化

如果有這樣的場景, 有大量的草叢(1000個)需要繪製, 單個草叢本身是一個如下的spine動畫:


默認的渲染,會造成大量的drawcall和大量的帶寬開銷,



如果草叢之間僅僅是xy/rotation/scale等不同, 可以考慮使用GPU instancing來解決。


每個草叢之間僅僅只有transformation matrix不一樣,我們可以把每個草叢的matrix拼接起來放到一個buffer裏面作爲instancing時的頂點屬性集合。



在實際繪製的時候,傳入GPU只有一個Spine mesh的頂點數據,draw call命令也只有一條, 額外需要做的僅僅是告訴GPU, 需要把這個mesh畫1000次,每一次實例繪製,從之前的buffer裏取出不同的變換矩陣即可。


不同的圖形API都有相應的方法來實現instancing, 不過要注意目前已知是opengles 3.1才比較好的支持了這個特性。


instancing畫出來的結果是這樣的, 滿幀60幀在跑,壓力全在GPU這。

觀察spector.js記錄的渲染過程:

只做了一次drawcall, 通過instancing可以畫出1000個實例。


--------------------------------------------


樂府互娛 是9102年成立的的一家非常年輕的遊戲公司, 位於上海目前遊戲新秀扎堆的漕河涇開發區, 有興趣進一步瞭解的可以點開我們的官網:http://www.lovengame.com。


目前我們正在熱招的技術崗位有:

  • unity資深開發 

  • cocos資深開發

  • 圖形程序員

  • golang遊戲開發

  • golang平臺開發

本文分享自微信公衆號 - Creator星球遊戲開發社區(creator-star)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章