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