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使用了這個術語.
從微軟的檔案能看到一個絕對正確的定義:
public void SetBoneOffsetMatrix( int bone, Matrix boneTransform ); boneTransform Microsoft.DirectX.Matrix A Matrix object that represents the bone offset matrix.
AssimpNet中則注釋道:
/// <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.
但是開發者顯然沒有打算修改這個注釋, 他解釋到:
這取決于你怎么看待變換, 在矩陣右乘的情況下, 你可以認為頂點進行了一次變換, 所以從網格空間到了骨骼空間. 但是在矩陣左乘的情況下, 你可以認為是空間進行了一次變化, 從骨骼空間到網格空間.
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是在系結姿勢下, 骨骼的逆轉換矩陣, 這里的定義還只是含糊不清.
在示例代碼中則有:
// 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類注釋如下:
/// <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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/27796.html
標籤:其他
