骨骼動(dòng)畫(huà)理論及程序部分實(shí)現(xiàn)(一)前向動(dòng)力學(xué)

前言

??由于數(shù)學(xué)公式的渲染BUG,后臺(tái)正常顯示的公式在前臺(tái)無(wú)法正常渲染,截了一個(gè)長(zhǎng)圖出來(lái)(可能會(huì)更新后面的文章,但長(zhǎng)圖無(wú)法頻繁更新,如有出入希望諒解):
長(zhǎng)圖


基本理論

??每個(gè)骨骼關(guān)節(jié)點(diǎn)(Joint)的位置公式可以寫(xiě)為P_i = P_{i-1} + rotate(D_i, P_{i-1}, \sum_{k=0}^{i-1}{\alpha_k})
??意思是第i個(gè)節(jié)點(diǎn)的位置,要用第i-1個(gè)點(diǎn)的位置,加上一個(gè)向量,這個(gè)向量是以初始骨骼方向?yàn)槌跏枷蛄?,繞著第i-1個(gè)點(diǎn)的位置,旋轉(zhuǎn)之前所有頂點(diǎn)旋轉(zhuǎn)的累加和。
??光是這么說(shuō)不好理解,可以看看這篇文章,里面圖解很詳細(xì)【翻譯】正向運(yùn)動(dòng)學(xué)的數(shù)學(xué)知識(shí)。
??知道了大致原理,但實(shí)現(xiàn)上還碰到了不少問(wèn)題,我們繼續(xù)學(xué)下面的理論。

子空間變換到父空間

變換理論

??用M_{C->P}表示子空間到父空間的變換,M表示為:\left[ \begin{matrix} i_x & i_y & i_z & 0 \\ j_x & j_y & j_z & 0 \\ k_x & k_y & k_z & 0 \\ t_x & t_y & t_z & 1 \\ \end{matrix}\right]\left[ \begin{matrix} U & 0 \\ T & 1 \\ \end{matrix}\right]??其中U包含旋轉(zhuǎn)和縮放,T表示位移,i,j,k向量是子空間的基向量(坐標(biāo)軸)在父空間的方向表示。
??例如,有一個(gè)父空間,和一個(gè)子空間,子空間繞著Z軸旋轉(zhuǎn)了γ度:

??沿方向的單位向量是,沿方向的單位向量是,沿方向的單位向量是,因此矩陣就表示為:

??此時(shí)如果在子空間軸上有一點(diǎn) ,我們拓展第四分量為1并左乘于矩陣:

??可得到新的向量??這就是子空間的在父空間的位置,還有一種右乘矩陣的寫(xiě)法:

??注意,左乘與右乘的Rotation和Translation矩陣都有區(qū)別。(這里的左乘與右乘是指“向量”左乘和“向量”右乘)

左乘矩陣

繞X旋轉(zhuǎn)

\left[ \begin{matrix} 1 & 0 & 0\\ 0 & \cos{\theta} & \sin{\theta}\\ 0 & -\sin{\theta} & \cos{\theta}\\ \end{matrix}\right]

繞Y旋轉(zhuǎn)

\left[ \begin{matrix} \cos{\theta} & 0 & -\sin{\theta}\\ 0 & 1 & 0\\ \sin{\theta} & 0 & \cos{\theta}\\ \end{matrix}\right]

繞Z旋轉(zhuǎn)

\left[ \begin{matrix} \cos{\theta} & \sin{\theta} & 0\\ -\sin{\theta} & \cos{\theta} & 0\\ 0 & 0 & 1\\ \end{matrix}\right]

平移

\left[ \begin{matrix} 1 & 0& 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ t_x & t_y & t_z & 1\\ \end{matrix}\right]=\left[ \begin{matrix} I & 0\\ T & 1\\ \end{matrix}\right]

右乘矩陣

繞X旋轉(zhuǎn)

