opengl 实现Skin Mesh(骨骼动画)- GPU

这篇博客详细介绍了如何在OpenGL中实现GPU皮肤蒙皮的骨骼动画,通过对比CPU蒙皮,阐述了GPU蒙皮的优势,即减少大量顶点信息的传输,利用GPU进行矩阵运算提高效率。文章详细讲解了数据结构的修改、绑定姿势的计算、模型空间到骨骼空间变换矩阵的建立,以及顶点着色器和像素着色器的实现,最终展示了GPU蒙皮骨骼动画的效果。
摘要由CSDN通过智能技术生成

上一篇博客实现了cpu skin(蒙皮)的骨骼动画,接下来我们要实现 gpu skin(蒙皮) 的骨骼动画

首先我们需要知道什么是蒙皮,在上一博客实现的骨骼动画中,哪一步是蒙皮 。在模型文件中可以读到的数据是顶点和骨骼的对应关系,从动画文件中可以读到每一帧骨骼的数据,而根据每一帧骨骼的数据计算出顶点位置的过程就叫做蒙皮,也就是对应上一篇博客代码中的ComputeVertPos函数。另外还要知道为什么要在gpu蒙皮, 它比在cpu蒙皮优势在哪里 。 其实读者如果有对照着上一篇博客自己实现过cpu skin,就会发现在每一帧都需要将模型所有的顶点信息计算出来重新上传到GPU,而随便一一个模型可能都有成千上万个顶点, 这是一个相对比较耗时的事情。本来在 可编程渲染管线出来之前,我们只能这么做 ,但是可编程渲染管线的出现,允许我们先将网格的顶点信息传递到GPU内存,然后将经过动画处理的骨骼矩阵传递给GPU,并在顶点着色器里计算动画的顶点位置和法线,然后在进行后续渲染 。相比每一帧向GPU发送成千上万个顶点,每一帧发送所有骨骼的变换矩阵要更加省时。而且另一方面,对于大量的矩阵运算,GPU的运算效率要比CPU高很多,这是由二者的硬件结构所决定的。我们知道什么是GPU 蒙皮 以及 为什么要在gpu 蒙皮,接下里我们就要实现在上一篇博客的基础上实现GPU 蒙皮

首先我们需要修改一下顶点的数据结构,在顶点着色器中计算顶点的位置,这需要将顶点关联的骨骼数据全部上传给GPU,所以我们新加了两个额外的数据 boneWeights 和 boneIndexs,其中boneWeights用于存储权重,每个顶点最多可以存储四个权重,也就是每个顶点最多可以加权四个骨骼,通常四个骨骼足以使网格的顶点动起来,Unity游戏引擎的骨骼动画好像也是最多四个骨骼,这里使用四分量浮点值来存储。而骨骼的索引储存在boneIndexs中,用于从骨骼结构中读取对应骨骼的数据,数据结构如下 

struct Vertex
{
	glm::vec3 pos; // 顶点在模型空间中的位置
	glm::vec2 texcoord;// 纹理坐标
	glm::vec4 boneWeights;// 所关联骨骼的权重 
	glm::vec4 boneIndexs; // 所关联骨骼的索引
	int startWeight;//所关联权重的起始索引
	int weightCount;// 所关联的权重总数
};

其他的数据结构如下 

struct Joint
{
	string name; //骨骼名称
	int parent_ID; //父骨骼在骨骼层次结构中的索引
	glm::vec3 pos; //初始姿势时骨骼在模型空间中的位置
	glm::quat orient;//初始姿势时骨骼坐标空间相对模型空间的旋转
};
typedef vector<Joint> JointList;

struct Vertex
{
	glm::vec3 pos; // 顶点在模型空间中的位置
	glm::vec2 texcoord;// 纹理坐标
	glm::vec4 boneWeights;// 所关联骨骼的权重 
	glm::vec4 boneIndexs; // 所关联骨骼的索引
	int startWeight;//所关联权重的起始索引
	int weightCount;// 所关联的权重总数
};
typedef vector<Vertex> VertexList;

struct  Weight
{
	int joint_ID;// 与该权重关联的骨骼在骨骼层次结构中的索引
	float bias; // 权重占比
	glm::vec3 pos; // 权重在所关联骨骼坐标空间中的位置
};
typedef vector<Weight> WeightList;

typedef vector<glm::mat4> MatrixList;
typedef vector<unsigned int> IndexBuffer;
struct Mesh
{
	string shader; // 纹理
	unsigned int texID; //纹理缓冲对象
	unsigned int VAO, VBO, EBO;// 顶点数组对象,顶点缓冲对象,索引缓冲对象
	VertexList verts; // 顶点数组(这里用数组来表示集合,数据结构是vertor)
	WeightList  weights;//权重数组
	IndexBuffer indexBuffer;// 索引数组
};
typedef vector<Mesh> MeshList;

