1. 背景
因爲某種緣由, 須要提取某個使用LayaAir開發的應用裏的模型. LayaAir自己是開源的, 因此讀取模型數據過程並不困難. 使用AssimpNet很快就輸出了正確的網格. 可是加入了骨骼以後, 模型馬上就毀了.
LayaAir模型中的一塊數據叫作bindPoseDatas, 這塊數據會保存到mesh._inverseBindPoses, 註釋是綁定動做逆矩陣.
而這個矩陣沒法簡單的對應到AssimpNet中的Bone.OffsetMatrix, 儘管註釋中寫道也被稱作inverse bind pose.
調查了一下LayaAir的導出方式, 它的工做流是先將模型導入到Unity, 而後經過插件將網格數據導出, 其中讀取的是Mesh.bindposes.
至此出現的3個大相徑庭的術語, 我感到事情沒有那麼簡單, 決定好好地調查一下綁定姿式究竟是什麼.
2. 蒙皮動畫
在蒙皮動畫中, 頂點再也不只受到一個關節的控制, 而是受到1個或者多個骨骼的控制. 在關節動畫中, 全部的動畫操做都是對着關節空間進行的, 而網格掛在關節上, 因此關節空間也就是網格空間. 可是在蒙皮動畫中, 全部的操做都是對着骨骼空間進行的, 那麼這裏就須要先進行一個從網格空間到骨骼空間的變換.
3. 綁定姿式
要完成這個變換, 就須要讓網格與骨骼產生關聯, 這個關聯操做叫作綁定(Bind), 綁定時模型的動做就被叫作綁定姿式(Bind Pose), 大多數狀況下綁定姿式是成T型的, 因此也叫作T-Pose. 綁定時骨骼基本上與模型的相關位置一一對應.綁定姿式是一個狀態, 通常用B表示, 與之相對的是當前姿式(Current Pose), 通常用C表示.
4. 綁定姿式矩陣與逆綁定姿式矩陣
Game Engine Architecture(2nd Edition)在11.5.2.1定義:
綁定姿式矩陣(Bind Pose Matrix), 是在綁定姿式時從關節空間變換到模型空間的矩陣
綁定姿式逆矩陣(Inverse Bind Pose Matrix), 是在綁定姿式時, 從模型空間變換到關節空間的矩陣
這裏提到的關節(Joint)就是骨骼.
注意到這裏提到的變換是到模型空間.
對於蒙皮動畫來講, 大多數狀況下關節再也不有意義, 全部的頂點均可以按照綁定姿式時在模型空間下的位置進行保存, 網格空間也就是模型空間.
但若是仍然保持了關節的結構, 那麼就須要先將頂點從網格空間變換到模型空間.
5. Bone Offset Matrix
這是一個Direct X系的術語, 而assimp使用了這個術語.
從微軟的文檔能看到一個絕對正確的定義:html
public void SetBoneOffsetMatrix( int bone, Matrix boneTransform ); boneTransform Microsoft.DirectX.Matrix A Matrix object that represents the bone offset matrix.
AssimpNet中則註釋道:git
/// <summary> /// Gets or sets the matrix that transforms from bone space to mesh space in bind pose. This matrix describes the /// position of the mesh in the local space of this bone when the skeleton was bound. Thus it can be used directly to determine a desired vertex /// position, given the world-space transform of the bone when animated, and the position of the vertex in mesh space. /// /// It is sometimes called an inverse-bind matrix or inverse-bind pose matrix. /// </summary>
這個註釋最後一句明確說道: Bone Offset Matrix就是綁定姿式逆矩陣, 可是第一句卻說, 這個矩陣是從骨骼空間到網格空間的一個變換. 有人甚至提交了一個問題: Offset matrix is wrong documented.
可是開發者顯然沒有打算修改這個註釋, 他解釋到:
github
這取決於你怎麼看待變換, 在矩陣右乘的狀況下, 你能夠認爲頂點進行了一次變換, 因此從網格空間到了骨骼空間. 可是在矩陣左乘的狀況下, 你能夠認爲是空間進行了一次變化, 從骨骼空間到網格空間.
6. 加入亂戰的Unity
Mesh.bindposes定義以下:動畫
The bind poses. The bind pose at each index refers to the bone with the same index. The bind pose is the inverse of the transformation matrix of the bone, when the bone is in the bind pose.
bindpose是在綁定姿式下, 骨骼的逆轉換矩陣, 這裏的定義還只是含糊不清.
在示例代碼中則有:this
// The bind pose is bone's inverse transformation matrix // In this case the matrix we also make this matrix relative to the root // So that we can move the root game object around freely bindPoses[0] = bones[0].worldToLocalMatrix * transform.localToWorldMatrix;
bindPose是骨骼的逆轉換矩陣, 在這裏咱們可讓這個矩陣相對與root, 這樣咱們就能自由地移動root物件了.
而後再結合bindpose定義這篇文章, 簡直完美匹配.
這些式子把模型空間給拋棄了, 引入了一個世界空間.
而且把bind pose的定義改爲了網格空間到骨骼空間的變換, 而不是模型空間到骨骼空間的變換.
7. AssimpNet的巨坑
儘管如今能夠確認Inverse Bind Pose, Bone Offset Matrix定義是一致的, 可是並不表明能夠直接使用這個矩陣.
矩陣是左乘仍是右乘, 旋轉是左手法則仍是右手法則, 對矩陣都是產生影響的.
觀察到網格和骨骼的位置已是一一對應了, 我決定直接計算綁定姿式逆矩陣.
可是怎麼嘗試都不對, 而後發現了AssimpNet的一個巨坑.
AssimpNet中Matrix類註釋以下:spa
/// <summary> /// Represents a 4x4 column-vector matrix (X base is the first column, Y base is the second, Z base the third, and translation the fourth). /// Memory layout is row major. Right handed conventions are used by default. /// </summary>
明確表示了該矩陣是列主序的, 那麼理論上就應該左乘向量.
對於TRS矩陣應該就有
TRS(t, r, s) = t * r * s
但實際上, 查看operator *的代碼插件
/// <summary> /// Performs matrix multiplication. Multiplication order is B x A. That way, SRT concatenations /// are left to right. /// </summary> /// <param name="a">First matrix</param> /// <param name="b">Second matrix</param> /// <returns>Multiplied matrix</returns> public static Matrix4x4 operator *(Matrix4x4 a, Matrix4x4 b) { return new Matrix4x4( a.A1 * b.A1 + a.B1 * b.A2 + a.C1 * b.A3 + a.D1 * b.A4, a.A2 * b.A1 + a.B2 * b.A2 + a.C2 * b.A3 + a.D2 * b.A4, a.A3 * b.A1 + a.B3 * b.A2 + a.C3 * b.A3 + a.D3 * b.A4, a.A4 * b.A1 + a.B4 * b.A2 + a.C4 * b.A3 + a.D4 * b.A4, a.A1 * b.B1 + a.B1 * b.B2 + a.C1 * b.B3 + a.D1 * b.B4, a.A2 * b.B1 + a.B2 * b.B2 + a.C2 * b.B3 + a.D2 * b.B4, a.A3 * b.B1 + a.B3 * b.B2 + a.C3 * b.B3 + a.D3 * b.B4, a.A4 * b.B1 + a.B4 * b.B2 + a.C4 * b.B3 + a.D4 * b.B4, a.A1 * b.C1 + a.B1 * b.C2 + a.C1 * b.C3 + a.D1 * b.C4, a.A2 * b.C1 + a.B2 * b.C2 + a.C2 * b.C3 + a.D2 * b.C4, a.A3 * b.C1 + a.B3 * b.C2 + a.C3 * b.C3 + a.D3 * b.C4, a.A4 * b.C1 + a.B4 * b.C2 + a.C4 * b.C3 + a.D4 * b.C4, a.A1 * b.D1 + a.B1 * b.D2 + a.C1 * b.D3 + a.D1 * b.D4, a.A2 * b.D1 + a.B2 * b.D2 + a.C2 * b.D3 + a.D2 * b.D4, a.A3 * b.D1 + a.B3 * b.D2 + a.C3 * b.D3 + a.D3 * b.D4, a.A4 * b.D1 + a.B4 * b.D2 + a.C4 * b.D3 + a.D4 * b.D4); }
註釋中很使人無語地寫道: a * b的含義是b * a, 你應該從左向右的對SRT作乘法
全部對矩陣進行計算的地方都須要注意, 除了TRS, 還有計算節點的LocalToWorld, 公式是:
ThisNode.LocalToWorld = RootNode.Transform * ChildNode1.Transform * … * ThisNode.Tranform
但實際代碼應該反過來寫成:
ThisNode.LocalToWorld = ThisNode.Transform * … * ChildNode1.Transform * RootNode.Transform
3d