\left[ \begin{matrix} 1 & 0& 0\\ 0 & \cos{\theta} & -\sin{\theta}\\ 0 & \sin{\theta} & \cos{\theta}\\ \end{matrix}\right]

繞Y旋轉(zhuǎn)

\left[ \begin{matrix} \cos{\theta} & 0 &\sin{\theta}\\ 0 & 1 & 0\\ -\sin{\theta} & 0 & \cos{\theta}\\ \end{matrix}\right]

繞Z旋轉(zhuǎn)

\left[ \begin{matrix} \cos{\theta} & -\sin{\theta} & 0\\ \sin{\theta} & \cos{\theta} & 0\\ 0 & 0 & 1\\ \end{matrix}\right]

平移

\left[ \begin{matrix} I & T\\ 0 & 1\\ \end{matrix}\right]
??注意,矩陣左乘和右乘不等于左手坐標(biāo)系變換矩陣和右手坐標(biāo)系變換矩陣,將兩個(gè)坐標(biāo)系矩陣互相轉(zhuǎn)換,是用:z*M*z其中z=\left[ \begin{matrix} 1 & 0 & 0\\ 0 & 1 & 0\\ 0 & 0 & -1\\ \end{matrix}\right]??可以看到轉(zhuǎn)換結(jié)果是:矩陣的a_{13}, a_{23}, a_{31}, a_{32}變?yōu)榱嗽瓉?lái)的相反數(shù),而繞X、繞Y變換矩陣的右手左乘矩陣恰好就是左手坐標(biāo)系右乘矩陣,而左右手坐標(biāo)系的繞Z旋轉(zhuǎn)矩陣都是一樣的。
??在這里,左乘與右乘是轉(zhuǎn)置的關(guān)系。

連續(xù)變換

??對(duì)于3D中的空間來(lái)說(shuō)往往不止一層,比如說(shuō)骨骼空間的層次:

  • 世界空間
    • 全親骨骼(模型空間)
      • 下半身
        • 上半身

……
??假如我們知道上半身骨骼下半身骨骼空間中的位置,以及所有父空間的相對(duì)位置和旋轉(zhuǎn),怎么推測(cè)到上半身骨骼在世界空間中的什么位置?
??答案是:首先求出上半身模型空間的位置,然后再推出上半身在世界空間的位置。M_{模型空間->世界空間}*(M_{下半身空間->模型空間}*T_{下半身})??而矩陣乘法符號(hào)結(jié)合律,因此括號(hào)可以去掉,而前面矩陣的乘法就可以寫(xiě)成:\Large M_{模型空間->世界空間}*M_{下半身空間->模型空間}*T_{下半身}\\=\Large M_{下半身空間->世界空間}*T_{下半身}??寫(xiě)成一個(gè)矩陣和向量的乘積,矩陣的含義就變?yōu)榱耍褐苯訌母缚臻g到模型空間的轉(zhuǎn)換矩陣。
??舉個(gè)例子,假設(shè)模型繞世界Z軸正方向旋轉(zhuǎn)了90°并向世界空間X軸負(fù)方向移動(dòng)1個(gè)單位,下半身繞模型空間Z軸正方向旋轉(zhuǎn)270°并向模型空間Y軸正方向移動(dòng)1個(gè)單位,上半身的關(guān)節(jié)點(diǎn)在下半身空間的(1, 1, 0)處:


??求上半身關(guān)節(jié)點(diǎn)在世界空間的位置。
??首先世界空間沒(méi)有父空間,因此它的M就是??模型空間:??下半身骨骼空間:??然后將的上半身關(guān)節(jié)點(diǎn)坐標(biāo)代入:??當(dāng)然,也可以用左乘的方式:

Opengl中的注意事項(xiàng)

??opengl中我們常進(jìn)行矩陣和向量的變幻:gl_Position = Projection * View * Translate * Rotate * Scale * vec4(pos, 1);,看起來(lái)是右乘,實(shí)際上,無(wú)論是矩陣和矩陣的乘法,還是矩陣和向量的乘法,以及變換矩陣的表示方法,都是左乘,按照從左至右的算法,肯定是錯(cuò)誤的,用筆計(jì)算時(shí),一定要將公式倒過(guò)來(lái)寫(xiě)。