class TestMD5 {
public:
	TestMD5();
	~TestMD5();
	bool LoadModel(string& path);
	unsigned int LoadTexture(string& path);
	void CreateVertexBuffer(Mesh& mesh);
	void ComputeMatrix(Mesh& mesh, const FrameSkeleton& skeleton);
	void BuildBindPose(JointList& jointList);
	void PrepareMesh(Mesh& mesh);
	void Update(float deltaTime);
	void Render(ShaderC shader);
	MD5Animation animation;	//动画类 后面会详细讲到
private:
	int numJonints; // 骨骼数量
	int numMeshes; //mesh数量
	JointList jointList;// 骨骼数组
	MeshList meshList;// mesh数组
	MatrixList inverseBindPose;
	MatrixList animatedBones;
};

另外从上一篇博客中可以知道我们从模型文件中读取到的是顶点与骨骼的对应关系,并没有顶点的位置数据,那么我们想要将渲染出整个mesh 就需要选定一个姿势(其实是处于该姿势时的骨骼架构)来计算出所有顶点的模型位置,而这个姿势就是所谓的绑定姿势,例如unity人型动画的绑定姿势就是 T-Pos ,在本文中 我们选择模型的默认姿势,也就从我们在上一篇博客中一开始加载出来的那个不会动的模型所处的姿势。这个我们放在BuildBindPose函数内实现 ,在每一个mesh数据被读取出来时 调用 PrepareMesh(mesh)函数 函数的实现如下 

void TestMD5::PrepareMesh(Mesh& mesh)
{
	for (unsigned int i = 0;i < mesh.verts.size();i++)
	{
		Vertex& vert = mesh.verts[i];
		vert.pos = glm::vec3(0);
		vert.boneIndexs = glm::vec4(0);
		vert.boneWeights = glm::vec4(0);

		for (int j = 0;j < vert.weightCount;j++)
		{
			Weight& weight = mesh.weights[vert.startWeight + j];
			Joint& joint = jointList[weight.joint_ID];
			glm::vec3 rotPos = joint.orient * weight.pos;
			vert.pos += (joint.pos + rotPos) * weight.bias;
			vert.boneWeights[j] = weight.bias;
			vert.boneIndexs[j] = weight.joint_ID;
		}
	}
}

上面的函数除了计算出绑定姿势mesh所有顶点的位置以外,还设置了顶点关联的骨骼索引以及骨骼的权重。

在选定了绑定姿势之后,我们需要计算出在绑定姿势下 模型空间到每一个骨骼空间的变换矩阵,因为在动画的播放中,无论骨骼如何变换,顶点在骨骼空间的位置是不会变化的,也就是说 我们可以将绑定姿势下顶点的模型空间位置(PosModel)传递到顶点着色器,再将模型空间到骨骼的变换矩阵传递到顶点着色器,就可以计算出顶点在骨骼空间下的位置(PosBone),然后Update函数中我们再计算出当前时刻所有骨骼到模型空间的变换矩阵,用该变换矩阵乘以顶点在骨骼空间下的位置,就可以得到当前时刻顶点在模型空间中的位置了 ,总结一下整个过程 顶点的坐标空间变换就是 模型空间 -> 骨骼空间 - > 模型空间 。接下来我们需要先计算出绑定姿势时 模型空间到每一个骨骼空间的变换矩阵。这个我们放在BuildBindPose函数里面做,在骨骼数据加载时调用,并将计算出来的矩阵保存到inverseBindPose 。 代码如下

void TestMD5::BuildBindPose(JointList& jointList)
{
	inverseBindPose.clear();

	JointList::const_iterator iter = jointList.begin();
	while (iter != jointList.end())
	{
		const Joint& joint = (*iter);
		glm::mat4 boneMatrix(1.0f);
		glm::mat4 boneTranstion = glm::translate(boneMatrix, joint.pos);
		glm::mat4 b
课程简介:本课程详细讲解基于Assimp C++库的模型读取模块,并且做了关于动画理论、关键帧插值、骨骼动画矩阵原理、骨骼动画读取与播放等知识的详细讲解,对于游戏行业或者三维可视化从业人员会有比较大的帮助。目前很多公司已经开始构建自己的底层图形引擎,其中动画就是重要的一个版块,本课程可以让学员从原理层面以及底层代码层面了解FBX、OBJ模型的读取本质,并且梳理程序架构,编写骨骼动画。2 课程解决优势:很多同学学习骨骼动画苦于无法找到详细的资料,其中卡主的问题点也比比皆是,比如FBX内嵌材质的读取,骨骼动画各类矩阵的应用,理论结合模型读取库读出来的数据如何一一对应等。我们的课程可以带领大家从原理+实践的角度进行学习,每一个知识点都会:a 推导基础公式及原理 b 一行一行进行代码实践从而能够保证每位同学都学有所得,能够看得懂,学得会,用得上,并且能够培养自主研究的能力。3 学习课程所得:学习本课程完毕之后,学员可以全方位的完全了解基于Assimp库的模型读取结构,了解每一个变量背后的含义,并且课程拥有随堂附赠的源代码,保证同学可以随时根据老师的代码纠正自己的错误。跟随课程一行一行写完代码的同学,可以获得自己的模型读取代码库,并且深度理解骨骼动画的原理与模型读取原理 本课程含有全源代码
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值