OpenGL ES 学习教程(11):Skin Mesh (骨骼动画)

发表于2017-11-22
评论0 4.2k浏览

这里开始讲解Skin Mesh骨骼动画,实际上Skin Mesh (骨骼动画)在整个OpenGL中属于比较难的一部分,Skin Mesh (骨骼动画) 这个名字,本身就会让很多人产生误解,以为只需要画出来几根骨头,然后再到骨头上去把顶点粘上去即可,其实并不是这样。事实上是没有骨骼这个实体的,骨骼只是大家为了形象的拟人表示 。


下面是FBX转换工具,在下载的工程的 Tools 中!!!

在程序中的骨骼,以Assimp为例 ,存放的其实是一个矩阵 以及 这个矩阵对一系列顶点 造成的影响 的权重,比如下图这个骨骼:


如上图红框中:

mName 就是当前骨骼的名字,这个名字很有用,因为 动画数据也是对应每个骨头的名字 的。

mNumWeights 代表这根骨头影响了多少个顶点。

mWeights 是具体对一个顶点产生的影响。 mVertexId 是顶点ID,mWeight 是对顶点产生的影响的强度  范围( 0.0,1.0 )。

mOffsetMatrix 保存的是将Bone 变换到世界空间的矩阵的逆矩阵。世界坐标系的点 经过这个矩阵的偏移 就变换到了骨骼坐标系中了。骨骼动画是在骨骼坐标系中进行的。

在我的代码里面,我把不同 Bone 中的相同顶点的  mWeights 提取出来 放到了 Vertex 数据中。


Vertex.h

#pragma once  
#include"glm\glm.hpp"  
#include"Weight.h"  
class Vertex   
{  
public:  
    glm::vec3 Position;  
    glm::vec3 animPosition;  
    glm::vec3 Normal;  
    glm::vec2 TexCoords;  
    Weight          Weights[VERTEX_MAX_BONE];  //限定每个顶点受 VERTEX_MAX_BONE 个骨骼影响;  
};  

Weight.h
#pragma once  
#include"gles2\gl2.h"  
#define VERTEX_MAX_BONE 10  
class Weight  
{  
public:  
    GLuint      boneid; //骨骼id,要找到对应的Bone,取Bone中的offsetMatrix;  
    float       weight; //权重,用于将多个骨骼的变换组合成一个变换矩阵,一个顶点的所有骨骼权重之和必须为1;  
public:  
    Weight()  
    {  
        weight = 0;  
        boneid = 0;  
    }  
};  

Model.h ( Line242 ) 中 把不同 Bone 中的相同顶点的  mWeights 提取出来 放到了 Vertex 数据中。
//权重;  
int currentbone = 0;  
for (size_t boneindex = 0; boneindex < mesh->mNumBones; boneindex++)  
{  
    for (size_t weightindex = 0; weightindex < mesh->mBones[boneindex]->mNumWeights; weightindex++)  
    {  
        if (mesh->mBones[boneindex]->mWeights[weightindex].mVertexId == i)  
        {  
            Weight weight;  
            weight.boneid = boneindex;  
            weight.weight = mesh->mBones[boneindex]->mWeights[weightindex].mWeight;  
            if (currentbone  == VERTEX_MAX_BONE)  
            {  
                cout << "Error: " << "bone count > " << VERTEX_MAX_BONE << endl;  
                getchar();  
            }  
            vertex.Weights[currentbone++] = weight;  
        }  
    }  
}  

上面的代码提取出了 每个顶点受到的不同的骨骼的影响强度。下面的代码提取出来所有的骨骼。offsetMatrix 存放上面提到的mOffsetMatrix ,finalMatrix存放经过父节点变换计算之后得到的最终的变换矩阵。

Bone.h

#pragma once  
#include"glm\glm.hpp"  
class Bone  
{  
public:  
    char                name[50];   //例如 joint1,与 Scene->Animation->Channels 中的Channel的name对应;  
    glm::mat4 offsetMatrix; //顶点坐标做成offsetmatrix 从模型空间到骨骼空间;  
    glm::mat4 finalMatrix;  
};  

Model.h ( Line291 ) 

//Process bones;  
for (size_t boneindex = 0; boneindex < mesh->mNumBones; boneindex++)  
{  
    Bone bone;  
    aiBone* bonesrc = mesh->mBones[boneindex];  
    memcpy(bone.name, bonesrc->mName.C_Str(), bonesrc->mName.length + 1);  
    for (size_t xindex = 0; xindex < 4; xindex++)  
    {  
        for (size_t yindex = 0; yindex < 4; yindex++)  
        {  
            bone.offsetMatrix[xindex][yindex] = bonesrc->mOffsetMatrix[yindex][xindex];  
        }  
    }  
    bones.push_back(bone);  
}  

上面获取了骨骼以及骨骼对顶点的影响,然后这都是一堆死的数据。就是一个死的模型,不会动。

所以还要提取 Animation 动画数据。

在Assimp 中,一个 Animation 下面会有很多个 Channel ,每个Channel 的名字都对应着 一个Bone的名字。每个Channel 影响着 同名的Bone。

如上图中:

mName 是当前Animation 的名字。

mDuration 是持续时间,以帧 为单位。

mTicksPerSecond 是每秒多少帧

mNumChannels 是有多少个子节点动画

mMeshChannels 暂时不了解是指什么



红色框中列出了 其中 10个 子节点动画。


上图红框是其中的一个节点的动画数据,Assimp中的一个 AnimationNode ,我提取出来存放到了一个 AnimationChannel中。

其中:

mNodeName   是当前动画节点的名字,对应一根骨头的名字

mNumPositionKeys 是这个动画节点中有多个个位移数据

mPositionKeys 是具体的位移数据


下图是 mPositionKeys 其中的一个




mTime 只当前帧

mValue 是具体的位移数据,注意前一帧、后一帧的 位移 并不是叠加的。而是 后一帧的位移  覆盖 前一帧的位移。

比如上图中 x 是没有变化的,说明这几帧中 x 轴 是没有 位移 的。

而不是每一帧都 在 x 轴上有 4.50749969 的位移。

我在这一点上折腾了几天。


我把 Assimp 中的 Animation 都提取出来放到 自己的 Animation 中。


Model.h ( Line79 )

// 处理所有的Animation;  
void processAnimation(const aiScene* scene)  
{  
    for (size_t animationindex = 0; animationindex < scene->mNumAnimations; animationindex++)  
    {  
        Animation animation;  
        aiAnimation* animationsrc = scene->mAnimations[animationindex];  
        //Animation 名字;  
        memcpy(animation.name, animationsrc->mName.C_Str(), animationsrc->mName.length + 1);  
        animation.duration = animationsrc->mDuration;  
        animation.ticksPerSecond = animationsrc->mTicksPerSecond;  
        animation.numChannels = animationsrc->mNumChannels;  
        //处理这个Animation下的所有的Channel(一个joint的动画集合);  
        for (size_t channelindex = 0; channelindex < animationsrc->mNumChannels; channelindex++)  
        {  
            AnimationChannel animationChannel;  
            aiNodeAnim* channel = animationsrc->mChannels[channelindex];  
            memcpy(animationChannel.nodeName, channel->mNodeName.C_Str(), channel->mNodeName.length);  
            //位移动画;  
            animationChannel.numPositionKeys = channel->mNumPositionKeys;  
            for (size_t positionkeyindex = 0; positionkeyindex < channel->mNumPositionKeys; positionkeyindex++)  
            {  
                AnimationChannelKeyVec3 animationChannelKey;  
                aiVectorKey vectorKey = channel->mPositionKeys[positionkeyindex];  
                animationChannelKey.time = vectorKey.mTime;  
                animationChannelKey.keyData.x = vectorKey.mValue.x;  
                animationChannelKey.keyData.y = vectorKey.mValue.y;  
                animationChannelKey.keyData.z = vectorKey.mValue.z;  
                animationChannel.positionKeys.push_back(animationChannelKey);  
            }  
            //旋转动画;  
            animationChannel.numRotationKeys = channel->mNumRotationKeys;  
            for (size_t rotationkeyindex = 0; rotationkeyindex < channel->mNumRotationKeys; rotationkeyindex++)  
            {  
                AnimationChannelKeyQuat animationChannelKey;  
                aiQuatKey quatKey = channel->mRotationKeys[rotationkeyindex];  
                animationChannelKey.time = quatKey.mTime;  
                animationChannelKey.keyData.x = quatKey.mValue.x;  
                animationChannelKey.keyData.y = quatKey.mValue.y;  
                animationChannelKey.keyData.z = quatKey.mValue.z;  
                animationChannelKey.keyData.w = quatKey.mValue.w;  
                animationChannel.rotationKeys.push_back(animationChannelKey);  
            }  
            //缩放动画;  
            animationChannel.numScalingKeys = channel->mNumScalingKeys;  
            for (size_t scalingindex = 0; scalingindex < channel->mNumScalingKeys; scalingindex++)  
            {  
                AnimationChannelKeyVec3 animationChannelKey;  
                aiVectorKey vectorKey = channel->mScalingKeys[scalingindex];  
                animationChannelKey.time = vectorKey.mTime;  
                animationChannelKey.keyData.x = vectorKey.mValue.x;  
                animationChannelKey.keyData.y = vectorKey.mValue.y;  
                animationChannelKey.keyData.z = vectorKey.mValue.z;  
                animationChannel.scalingKeys.push_back(animationChannelKey);  
            }  
            animation.channels.push_back(animationChannel);  
        }  
        animations.push_back(animation);  
    }  
}  


到这里 Bone 、Animation 都提取完了,剩下的就是在每一帧中更新 Vertex 的 Position 。


下面是示例工程,在 Project 文件夹中!!


在实例工程的 Model.h  Line139 中,在 glDrawElements 前 进行了 更新 Vertex 的Position的操作。 

void OnDraw()  
{  
    framecount++;  
    Node rootNode;  
    for (size_t nodeindex = 0; nodeindex < nodes.size(); nodeindex++)  
    {  
        Node node = nodes[nodeindex];  
        if (strcmp(node.parentName,"")==0)  
        {  
            rootNode = node;  
            break;  
        }  
    };  
    globalInverseTransform = rootNode.transformation;  
    globalInverseTransform=glm::inverse(globalInverseTransform);  
    transforms.resize(meshes[0].bones.size());  
    glm::mat4 identity;  
    glm::mat4 rootnodetransform;  
    TransformNode(rootNode.name, framecount, identity * rootnodetransform);  
    for (size_t boneindex = 0; boneindex < meshes[0].bones.size(); boneindex++)  
    {  
        transforms[boneindex] = meshes[0].bones[boneindex].finalMatrix;  
    }  
    //更新Vertex Position;  
    for (size_t vertexindex = 0; vertexindex < meshes[0].vertices.size(); vertexindex++)  
    {  
        Vertex vertex = meshes[0].vertices[vertexindex];  
        //glm::vec4 animPosition;  
        glm::mat4 boneTransform;  
        //计算权重;  
        for (int weightindex = 0; weightindex < VERTEX_MAX_BONE; weightindex++)  
        {  
            Weight weight = vertex.Weights[weightindex];  
            Bone bone = this->meshes[0].bones[weight.boneid];  
            boneTransform += bone.finalMatrix * weight.weight;  
            //animPosition += glm::vec4(vertex.Position, 1)* bone.offsetMatrix*weight.weight;  
        }  
        glm::vec4 animPosition(vertex.Position, 1.0f);  
        animPosition = boneTransform * animPosition;  
        vertex.animPosition = glm::vec3(animPosition);  
        meshes[0].vertices[vertexindex] = vertex;  
    }  
} 

更新 Vertex 的Position需要经过一系列计算:

1、找到 Root 节点,获取 Root 节点的 transformation 矩阵的逆矩阵!( Model.h  Line154 )

globalInverseTransform = rootNode.transformation;  
globalInverseTransform=glm::inverse(globalInverseTransform);  


2、从Root节点往下推,对下面的每一个节点,获取节点的transformation 矩阵,作为 nodeTransformation 默认值。

然后找到 同名的 AnimationChannel ,获取当前帧的 Position、Rotate、Scaing 矩阵,相乘 赋值给 nodeTransformation 。


3、找到同名的 Bone ,还记得上面 Bone 里面有一个 finalOffsetMatrix 用来存放最终变换后的矩阵 。( Model.h  Line115 )

bone.finalMatrix =globalInverseTransform * parenttransform * nodeTransformation * bone.offsetMatrix ;  


4、更新子节点,把父节点的 nodeTransformation  乘以 当前节点的 nodeTransformation  然后传递给子节点继续运算,这样把 父节点的变化 影响到子节点。


5、对每一个顶点,查询对应的Bone 的 finalOffsetMatrix ,乘以对应的权重,然后这个顶点的所有的 Bone 相加,计算出最终顶点的位移矩阵。( Model.h  Line163 )

for (size_t boneindex = 0; boneindex < meshes[0].bones.size(); boneindex++)  
{  
    transforms[boneindex] = meshes[0].bones[boneindex].finalMatrix;  
}  
//更新Vertex Position;  
for (size_t vertexindex = 0; vertexindex < meshes[0].vertices.size(); vertexindex++)  
{  
    Vertex vertex = meshes[0].vertices[vertexindex];  
    //glm::vec4 animPosition;  
    glm::mat4 boneTransform;  
    //计算权重;  
    for (int weightindex = 0; weightindex < VERTEX_MAX_BONE; weightindex++)  
    {  
        Weight weight = vertex.Weights[weightindex];  
        Bone bone = this->meshes[0].bones[weight.boneid];  
        boneTransform += bone.finalMatrix * weight.weight;  
        //animPosition += glm::vec4(vertex.Position, 1)* bone.offsetMatrix*weight.weight;  
    }  
    glm::vec4 animPosition(vertex.Position, 1.0f);  
    animPosition = boneTransform * animPosition;  
    vertex.animPosition = glm::vec3(animPosition);  
    meshes[0].vertices[vertexindex] = vertex;  
}  
}  

然后 GL 在 Draw的时候就是已经更新的数据了。

示例项目运行效果图:


运行效率很低,代码有很多问题,但是比较简单的可以了解 SkinMesh 的计算方式。


示例项目下载:http://pan.baidu.com/s/1c1ojLyK

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论