OpenGL ES 学习教程(11):Skin Mesh (骨骼动画)
这里开始讲解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);
然后找到 同名的 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; } }
然后 GL 在 Draw的时候就是已经更新的数据了。
示例项目运行效果图:
运行效率很低,代码有很多问题,但是比较简单的可以了解 SkinMesh 的计算方式。
示例项目下载:http://pan.baidu.com/s/1c1ojLyK