骨骼旋轉(zhuǎn)中的空間變換

??如果仔細(xì)想前面的理論,其實(shí)存在著幾個(gè)問(wèn)題。
注:這里提前說(shuō)明,下面一段偏向于理論,實(shí)現(xiàn)上會(huì)容易一些!
??首先,上例我們默認(rèn)子骨骼空間都是從父骨骼空間的原點(diǎn)處出發(fā)開(kāi)始變換的;但是實(shí)際上,骨骼空間有自己的初始值(移動(dòng)和旋轉(zhuǎn))。這可能容易混淆。
??例如,上半身骨骼節(jié)點(diǎn)在下半身空間中繞X軸正方向旋轉(zhuǎn)90°向下半身空間Y軸移動(dòng)一個(gè)單位,這是我們所說(shuō)的骨骼變換,但實(shí)際上,上半身節(jié)點(diǎn)本身就處在下半身空間的某個(gè)位置,也可能骨骼空間有一個(gè)初始旋轉(zhuǎn)值,因此變換實(shí)際上的過(guò)程會(huì)復(fù)雜化:V_p = M_{p_c}M_{c->p}V_c??其中M_{c->p}還是上文講的變換矩陣,而M_{p_c}是子骨骼初始空間到父空間的變換。
??舉個(gè)例子,空間Child在空間Parent(0, 1, 0)處,基向量方向和Parent保持一致,隨后ChildX旋轉(zhuǎn)了90°,并向ParentY軸移動(dòng)了一個(gè)單位,求Child坐標(biāo)為(1, 1, 0)的空間點(diǎn)在變換后在Parent空間的位置。

黑色是Parent
??我們使用右乘公式:??這顯然是我們需要的結(jié)果。
??理所當(dāng)然的,從子骨骼到世界空間的一系列變換都需要多這一過(guò)程。這其實(shí)是很麻煩的,骨骼鏈很長(zhǎng),每一個(gè)骨骼都要計(jì)算自身基于父骨骼的變換,因此我們可以選擇另一種方式。
??骨骼空間的初始位置定義是可以由我們決定的,由此,我們約定,子骨骼空間的初始狀態(tài)都沒(méi)有基于父骨骼空間旋轉(zhuǎn),如上例,沒(méi)有旋轉(zhuǎn)能方便很多。
??然后換一種思考方式,所有骨骼空間的初始狀態(tài)都是從父骨骼的完全拷貝,而變換則變成了從原點(diǎn)到結(jié)束點(diǎn)的累積變換。
??如上例,假如我們先將兩個(gè)矩陣相乘,得到這個(gè)結(jié)果:??就像是子骨骼空間初始就和父骨骼空間一致,然后先旋轉(zhuǎn),再移動(dòng)了“子骨骼在父骨骼空間的位置”“子骨骼在父空間的移動(dòng)”的加和,如此來(lái),每一層的計(jì)算再次變得簡(jiǎn)單起來(lái)。


??這個(gè)問(wèn)題解決了,讓我們思考第二個(gè)問(wèn)題。
??我們渲染管道要的不是骨骼關(guān)節(jié)點(diǎn)(Joint)的位置,而是每個(gè)頂點(diǎn)在世界空間的位置,根據(jù)關(guān)節(jié)點(diǎn)在世界空間的坐標(biāo)變換或變換的加權(quán)平均,得到頂點(diǎn)在世界空間所在的位置。
??模型文件給出的頂點(diǎn)位置是對(duì)象坐標(biāo)系下的,傳入vertex shader中的也是對(duì)象坐標(biāo)的頂點(diǎn)。(這里的對(duì)象坐標(biāo)是模型各頂點(diǎn)的初始坐標(biāo),可以理解為未經(jīng)變換的世界坐標(biāo))
??而我們上述所講的變換,所需要傳入的是頂點(diǎn)在骨骼坐標(biāo)系下的位置。如果說(shuō)一個(gè)頂點(diǎn)只受一個(gè)骨骼影響,我們還可以算出頂點(diǎn)在骨骼空間的相對(duì)位置再傳入shader,但很多頂點(diǎn)會(huì)受到多個(gè)骨骼影響,受到加權(quán)平均。
??以下是一個(gè)vertex shader骨骼動(dòng)畫(huà)的基本寫(xiě)法:

#version 330
layout(location = 15) in vec3 aPos;//頂點(diǎn)位置
layout(location = 14) in vec3 aNormal;//法向量方向
layout(location = 13) in vec2 aTexCoord;//UV坐標(biāo)
layout(location = 12) in vec4 boneIndexs;//受到影響骨骼索引1個(gè)到4個(gè)
layout(location = 11) in vec4 boneWeights;//每個(gè)骨骼權(quán)重
layout(location = 10) in float weightFormula;//記錄受到幾個(gè)骨骼影響
//給fragment shader
out vec2 TexCoord;
out vec3 FragPos;
out vec3 Normal;
//M包括對(duì)模型整體的移動(dòng)旋轉(zhuǎn)縮放,VP包括視野View矩陣和透視Projection矩陣
uniform mat4 MVP;
uniform mat4 M;
//每個(gè)骨骼的空間變換矩陣
#define MAX_BONE 230
uniform mat4 bones[MAX_BONE];

void main(){
    vec4 newPosition = vec4(aPos, 1.0);
    vec4 newNormal = vec4(aNorml, 0.0);//法向量只有旋轉(zhuǎn)和縮放,沒(méi)有移動(dòng)

    int index1 = int(boneIndexs.x);//索引取整數(shù)
    int index2 = int(boneIndexs.y);
    int index3 = int(boneIndexs.z);
    int index4 = int(boneIndexs.w);

    if(weightFormula == 0){//BDEF1
        newPosition = bones[index1] * newPosition;
        newNormal = bones[index1] * newNormal;
    }else if(weightFormula == 1 || weightFormula == 3){//BDEF2 or SDEF
        newPosition = (bones[index1] * newPosition) * boneWeights.x + (bones[index2] * newPosition) * boneWeights.y;
        newNormal = (mat3(bones[index1])*aNormal) * boneWeights.x + (mat3(bones[index2]) * aNormal) * boneWeights.y;
    }
//....
}

??顯然提前算出頂點(diǎn)在骨骼空間的位置是不可能的,而傳入所有骨骼的信息到uniform值中是不合算的。
??于是我們?cè)俅斡米儞Q矩陣解決,我們約定,骨骼空間初始基向量和父空間保持一致(既沒(méi)有旋轉(zhuǎn)),這樣一系列從祖宗到孫子骨骼空間的初始狀態(tài)都沒(méi)有旋轉(zhuǎn),只有移動(dòng),于是想要得到頂點(diǎn)的骨骼空間坐標(biāo),只需要令頂點(diǎn)的對(duì)象空間坐標(biāo)減去骨骼關(guān)節(jié)點(diǎn)在對(duì)象空間的位置就好。
??例如,全親骨在世界(或?qū)ο螅┳鴺?biāo)系原點(diǎn),右肘關(guān)節(jié)在世界坐標(biāo)系(-3, 10, 0),一個(gè)可能受到右肘關(guān)節(jié)影響的頂點(diǎn)V_w處于世界坐標(biāo)系的(-3, 10, 1),由于上述約定,只需要令頂點(diǎn)V_w減去右肘關(guān)節(jié)的坐標(biāo),即可得到頂點(diǎn)V處于右肘關(guān)節(jié)的的坐標(biāo)V_l = (0, 0, 1)

