MD5模型的格式、导入与顶点蒙皮式骨骼动画I

4 篇文章 0 订阅

MD5模型的格式、导入与顶点蒙皮式骨骼动画I

MD5模型是ID公司第一款真正意义上的骨骼格式模型,在04年随着Doom3一起面世,经过几个版本的变更,现在在骨骼模型格式中依然有其重要地位。本文记录一下ZWModelMD5中的一些细节,先是稍微笔记一下骨骼模型的基本概念和MD5文件的格式与导入。——ZwqXin.com

[MD2格式模型的格式、导入与帧动画]

[MD3模型的格式、导入与骨骼概念动画]

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/model-md5-format-import-animation-1.html

经过MD2的帧动画和MD3的骨骼概念动画,当然还有MD4/MDL的尝试,在那个骨骼模型开始风行的时代,MD5作为骨骼动画出现了。在今天,3D模型通常分为静态模型、帧动画模型、骨骼动画模型,它们分别应用于不同的场合,静态模型就不用说了,帧动画模型主要用于人物动作简单、固定、与场景不怎么需要交互的场合,而骨骼动画模型就是与此相对了。

骨骼的这个概念与我们人体的骨骼还是类似的。我们可以把自己看做一堆骨骼,然后外面蒙上一层肌肉啊皮啊什么的,然后这些肌肉啊皮肤啊的就跟随骨骼的运动而运动。当然了,重要的是我们体内还有那么多器官,那些MD5人体和怪物模型就没有了(笑)。骨骼与骨骼之间是用骨骼节点连接的,我们称骨骼为Bone,称骨骼节点为Joint,一根bone的一端或两端连着两个Joint,而一个Joint可能连着数条Bone。骨骼模型的描述也分为以Bone为主和以Joint为主,MD5是后者。你可以认为Joint就是控制点,通过控制Joint的位置和旋转,可以控制整个骨骼,而整个骨骼也就影响模型的外皮(顶点网格),于是动画模式建立了。Joint的集合可以用一个树的数据结构描述——跟MD3一样,有一个总的父节点,总的父节点下连着一个或多个子节点,这些子节点本身也作为父节点下连一个或多个子节点……父节点的移动直接先作用到子节点上(抬动肩关节时手臂节点也跟着作同样的运动,之后手肘节点跟着手臂节点作同样移动……类推到指尖节点),再叠加上子节点本身的移动(手臂节点本身可以再那基础上作移动,其影响共同作用到手肘节点……用身体摆摆姿势,这其实是很形象的),于是这个前向的驱动模式建立了。每个Joint的运动信息可以抽象成一个变换矩阵M([乱弹OpenGL中的矩阵变换(上)] ),这样这个驱动模型可以看做是每个时刻给予每个节点一个变换矩阵,变换节点的位置和旋向以驱动骨架。

既然骨架模型建立了,接下来就是骨架与模型顶点数据的关系。骨骼模型本身渲染出来的不是骨架,而是组成网格(皮肤)的一堆顶点。这堆顶点是怎样定义的呢?在MD2中,每帧都包含一堆顶点位置数据,结果就是程序需要存储大规模的顶点位置数据。MD5则不直接储存顶点位置数据,而是让程序每帧”计算“出来。在MD5的文件中的网格数据包括纹理坐标(因为最后的顶点数目是固定的,做一纹理坐标数据的数目与之一致)、索引(把顶点组成三角面片,因为最后的顶点是有序的,前一帧的顶点跟后一帧的一一对应,所以只要按这个次序定义索引即可)、节点权重(weight,这就是关联骨骼节点跟顶点的东西,下述);一个顶点数据有一个纹理坐标、一个或多个weight组成,然后索引数据组织顶点。与以往不同的是,这里面没有法线数据,MD5采用的是与3DS([用Indexed-VBO渲染3DS模型] )和OBJ([OBJ模型文件的结构、导入与渲染Ⅰ] )一样的策略,让程序自己去计算。

一个weight包含了它对应的Joint的索引(这样一来就建立了 vertex->weight->joint的连接),一个位置值(pos)和一个作用比率(bias)。一个顶点的计算公式如下:

  1. VertexPos = (MJ-0 * weight[index0].pos * weight[index0].bias) + ... + (MJ-N * weight[indexN].pos * weight[indexN].bias)  

其中,MJ-x表示第x个weight对应的节点Joint的变换矩阵。作用比率bias的总和需要是1(100%),这样一个顶点位置可以看作是各个经过矩阵变换后的weight位置的加权平均。而这个Joint矩阵在动画过程中变化的话,结果就是对应计算出来的顶点位置也跟着变化了。这就是骨架驱动皮肤的过程,也称为”蒙皮“。这步计算可以在CPU上执行,也可以在GPU上执行——通过vertex shadr执行蒙皮,就称为”顶点蒙皮(vertex skinning)“,我将在下篇文章讲述。

