opengl 实现Skin Mesh(骨骼动画) — CPU

模型文件以及相应代码下载地址

首先需要知道什么是骨骼动画,关于骨骼动画相关概念读者可以参考这篇博客,因为我没法讲述的比这个更清楚,所以这里我们抄袭一些关于讲述骨骼动画的概念方便读者对照理解

3D模型动画的基本原理和分类

3D模型动画的基本原理是让模型中各顶点的位置随时间变化。主要种类有Morph动画,关节动画和骨骼蒙皮动画(Skinned Mesh)。从动画数据的角度来说,三者一般都采用关键帧技术,即只给出关键帧的数据,其他帧的数据使用插值得到。但由于这三种技术的不同,关键帧的数据是不一样的

Morph(渐变,变形)动画是直接指定动画每一帧的顶点位置,其动画关键中存储的是Mesh所有顶点在关键帧对应时刻的位置,这样不同时间的顶点位置就是相邻帧之前的位置进行差值得到。

关节动画的模型不是一个整体的Mesh,而是分成很多部分(Mesh),通过一个父子层次结构将这些分散的Mesh组织在一起,父Mesh带动其下子Mesh的运动,各Mesh中的顶点坐标定义在自己的坐标系中,这样各个Mesh是作为一个整体参与运动的。动画帧中设置各子Mesh相对于其父Mesh的变换(主要是旋转,当然也可包括移动和缩放),通过子到父,一级级的变换累乘得到该Mesh在整个动画模型所在的坐标空间中的变换,从而确定每个Mesh在世界坐标系中的位置和方向,然后以Mesh为单位渲染即可。关节动画的问题是,各部分Mesh中的顶点是固定在其Mesh坐标系中的,这样在两个Mesh结合处就可能产生裂缝 ,如下图

从上面的问题我们是可以想到的一个解决方法,那就是靠近关节的顶点由两根骨骼来控制,这样关节处的顶点就会因为受到两根骨骼的拉扯而改变位置消除缝隙。 骨骼蒙皮动画的出现解决了关节动画的裂缝问题,骨骼动画的基本原理可概括为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。

Skinned Mesh原理和结构分析

Skinned Mesh中文一般称作骨骼蒙皮动画,正如其名,这种动画中包含骨骼(Bone)和蒙皮(Skinned Mesh)两个部分,Bone的层次结构和关节动画类似,Mesh则和关节动画不同:关节动画中是使用多个分散的Mesh,而Skinned Mesh中Mesh是一个整体,也就是说只有一个Mesh,实际上如果没有骨骼让Mesh运动变形,Mesh就和静态模型一样了。Skinned Mesh技术的精华在于蒙皮,所谓的皮并不是模型的贴图(也许会有人这么想过吧),而是Mesh本身,蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之 上,而且每个顶点可以被多个骨骼所控制,这样在关节处的顶点由于同时受到父子骨骼的拉扯而改变位置就消除了裂缝。Skinned Mesh这个词从字面上理解似乎是有皮的模型,哦,如果贴图是皮,那么普通静态模型不也都有吗?所以我觉得应该理解为具有蒙皮信息的Mesh或可当做皮肤 用的Mesh,这个皮肤就是Mesh。而为了有皮肤功能,Mesh还需要蒙皮信息,即Skin数据,没有Skin数据就是一个普通的静态Mesh了。 Skin数据决定顶点如何绑定到骨骼上。顶点的Skin数据包括顶点受哪些骨骼影响以及这些骨骼影响该顶点时的权重(weight),另外对于每块骨骼还 需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间。骨骼控制蒙皮(顶点)运动,而骨骼本身的运动呢?当然是动画数据了。每个关键帧中包含时间和骨骼运动信息,运动信息可以 用一个矩阵直接表示骨骼新的变换,也可用四元数表示骨骼的旋转,也可以随便自己定义什么只要能让骨骼动就行。除了使用编辑设定好的动画帧数据,也可以使用 物理计算对骨骼进行实时控制

骨骼动画的实现

到这里我们应该对骨骼动画有了一个清楚的认知,接下来我们开始自己使用opengl实现一个骨骼动画 首先我们需要一个带动画的模型,可以在文章开头出下载,该模型的格式.md5mesh,它对应的动画文件格式为.md5anim,MD5模型是ID公司第一款真正意义上的骨骼格式模型,之所以选择这种模型是因为它可以像obj模型一样通过将后缀名改为txt来查看文件内容,下图是本文提供的.md5mesh的文件内容 .

可以看到它的数据组织形式有些类似obj模型,同样的定义了一些关键词,用来标识后面跟着的数据代表什么意思 ,而且大多数关键词的含义很明显,例如MD5Version (MD5文件版本) numJoints(骨骼总数) numMesh(网格总数) ,这边只截了前面的数据,整个.md5mesh的数据格式如下 :

MD5Version <int:version>
commandline <string:commandline>

numJoints <int:numJoints>
numMeshes <int:numMeshes>

joints {
<string:name> <int:parentIndex> ( <vec3:position> ) ( <vec3:orientation> )
...
}

mesh {
shader <string:texture>

numverts <int:numVerts>
vert <int:vertexIndex> ( <vec2:texCoords> ) <int:startWeight> <int:weightCount>
...

numtris <int:numTriangles>
tri <int:triangleIndex> <int:vertIndex0> <int:vertIndex1> <int:vertIndex2>
...

numweights <int:numWeights>
weight <int:weightIndex> <int:jointIndex> <float:weightBias> ( <vec3:weightPosition> )
...

}
...