??從這里我們就可以看到方才約定的好處,可以不用去計(jì)算父骨骼的旋轉(zhuǎn)。
??現(xiàn)在我們將其構(gòu)造為矩陣:??其含義為,傳入一個(gè)對(duì)象空間的坐標(biāo),可將其變?yōu)楫?dāng)前骨骼坐標(biāo)系的坐標(biāo),我們稱這個(gè)矩陣為初始綁定矩陣,稱這個(gè)變換過(guò)程為參考姿勢(shì)下的骨骼初始逆變換。
??具體使用方式,是和前面的空間轉(zhuǎn)換結(jié)合,以右乘的寫(xiě)法如下:??其中,前面矩陣的乘積,便是我們要傳遞給shader的矩陣

代碼樣例

??提前聲明:這個(gè)代碼是我MMD Viewer程序的一部分,等以后完善了,可能放出完整代碼,現(xiàn)在肯定是不能跑的。

類型聲明

namespace VPD {
    struct Bone {
        std::string name;
        glm::vec3 translate;
        glm::quat quaternion;
    };
    enum class Coor {
        LEFT,
        RIGHT
    };
    class File {
    public:
        std::vector<Bone> bones;
        std::string useModelName;
        static File* from_file(std::string filename, Coor coor = Coor::LEFT, std::string source_encoding = "shift-jis");

        Bone* operator[](std::string name) {
            for (Bone& bone : bones) {
                if (bone.name == name) {
                    return &bone;
                }
            }
            return nullptr;
        }
    private:
        File() {};
    };

??VPD是MikuMikuDance的姿勢(shì)文件,以文本方式存儲(chǔ)(既可以直接右鍵閱讀更改,動(dòng)作數(shù)據(jù)VMD不能),存儲(chǔ)格式就是:骨骼名稱、移動(dòng)、旋轉(zhuǎn)(上面的Bone)。Coor是坐標(biāo)系的枚舉,因?yàn)镸MD是DirectX寫(xiě)的,用的是左手坐標(biāo)系,而我用的是Opengl仿寫(xiě),用的是右手坐標(biāo)系。File是VPD文件的抽象。
??from_file是用來(lái)解析文件并返回File對(duì)象指針,我就不放具體代碼了。左右手坐標(biāo)系轉(zhuǎn)換我說(shuō)一下,位置可以直接讓Z軸取反就可以,四元數(shù)可以用glm::mat3_cast轉(zhuǎn)換為矩陣,然后用上面提到的理論,左右都乘上Z,再用glm::quat_cast轉(zhuǎn)換回四元數(shù)即可。

namespace Animation{
    struct BNode {
        BNode(PMX::Bone& _bone) : bone(_bone) {};
        int32_t index;
        PMX::Bone& bone;
        BNode* parent;
        std::vector<BNode*> childs;
        glm::mat4 Mconv;
    };

    class BoneManager
    {
    public:
        BoneManager(PMX::File* model);
        ~BoneManager();
        BNode* operator[](std::string name);

        std::vector<BNode*> linearList;
        std::vector<BNode*> roots;
        
    };
}

??然后是骨骼管理,BNode作為骨骼樹(shù)的節(jié)點(diǎn),記錄骨骼本身、親骨、子骨,以及最終的變換矩陣。
??骨骼管理,構(gòu)造方法接受一個(gè)PMX模型文件對(duì)象,PMX是MikuMikuDance的模型文件,存儲(chǔ)了模型的各類數(shù)據(jù)。
??骨骼管理采用雙索引方式:線性索引和樹(shù)形索引。PMX文件本身采用線性索引,各種關(guān)于骨骼的記錄都是線性index,而構(gòu)造變換矩陣時(shí),我們希望從根節(jié)點(diǎn)開(kāi)始構(gòu)造,這樣省下了遞歸、重復(fù)構(gòu)造父節(jié)點(diǎn)的變換矩陣;注意,PMX文件可能存在不止一個(gè)根節(jié)點(diǎn),因此存儲(chǔ)的是每個(gè)樹(shù)的根節(jié)點(diǎn),而骨骼管理存儲(chǔ)就可以看做“森林”。

    BoneManager::BoneManager(PMX::File* model) {
        linearList.resize(model->bones.size());
        for (int32_t i = 0; i < linearList.size(); ++i) {//線性初始化
            linearList[i] = new BNode(model->bones[i]);
            BNode& curr_node = *linearList[i];
            curr_node.index = i;
        }

        for (BNode* node : linearList) {
            if (node->bone.parentBoneIndex != -1) {//非根節(jié)點(diǎn)
                node->parent = linearList[node->bone.parentBoneIndex];//認(rèn)個(gè)爹
                linearList[node->bone.parentBoneIndex]->childs.push_back(node);//讓爹認(rèn)自己這個(gè)兒子
            }
            else {//根節(jié)點(diǎn)
                node->parent = nullptr;
                roots.push_back(node);//交給根節(jié)點(diǎn)列表
            }
        }
    };
    BoneManager::~BoneManager() {
        for (BNode* node : linearList) {
            delete node;
        }
    }
    BNode* BoneManager::operator[](std::string name) {
        for (BNode* node : linearList) {
            if (node->bone.localName == name) {
                return node;
            }
        }
        return nullptr;
    };
    class Pose {
    public:
        Animation::BoneManager boneManager;

        Pose(PMX::File* model, File* file) : boneManager(model){
        //需要一個(gè)模型文件和一個(gè)VPD文件,直接構(gòu)造骨骼管理器,因?yàn)镻ose類處于VPD的名稱空間下,因此File前不比加名稱空間
            std::stack<Animation::BNode*> traversal;//一個(gè)用來(lái)深度遍歷森林非遞歸寫(xiě)法的棧
            for (Animation::BNode* root : boneManager.roots) {//遍歷森林里的每一顆樹(shù)
                traversal.push(root);
                do {
                    Animation::BNode* currNode = traversal.top();
                    Bone* bone = (*file)[currNode->bone.localName];//VPD名稱空間下的Bone
                    if (bone == nullptr) {//這個(gè)骨骼沒(méi)有在記錄中出現(xiàn)
                        if (currNode->parent == nullptr) {//且是根節(jié)點(diǎn)
                            currNode->Mconv = glm::translate(glm::mat4(1), currNode->bone.position);//就等于自己在對(duì)象空間的位置
                        }
                        else {//不是根節(jié)點(diǎn)
                            currNode->Mconv = currNode->parent->Mconv * glm::translate(glm::mat4(1), currNode->bone.position - currNode->parent->bone.position);//親骨空間變換累積自己空間的變換
                        }
                    }
                    else {// 如果記錄存在
                        if (currNode->parent == nullptr) {//且是根節(jié)點(diǎn)
                            currNode->Mconv = glm::translate(glm::mat4(1), currNode->bone.position + bone->translate) * glm::mat4_cast(bone->quaternion);
                        }
                        else {
                            currNode->Mconv = currNode->parent->Mconv * (glm::translate(glm::mat4(1), currNode->bone.position - currNode->parent->bone.position + bone->translate) * glm::mat4_cast(bone->quaternion));
                        }
                    }


                    traversal.pop();//彈出棧
                    for (auto iter = currNode->childs.rbegin(); iter != currNode->childs.rend(); iter++) {//將當(dāng)前節(jié)點(diǎn)所有子骨骼壓入棧中
                        traversal.push(*iter);
                    }
                } while (!traversal.empty());//如果還有骨骼沒(méi)有解析,就繼續(xù)解析
            }

            for (Animation::BNode* node : boneManager.linearList) {
                node->Mconv *= glm::translate(glm::mat4(1), -node->bone.position);
            }//對(duì)所有骨骼空間加上骨骼空間的初始逆變換。
        }

        void setUniform(Shader* shader) {//給Vertex Shader
            glm::mat4* m = new glm::mat4[boneManager.linearList.size()];
            for (int i = 0; i < boneManager.linearList.size(); i++) {
                m[i] = boneManager.linearList[i]->Mconv;
            }
            glUniformMatrix4fv(glGetUniformLocation(shader->ID, "bones"), boneManager.linearList.size(), GL_FALSE, (const GLfloat*)m);
            delete m;
        }
    };