一个MD5模型包含两个文件,其中.md5mesh后缀的文件包含了该模型的几何体数据(mesh),而.md5anim后缀的文件则包含了该模型的动画信息。这一点与MD3模型是一样的,只不过很多方面看上去更为规范,没有在[MD3模型的格式、导入与骨骼概念动画]文末提及的那些令人不爽的“小提示"。另一点很本质上不同的是,md5的两个文件都是文本文件,这当然提供了更大的方便性,但同时也容易出现文件被乱改的问题(当然了,本来idSoft就只是想自用而已)。

一个MD5可以只有md5mesh文件,这样模型只不过不含动画信息而已。而这时候出来的模型的姿态被称为Bind-pose。以前看视频看人用maya建模(就是看那部《堕落的艺术》的幕后花粹时),在修改模型,未定义动作之前,人物会呈现一个站立并两手平举的姿态。这就是一个模型的bind-pose姿态吧。这个概念在顶点蒙皮过程中尤显重要,不过你只需要记住这就是没有动画信息(没有md5anim)时候给予模型的一个”预设姿势“好了。下面看看文件结构

md5mesh:
  1. joints {  
  2.     "origin"    -1 ( -0.000000 0.016430 -0.006044 ) ( 0.707107 0.000000 0.707107 )      //   
  3.     "body"    0 ( -0.0000002384 0 56.5783920288 ) ( 0.507041 -0.578614 0.354181 )     // origin  
  4.   ....  
  5. }  

那些版本号啊XX总数的就不管了,从md5mesh文件开头看起,首先是Joint的定义:名称、父节点序号(-1说明本身是总父节点,这个序号其实就是行号了,譬如上面”origin“节点的序号就是0,无父节点; "body"节点序号是1,父节点序号是0,也就是说父节点是”origin“)、bind-pose姿态下节点的位置(位移)和旋转(旋转用四元数【[GimbalLock万向节锁与四元数旋转] 】表达,括号里是xyz,需程序自行计算w值)——后面两者可以组成一个变换矩阵Mself-bindpose,即bindpose姿态下各个节点自身的变换矩阵,如果给这个矩阵依次向上左乘该节点的树分支上各级父节点的变换矩阵,得到就是bindpose下该节点的真正变换矩阵MJ-x(bindpose)了。

md5mesh:
  1. mesh {  //一个网格对象  
  2.     shader "body1.tga"  //该网格对象的纹理  
  3.   
  4.     numverts 590   //顶点数据:vert 序号 (纹理坐标) 对应weight的起始序号 weight总数  
  5.     vert 0 ( 0.394531 0.513672 ) 0 2  
  6.         .....  
  7.   
  8.         numtris 888  //索引数据: tri 序号 三角面片对应的顶点数据的序号  
  9.         tri 0 0 2 1  
  10.         .....  
  11.   
  12.         numweights 967 //权重数据:weight 序号 对应的Joint的序号 比率bias值 (位置值)  
  13.     weight 0 5 1.000000 ( 6.175774 8.105262 -0.023020 )  
  14.         .....  
  15. }  

md5mesh文件后面部分就是一个个网格对象(mesh)的数据了。看上面注释,跟前面的讲述是一致的。注意这里vert末尾两个数据是对应下面那堆weight的,而且总是相邻的一个或多个weight,所以只需要第一个的序号和连续的weight的个数就可以确定了。顶点仅会被附近的weight影响。

接下来看md5anim:

  1. hierarchy {   //Joint 名字 父节点序号 flag 影响的帧数据起始索引
  2.     "origin"    -1 63 0 //  
  3.     "Body"  0 63 6  // origin  
  4.         ....  
  5. }  
  6.   
  7. bounds {  //每帧的包围盒  
  8.     ( ... ) ( ... )  
  9. }  
  10.   
  11. baseframe{  //  基础帧数据
  12.     ( ... ) ( ... )  
  13. }  
  14.   
  15. frame 0 {   //帧0数据
  16. ...  
  17. }  
  18.   
  19. frame 1 {  
  20. ...  
  21. }  
  22. ...  

