用3ds导入spk文件好小坐标多_MD2格式模型的格式、导入与帧动画

349cd19f8727fb2098efa5752a5c179f.png

MD2模型是一种古老的支持帧动画的模型格式,IDSoftware公司的游戏引擎id Tech 2所定义并采用的模型的格式。本文主要记录一下最近写的一个MD2格式导入类ZWModelMD2的一些细节。——ZwqXin.com

  • ZWModel3DS:[3DS文件结构的初步认识] / [用Indexed-VBO渲染3DS模型]
  • ZWModelOBJ:[OBJ模型文件的结构、导入与渲染Ⅰ] / [OBJ模型文件的结构、导入与渲染Ⅱ]

42a01495deb237dd78aebbab9ebb4f29.png

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

MD2格式最早是跟IDSoftware的游戏QuakeⅡ一起为世人所熟知的。它最大的特性是“自定义”头文件,支持帧动画(在当今来看这种动画模式比起骨骼动画还是颇简陋的)。帧动画顾名思义,就是通过高速幻灯片的方式展现一个动画,日本90年代之前的动画其实也都是这么一个原理。要达到模型的运动细致的效果,必然需要大量的帧数——最理想的情况当然是渲染程序中的每一帧都能转换一次模型的帧(这是不可能的,会导致庞大数据存储)。好吧,使用关健帧技术,每隔一段时间换一帧,期间的靠插值得出。但是关健帧之间相差太大是会导致模型的运动十分不自然的——所以即使是关健帧,也得足够多才能满足需求。这是MD2模型的局限——模型要越细致,则所需的关键帧数要越多,则所需存储的顶点数据要越多,则模型体积越大,则导入模型所花费的时间越长(渲染时间也会增加,导致程序帧率下降,导致模型的渲染被程序拖慢,造成幻灯片效果,模型也显得不细致)。

但是一来它的渲染原理易于理解,二来网上资源也不少,还是写了一个导入类ZWModelMD2。md2文件的特点如下:

  1. 1.它具有文件头(就好像BMP[Bmp文件的结构与基本操作]一样,里面宏观地记入了这个模型的整体信息),包含各顶点属性的总数、帧数、纹理大小、属性数据所在文件的位置等等一系列信息,而不像3ds、obj那样读到哪算哪。
  2. 2.二进制文件,它的数据都是对齐的,譬如md2文件中的顶点索引都是unsigned short,每个顶点索引在文件中占的大小都是sizeof(unsigned short)不变。
  3. 3.它没有类似3ds、obj和其他许多模型格式一样有网格对象的概念,或者说整个MD2模型就是一个整体,唯一的顶点属性集,唯一的属性索引集,唯一的纹理
  4. 4.它按关键帧的顺序把顶点集连续存放,形成一个庞大的总顶点集。但顶点索引只有一份,因为每个顶点集中对应索引的顶点是模型的“同一个位置”。
  5. 5.模型和纹理分开,模型文件里没有指明纹理的名称。
  6. 6.没有法线数据,顶点位置和纹理坐标各一套索引

根据以上的特征,可以认为md2文件的读入是十分十分方便的,关键是负责读入的数据结构要符合md2文件规范。从文件头开始:

  1. //MD2文件文件头标准
  2. typedef struct tagMd2Header
  3. {
  4. int nMagic; // The magic number used to identify the file.
  5. int nVersion; // The file version number (must be 8).
  6. int nSkinWidth; // The width in pixels of our image.
  7. int nSkinHeight; // The height in pixels of our image.
  8. int nFrameSize; // The size in bytes the frames are.
  9. int nNumSkins; // The number of skins associated with the model.
  10. int nNumVertices; // The number of vertices.
  11. int nNumTexCoords; // The number of texture coordinates.
  12. int nNumFaces; // The number of faces (triangles).
  13. int nNumGlCommands; // The number of gl commands.
  14. int nNumFrames; // The number of animated frames.
  15. int nOffsetSkins; // The offset in the file for the skin data.
  16. int nOffsetTexCoords; // The offset in the file for the texture data.
  17. int nOffsetFaces; // The offset in the file for the face data.
  18. int nOffsetFrames; // The offset in the file for the frames data.
  19. int nOffsetGlCommands; // The offset in the file for the gl commands data.
  20. int nOffsetEnd; // The end of the file offset.
  21. }Md2Header;
  22. 程序读入文件头:
  23. fread_s(&m_ModelMD2.MD2Header, sizeof(Md2Header), sizeof(BYTE),
  24. sizeof(Md2Header) , m_FilePointer);

