蒙皮動畫的綁定姿勢


    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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM