« OpenGL/GLSL数据传递小记(2.x)GimbalLock万向节锁与四元数旋转 »

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

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


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

本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
      原文地址:http://www.zwqxin.cn/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. 程序读入文件头:
  24.     fread_s(&m_ModelMD2.MD2Header, sizeof(Md2Header), sizeof(BYTE), 
  25.         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.  
  7. //Load Texcoords Data
  8. TexcoordInfo *pTexcoordInfo = new TexcoordInfo[pModel->MD2Header.nNumTexCoords];
  9.  
  10. fseek(m_FilePointer, pModel->MD2Header.nOffsetTexCoords, SEEK_SET);
  11.  
  12. fread_s(pTexcoordInfo, pModel->MD2Header.nNumTexCoords * sizeof(TexcoordInfo), 
  13.     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.  
  9. typedef struct tagTexcoordInfo
  10. {
  11.     short u;
  12.     short v;
  13. }TexcoordInfo;
  14.  
  15. typedef struct tagFrameVertInfo
  16. {
  17.     VertPosInfo   vertSrc;
  18.     unsigned char normalIndex; 
  19. }FrameVertInfo;
  20.  
  21. typedef struct tagFrameInfo
  22. {
  23.     float         fScale[3];
  24.     float         fTranslate[3];
  25.     char          szName[16];
  26.     FrameVertInfo *frameVertexInfo;
  27. }FrameInfo;
  28.  
  29. typedef struct tagFaceInfo
  30. {
  31.     unsigned short nVertIndex[3];
  32.     unsigned short nTexcoordIndex[3];
  33. }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. // 帧信息
  11. typedef struct tag3DFrame
  12. {
  13.     std::vector<Vector3>   Verts;    // 对象的顶点位置
  14.     Vector3               *pNormals; // 对象的顶点法线
  15. }t3DFrame;
  16.  
  17. // 模型信息结构体
  18. typedef struct tag3DModel 
  19. {
  20.     bool                    bIsTextured;// 是否使用纹理
  21.     unsigned int            nNumIndexes;// 顶点信息索引数目
  22.     unsigned int            nNumPosVBOs;// 顶点位置VBO数目
  23.     Md2Header               MD2Header;  // MD2头文件
  24.     std::vector<TexCoord>   Texcoords;  // 对象帧 - 顶点纹理坐标信息
  25.     t3DFrame               *pFrames;    // 对象帧 - 顶点位置/法线信息
  26.     unsigned short         *pIndexes;   // 对象的顶点索引
  27.     GLuint                 *pPosVBO;    // 顶点位置VBO句柄
  28.     ......
  29. }t3DModel;
  30.  
  1.  
  2. //导入文件数据
  3.  
  4. // 缓存当前对象的索引
  5. std::map<tVertInfo, unsigned short> vObjectIndexMap;
  6.  
  7.   //填充模型信息
  8.  
  9. pModel->pIndexes = new unsigned short[pModel->MD2Header.nNumFaces * VERTEX_OF_FACE];   
  10.  
  11. pModel->pFrames  = new t3DFrame[pModel->MD2Header.nNumFrames];
  12.  
  13. VertPosInfo  vCurVertPos;
  14. TexcoordInfo vCurTexcoord;
  15. int          nCurFaceVertIndex = 0;
  16. int          nCurIndexDataIdx  = 0;
  17. for(int i = 0; i < pModel->MD2Header.nNumFaces; ++i)
  18. {
  19.     for(int j = 0; j < VERTEX_OF_FACE; ++j)
  20.     {
  21.         nCurFaceVertIndex = pFaceInfo[i].nVertIndex[j];
  22.  
  23.         vCurVertPos  = pframeInfo[0].frameVertexInfo[nCurFaceVertIndex].vertSrc;
  24.         vCurTexcoord = pTexcoordInfo[pFaceInfo[i].nTexcoordIndex[j]];
  25.  
  26.         std::map<tVertInfo, unsigned short>::iterator pFind 
  27.             = vObjectIndexMap.find(tVertInfo(vCurVertPos, vCurTexcoord));
  28.  
  29.         if(vObjectIndexMap.end() != pFind)
  30.         {
  31.             pModel->pIndexes[nCurIndexDataIdx++] = pFind->second; //索引指向重复的信息的位置
  32.         }
  33.         else
  34.         {
  35.             //计算各帧的顶点位置
  36.             for(int k = 0; k < pModel->MD2Header.nNumFrames; ++k)
  37.             {
  38.                 pModel->pFrames[k].Verts.push_back(Vector3(
  39.                     pframeInfo[k].fScale[0] * pframeInfo[k].frameVertexInfo[nCurFaceVertIndex].vertSrc.x
  40.                 + pframeInfo[k].fTranslate[0], ...,  ...));
  41.             }
  42.  
  43.             //计算顶点纹理坐标(每帧一致)
  44.             pModel->Texcoords.push_back(TexCoord(float(vCurTexcoord.u) / pModel->MD2Header.nSkinWidth,
  45.                     float(vCurTexcoord.v) / pModel->MD2Header.nSkinHeight));
  46.  
  47.             //为新加的信息的位置添加新索引
  48.             pModel->pIndexes[nCurIndexDataIdx++] = pModel->Texcoords.size() - 1;
  49.  
  50.             //缓存查询表
  51.             vObjectIndexMap.insert(std::pair<tVertInfo, unsigned short>(tVertInfo(vCurVertPos, vCurTexcoord), 
  52.                 pModel->Texcoords.size() - 1));
  53.         }
  54.     }
  55. }
  56.  
  57. pModel->nNumIndexes = nCurIndexDataIdx;
  58.  
  59. //清除导入文件数据

 之后计算法线数据,生成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.  
  4.   gl_Position = gl_ModelViewProjectionMatrix * vec4(pos, 1.0);

 三个帧速调用:

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

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

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

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2024 ZwqXin. All Rights Reserved. Theme edited from ipati.