Skeleton with Assimp 骨骼動畫解析

  • 2019 年 10 月 3 日
  • 筆記

Skeleton with Assimp 骨骼動畫解析

骨骼動畫是圖形學中十分常見應用很廣泛的一個技術,也是比較基礎的內容,作為圖形學的工程師需要將這一部分內容梳理清晰,主要關鍵在於幾點:第一,分清楚骨骼節點兩個概念;第二,熟悉使用 Assimp(或者其他的)的解析方式,並編程實現骨骼的解析和動畫的播放。

理解骨骼

首先,為什麼會有骨骼動畫這麼一種東西的存在呢?如果我們從我們自己的身體上觀察,就可以發現,我們全身可以活動的部分,其內部基本都有一根主要的骨頭,比如小臂的揮動,小臂上所有的肌肉皮膚都一起和骨骼運動。回過頭來看 3D 繪製,通常我們需要繪製的是一個 mesh, 也就是物體的表面部分,可以認為是一張皮。我們希望繪製的對象也可以像人體一樣做一些動作。那麼同樣的,我們將這張皮上面的每一個最小單位,如頂點(vertex)都綁定一根骨頭上去,骨頭怎麼動,皮就怎麼動。這個部分,叫做蒙皮(skin rigging),是由藝術家完成的[1](真實情況中,一個點可能受多個骨骼影響,需要確定具體的權重)。這樣,我們只需要考慮有限的幾個骨頭的運動就可以描述人體整體的運動,簡化了很多,同時也是對自然規律的模擬。

節點又是什麼?

節點就是一個點,在骨骼的語境中,可能稱為「關節」,但是關節不同與骨骼。Keep in mind: 骨骼是有長度的線段,僅有空間中一個點的位置無法描述骨骼。只有用兩個點,才能組成一條線段,只有線段才能代表骨骼。即使在 Unity 或其他軟件中,用 node, 節點,關節 代表骨骼,但是心裏要清楚這一點。如下圖,RA 是一個骨骼,RB, BC 都分別是骨骼,但是我們不能說 A 是一個骨骼,單獨提 A 是沒有意義的,這隻能是一個點。

藝術家的工作,將所有的頂點,都和骨骼綁定起來,顯然,下圖這個骨骼的配套的 mesh 的上面部分的頂點,都與 RA 骨骼綁定起來了,而在左下方和右下方的頂點,基本綁定在 BC 和 DE 兩個骨骼上。在這幾個節點處的頂點,會與多個骨骼綁定,每個骨骼有一定的權重。同時,藝術家會提供一個動畫的關鍵幀的骨骼的姿態,即在關鍵幀時,每一個骨骼的位置。

    A -                            B---- R ----- D    |             |    |             |    |             |    C             E    ------------------    R -> A  R -> B -> C  R -> D -> E  

根據藝術家提供的數據,究竟我們怎麼確定每一個點的位置呢? 首先骨骼之間存在父子結構關係,上圖的 RB 骨骼是 BC 骨骼的父親。我們也可以用節點來描述,那麼就是 R 點是 B 點的 父親, B 點是 C 點的父親(如上圖箭頭所示)。在父子層級關係上我們用骨骼或者用節點都是可以描述的,其本質上描述的是同一個骨骼,就是圖中畫的那樣。Assimp 用來幫助解析 FBX 文件,我們從 Assimp 中獲取所有的信息。對於任意時刻的骨骼的位置,Assimp 提供每個骨骼相對上一級的變換,以 transform matrix 表示,從根節點開始遍歷,就可以得到每一個骨骼相對根節點的變換,如果認為根節點就是在世界坐標系的中心,這就是從 bone space -> world space 的變換了。bone space 就是以這個骨骼當中的某個點作為坐標系的原點,具體是哪一個點,其實我們也是無法得知的,這個信息對於計算和理解都不重要,只要知道 bone space 就是骨骼的局部坐標系,知道綁定了這個骨骼的每一個點,在這個局部坐標系當中的位置即可。對於綁定了這個骨骼的每一個點,設其在 bone space 中的位置為 bone_pos, 那麼,其在世界坐標系中的位置就是 bone_pos 乘上計算出來的變換矩陣。

Assimp 解析指南

使用 Assimp 加載 FBX 文件獲得 aiScene 這是所有數據的入口。

Bone

aiBone *bone = aiScene->mMeshes[]->mBones[];

mBones 數組裏面存儲了所有的骨骼,每個骨骼存儲對應綁定的頂點和該頂點的權重,以及一個 mOffsetMatrix 這個矩陣十分有用,後文提及。

Node

aiNode *root_node = aiScene->mRootNode  // root node  aiNode *child_node = root_node->mChildren[i]  // get child node

aiNode 中除了存儲父子關係相關信息外,最重要的屬性就是 mTransformation 這就是相對於上一級 node 的變換矩陣。

這裡我們又見到了 node 和 bone 兩個說法,在 Assimp 的規定中,每一個 bone 必定會有一個相同名稱的 node 與其對應,反過來不成立。每一個骨骼的變換矩陣就是同名節點的變換矩陣。

Bind Pose

Bind Pose 是根據 mesh 的頂點信息,不考慮骨骼,直接繪製得到的結果,也就是繪製對象初始的狀態。另外,也可以用上 Assimp 讀取出來的數據來驗證。[2]

前文提到,bone space 的頂點位置是計算的前提,但是實際上,我們讀取的到的 mesh 的頂點位置,是以 model space 也即模型空間來描述的,正因為如此,我們可以直接繪製出初始狀態的模型來。那麼如何得到 bone space 的位置呢?從理論上來說,逐級遍歷得到 BoneToWorld transform matrix, 這個矩陣的逆矩陣就是 WorldToBone transform matrix, 即:

final_pos = (transform_matrix) * (transform_matrix)^(-1) * world_pos;  // means: final_pos = world_pos;

看起來無意義吧,因為逐級計算矩陣再求逆這個操作實在複雜,Assimp 直接提供了這個逆矩陣,就是 mOffsetMatrix, 上式可以寫作:

final_pos = (transform_matrix) * (offset_matrix) * world_pos; // means: final_pos = world_pos;

可以利用這個方法驗證 FBX 讀取和計算是否正確。正常情況下應該和直接繪製的結果一樣,如果不一樣,就是某個地方出錯了(最有可能出錯的地方是逐級遍歷節點計算變換矩陣)。

Animation

得到 bone space position 以後,計算每一幀的姿態就很簡單了。aiScene 包含了若干個 aiAnimation,每個代表一組動畫,每個 aiAnimation 包含一個 aiNodeAnim 的數組,稱之為 mChannels, 根據 aiNodeAnim->mNodeName 找到對應的 node,那麼這個 node 在某個特定時刻的 transform matrix 就可以通過對 Position, Rotation and Scaling 的插值計算出來,該矩陣仍然只代表相對父節點的變換,仍然通過逐級遍歷得到每個節點的在特定時刻的,AnimationBoneToWorldTransformMatrix. 最後的計算:

final_pos = (animation_transform_matrix) * (offset_matrix) * world_pos;

Reference

[1] Skeletal Animation With Assimp

[2] can’t get bones/skinning to work

Asset-Importer-Lib Documentation

Bones Animation – Matrices and calculations