老实说我觉得md5anim文件特别别扭,虽然理解起来不难。首先文件的开头也是joint的信息,不过这里主要针对帧数据,尾部的数据是一个索引值(nStartIndex),指向后面每一帧(frame x)的数据堆里, flag是一个bit位。嘛,这样看吧。MD5虽然不是帧动画,但它依然有”关健帧“的概念(也可以说这是动画本身的概念),模型的某个动画由有限个关健帧穿插并近邻插值而成,但MD5不同于MD2之处在于它只需要每个关健帧骨骼节点Joint的数据。为了替换上面bindpose的顶点计算公式,我们需要的只是每个joint在动画期间的变换矩阵MJ,但我们为了能在关健帧之间合理插值,通常并不直接保存矩阵而是分别保存位移信息(transform-vector3)和旋转信息(Rotation-quternion)。这个文件主要包含的就是这每个关健帧下每个Joint的这两个数据,当然还包括关健帧数目。至于这文件里的每帧的模型包围盒信息,并不是必要的。

在上面的baseframe里有与Joint数目相等的行数,把每行看作一个joint的位移信息+旋转信息(6个数字,这跟md5mesh文件开头joint的bindpose信息是一样的格式),但这里的baseframe数据无实际意义,仅表示一个”基础数值“,对于第x个关健帧,就拿下面frame x里的某些数据替换这些”基础信息“,具体每个joint要拿哪些数据去替换,正就是开头的索引值(nStartIndex)和flag决定的了。nStartIndex决定了替换开始对应数据堆的位置,nflag决定替换6个数字中的哪几个(flag分别与1、2、4、8、16、32作逻辑与,第一个出现为真的时候就拿nStartIndex处的数据替换掉,第二个出现真的时候就拿nStartIndex+1处的数据替换掉...如果逻辑与结果为假则不替换直接用回basefame里对应的数据)。这样下来就能取得我们要的"每个关健帧下每个Joint的位移信息+旋转信息"。

导入代码没什么特别的,也就按步骤进行”文件->一定数据结构下的内存数据“的转换。但确实颇冗长,尤其我还是以C语言方式进行读文件的……最后计算法线、生成VBO等都跟以前的模型导入流程差不多,有些细节地方我将在下篇文章提及。最后给出我用于导入的数据结构:

C++代码
  1. //包围盒信息  
  2. typedef struct tag3DBound  
  3. {  
  4.     Vector3               vMin;  
  5.     Vector3               vMax;  
  6. }t3DBound;  
  7.   
  8. // 模型的帧动画信息  
  9. typedef struct tag3DFrameInfo  
  10. {  
  11.     unsigned int            nFrameCount;      // 总帧数  
  12.     unsigned int            nAnimComponent;   // 每帧动画数据量  
  13.     unsigned int            nCurFrame;        // 当前帧  
  14.     unsigned int            nNextFrame;       // 下一帧  
  15.     unsigned int            nStartFrame;      // 开始帧  
  16.     unsigned int            nEndFrame;        // 结束帧  
  17.     float                   fSecPerKeyFrame;  // 关健帧间隔  
  18.     float                   fCurBlendValue;   // 当前融合变量  
  19.     DWORD                   DStartPlot;       // 开始时点  
  20.     t3DBound               *tBoundingBox;     // 包围盒  
  21. }t3DFrameInfo;  
  22.   
  23. // Joint属性  
  24. typedef struct tag3DJointInfo  
  25. {  
  26.     Vector3    vTransform;  
  27.        Quaternion qRotatation;  
  28. }t3DJointInfo;  
  29.   
  30. // Joint模型关节点信息  
  31. typedef struct tag3DJoint  
  32. {  
  33.     char                      szJointName[MNAME]; // Joint名称  
  34.     int                       nParentJointIndex;  // 父关节点索引  
  35.     Matrix16                  BindPoseMatrix;     // Joint 基本变换(位移和旋转)矩阵  
  36.     Matrix16                  BindPoseMatrixInv;  // Joint 基本变换(位移和旋转)矩阵的逆矩阵  
  37.     std::vector<t3DJointInfo> FramePoseInfoVec;   // Joint 帧位移和旋转  
  38.     BYTE                      nAffectFlags;       // 产生影响的顶点数据对象的标记  
  39.     unsigned int              nAffectStartIndex;  // 产生影响的顶点数据对象在帧数据的起始位置  
  40. }t3DJoint;  
  41.   
  42. //顶点权位信息  
  43. typedef struct tag3DWeight  
  44. {  
  45.     unsigned int nAttachJoint;  
  46.     float        fBias;  
  47.     Vector3      vPos;  
  48. }t3DWeight;  
  49.   
  50. typedef struct tag3DVectorInfo  
  51. {  
  52.     unsigned int nWeightStartIndex;  
  53.     unsigned int nWeightCount;  
  54. }VectorWeightInfo;  
  55.   
  56. // 网格对象信息  
  57. typedef struct tag3DObject  
  58. {  
  59.     GLuint                 nDiffuseMap;  
  60.     Vector3               *pPosVerts;  
  61.     Vector3               *pNormals;  
  62.     TexCoord              *pTexcoords;  
  63.     t3DWeight             *pPosWeights;  
  64.     VectorWeightInfo      *pVecWeightInfo;  
  65.     unsigned short        *pIndexes;  
  66.     unsigned int           nNumIndexes;  
  67.     unsigned int           nNumVerts;  
  68.     unsigned int           nNumWeights;  
  69.     GLuint                 nPosVBO;  
  70.     GLuint                 nNormVBO;  
  71.     GLuint                 nTexcoordVBO;  
  72.     GLuint                 nWeightVBO;  
  73.     GLuint                 nWightCountVBO;  
  74.     GLuint                 nJointIndexVBO;  
  75.     GLuint                 nIndexVBO;  
  76. }t3DObject;  
  77.   
  78. // 模型信息结构体  
  79. typedef struct tag3DModel   
  80. {  
  81.     bool                    bVisable;     // 是否渲染  
  82.     bool                    bIsTextured;  // 是否使用纹理  
  83.     bool                    bHasAnim;     // 是否含动画信息  
  84.     GLuint                  TexObjMap;    // 纹理对象  
  85.     std::vector<t3DObject>  t3DObjVec;    // 网格对象列表  
  86.     std::vector<t3DJoint>   t3DJointVec;  // 骨骼点列表  
  87.     t3DFrameInfo            tFrameInfo;   // 帧信息  
  88. }t3DModel;  

