MD2模型是一种古老的支持帧动画的模型格式,IDSoftware公司的游戏引擎id Tech 2所定义并采用的模型的格式。本文主要记录一下最近写的一个MD2格式导入类ZWModelMD2的一些细节。——ZwqXin.com
- ZWModel3DS:[3DS文件结构的初步认识] / [用Indexed-VBO渲染3DS模型]
- ZWModelOBJ:[OBJ模型文件的结构、导入与渲染Ⅰ] / [OBJ模型文件的结构、导入与渲染Ⅱ]
本文来源于 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.它具有文件头(就好像BMP[Bmp文件的结构与基本操作]一样,里面宏观地记入了这个模型的整体信息),包含各顶点属性的总数、帧数、纹理大小、属性数据所在文件的位置等等一系列信息,而不像3ds、obj那样读到哪算哪。
- 2.二进制文件,它的数据都是对齐的,譬如md2文件中的顶点索引都是unsigned short,每个顶点索引在文件中占的大小都是sizeof(unsigned short)不变。
- 3.它没有类似3ds、obj和其他许多模型格式一样有网格对象的概念,或者说整个MD2模型就是一个整体,唯一的顶点属性集,唯一的属性索引集,唯一的纹理。
- 4.它按关键帧的顺序把顶点集连续存放,形成一个庞大的总顶点集。但顶点索引只有一份,因为每个顶点集中对应索引的顶点是模型的“同一个位置”。
- 5.模型和纹理分开,模型文件里没有指明纹理的名称。
- 6.没有法线数据,顶点位置和纹理坐标各一套索引
根据以上的特征,可以认为md2文件的读入是十分十分方便的,关键是负责读入的数据结构要符合md2文件规范。从文件头开始:
- //MD2文件文件头标准
- typedef struct tagMd2Header
- {
- int nMagic; // The magic number used to identify the file.
- int nVersion; // The file version number (must be 8).
- int nSkinWidth; // The width in pixels of our image.
- int nSkinHeight; // The height in pixels of our image.
- int nFrameSize; // The size in bytes the frames are.
- int nNumSkins; // The number of skins associated with the model.
- int nNumVertices; // The number of vertices.
- int nNumTexCoords; // The number of texture coordinates.
- int nNumFaces; // The number of faces (triangles).
- int nNumGlCommands; // The number of gl commands.
- int nNumFrames; // The number of animated frames.
- int nOffsetSkins; // The offset in the file for the skin data.
- int nOffsetTexCoords; // The offset in the file for the texture data.
- int nOffsetFaces; // The offset in the file for the face data.
- int nOffsetFrames; // The offset in the file for the frames data.
- int nOffsetGlCommands; // The offset in the file for the gl commands data.
- int nOffsetEnd; // The end of the file offset.
- }Md2Header;
- 程序读入文件头:
- fread_s(&m_ModelMD2.MD2Header, sizeof(Md2Header), sizeof(BYTE),
- 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的最后部分)。再来看看读入纹理坐标:
- typedef struct tagTexcoordInfo
- {
- short u;
- short v;
- }TexcoordInfo;
- //Load Texcoords Data
- TexcoordInfo *pTexcoordInfo = new TexcoordInfo[pModel->MD2Header.nNumTexCoords];
- fseek(m_FilePointer, pModel->MD2Header.nOffsetTexCoords, SEEK_SET);
- fread_s(pTexcoordInfo, pModel->MD2Header.nNumTexCoords * sizeof(TexcoordInfo),
- sizeof(TexcoordInfo), pModel->MD2Header.nNumTexCoords, m_FilePointer);
很迅速,fseek根据文件头里的偏移量定位文件指针,然后一个fread_s搞掂。再一次pay you attention:这类TexcoordInfo里的数据格式不是自己去定的,而要按md2文件格式的标准来(这里的纹理坐标不是规范化到[0,1]的,除以纹理的宽高后才是)。给出整个用于读入的标准数据结构(文件头上面给出了):
- //导入结构定义
- typedef struct tagVertPosInfo
- {
- unsigned char x;
- unsigned char y;
- unsigned char z;
- }VertPosInfo;
- typedef struct tagTexcoordInfo
- {
- short u;
- short v;
- }TexcoordInfo;
- typedef struct tagFrameVertInfo
- {
- VertPosInfo vertSrc;
- unsigned char normalIndex;
- }FrameVertInfo;
- typedef struct tagFrameInfo
- {
- float fScale[3];
- float fTranslate[3];
- char szName[16];
- FrameVertInfo *frameVertexInfo;
- }FrameInfo;
- typedef struct tagFaceInfo
- {
- unsigned short nVertIndex[3];
- unsigned short nTexcoordIndex[3];
- }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这种一张纹理还得兼顾模型各个部位的格式里,这种情况是十分常见的。在用索引的时候索引到这个点怎么办,是用第一个纹理坐标还是第二个?
是的,导入数据方便快速是一回事,怎么转化为用于存储的数据结构是另一回事:推倒重来。
- //标识独立顶点的结构体(不含法线信息)
- typedef struct tagVertInfo
- {
- tagVertInfo(const VertPosInfo &pos, const TexcoordInfo &tc)
- { InfoVertPos = pos; InfoTexcoord = tc; }
- VertPosInfo InfoVertPos;
- TexcoordInfo InfoTexcoord;
- }tVertInfo;
- // 帧信息
- typedef struct tag3DFrame
- {
- std::vector<Vector3> Verts; // 对象的顶点位置
- Vector3 *pNormals; // 对象的顶点法线
- }t3DFrame;
- // 模型信息结构体
- typedef struct tag3DModel
- {
- bool bIsTextured;// 是否使用纹理
- unsigned int nNumIndexes;// 顶点信息索引数目
- unsigned int nNumPosVBOs;// 顶点位置VBO数目
- Md2Header MD2Header; // MD2头文件
- std::vector<TexCoord> Texcoords; // 对象帧 - 顶点纹理坐标信息
- t3DFrame *pFrames; // 对象帧 - 顶点位置/法线信息
- unsigned short *pIndexes; // 对象的顶点索引
- GLuint *pPosVBO; // 顶点位置VBO句柄
- ......
- }t3DModel;
- //导入文件数据
- // 缓存当前对象的索引
- std::map<tVertInfo, unsigned short> vObjectIndexMap;
- //填充模型信息
- pModel->pIndexes = new unsigned short[pModel->MD2Header.nNumFaces * VERTEX_OF_FACE];
- pModel->pFrames = new t3DFrame[pModel->MD2Header.nNumFrames];
- VertPosInfo vCurVertPos;
- TexcoordInfo vCurTexcoord;
- int nCurFaceVertIndex = 0;
- int nCurIndexDataIdx = 0;
- for(int i = 0; i < pModel->MD2Header.nNumFaces; ++i)
- {
- for(int j = 0; j < VERTEX_OF_FACE; ++j)
- {
- nCurFaceVertIndex = pFaceInfo[i].nVertIndex[j];
- vCurVertPos = pframeInfo[0].frameVertexInfo[nCurFaceVertIndex].vertSrc;
- vCurTexcoord = pTexcoordInfo[pFaceInfo[i].nTexcoordIndex[j]];
- std::map<tVertInfo, unsigned short>::iterator pFind
- = vObjectIndexMap.find(tVertInfo(vCurVertPos, vCurTexcoord));
- if(vObjectIndexMap.end() != pFind)
- {
- pModel->pIndexes[nCurIndexDataIdx++] = pFind->second; //索引指向重复的信息的位置
- }
- else
- {
- //计算各帧的顶点位置
- for(int k = 0; k < pModel->MD2Header.nNumFrames; ++k)
- {
- pModel->pFrames[k].Verts.push_back(Vector3(
- pframeInfo[k].fScale[0] * pframeInfo[k].frameVertexInfo[nCurFaceVertIndex].vertSrc.x
- + pframeInfo[k].fTranslate[0], ..., ...));
- }
- //计算顶点纹理坐标(每帧一致)
- pModel->Texcoords.push_back(TexCoord(float(vCurTexcoord.u) / pModel->MD2Header.nSkinWidth,
- float(vCurTexcoord.v) / pModel->MD2Header.nSkinHeight));
- //为新加的信息的位置添加新索引
- pModel->pIndexes[nCurIndexDataIdx++] = pModel->Texcoords.size() - 1;
- //缓存查询表
- vObjectIndexMap.insert(std::pair<tVertInfo, unsigned short>(tVertInfo(vCurVertPos, vCurTexcoord),
- pModel->Texcoords.size() - 1));
- }
- }
- }
- pModel->nNumIndexes = nCurIndexDataIdx;
- //清除导入文件数据
之后计算法线数据,生成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。
- //vertex shader:
- vec3 pos = mix(attrib_position, attrib_nextposition, u_fBlender);
- gl_Position = gl_ModelViewProjectionMatrix * vec4(pos, 1.0);
三个帧速调用:
- void SetAnimationFrames(unsigned int nStartFrame, unsigned int nEndFrame, float fSecondsPerKeyFrame);
- void SetStaticFrame(unsigned int nStaticFrame);
- void SetAnimationAllFrames(float fSecondsPerKeyFrame);
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/md2-model-format-import-animation.html