//VertexShader
#version 330 core

layout(location = 15) in vec3 aPos;
layout(location = 14) in vec3 aNormal;
layout(location = 13) in vec2 aTexCoord;
layout(location = 12) in vec4 boneIndexs;
layout(location = 11) in vec4 boneWeights;
layout(location = 10) in float weightFormula;

out vec2 TexCoord;
out vec3 FragPos;
out vec3 Normal;

uniform mat4 transform;
uniform mat4 rotateMat;
uniform mat4 scaleMat;
uniform mat4 viewMat;
uniform mat4 projMat;

//如果不愿意固定寫(xiě)法,可以改Shader類代碼,反正Shader程序是運(yùn)行時(shí)編譯。
#define MAX_BONE 230
uniform mat4 bones[MAX_BONE];

void main(){

    vec4 newPosition = vec4(aPos, 1.0);
    vec4 newNormal = vec4(aNormal, 0.0);//法向量不需要移動(dòng)

    int index1 = int(boneIndexs.x);
    int index2 = int(boneIndexs.y);
    int index3 = int(boneIndexs.z);
    int index4 = int(boneIndexs.w);

    if(weightFormula == 0){//BDEF1
        newPosition = bones[index1] * newPosition;
        newNormal = bones[index1] * newNormal;
    }else if(weightFormula == 1 || weightFormula == 3){//BDEF2 or SDEF
        newPosition = (bones[index1] * newPosition) * boneWeights.x + (bones[index2] * newPosition) * boneWeights.y;
        newNormal = bones[index1]*newNormal * boneWeights.x + bones[index2] * newNormal * boneWeights.y;
    }else if(weightFormula == 2 || weightFormula == 4){//BDEF4 or QDEF
        newPosition = (bones[index1] * newPosition) * boneWeights.x + (bones[index2] * newPosition) * boneWeights.y + (bones[index3] * newPosition) * boneWeights.z + (bones[index4] * newPosition) * boneWeights.w;
        newNormal = bones[index1]*newNormal*boneWeights.x + bones[index2]*newNormal*boneWeights.y + bones[index3]*newNormal*boneWeights.z + bones[index4]*newNormal*boneWeights.w;
    }

    gl_Position = projMat * viewMat * transform * rotateMat * scaleMat * newPosition;
    FragPos = vec3(transform * rotateMat * scaleMat * newPosition);

    Normal = vec3(rotateMat * scaleMat * newNormal);
    TexCoord = aTexCoord;
}

??gl_test窗口中便是程序生成的姿勢(shì)動(dòng)畫(huà)。
??腿上的姿勢(shì)不對(duì),是因?yàn)槌苏騽?dòng)力學(xué)外,還有反向動(dòng)力學(xué),請(qǐng)看我的下一篇文章:骨骼動(dòng)畫(huà)理論及程序?qū)崿F(xiàn)(二)反向動(dòng)力學(xué)

其他

??理論上前向動(dòng)力學(xué)的應(yīng)用差不多到此為止了,不過(guò)MikuMikuDance本身還是有其他坑,因此此部分可以跳過(guò)。

??我們前往MMD,關(guān)閉所有IK解算:
??可以發(fā)現(xiàn),就算沒(méi)有IK解算,腿部骨骼姿勢(shì)也并非兩腿站直,雖然這樣的腿部姿勢(shì)也不錯(cuò),但這并非我們想要的。