注意,对于Bindpose的Joint信息我是直接作为矩阵存储的(它的逆矩阵在顶点蒙皮的时候有用,所以也预先存储了),而动画过程中的Joint信息我是作为位移+旋转信息存储的(为了在关键帧中插值)。现在它们都在同一结构体内,迟些时候我应该会分开它们的(分开mesh部分和anim部分)。VBO部分有三个比较特殊的:nWeightVBO(weight的比率bias,用vec4传输,也就是说如果影响一个顶点的weight多于4个,我会把它们压成4个);nWightCountVBO(实际的weight数目);nJointIndexVBO(该weight对应的Joint的序号),这些对于顶点蒙皮是有用的,所以需要作为顶点属性传入vertex-sahder。

在上一篇文章中简单介绍了MD5模型的格式和载入,本文将从渲染的层面上继续笔记一下“顶点蒙皮”(vertex-skinning)的实现,以及骨骼节点Joint的变换矩阵向vertex-shader(GLSL)传输的其中几种方法。——ZwqXin.com

上篇文章见:[MD5模型的格式、导入与顶点蒙皮式骨骼动画I]

其他模型格式的文章见:
[用Indexed-VBO渲染3DS模型]
[OBJ模型文件的结构、导入与渲染Ⅰ]
[MD2格式模型的格式、导入与帧动画]
[MD3模型的格式、导入与骨骼概念动画]

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/model-md5-format-import-animation-2.html

首先要说一下的是,如果只是把蒙皮工作完全放在CPU端进行计算的话,那么只看上篇文章已经足够了——每一帧执行各个顶点的计算公式,其中的Joint矩阵由各个关健帧下该Joint的位移和旋转信息插值而来。只不过这样做的话,要承受帧率悲剧的痛苦罢了。现代的骨骼蒙皮主要都是在GPU端做的,这就是vertex-skinning On GPU。在上篇中提及一个顶点的计算公式如下:

  1. VertexPos = (MJ-0 * weight[index0].pos * weight[index0].bias) + ... + (MJ-N * weight[indexN].pos * weight[indexN].bias)

我们要做的只不过是把这个公式交给shader进行并行计算罢了。公式的右边都是原材料,我们一一细数一下:

  • weight[index0]....weight[indexN],指定该顶点关联的是哪些weight,以及weiht的总数,这个是直接从md5mesh文件的vert字段读入的wight信息;
  • pos、bias,同样,是从md5mesh文件的weight字段读入的信息;
  • MJ-x,是各帧经过插值计算得来Joint矩阵,其中下标x(对应哪个joint)也是由weight字段读入的信息;

可见,这些都是已知的,直接都丢给shader做就OK了?哪有那么简单。传给vertex-shader的是顶点本身,如果预先都传入0值,那也还要吧上述信息传给shader——怎么传呢?作为一个顶点的属性的话,它们的量太多了——按一个顶点最多受4个weight影响来计算,那是4个bias+4个pos+4个矩阵=4个float+12个float+48个float(矩阵的上4X3)=64个float,作为顶点属性传入的话这很难让人接受。

我们要从矩阵空间的角度去考虑。空间变换([乱弹OpenGL中的矩阵变换(上)] /[乱弹OpenGL中的矩阵变换(下)] )在这里起着一个比纯数学公式变换更重要的作用,因为很难通过数学证明的方式把上式变换成以下将提及的另一个公式。

我们最终想要的东西是什么?没错,该模型每一帧所有顶点在”模型空间“下的坐标位置!(至于把模型空间的点转换到世界空间乃至裁剪空间这些并不是模型导入和自身渲染阶段要处理的事情,虽然同样要在vertex shader里完成。)这个模型坐标系下的坐标如果不在CPU进行所有帧的计算,那还有一个选择,就是从别的坐标系转换过来!我们手头上有哪个坐标系下的模型坐标呢?还记得上篇中提及的BindPose姿态吗?在那个姿态下的顶点坐标都是可以在无须动画信息的情况下计算出来的——它也是模型坐标系下的坐标,但对应bindpose的姿态,不妨给予它一个别名——bindpose坐标系下的坐标。好了,每一帧,我们手上有一个bindpose坐标系下的顶点位置,以及该帧各骨骼节点(Joint)的变换矩阵MJ-x,我们怎样把它们转换成该帧下的顶点位置(VertexPos)呢?

还有法宝!bindpose坐标系下的Joint的坐标位置都是经过变换得来的。是的,上篇刚开始谈到md5mesh文件格式的时候引入的MJ-x(bindpose)!它把对应第x个Joint的weight的位置(weight.pos)转换到bindpose坐标系,那么对于其他东西呢?Joint它自身呢?是的,经过变换后的Joint在其bindpose坐标系下与MJ-x(bindpose)是等价的(位移和旋转),所以反过来想,变换前的Joint的坐标为(0,0,0)——MJ-x(bindpose)把第x个joint从它的本地空间(姑且称为joint本地空间)变换到bindpose空间。所以,我们可以直接从每个Joint的角度去观看所有weight,以及与这个Joint有关的顶点。仔细想想,上面的公式中只有Joint的变换矩阵是可变参数,也就是说,只要从joint的角度去看它对应的顶点的话——所有顶点都是静止的,固定的!

这一点认识摆在我们人体骨骼与皮肤关系上也许更容易直观感受。如此简单却如此重要——任何一帧,对于一个骨骼节点Joint来说,关联的所有顶点的位置都是恒定的——这个位置怎么获得?既然这个位置坐标左乘矩阵MJ-x(bindpose)进行坐标表换后会变成bindpose下的坐标,那反过来:把bindpose坐标系下的一个顶点VertexPosbindpose左乘该变换的逆矩阵MJ-x(bindpose)-1就可以获得了。获得这个位置(VertexPosJ-x)后,某个动画帧下,左乘该Joint的变换矩阵MJ-x,就是该帧下该顶点的”模型空间“下的坐标位置了(上面不提及了bindpose空间也就是一个模型空间嘛)!

等等!在我们的程序里,一个顶点是通过weight对应至少一个至多四个的Joint的! 那么这个顶点按上面的法子变换出某帧下的模型空间的坐标岂不是有1~4个?不错,所以对应的weight的比率bias再次对这些坐标进行加权平均,最后得到的就是同时受1~4个Joint影响的顶点的真正模型空间坐标位置:

第二个公式:
  1. VertexPos, = [(weight[index0].bias * MJ-0 * MJ-0(bindpose)-1 ) + ... + (weight[indexN].bias * MJ-N * MJ-N(bindpose)-1 )]  * VertexPosbindpose     

怎么样?有没有兴趣来证明一个第二个式子等价于第一个式子(VertexPos,  = VertexPos)?

在第二个式子里,所有bindpose变量都是可以预先计算好的,bias也是固定的,对应哪些joint、多少个joint,这都是固定的。而且 VertexPosbindpose(一个vec3)、bias(1~4个float,可用一个vec4表示)、jointIndex(1~4个int,也可以用一个vec4表示)、jointCount(1个int),这些都可以作为顶点属性attribute传入GLSL shader(现在知道上篇中末尾数据结构那三个特殊的VBO是干什么的了吧);不妨设MJ-x,= MJ-x * MJ-x(bindpose)-1,骨骼节点有多少个它就有多少个,跟顶点数无关,而且需要每帧更新(直接算出MJ-x,)——这样的变量(MJ-x,)必然要以uniform的形式传入GLSL shader。

至今,骨骼的顶点蒙皮(vertex-skinning)的大貌已经揭示完成了。

下面看看怎么把Joint的变换矩阵(说的是MJ-x,)向vertex-shader传输。简单的,大致有四种方法:

  1. Uniform Array
  2. Uniform Buffer Object
  3. 2D Texture
  4. Texture Buffer Object