关于文件头5行的含义上面已经说到过,接下来是 joints部分,joints关键词后使用一对大括号来标识数据,定义了整个模型的骨骼架构,每一行代表着一个关节,例如

"sheath"  0  ( 11.004813 -3.177138 31.702473 ) ( 0.307041 -0.578614 0.354181 )	// origin

双引号里存储着骨骼名称,骨骼名称之后的下一个参数 0 是父骨骼在骨骼层次结构中的索引,根骨骼在骨骼层次结构中的索引为0,由于它没有父骨骼,它的父骨骼索引为-1.在父骨骼索引之后是两个三维向量,分别代表着该骨骼在模型空间的位置(和 该骨骼在模型空间的方向(也可以理解为该骨骼的坐标空间原点在模型空间的位置以及该空间相对模型空间的旋转),需要注意的是这里的位置与旋转都是相对于模型空间 而不是相对于其父骨骼的坐标空间。这里所说的模型空间也就是根骨骼所处的父骨骼空的间,比如上文提到的Scene_Root。

在joints部分之后,是一堆以mesh关键词开头,用一对大括号标识的数据,用于描述一个mesh(网格)。从obj模型加载这一节中我们可以了解到一个mesh的基本数据。当然这里又多了一些其他的数据,接下来看一下这些数据所表示的含义,

首先第一行以关键词shader开头,后面跟着一个文件路径,它表示该mesh使用的纹理的路径,可能是相对于.md5mesh文件的相对路径,也可能是在计算机上的绝对路径,这取决于建模人员的想法。

在shader之后是顶点数据,第一行以关键词numverts开头,后面的值表示该mesh包含多少个顶点 。而单个顶点由vert标识,一行代表着一个顶点 ,例如

vert 0 ( 0.394531 0.513672 ) 0 1

跟在vert后面的 0 是该顶点在顶点数组中的索引,再后面的(0.394531 0.513672)是一个二维分量,表示该顶点的纹理坐标, 最后是两个整数,1 代表影响此顶点的权重总数  0 代表影响该顶点的权重的起始索引,这样可以将每个顶点关联到一个或者多个权重,顶点的最终位置是每个权重的位置乘以权重占比zhi(关于权重的定义后面会详细说明,这里只需要记住顶点的位置由与它相关联的权重决定)

在顶点数组之后是三角形数组,也就是常说的面(face),第一行以关键词numtri开头,描述了该mesh包含多少个三角形,而单个三角形由关键词tri标识,每一行描述了一个三角形,例如

tri 0 0 2 1

跟在tri后面的 0 代表着该三角形在三角形数字中索引,再后面的三个数字 0 2 1 则分别代表了三角形的三个顶点在顶点数组中的索引,根据顶点数组和顶点在数组的索引可以得到对应的顶点数据

三角形数组后面就是前面提到的权重数据,第一行以numweights作为关键词开头,描述了该mesh中定义的权重数,而单个权重以weight标识,每一行代表一个权重定义,例如

weight 0 5 1.000000 ( 6.175774 8.105262 -0.023020 )

跟在weight后面的0代表该权重在权重数组中的索引,再后面的 5 表示与这个权重关联的骨骼在骨骼层次结构中的索引,1.000000 表示当前权重占比,表示了当前权重对 跟它相关联顶点的影响程度   最后是一个三分量向量,表示当前权重在 与它相关联的骨骼空间坐标中的位置。

简单梳理一下数据之间的关系,前面提到顶点的位置由一个或者多个权重决定 ,权重在模型空间中的位置则是由与它相关联的骨骼所决定,也可以说每个顶点的位置由一个或者多个骨骼决定。其实权重可以简单看成在某个骨骼的空间坐标内定义了一个虚拟点,该虚拟顶点会随着骨骼的坐标空间一起变换, 顶点的位置就是由一个或者多个这样虚拟顶点在模型空间中的位置乘以权重占比 再进行累加得到的,具体的实现可以参照后面的代码

文件加载

,md5mesh文件中的数据格式已经说过了,接下来就需要根据这些数据定义对应的数据结构。

    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;// 纹理坐标
        int startWeight;//所关联权重的起始索引
        int weightCount;// 所关联的权重总数
    };
    typedef vector<Vertex> VertexList;

    struct  Weight
    {
        int joint_ID;// 与该权重关联的骨骼在骨骼层次结构中的索引
        float bias; // 权重占比
        glm::vec3 pos; // 权重在所关联骨骼坐标空间中的位置
    };
    typedef vector<Weight> WeightList;
    
    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  MD5Model
    {
    public:
        MD5Model();
        ~ MD5Model();
        bool LoadModel(string& path);
        unsigned int LoadTexture(string& path);
        void CreateVertexBuffer(Mesh& mesh);
        void ComputeVertPos(Mesh& mesh);
        void Update(float deltaTime);
        void Render(Shader shader);
    private:
        int numJonints; // 骨骼数量
        int numMeshes; //mesh数量
        JointList jointList;// 骨骼数组
        MeshList meshList;// mesh数组
        MD5Animation animation;	//动画类 后面会详细讲到
    };

上面的数据结构基本上与文件中的数据一一对应,大部分数据结构都使用结构体来定义,只有模型定义了一个MD5Model类,除了存储骨骼和mesh,还添加了一个MD5Animation类的引

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值