??打開(kāi)PMXEditor,隨意選擇腿上的一個(gè)頂點(diǎn),觀察影響頂點(diǎn)的骨骼:
??是左足(既大腿根)和左膝蓋(hiza)的D骨,觀察這種骨骼的屬性
??可以發(fā)現(xiàn),這些D骨都有“賦予親”的選項(xiàng),或者說(shuō),這些骨骼有兩個(gè)親骨;這種骨骼在大腿、胳膊、眼睛上都有,和賦予親骨位置相同。
??我們之前的程序不完全正確,是因?yàn)橥耆珱](méi)考慮賦予親的問(wèn)題。
??如果繼續(xù)觀察,可以發(fā)現(xiàn),賦予親骨和賦予子骨擁有共同的親骨,例如左膝和左膝D的親骨都是左足。

??如果問(wèn)為何這樣設(shè)計(jì)?我想是這樣,IK鏈上的節(jié)點(diǎn)為了IK解算,不吃前向動(dòng)力學(xué),也就是說(shuō)你怎么扭,骨骼都很別扭,有了D骨作為中間層,默認(rèn)情況不更改D骨的tranform不會(huì)和IK解算沖突,如果想要前向動(dòng)力學(xué)操控腿,不需要關(guān)閉IK,只要更改D骨就可以了。
??由此需要改動(dòng)的地方增加了不少,賦予親由于其擁有賦予權(quán)重的概念,不能簡(jiǎn)單的當(dāng)做親子骨來(lái)看。首先要更改BNode的存儲(chǔ)結(jié)構(gòu):

struct BNode {
    BNode(PMX::Bone& _bone) : bone(_bone) {};
    int32_t index;
    PMX::Bone& bone;
    BNode* parent = nullptr;
    std::vector<BNode*> childs;
    //新增的賦予親上下級(jí)索引
    bool haveAppendParent = false;
    BNode* appendParent = nullptr;
    float appendWeight;
    std::vector<BNode*> appendChilds;
         
    glm::vec3 position;
    glm::quat rotate;
}

??構(gòu)建骨骼樹(shù)時(shí),要給相應(yīng)值初始化。

if (node->bone.haveAppendRotate() || node->bone.haveAppendTranslate()) {
    node->haveAppendParent = true;
    node->appendParent = linearList[node->bone.appendParentBoneIndex];
    node->appendWeight = node->bone.appendWeight;
    linearList[node->bone.appendParentBoneIndex]->appendChilds.push_back(node);
}

??然后,前向動(dòng)力學(xué)遍歷樹(shù)的地方,如果有賦予親另算:

if (currNode->haveAppendParent) {//有賦予親的另算
    std::string parentName = model->bones[currNode->bone.appendParentBoneIndex].localName;
    Bone* appendParentRecord = (*file)[parentName];
    glm::vec3 totalTran(0);//默認(rèn)的移動(dòng)和旋轉(zhuǎn)
    glm::quat totalRot = glm::quat(1, 0, 0, 0);
    if (appendParentRecord != nullptr) {//如果賦予親在pose文件中有存儲(chǔ)
        if (currNode->bone.haveAppendTranslate()) {
            totalTran = appendParentRecord->translate * currNode->appendWeight;
        }
        if (currNode->bone.haveAppendRotate()) {
            totalRot = glm::quat(glm::eulerAngles(appendParentRecord->quaternion * currNode->appendWeight));
        }
    }
    if (bone != nullptr) {//如果自身有記錄
        totalTran += bone->translate;
        totalRot *= bone->quaternion;
    }
    currNode->position = currNode->parent->getLocalMat() * glm::vec4(currNode->bone.position - currNode->parent->bone.position + totalTran, 1);
    currNode->rotate = currNode->parent->rotate * totalRot;
}

??如此,顯示的畫(huà)面和MMD中就一致了。

引用

[原創(chuàng)] 骨骼運(yùn)動(dòng)變換的數(shù)學(xué)計(jì)算過(guò)程詳解
很經(jīng)典的博客,骨骼初始逆變換我是在看到一些Github骨骼動(dòng)畫(huà)源碼才知曉的,百度了一下發(fā)現(xiàn)了這個(gè)文章,真的不錯(cuò)!

借物:model女仆麗塔-洛絲薇瑟2.0 來(lái)自神帝宇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容