其中最直接的当然是Uniform Array啦,定义一个uniform mat4 matJoint[MAX_JOINT],然后把各MJ-x,直接连成一个数组给传入GLSL就OK了。但问题是GLSL中uniform的个数有限制,如果骨骼节点太多就会超出这个限制了,而且你也不好定MAX_JOINT这个const值。Uniform Buffer Object(UBO)能够解决这个限制,但鉴于不熟,我就不多说。不知道有没有人看过我之前的一篇文章【[Vertex Texture Fetch 顶点纹理拾取] 】,里面提到一个很重要的观点:纹理=数组。没错,我们可以直接把数据放进一张纹理里,然后让shader用sampler去检索出所需要的数据啊!只不过要建立纹理,且纹理的检索有点麻烦(纹素的原点在其中心)也可能会出一丁点精度问题(我觉得可以忽略这些小问题啦)。我这里主要介绍一种新的方式:Texture Buffer Object(TBO)。

TBO是又一种Buffer Object,跟VBO([学一学,VBO] )、FBO([学一学,FBO] )、PBO一样,是一种对Buufer Object的使用方式(另外一提的是Uniform Buffer Object[UBO]也是)。但是它事实上十分简单——它的目的是让一个Buffer Objext内的数据(buffer data)能够被shader作为一个纹理般读取。注意这个纹理只可能是一维的,而且不可以有mipmap、filter,不然是不可能映射到buffer object的buffer里的。

C++代码 初始化
  1. glGenBuffers(1, &pModel->JointMatInfo.nBufferObject);  
  2.   
  3. glBindBuffer(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nBufferObject);  
  4.   
  5. glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);  
  6.   
  7. glGenTextures(1, &pModel->JointMatInfo.nTexHandleJointMat);  
  8.   
  9. glBindTexture(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nTexHandleJointMat);  
  10.   
  11. glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, pModel->JointMatInfo.nBufferObject);  
  12.   
  13. pModel->JointMatInfo.nTexObjJointMat = GL_TEXTURE1;  
  14.   
  15. m_pJointMatrixBuffer = new GLfloat[MATRIX4X3ELEMS * nJointCount];  
  16.   
  17. memset(m_pJointMatrixBuffer, 0, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat));  

初始化TBO很简单,也就是建立一个Buffer Object(数据可以为空可以不为空,反正后面每帧我都会重新填写数据,初始化时数据参量为NULL即可),建立一个纹理,然后用一个glTexBuffer的函数关联两者即可(注意所有的target需要一致为GL_TEXTURE_BUFFER)。m_pJointMatrixBuffer是为了后面填充数据准备的,因为MJ-x,只有上面4列3行有实际意义(尾行一定是0,0,0,1的),所以可以趁机传输少一点数据,在shader里再还原成mat4。

C++代码 渲染部分
  1.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i     ] = mtInterpolated.mt[0];  
  2.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  1] = mtInterpolated.mt[4];  
  3.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  2] = mtInterpolated.mt[8];  
  4.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  3] = mtInterpolated.mt[12];  
  5.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  4] = mtInterpolated.mt[1];  
  6.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  5] = mtInterpolated.mt[5];  
  7.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  6] = mtInterpolated.mt[9];  
  8.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  7] = mtInterpolated.mt[13];  
  9.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  8] = mtInterpolated.mt[2];  
  10.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i +  9] = mtInterpolated.mt[6];  
  11.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 10] = mtInterpolated.mt[10];  
  12.                 m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 11] = mtInterpolated.mt[14];  
  13.   
  14.             glBindBuffer(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nBufferObject);  
  15.   
  16.             glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);  
  17.   
  18.             glBufferSubData(GL_TEXTURE_BUFFER, 0, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), m_pJointMatrixBuffer);  
  19.   
  20.             glBindBuffer(GL_TEXTURE_BUFFER, NULL);  
  21.   
  22.             glActiveTexture(m_ModelMD5.JointMatInfo.nTexObjJointMat);  
  23.   
  24.             glBindTexture(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nTexHandleJointMat);  
  25.   
  26.             ...  

渲染部分首先就是更新当前的TBO数据了,其中 mtInterpolated.mt就是MJ-x,了。更新TBO只是纯粹更新那个Buffer Object而已,跟纹理无关(至于针对buffer object数据传输的优化方式,诸如stream update啦使用带参的glMapBufferRange啦,我就先不进行喽)。然后在VBO渲染前把纹理启用并传输给shader就可以了(注意依然是GL_TEXTURE_BUFFER)。vertex-shader进行顶点蒙皮的代码如下:

ModelVertexSkinningTB.vert     - Texture Buffer Object方式的vertex-skinning:
  1. #version 140  
  2.   
  3. in vec3 attrib_position;  
  4. in vec3 attrib_normal;  
  5. in vec2 attrib_texcoord;  
  6.   
  7. in vec4  attrib_weightbias;  
  8. in float attrib_weightcount;  
  9. in vec4  attrib_weightjoint;  
  10.   
  11. uniform samplerBuffer jointtex;  
  12.   
  13. ...  
  14.   
  15. void main(void)  
  16. {  
  17.    int nWeightCount = int(attrib_weightcount);    
  18.      
  19.    vec4 attribPos = vec4(attrib_position, 1.0);  
  20.      
  21.    mat4 mtRes = mat4(1.0);//mat4(0.0)  
  22.   
  23.    if(nWeightCount > 0)  
  24.    {  
  25.       mtRes = mat4(0.0);  
  26.   
  27.       for(int i = 0; i < nWeightCount; ++i)  
  28.       {  
  29.         mtRes[0] += texelFetch(jointtex, int(3 * attrib_weightjoint[i])    ) * attrib_weightbias[i];  
  30.         mtRes[1] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 1) * attrib_weightbias[i];  
  31.         mtRes[2] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 2) * attrib_weightbias[i];  
  32.       }  
  33.         
  34.       mtRes[3] = vec4(0.0, 0.0, 0.0, 1.0);  
  35.    }  
  36.   
  37.    mat4 mtInvRes = transpose(mtRes);  
  38.   
  39.    vec4 resPos = mtInvRes * attribPos;  
  40.      
  41.    resPos = resPos / resPos.w;  
  42.   
  43.    ....  
  44. }  

注意sampler名为samplerBuffer,一维纹理数据只能通过texelFetch去取。因为在纹理里是(RGBA)(RGBA)...这样的结构,所以对于一个骨骼节点Joint的矩阵,3个fetch就取够12个矩阵元素了。最后再给出Uniform Array和2D Texture方式的GLSL顶点shader的代码片段吧:

ModelVertexSkinningUA.vert     - Uniform Array方式的vertex-skinning:
  1. #define MAX_JOINT 71  
  2.   
  3. uniform mat4 jointMatrix[MAX_JOINT];  
  4.   
  5. void main(void)  
  6. {  
  7.    int nWeightCount = int(attrib_weightcount);  
  8.   
  9.    vec4 attribPos = vec4(attrib_position, 1.0);  
  10.   
  11.    mat4 mtRes = mat4(1.0);  
  12.   
  13.    if(nWeightCount > 0)  
  14.    {  
  15.        mtRes = mat4(0.0);  
  16.   
  17.         for(int i = 0; i < nWeightCount; ++i)  
  18.         {  
  19.             mtRes += jointMatrix[int(attrib_weightjoint[i])] * attrib_weightbias[i];  
  20.         }  
  21.    }  
  22.   
  23.    vec4 resPos = mtRes * attribPos;  
  24.      
  25.    resPos = resPos / resPos.w;  
  26.   
  27.    ....  
  28. }  
