Skin Mesh 骨骼动画 算是一个比较重点的学习目标,从两年前就开始要学习,但是断断续续却一直没有完整的看完,直到来深圳,每天在地铁上才陆陆续续的看完。
写完 Skin Mesh 这篇就要暂停一下了。计划做一个小游戏娱乐大众。
Skin Mesh (骨骼动画) 这个名字,多多少少让人产生误解。一开始我总以为要画出来几根骨头,然后再到骨头上去把顶点粘上去。
其实不是这样。
事实上 是没有 骨骼 这个实体的,骨骼只是大家为了 形象的拟人表示 。
转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
下面是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 个骨骼影响;
};
#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;
}
};
//权重;
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 ) 转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
//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个 子节点动画。转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
上图红框是其中的一个节点的动画数据,Assimp中的一个 AnimationNode ,我提取出来存放到了一个 AnimationChannel中。
其中:
mNodeName 是当前动画节点的名字,对应一根骨头的名字
mNumPositionKeys 是这个动画节点中有多个个位移数据
mPositionKeys 是具体的位移数据
下图是 mPositionKeys 其中的一个 转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
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 文件夹中!!
转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
在实例工程的 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);
然后找到 同名的 AnimationChannel ,获取当前帧的 Position、Rotate、Scaing 矩阵,相乘 赋值给 nodeTransformation 。
3、找到同名的 Bone ,还记得上面 Bone 里面有一个 finalOffsetMatrix 用来存放最终变换后的矩阵 。( Model.h Line115 )
bone.finalMatrix =globalInverseTransform * parenttransform * nodeTransformation * bone.offsetMatrix ;
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;
}
}
示例项目运行效果图:转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
运行效率很低,代码有很多问题,但是比较简单的可以了解 SkinMesh 的计算方式。
示例项目下载:
http://pan.baidu.com/s/1c1ojLyK