MD5模型的格式、导入与顶点蒙皮式骨骼动画I
MD5模型的格式、导入与顶点蒙皮式骨骼动画I
2011-10-6 21:14:49 | 发布:zwqxin
MD5模型是ID公司第一款真正意义上的骨骼格式模型,在04年随着Doom3一起面世,经过几个版本的变更,现在在骨骼模型格式中依然有其重要地位。本文记录一下ZWModelMD5中的一些细节,先是稍微笔记一下骨骼模型的基本概念和MD5文件的格式与导入。——ZwqXin.com
本文来源于 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)。一个顶点的计算公式如下:
- 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)时候给予模型的一个”预设姿势“好了。下面看看文件结构
- joints {
- "origin" -1 ( -0.000000 0.016430 -0.006044 ) ( 0.707107 0.000000 0.707107 ) //
- "body" 0 ( -0.0000002384 0 56.5783920288 ) ( 0.507041 -0.578614 0.354181 ) // origin
- ....
- }
那些版本号啊XX总数的就不管了,从md5mesh文件开头看起,首先是Joint的定义:名称、父节点序号(-1说明本身是总父节点,这个序号其实就是行号了,譬如上面”origin“节点的序号就是0,无父节点; "body"节点序号是1,父节点序号是0,也就是说父节点是”origin“)、bind-pose姿态下节点的位置(位移)和旋转(旋转用四元数【[GimbalLock万向节锁与四元数旋转] 】表达,括号里是xyz,需程序自行计算w值)——后面两者可以组成一个变换矩阵Mself-bindpose,即bindpose姿态下各个节点自身的变换矩阵,如果给这个矩阵依次向上左乘该节点的树分支上各级父节点的变换矩阵,得到就是bindpose下该节点的真正变换矩阵MJ-x(bindpose)了。
- mesh { //一个网格对象
- shader "body1.tga" //该网格对象的纹理
- numverts 590 //顶点数据:vert 序号 (纹理坐标) 对应weight的起始序号 weight总数
- vert 0 ( 0.394531 0.513672 ) 0 2
- .....
- numtris 888 //索引数据: tri 序号 三角面片对应的顶点数据的序号
- tri 0 0 2 1
- .....
- numweights 967 //权重数据:weight 序号 对应的Joint的序号 比率bias值 (位置值)
- weight 0 5 1.000000 ( 6.175774 8.105262 -0.023020 )
- .....
- }
md5mesh文件后面部分就是一个个网格对象(mesh)的数据了。看上面注释,跟前面的讲述是一致的。注意这里vert末尾两个数据是对应下面那堆weight的,而且总是相邻的一个或多个weight,所以只需要第一个的序号和连续的weight的个数就可以确定了。顶点仅会被附近的weight影响。
接下来看md5anim:
- hierarchy { //Joint 名字 父节点序号 flag 影响的帧数据起始索引
- "origin" -1 63 0 //
- "Body" 0 63 6 // origin
- ....
- }
- bounds { //每帧的包围盒
- ( ... ) ( ... )
- }
- baseframe{ // 基础帧数据
- ( ... ) ( ... )
- }
- frame 0 { //帧0数据
- ...
- }
- frame 1 {
- ...
- }
- ...
老实说我觉得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等都跟以前的模型导入流程差不多,有些细节地方我将在下篇文章提及。最后给出我用于导入的数据结构:
- //包围盒信息
- typedef struct tag3DBound
- {
- Vector3 vMin;
- Vector3 vMax;
- }t3DBound;
- // 模型的帧动画信息
- typedef struct tag3DFrameInfo
- {
- unsigned int nFrameCount; // 总帧数
- unsigned int nAnimComponent; // 每帧动画数据量
- unsigned int nCurFrame; // 当前帧
- unsigned int nNextFrame; // 下一帧
- unsigned int nStartFrame; // 开始帧
- unsigned int nEndFrame; // 结束帧
- float fSecPerKeyFrame; // 关健帧间隔
- float fCurBlendValue; // 当前融合变量
- DWORD DStartPlot; // 开始时点
- t3DBound *tBoundingBox; // 包围盒
- }t3DFrameInfo;
- // Joint属性
- typedef struct tag3DJointInfo
- {
- Vector3 vTransform;
- Quaternion qRotatation;
- }t3DJointInfo;
- // Joint模型关节点信息
- typedef struct tag3DJoint
- {
- char szJointName[MNAME]; // Joint名称
- int nParentJointIndex; // 父关节点索引
- Matrix16 BindPoseMatrix; // Joint 基本变换(位移和旋转)矩阵
- Matrix16 BindPoseMatrixInv; // Joint 基本变换(位移和旋转)矩阵的逆矩阵
- std::vector<t3DJointInfo> FramePoseInfoVec; // Joint 帧位移和旋转
- BYTE nAffectFlags; // 产生影响的顶点数据对象的标记
- unsigned int nAffectStartIndex; // 产生影响的顶点数据对象在帧数据的起始位置
- }t3DJoint;
- //顶点权位信息
- typedef struct tag3DWeight
- {
- unsigned int nAttachJoint;
- float fBias;
- Vector3 vPos;
- }t3DWeight;
- typedef struct tag3DVectorInfo
- {
- unsigned int nWeightStartIndex;
- unsigned int nWeightCount;
- }VectorWeightInfo;
- // 网格对象信息
- typedef struct tag3DObject
- {
- GLuint nDiffuseMap;
- Vector3 *pPosVerts;
- Vector3 *pNormals;
- TexCoord *pTexcoords;
- t3DWeight *pPosWeights;
- VectorWeightInfo *pVecWeightInfo;
- unsigned short *pIndexes;
- unsigned int nNumIndexes;
- unsigned int nNumVerts;
- unsigned int nNumWeights;
- GLuint nPosVBO;
- GLuint nNormVBO;
- GLuint nTexcoordVBO;
- GLuint nWeightVBO;
- GLuint nWightCountVBO;
- GLuint nJointIndexVBO;
- GLuint nIndexVBO;
- }t3DObject;
- // 模型信息结构体
- typedef struct tag3DModel
- {
- bool bVisable; // 是否渲染
- bool bIsTextured; // 是否使用纹理
- bool bHasAnim; // 是否含动画信息
- GLuint TexObjMap; // 纹理对象
- std::vector<t3DObject> t3DObjVec; // 网格对象列表
- std::vector<t3DJoint> t3DJointVec; // 骨骼点列表
- t3DFrameInfo tFrameInfo; // 帧信息
- }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。在上篇中提及一个顶点的计算公式如下:
- 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影响的顶点的真正模型空间坐标位置:
- 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传输。简单的,大致有四种方法:
- Uniform Array
- Uniform Buffer Object
- 2D Texture
- 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里的。
- glGenBuffers(1, &pModel->JointMatInfo.nBufferObject);
- glBindBuffer(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nBufferObject);
- glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);
- glGenTextures(1, &pModel->JointMatInfo.nTexHandleJointMat);
- glBindTexture(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nTexHandleJointMat);
- glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, pModel->JointMatInfo.nBufferObject);
- pModel->JointMatInfo.nTexObjJointMat = GL_TEXTURE1;
- m_pJointMatrixBuffer = new GLfloat[MATRIX4X3ELEMS * nJointCount];
- 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。
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i ] = mtInterpolated.mt[0];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 1] = mtInterpolated.mt[4];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 2] = mtInterpolated.mt[8];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 3] = mtInterpolated.mt[12];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 4] = mtInterpolated.mt[1];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 5] = mtInterpolated.mt[5];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 6] = mtInterpolated.mt[9];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 7] = mtInterpolated.mt[13];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 8] = mtInterpolated.mt[2];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 9] = mtInterpolated.mt[6];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 10] = mtInterpolated.mt[10];
- m_pJointMatrixBuffer[MATRIX4X3ELEMS * i + 11] = mtInterpolated.mt[14];
- glBindBuffer(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nBufferObject);
- glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);
- glBufferSubData(GL_TEXTURE_BUFFER, 0, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), m_pJointMatrixBuffer);
- glBindBuffer(GL_TEXTURE_BUFFER, NULL);
- glActiveTexture(m_ModelMD5.JointMatInfo.nTexObjJointMat);
- glBindTexture(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nTexHandleJointMat);
- ...
渲染部分首先就是更新当前的TBO数据了,其中 mtInterpolated.mt就是MJ-x,了。更新TBO只是纯粹更新那个Buffer Object而已,跟纹理无关(至于针对buffer object数据传输的优化方式,诸如stream update啦使用带参的glMapBufferRange啦,我就先不进行喽)。然后在VBO渲染前把纹理启用并传输给shader就可以了(注意依然是GL_TEXTURE_BUFFER)。vertex-shader进行顶点蒙皮的代码如下:
- #version 140
- in vec3 attrib_position;
- in vec3 attrib_normal;
- in vec2 attrib_texcoord;
- in vec4 attrib_weightbias;
- in float attrib_weightcount;
- in vec4 attrib_weightjoint;
- uniform samplerBuffer jointtex;
- ...
- void main(void)
- {
- int nWeightCount = int(attrib_weightcount);
- vec4 attribPos = vec4(attrib_position, 1.0);
- mat4 mtRes = mat4(1.0);//mat4(0.0)
- if(nWeightCount > 0)
- {
- mtRes = mat4(0.0);
- for(int i = 0; i < nWeightCount; ++i)
- {
- mtRes[0] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) ) * attrib_weightbias[i];
- mtRes[1] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 1) * attrib_weightbias[i];
- mtRes[2] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 2) * attrib_weightbias[i];
- }
- mtRes[3] = vec4(0.0, 0.0, 0.0, 1.0);
- }
- mat4 mtInvRes = transpose(mtRes);
- vec4 resPos = mtInvRes * attribPos;
- resPos = resPos / resPos.w;
- ....
- }
注意sampler名为samplerBuffer,一维纹理数据只能通过texelFetch去取。因为在纹理里是(RGBA)(RGBA)...这样的结构,所以对于一个骨骼节点Joint的矩阵,3个fetch就取够12个矩阵元素了。最后再给出Uniform Array和2D Texture方式的GLSL顶点shader的代码片段吧:
- #define MAX_JOINT 71
- uniform mat4 jointMatrix[MAX_JOINT];
- void main(void)
- {
- int nWeightCount = int(attrib_weightcount);
- vec4 attribPos = vec4(attrib_position, 1.0);
- mat4 mtRes = mat4(1.0);
- if(nWeightCount > 0)
- {
- mtRes = mat4(0.0);
- for(int i = 0; i < nWeightCount; ++i)
- {
- mtRes += jointMatrix[int(attrib_weightjoint[i])] * attrib_weightbias[i];
- }
- }
- vec4 resPos = mtRes * attribPos;
- resPos = resPos / resPos.w;
- ....
- }
- uniform sampler2D jointtex;
- void main(void)
- {
- int nWeightCount = int(attrib_weightcount);
- vec4 attribPos = vec4(attrib_position, 1.0);
- ivec2 jointTexSize = textureSize(jointtex, 0);
- float fTexcoordStepU = 1.0 / 3.0;
- float fTexcoordStepV = 1.0 / jointTexSize.y;
- vec4 vMtX = vec4(0.0);
- vec4 vMtY = vec4(0.0);
- vec4 vMtZ = vec4(0.0);
- for(int i = 0; i < nWeightCount; ++i)
- {
- vMtX += texture2D(jointtex, vec2(fTexcoordStepU * 0.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
- vMtY += texture2D(jointtex, vec2(fTexcoordStepU * 1.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
- vMtZ += texture2D(jointtex, vec2(fTexcoordStepU * 2.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
- }
- vec4 resPos;
- resPos.x = dot(vMtX, attribPos);
- resPos.y = dot(vMtY, attribPos);
- resPos.z = dot(vMtZ, attribPos);
- resPos.w = 1.0;
- ....
- }