ModelVertexSkinningTD.vert     - 2D Texture方式的vertex-skinning:
  1. uniform sampler2D jointtex;  
  2.   
  3. void main(void)  
  4. {  
  5.    int nWeightCount = int(attrib_weightcount);  
  6.   
  7.    vec4 attribPos = vec4(attrib_position, 1.0);  
  8.   
  9.    ivec2 jointTexSize = textureSize(jointtex, 0);  
  10.   
  11.    float fTexcoordStepU = 1.0 / 3.0;  
  12.    float fTexcoordStepV = 1.0 / jointTexSize.y;  
  13.   
  14.    vec4 vMtX = vec4(0.0);  
  15.    vec4 vMtY = vec4(0.0);  
  16.    vec4 vMtZ = vec4(0.0);  
  17.      
  18.    for(int i = 0; i < nWeightCount; ++i)  
  19.    {  
  20.       vMtX += texture2D(jointtex, vec2(fTexcoordStepU * 0.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];  
  21.        
  22.       vMtY += texture2D(jointtex, vec2(fTexcoordStepU * 1.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];  
  23.        
  24.       vMtZ += texture2D(jointtex, vec2(fTexcoordStepU * 2.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];  
  25.    }  
  26.   
  27.    vec4 resPos;     
  28.    resPos.x = dot(vMtX, attribPos);  
  29.    resPos.y = dot(vMtY, attribPos);     
  30.    resPos.z = dot(vMtZ, attribPos);  
  31.    resPos.w = 1.0;  
  32.   
  33.    ....  
  34. }  

MD5模型的格式、导入与顶点蒙皮式骨骼动画

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
OpenGL ES 3.0 英文版 第1章——OpenGL ES 3.0简介   第1章简单介绍OpenGL ES,概述了OpenGL ES 3.0图形管线,讨论了OpenGL ES 3.0的设计理念和限制,最后介绍了OpenGL ES 3.0中使用的一些约定和类型。   第2章——你好,三角形:一个OpenGL ES 3.0示例   第2章介绍绘制三角形的一个简单OpenGL ES 3.0示例。我们的目的是说明OpenGL ES 3.0程序的样子,向读者介绍一些API概念,并说明如何构建和运行OpenGL ES 3.0示例程序。   第3章——EGL简介   第3章介绍EGL——为OpenGL ES 3.0创建表面和渲染上下文的API。我们说明与原生窗口系统通信、选择配置和创建EGL渲染上下文及表面的方法,传授足够多的EGL知识,你可以了解到启动OpenGL ES 3.0进行渲染所需的所有知识。   第4章——着色器和程序   着色器对象和程序对象是OpenGL ES 3.0中最基本的对象。第4章介绍创建着色器对象、编译着色器和检查编译错误的方法。这一章还说明如何创建程序对象、将着色器对象连接到程序对象以及链接最终程序对象的方法。我们讨论如何查询程序对象的信息以及加载统一变量(uniform)的方法。此外,你将学习有关源着色器和程序二进制代码之间的差别以及它们的使用方法。   第5章——OpenGL ES着色语言   第5章介绍编写着色器所需的着色语言的基础知识。这些着色语言基础知识包括变量和类型、构造器、结构、数组、统一变量、统一变量块(uniform block)和输入/输出变量。该章还描述着色语言的某些更细微的部分,例如精度限定符和不变性。   第6章——顶点属性、顶点数组和缓冲区对象   从第6章开始(到第11章为止),我们将详细介绍管线,教授设置和编程图形管线各个部分的方法。这一旅程从介绍几何形状输入图形管线的方法开始,包含了对顶点属性、顶点数组和缓冲区对象的讨论。   第7章——图元装配和光栅化   在前一章讨论几何形状输入图形管线的方法之后,第7章将讨论几何形状如何装配成图元,介绍OpenGL ES 3.0中所有可用的图元类型,包括点精灵、直线、三角形、三角形条带和三角扇形。此外,我们还说明了在顶点上进行坐标变换的方法,并简单介绍了OpenGL ES 3.0管线的光栅化阶段。   第8章——顶点着色器   我们所介绍的管线的下一部分是顶点着色器。第8章概述了顶点着色器如何融入管线以及OpenGL ES 着色语言中可用于顶点着色器的特殊变量,介绍了多个顶点着色器的示例,包括逐像素照明和蒙皮(skinning)。我们还给出了用顶点着色器实现OpenGL ES 1.0(和1.1)固定功能管线的示例。   第9章——纹理   第9章开始介绍片段着色器,描述OpenGL ES 3.0中所有可用的纹理功能。该章提供了创建纹理、加载纹理数据以及纹理渲染的细节,描述了纹理包装模、纹理过滤、纹理格式、压缩纹理、采样器对象、不可变纹理、像素解包缓冲区对象和Mip贴图。该章介绍了OpenGL ES 3.0支持的所有纹理类型:2D纹理、立方图、2D纹理数组和3D纹理。   第10章——片段着色器   第9章的重点是如何在片段着色器中使用纹理,第10章介绍编写片段着色器所需知道的其他知识。该章概述了片段着色器和所有可用的特殊内建变量,还演示了用片段着色器实现OpenGL ES 1.1中所有固定功能技术的方法。多重纹理、雾化、Alpha测试和用户裁剪平面的例子都使用片段着色器实现。   第11章——片段操作   第11章讨论可以适用于整个帧缓冲区或者在OpenGL ES 3.0片段管线中执行片段着色器后适用于单个片段的操作。这些操作包括剪裁测试、模板测试、深度测试、多重采样、混合和抖动。本章介绍OpenGL ES 3.0图形管线的最后阶段。   第12章——帧缓冲区对象   第12章讨论使用帧缓冲区对象渲染屏幕外表面。帧缓冲区对象有多种用法,最常见的是渲染到一个纹理。本章提供API帧缓冲区对象部分的完整概述。理解帧缓冲区对象对于实现许多高级特效(如反射、阴影贴图和后处理)至关重要。   第13章——同步对象和栅栏   第13章概述同步对象和栅栏,它们是在OpenGL ES 3.0主机应用和GPU执行中同步的有效图元。我们讨论同步对象和栅栏的使用方法,并以一个示例作为结束。   第14章——OpenGL ES 3.0高级编程   第14章是核心章节,将本书介绍的许多主题串联在一起。我们已经选择了高级渲染技术的一个样本,并展示了实现这些功能的示例。该章包含使用法线贴图的逐像素照明、环境贴图、粒子系统、图像后处理、程序纹理、阴影贴图、地形渲染

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值