
前言
??由于數(shù)學(xué)公式的渲染BUG,后臺(tái)正常顯示的公式在前臺(tái)無(wú)法正常渲染,截了一個(gè)長(zhǎng)圖出來(lái)(可能會(huì)更新后面的文章,但長(zhǎng)圖無(wú)法頻繁更新,如有出入希望諒解):
基本理論
??每個(gè)骨骼關(guān)節(jié)點(diǎn)(Joint)的位置公式可以寫(xiě)為
??意思是第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表示為:
或
??其中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)
繞Y旋轉(zhuǎn)
繞Z旋轉(zhuǎn)
平移
右乘矩陣
繞X旋轉(zhuǎn)
繞Y旋轉(zhuǎn)
繞Z旋轉(zhuǎn)
平移
??注意,矩陣左乘和右乘不等于左手坐標(biāo)系變換矩陣和右手坐標(biāo)系變換矩陣,將兩個(gè)坐標(biāo)系矩陣互相轉(zhuǎn)換,是用:其中
??可以看到轉(zhuǎn)換結(jié)果是:矩陣的
變?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è)到上半身骨骼在世界空間中的什么位置?
??答案是:首先求出上半身在模型空間的位置,然后再推出上半身在世界空間的位置。??而矩陣乘法符號(hào)結(jié)合律,因此括號(hào)可以去掉,而前面矩陣的乘法就可以寫(xiě)成:
??寫(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)在下半身空間的處:

??求上半身關(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ù)雜化:??其中
還是上文講的變換矩陣,而
是子骨骼初始空間到父空間的變換。
??舉個(gè)例子,空間Child在空間Parent的處,基向量方向和Parent保持一致,隨后Child繞X旋轉(zhuǎn)了90°,并向Parent的Y軸移動(dòng)了一個(gè)單位,求Child坐標(biāo)為
的空間點(diǎn)在變換后在Parent空間的位置。

??理所當(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)系,一個(gè)可能受到右肘關(guān)節(jié)影響的頂點(diǎn)
處于世界坐標(biāo)系的
,由于上述約定,只需要令頂點(diǎn)
減去右肘關(guān)節(jié)的坐標(biāo),即可得到頂點(diǎn)V處于右肘關(guān)節(jié)的的坐標(biāo)
:

??現(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;
}

??腿上的姿勢(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ò)。

??打開(kāi)PMXEditor,隨意選擇腿上的一個(gè)頂點(diǎn),觀察影響頂點(diǎn)的骨骼:


??我們之前的程序不完全正確,是因?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)自神帝宇