http://tfc.duke.free.fr/old/models/md2.htm
Quake2
的MD2文件格式
介绍:
“
好,一个新的MD2圣经:)”,虽然说明MD2的指南满天飞,但我这次会给出一个新方式来渲染。
什么是MD2呢?其实他是一个最早被用在Q2引擎中的一种3D模型格式,下面我会介绍如何加载并用OPENGL实时渲染。
也许你认为没有必要再捡起这种老掉牙的格式,可我们确实有理由来了解他的真谛。首先,MD2是一个非常适合初学者的文件格式,然后呢…..
然后,他完全地免费!!!呵呵。
文章内容包括:
MD2
文件格式
开发一个CMD2MODEL类
存取一个MD2模型
显示模型
动画
特别说明的是,这里的源代码是完全的免费的,如果你“不小心”看到这篇文章的话,希望你来自奇妙的C++的丛林,当然也要有在OPENGL的王国从政的经历。好,我们开始吧。
MD2
文件格式
和其他的文件格式一样,MD2文件也是有两部分组成:文件头和数据区。
文件头中包含了许多重要的信息,如数据区的大小、MAGIC标志、版本号等,当然他的大小是确定的,所以在程序中我们通常有一个结构来表示。数据区就有很大的不同,由于每个文件包含的图元和纹理不一样,所以他们一般会有很大的区别。
文件头中包含了许多重要的信息,如数据区的大小、MAGIC标志、版本号等,当然他的大小是确定的,所以在程序中我们通常有一个结构来表示。数据区就有很大的不同,由于每个文件包含的图元和纹理不一样,所以他们一般会有很大的区别。
具体结构如下图:
下面是MD2的文件头定义:
// md2 header
typedef struct
{
int ident; // 文件标识. 必须是"IPD2"
int version; // md2 版本. 等于 8
int skinwidth; // 纹理宽度
int skinheight; //纹理高度
int framesize; // 每一帧占的字节数
int num_skins; // 纹理的数目
int num_xyz; // 三维空间中点数
int num_st; // 纹理的坐标数
int num_tris; //三角形的数目
int num_glcmds; // OPENGL命令数目
int num_frames; // 文件包含的帧数
int ofs_skins; // 纹理名称的偏移量(每个64字节)
int ofs_st; // 存储纹理坐标偏移量
int ofs_tris; // 存储三角形地址偏移量
int ofs_frames; //存储帧的地址偏移量
int ofs_glcmds; // 存储OPENGL命令的地址偏移量
int ofs_end; // 文件结尾的偏移量
} md2_t;
那么,我会简要解释一下变量的用途:
首先,准备好灯光。。。呵呵
下面请“
ident
”又名“Magic Number”.
Q:
请问你对自己在MD2中的职责有何理解?
A:
咳,由于时间紧迫,我就长话短说。比较形象的说法就是:当MD2 loader要拉人下舞场(内存)时,会问你是“IPD2”吗?如果不是,那就不会有后面的探戈了。
看来在接下来的节目中,时间是我们不得不考虑的因素,下面我当全职介绍:
“
version
”,MD2文件的版本,值等于8
“
skinwidth
”、“
skinheight
”纹理的宽度和长度。由于MD2的纹理和模型分开,所以这里的数据基本上没有什么用处。
“
framesize
”
制定一帧所占的字节数,那么什么是帧呢?如果你对动画的原理有所了解的话,我就没有往下些了。帧就是特定时间点的一个图片。在3D场景中,图形都是用图元表示,而这些图元需要一定的存储空间,这就是FRAMESIZE的工作。
MD2
文件大部分多有199个FRAME,这199个FRAME经过一定的组合表示21种动画。
“
num_skins
”MD2模型的纹理数目。
由于上面有解释,其他的我不自一一说明,详细信息请参考英文原版,我将会在文章的结尾给出链接。
下面我们来看看 num_frames。他指出这个MD2模体有多少帧。事实上,他们是有一定时间间隔的关键帧,由于文件要读如内存,一个动作要包含200到300帧是不怎么现实的,如果你有特别的需求,也可以这样做,呵呵。
那么既然关键帧是不连续的,那么我们怎么保证模体动画的连续性,解决的方法就是用线性插值的方式来动态生成中间的帧。如下图:
说完文件头,我们继续对文件余下的内容进行详细的说明和表示:
与头文件一样,我们还是用结构体来表示帧,向量,和OPENGL命令。
首先,我们介绍一下向量,我们不用复杂的类来表示他,而只是选择了一个简单的数组。
typedef float vec3_t[3];
每一个模体都有num_frame * num_xyz个向量,下面是单个向量的结构:
// vertex
typedef struct
{
unsigned char v[3]; // 压缩的向量 坐标(x, y, z)
unsigned char lightnormalindex; // 向量的索引
} vertex_t;
你可能注意到了 unsigned char v[3]; 是的,这个向量的表示的是已经压缩过的数据(听起来很专业:)),在计算中我们会把这些数据解压,然后熏染,所以模型不会受到太大的影响。
Lightnormalindex是一个向量的索引。
对于一个模体的向量,还会有一个纹理坐标:
// 纹理坐标
typedef struct
{
short s;
short t;
} texCoord_t;
和向量的表示一样,纹理的数据也进行了压缩,在图形渲染时,我们会解压这些数据。
Like for vertices, data is compresed. Here we use short (2 bytes) instead of float (4 bytes) for storing texture coordinates. But to use them, we must convert them to float because texture coordinates range from 0.0 to 1.0, and if we kept short values, we could have only 0 or 1 and any intermediate value! So how to uncompress them? It's quite simple. Divide the short value by the texture size :
公式如下:
RealST[i].s = (float)texCoord[i].s / header.skinwidth;
RealST[i].t = (float)texCoord[i].t / header.skinheight;
关键帧的结构如下:
// frame
typedef struct
{
float scale[3]; // 压缩,解压码
float translate[3]; //平移向量
char name[16]; // 帧名称
vertex_t verts[1]; // 帧向量开始的地方
} frame_t;
scale[3]
就是我们在上面说的解压的关键角色,这个数组中储存了缩放的倍数,translate中主要用来储存缩放后的平移量。
那么关键帧的一个点实际值的表示方法如下:
vertex.x = (frame.verts[i].v[0] * frame.scale[0]) + frame.translate[0]
vertex.y = (frame.verts[i].v[1] * frame.scale[1]) + frame.translate[1]
vertex.z = (frame.verts[i].v[2] * frame.scale[2]) + frame.translate[2]
其中vertex就是模体中实际向量值。值得书名的是:frame.verts数组的索引范围是:
0-
(num_xyz – 1)
下面我们列出他们之间的关系图:
在真正的渲染过程中,我们需要把三角型的顶点向量和纹理坐标联系起来,那么我们会建一个结构体,考虑到MD2的文件结构,结构体如下:
// triangle
typedef struct
{
short index_xyz[3]; // 向量索引
short index_st[3]; // 纹理索引
} triangle_t;
你可能已经注意到,我们现在已经把每一个三角型的定点已经和三角型的纹理对应上了,渲染代码如下:
glBegin( GL_TRIANGLES );
// draw each triangle
for( int i = 0; i < header.num_tris; i++ )
{
// draw triangle #i
for( int j = 0; j < 3; j++ )
{
// k is the frame to draw
// i is the current triangle of the frame
// j is the current vertex of the triangle
glTexCoord2f( (float)TexCoord[ Meshes[i].index_st[j] ].s / header.skinwidth,
(float)TexCoord[ Meshes[i].index_st[j] ].t / header.skinheight );
glNormal3fv( anorms[ Vertices[ Meshes[i].index_xyz[j] ].lightnormalindex ] );
glVertex3f( (Vertices[ Meshes[i].index_xyz[j] ].v[0] * frame[k].scale[0]) + frame[k].translate[0],
(Vertices[ Meshes[i].index_xyz[j] ].v[1] * frame[k].scale[1]) + frame[k].translate[1],
(Vertices[ Meshes[i].index_xyz[j] ].v[2] * frame[k].scale[2]) + frame[k].translate[2] );
}
}
glEnd();
opengl命令应用,我不在翻译,参照原版。
下面是整个文件的结构图:
http://tfc.duke.free.fr/old/models/md2.htm