shader 雪 顶点位移_MD5模型的格式、导入与顶点蒙皮式骨骼动画II

ccf7b630ac92e2593c85306f9a2e8363.png

在上一篇文章中简单介绍了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. glBindBuffer(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nBufferObject);
  3. glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);
  4. glGenTextures(1, &pModel->JointMatInfo.nTexHandleJointMat);
  5. glBindTexture(GL_TEXTURE_BUFFER, pModel->JointMatInfo.nTexHandleJointMat);
  6. glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, pModel->JointMatInfo.nBufferObject);
  7. pModel->JointMatInfo.nTexObjJointMat = GL_TEXTURE1;
  8. m_pJointMatrixBuffer = new GLfloat[MATRIX4X3ELEMS * nJointCount];
  9. 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. glBindBuffer(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nBufferObject);
  14. glBufferData(GL_TEXTURE_BUFFER, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), NULL, GL_STREAM_DRAW);
  15. glBufferSubData(GL_TEXTURE_BUFFER, 0, MATRIX4X3ELEMS * nJointCount * sizeof(GLfloat), m_pJointMatrixBuffer);
  16. glBindBuffer(GL_TEXTURE_BUFFER, NULL);
  17. glActiveTexture(m_ModelMD5.JointMatInfo.nTexObjJointMat);
  18. glBindTexture(GL_TEXTURE_BUFFER, m_ModelMD5.JointMatInfo.nTexHandleJointMat);
  19. ...

渲染部分首先就是更新当前的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. in vec3 attrib_position;
  3. in vec3 attrib_normal;
  4. in vec2 attrib_texcoord;
  5. in vec4 attrib_weightbias;
  6. in float attrib_weightcount;
  7. in vec4 attrib_weightjoint;
  8. uniform samplerBuffer jointtex;
  9. ...
  10. void main(void)
  11. {
  12. int nWeightCount = int(attrib_weightcount);
  13. vec4 attribPos = vec4(attrib_position, 1.0);
  14. mat4 mtRes = mat4(1.0);//mat4(0.0)
  15. if(nWeightCount > 0)
  16. {
  17. mtRes = mat4(0.0);
  18. for(int i = 0; i < nWeightCount; ++i)
  19. {
  20. mtRes[0] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) ) * attrib_weightbias[i];
  21. mtRes[1] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 1) * attrib_weightbias[i];
  22. mtRes[2] += texelFetch(jointtex, int(3 * attrib_weightjoint[i]) + 2) * attrib_weightbias[i];
  23. }
  24. mtRes[3] = vec4(0.0, 0.0, 0.0, 1.0);
  25. }
  26. mat4 mtInvRes = transpose(mtRes);
  27. vec4 resPos = mtInvRes * attribPos;
  28. resPos = resPos / resPos.w;
  29. ....
  30. }

注意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. uniform mat4 jointMatrix[MAX_JOINT];
  3. void main(void)
  4. {
  5. int nWeightCount = int(attrib_weightcount);
  6. vec4 attribPos = vec4(attrib_position, 1.0);
  7. mat4 mtRes = mat4(1.0);
  8. if(nWeightCount > 0)
  9. {
  10. mtRes = mat4(0.0);
  11. for(int i = 0; i < nWeightCount; ++i)
  12. {
  13. mtRes += jointMatrix[int(attrib_weightjoint[i])] * attrib_weightbias[i];
  14. }
  15. }
  16. vec4 resPos = mtRes * attribPos;
  17. resPos = resPos / resPos.w;
  18. ....
  19. }

ModelVertexSkinningTD.vert - 2D Texture方式的vertex-skinning:

  1. uniform sampler2D jointtex;
  2. void main(void)
  3. {
  4. int nWeightCount = int(attrib_weightcount);
  5. vec4 attribPos = vec4(attrib_position, 1.0);
  6. ivec2 jointTexSize = textureSize(jointtex, 0);
  7. float fTexcoordStepU = 1.0 / 3.0;
  8. float fTexcoordStepV = 1.0 / jointTexSize.y;
  9. vec4 vMtX = vec4(0.0);
  10. vec4 vMtY = vec4(0.0);
  11. vec4 vMtZ = vec4(0.0);
  12. for(int i = 0; i < nWeightCount; ++i)
  13. {
  14. vMtX += texture2D(jointtex, vec2(fTexcoordStepU * 0.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
  15. vMtY += texture2D(jointtex, vec2(fTexcoordStepU * 1.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
  16. vMtZ += texture2D(jointtex, vec2(fTexcoordStepU * 2.5, (attrib_weightjoint[i] + 0.5) * fTexcoordStepV)) * attrib_weightbias[i];
  17. }
  18. vec4 resPos;
  19. resPos.x = dot(vMtX, attribPos);
  20. resPos.y = dot(vMtY, attribPos);
  21. resPos.z = dot(vMtZ, attribPos);
  22. resPos.w = 1.0;
  23. ....
  24. }

f2990e16627cf9b5d52936ae42f9e7b7.png

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明

2011-10-7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值