OpenGL ES 学习教程(十一) Skin Mesh (骨骼动画)

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 个骨骼影响;

};

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 ) 转自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);

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的时候就是已经更新的数据了。

示例项目运行效果图:转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn


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


示例项目下载:

http://pan.baidu.com/s/1c1ojLyK


  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值