简简单单就完成了这么多数据的读入。nMagic是MD2文件的标识符(类似3ds的primaryChunk),接下来是version、纹理大小、一个帧的数据的大小、纹理数量、每帧的顶点数量、纹理坐标数量、面数、GLcommand数、帧数以及各项数据相对文件起始位置的偏移量。其中,纹理数量和纹理数据的偏移处的数据都是废的(根据上面提到的第三点和第五点)。glcommand其实就是GL_TRIANLE_STRIP/LOOP之类的(MD2文件针对当时独大的OpenGL提供了优化方式,数据里另外存储了一些利于GL应用的东西,但是我怀疑多少模型的作者真正会这样做,感觉不靠谱,有兴趣的可以看看这个网址:http://tfc.duke.free.fr/coding/md2-specs-en.html的最后部分)。再来看看读入纹理坐标:

  1. typedef struct tagTexcoordInfo
  2. {
  3. short u;
  4. short v;
  5. }TexcoordInfo;
  6. //Load Texcoords Data
  7. TexcoordInfo *pTexcoordInfo = new TexcoordInfo[pModel->MD2Header.nNumTexCoords];
  8. fseek(m_FilePointer, pModel->MD2Header.nOffsetTexCoords, SEEK_SET);
  9. fread_s(pTexcoordInfo, pModel->MD2Header.nNumTexCoords * sizeof(TexcoordInfo),
  10. sizeof(TexcoordInfo), pModel->MD2Header.nNumTexCoords, m_FilePointer);

很迅速,fseek根据文件头里的偏移量定位文件指针,然后一个fread_s搞掂。再一次pay you attention:这类TexcoordInfo里的数据格式不是自己去定的,而要按md2文件格式的标准来(这里的纹理坐标不是规范化到[0,1]的,除以纹理的宽高后才是)。给出整个用于读入的标准数据结构(文件头上面给出了):

  1. //导入结构定义
  2. typedef struct tagVertPosInfo
  3. {
  4. unsigned char x;
  5. unsigned char y;
  6. unsigned char z;
  7. }VertPosInfo;
  8. typedef struct tagTexcoordInfo
  9. {
  10. short u;
  11. short v;
  12. }TexcoordInfo;
  13. typedef struct tagFrameVertInfo
  14. {
  15. VertPosInfo vertSrc;
  16. unsigned char normalIndex;
  17. }FrameVertInfo;
  18. typedef struct tagFrameInfo
  19. {
  20. float fScale[3];
  21. float fTranslate[3];
  22. char szName[16];
  23. FrameVertInfo *frameVertexInfo;
  24. }FrameInfo;
  25. typedef struct tagFaceInfo
  26. {
  27. unsigned short nVertIndex[3];
  28. unsigned short nTexcoordIndex[3];
  29. }FaceInfo;

从下往上看,这里:一个面数据由3个顶点索引和3个纹理坐标索引组成;一个帧数据由3个方向上的缩放因子和偏移因子,帧名字(晕死)和一系列帧顶点元数据组成,帧顶点元数据指针在读入时要根据头文件指示的每帧顶点数new出来的;帧顶点元数据又细分为一个顶点元数据和一个法线索引(后者用于GLcommand方式绘制时,对我来说是无用数据);顶点元数据就是3个方向的BYTE。

这里说一下后面要怎样取真正的顶点数据:顶点位置 = 顶点元 * 缩放因子 + 偏移因子。你说它省存储吧,它倒省得很彻底:导入文件时把庞大的顶点区(第5点提及)的数据都归化为一个个BYTE存储[0,255],从文件导出时再由统一的缩放和偏移因子来还原,硬把原本至少4字节的东西弄成1字节(先撇开顶点粗糙度不提)。

好吧。导完数据,知道公式,直接拿数据渲染,顺便计算一下法线,设定每隔n帧换一回顶点数据。OK,完工,本文完结,拜拜。

汗,才不会那么便宜呢!如果你是用传统的glVertex3f之类的,的确以上可谓全部。但我是要用VBO来渲染呢,VBO。在[OBJ模型文件的结构、导入与渲染Ⅱ]里我提到应用VBO的两大点准绳:一VBO一纹理这个不用考虑了,一个md2文件才一个纹理。问题是上面提到的第6点:位置和纹理坐标各一套索引,不符合第2个准绳:一份VBO里的各顶点属性VBO(不包括索引VBO)的大小应该一致。不要以为把纹理坐标一个个跟顶点位置配对就可以了,考虑这情况:顶点A(3.8, 4.2, 5.0)是2个面的共点,第一个面在该点的纹理坐标(0.84,0.36)第二个面在该点的纹理坐标(0.74, 0.11)。在MD2这种一张纹理还得兼顾模型各个部位的格式里,这种情况是十分常见的。在用索引的时候索引到这个点怎么办,是用第一个纹理坐标还是第二个?

是的,导入数据方便快速是一回事,怎么转化为用于存储的数据结构是另一回事:推倒重来。

  1. //标识独立顶点的结构体(不含法线信息)
  2. typedef struct tagVertInfo
  3. {
  4. tagVertInfo(const VertPosInfo &pos, const TexcoordInfo &tc)
  5. { InfoVertPos = pos; InfoTexcoord = tc; }
  6. VertPosInfo InfoVertPos;
  7. TexcoordInfo InfoTexcoord;
  8. }tVertInfo;
  9. // 帧信息
  10. typedef struct tag3DFrame
  11. {
  12. std::vector<Vector3> Verts; // 对象的顶点位置
  13. Vector3 *pNormals; // 对象的顶点法线
  14. }t3DFrame;
  15. // 模型信息结构体
  16. typedef struct tag3DModel
  17. {
  18. bool bIsTextured;// 是否使用纹理
  19. unsigned int nNumIndexes;// 顶点信息索引数目
  20. unsigned int nNumPosVBOs;// 顶点位置VBO数目
  21. Md2Header MD2Header; // MD2头文件
  22. std::vector<TexCoord> Texcoords; // 对象帧 - 顶点纹理坐标信息
  23. t3DFrame *pFrames; // 对象帧 - 顶点位置/法线信息
  24. unsigned short *pIndexes; // 对象的顶点索引
  25. GLuint *pPosVBO; // 顶点位置VBO句柄
  26. ......
  27. }t3DModel;
  28. //导入文件数据
  29. // 缓存当前对象的索引
  30. std::map<tVertInfo, unsigned short> vObjectIndexMap;
  31. //填充模型信息
  32. pModel->pIndexes = new unsigned short[pModel->MD2Header.nNumFaces * VERTEX_OF_FACE];
  33. pModel->pFrames = new t3DFrame[pModel->MD2Header.nNumFrames];
  34. VertPosInfo vCurVertPos;
  35. TexcoordInfo vCurTexcoord;
  36. int nCurFaceVertIndex = 0;
  37. int nCurIndexDataIdx = 0;
  38. for(int i = 0; i < pModel->MD2Header.nNumFaces; ++i)
  39. {
  40. for(int j = 0; j < VERTEX_OF_FACE; ++j)
  41. {
  42. nCurFaceVertIndex = pFaceInfo[i].nVertIndex[j];
  43. vCurVertPos = pframeInfo[0].frameVertexInfo[nCurFaceVertIndex].vertSrc;
  44. vCurTexcoord = pTexcoordInfo[pFaceInfo[i].nTexcoordIndex[j]];
  45. std::map<tVertInfo, unsigned short>::iterator pFind
  46. = vObjectIndexMap.find(tVertInfo(vCurVertPos, vCurTexcoord));
  47. if(vObjectIndexMap.end() != pFind)
  48. {
  49. pModel->pIndexes[nCurIndexDataIdx++] = pFind->second; //索引指向重复的信息的位置
  50. }
  51. else
  52. {
  53. //计算各帧的顶点位置
  54. for(int k = 0; k < pModel->MD2Header.nNumFrames; ++k)
  55. {
  56. pModel->pFrames[k].Verts.push_back(Vector3(
  57. pframeInfo[k].fScale[0] * pframeInfo[k].frameVertexInfo[nCurFaceVertIndex].vertSrc.x
  58. + pframeInfo[k].fTranslate[0], ..., ...));
  59. }
  60. //计算顶点纹理坐标(每帧一致)
  61. pModel->Texcoords.push_back(TexCoord(float(vCurTexcoord.u) / pModel->MD2Header.nSkinWidth,
  62. float(vCurTexcoord.v) / pModel->MD2Header.nSkinHeight));
  63. //为新加的信息的位置添加新索引
  64. pModel->pIndexes[nCurIndexDataIdx++] = pModel->Texcoords.size() - 1;
  65. //缓存查询表
  66. vObjectIndexMap.insert(std::pair<tVertInfo, unsigned short>(tVertInfo(vCurVertPos, vCurTexcoord),
  67. pModel->Texcoords.size() - 1));
  68. }
  69. }
  70. }
  71. pModel->nNumIndexes = nCurIndexDataIdx;
  72. //清除导入文件数据

之后计算法线数据,生成VBO,就跟3DS,OBJ类似了([用Indexed-VBO渲染3DS模型] [OBJ模型文件的结构、导入与渲染Ⅱ])。帧数据(位置、法线)的VBO有两套方案:一是每关键帧一个位置VBO一个法线VBO,渲染时候直接换VBO来绑定就好了;二是只生成两个位置VBO和两个法线VBO,在换关健帧的时候也重新给VBO传输数据。存储上的优劣是显而易见的:前者在传输完数据就可以扔掉内存中的帧数据了(位置和法线),但是数据都往VBO去了,一个200帧的MD2模型共需要400个VBO;后者要把帧数据都留在内存,但是同情形下只需4个VBO。前者利于CPU后者利于GPU。执行速度方面,后者要重传VBO数据,明显更浪费点CPU,但是这里我用的是双缓冲策略,一者在使用中的时候另一者可传输数据,只要关健帧之间的时间间隔不要太离谱(譬如0.1s以下),觉得FPS上不会有太大区别(模型特别巨型则另说)——总之两者都实现了(在Import函数参数里指定,或者后者可转为前者,逆转不能),就是按实际需求情况来了。

Animation帧动画方面,建立一个时间plot给它更新就好了。在插值上,可以利用CPU时间来插(严重不推荐),也可以用Shader插:简单的先行插值函数mix。ZWModelMD2既有shader版本也有非shader版本,这个不怎么复杂。因为至少有两个VBO,所以shader里可以有两个position attribute和两个normal attribute(关于怎么关联attribute变量数据,看此文:[OpenGL/GLSL数据传递小记(2.x)]),当然了,还有一个表征关健帧之间过渡完成百分比的融合度unifrom:u_fBlender。

  1. //vertex shader:
  2. vec3 pos = mix(attrib_position, attrib_nextposition, u_fBlender);
  3. gl_Position = gl_ModelViewProjectionMatrix * vec4(pos, 1.0);

三个帧速调用:

  1. void SetAnimationFrames(unsigned int nStartFrame, unsigned int nEndFrame, float fSecondsPerKeyFrame);
  2. void SetStaticFrame(unsigned int nStaticFrame);
  3. void SetAnimationAllFrames(float fSecondsPerKeyFrame);

382169dcfaf02cccef56ad73dfa6cdd2.png

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

2011-2-17

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值