本文介绍MD2文件的格式,并介绍使用OpenGL显示MD2文件的方法。
首先,我们必须要搞清几个问题:
1、动画的实现原理
2、MD2文件的数据存储格式
3、OpenGL显示动画的方法
一、动画的原理
动画就是连续出现的画面,在3D动画中,在一个在两个差别很大的动作之间进行插值,使得3D模型的各个部分连续运动而得到动画的效果。比如:将手臂在左边时的3D模型和手臂在右边时的3D模型进行保留,然后根据时间在这两个模型之间进行插值,让其在某个时刻显示其在中间的模型,如此连续的显示便构成了动画的效果。
因此,MD2文件中便存储了动画的各个关键帧,只不过可能某些动作的完成需要多个关键帧,另外,我们了解了动画的原理,我们便知道,在动画的运动过程中,模型的顶点个数和纹理是相同的,只是在某个时刻模型的顶点坐标有差异。
二、MD2文件数据的格式
要搞清楚MD2文件的格式必须要知道其中都存储了那些数据,MD2动画由两个文件组成,一个是以.MD2为后缀的文件,其中保留了动画模型的各个点的信息,包括:顶点坐标、纹理坐标、纹理名称、三角形索引等信息。另一个是一个图片文件,可以是多种格式的图片,本文中使用的是BMP文件。
1、文件头
要搞清楚MD2文件中各种数据的大小和存储位置就必须要先分析文件头,我们使用下面的结构体来描述文件头:
Code
/** MD2文件头 */
struct tMd2Header
{
int magic; /**< 文件标志 */
int version; /**< 文件版本号 */
int skinWidth; /**< 纹理宽度 */
int skinHeight; /**< 纹理高度 */
int frameSize; /**< 每一帧的字节数 */
int numSkins; /**< 纹理数目 */
int numVertices; /**< 顶点数目(每一帧中) */
int numTexCoords; /**< 纹理坐标数目 */
int numTriangles; /**< 三角行数目 */
int numGlCommands; /**< gl命令数目 */
int numFrames; /**< 总帧数 */
int offsetSkins; /**< 纹理的偏移位置 */
int offsetTexCoords; /**< 纹理坐标的偏移位置 */
int offsetTriangles; /**< 三角形索引的偏移位置 */
int offsetFrames; /**< 第一帧的偏移位置 */
int offsetGlCommands; /**< OPenGL命令的偏移位置 */
int offsetEnd; /**< 文件结尾偏移位置 */
};
下面对各个变量进行解释:
magic:是表明该文件是MD2文件的标志,它必须等于"IPD2",不然就不是一个MD2文件。
version:表明该文件的版本,本文中,它的值为8。
skinWidth:纹理的宽度,我们用这个参数来对纹理坐标进行解压。当然,因为纹理是与MD2文件分离的,你也可以到文件中去获取。
skinHeight:纹理的高度,它的用途同上。
frameSize:表明每个关键帧的大小,它决定了我们每次读取关键帧时的数据读取量。
numSkins:表明纹理的个数,本文中只有一个纹理。
numVertices:每帧中顶点的个数,我们用这个参数决定读取顶点信息时的数据读取量。
numTexCoords:纹理坐标的个数,我们用这个参数决定读取纹理坐标时的数据读取量。
numTriangles:三角形个数,在动画模型中,使用三角形索引来绘制一个面。
numGlCommands:OpenGL命令的条数,本文中未使用这个参数。
numFrames:总帧数,它决定了我们需要读取的帧信息量。
offsetSkins:纹理名称在文件中的偏移量,读取纹理名称从它指定的地方开始。
offsetTexCoords:纹理坐标在文件中的偏移量,读取纹理坐标从它指定的地方开始。
offsetTriangles:面顶点索引在文件中的偏移量,读取面顶点索引从它指定的地方开始。
offsetFrames:第一帧的位置,读取帧信息时从它指定的地方开始。
offsetGlCommands:OpenGL命令在文件中的偏移量,文中未使用这个参数。
offsetEnd:文件结束的位置,这个参数可以用来检查该文件的完整性。
2、顶点结构
MD2文件中的顶点是经过压缩的,它包含的这样的一个结构:
Code
/** 帧中的顶点结构 */
struct tMd2AliasFrame
{
float scale[3];//坐标的缩放比例
float translate[3];//坐标的偏移量
char name[16];//顶点所属的帧名
tMd2AliasTriangle aliasVertices[1];//压缩的顶点
};
/** 压缩的顶点顶点结构 */
struct tMd2AliasTriangle
{
BYTE vertex[3];//压缩的x,y,z值
BYTE lightNormalIndex;//法向量索引
};
每一帧都是由帧大小(frameSize)个顶点组成,因此,每个帧占用的空间为:sizeof(tMd2AliasFrame)*frameSize。
2、纹理名称
MD2文件中纹理名称是长度为64的字符序列,我们这样表示:
/*
* 纹理名字
*/
typedef
char
tMd2Skin[
64
];
3、纹理坐标
MD2文件中的纹理坐标也是经过压缩的,它的结构如下:
/*
* 纹理坐标结构
*/
struct
tMd2TexCoord
{
short
u, v;
};
在读取纹理坐标后需要对其进行解压,公式为:U = u / skinWidth; V = v / skinHeight。
4、面结构
我们说过了,MD2文件中的使用面结构组成一个三角形,面结构保存了该三角形的三个顶点在帧顶点中的索引,和三个顶点所对应的纹理坐标在纹理坐标序列中的索引。
/*
* 面结构
*/
struct
tMd2Face
{
short
vertexIndices[
3
];
//
顶点索引
short
textureIndices[
3
];
//
纹理索引
};
三、辅助结构
因为MD2文件数据本身是压缩过的,因此为了得到真正能有的信息,我们必须要定义一些辅助结构来存储转换后的数据。
1、顶点结构
顶点结构用来存储解压后的顶点信息。
/*
* 解压后的顶点结构
*/
struct
tMd2Triangle
{
float
vertex[
3
];
//
顶点坐标
float
normal[
3
];
//
法向量
};
2、面结构
面结构用来存储每个三角形面的三个点的顶点坐标索引和纹理坐标索引。
/*
* 面信息
*/
struct
tFace
{
int
vertIndex[
3
];
/*
*< 顶点索引
*/
int
coordIndex[
3
];
/*
*< 纹理坐标索引
*/
};
3、关键帧结构
关键帧结构用来存储关键帧的名称和它包含的所有的顶点信息。
/*
* 关键帧结构
*/
struct
tMd2Frame
{
char
strName[
16
];
//
关键帧名称
tMd2Triangle
*
pVertices;
//
帧中顶点信息
};
4、动作信息结构
动作信息结构用来存放该动作的名称和该动作包含的起始关键帧索引和结束关键帧索引。
/*
* 动作信息结构体
*/
struct
tAnimationInfo
{
char
strName[
255
];
/*
*< 帧的名称
*/
int
startFrame;
/*
*< 开始帧
*/
int
endFrame;
/*
*< 结束帧
*/
};
5、关键帧结构
关键帧结构用来存储当前帧中顶点、面、纹理坐标信息。
Code
/** 对象信息结构体 */
struct t3DObject
{
int numOfVerts; /**< 模型中顶点的数目 */
int numOfFaces; /**< 模型中面的数目 */
int numTexVertex; /**< 模型中纹理坐标的数目 */
int materialID; /**< 纹理ID */
bool bHasTexture; /**< 是否具有纹理映射 */
char strName[255]; /**< 对象的名称 */
Vector3 *pVerts; /**< 对象的顶点 */
Vector3 *pNormals; /**< 对象的法向量 */
Vector2 *pTexVerts; /**< 纹理UV坐标 */
tFace *pFaces; /**< 对象的面信息 */
};
6、模型信息结构
模型信息结构用来存放动画的全部信息,包括:关键帧链表,材质链表和动作信息链表等。
Code
/** 模型信息结构体 */
struct t3DModel
{ int numOfObjects; /**< 模型中对象的数目 */
int numOfMaterials; /**< 模型中材质的数目 */
int numOfAnimations; /**< 模型中动作的数目 */
int currentAnim; /**< 帧索引 */
int currentFrame; /**< 当前帧 */
vector<tAnimationInfo> pAnimations; /**< 帧信息链表 */
vector<tMaterialInfo> pMaterials; /**< 材质链表信息 */
vector<t3DObject> pObject; /**< 模型中对象链表信息 */
};
四、实现过程
我们构建好了用于存储数据的结构,下面介绍实现动画的过程,我们将整个过程分为三个部分:读取原始数据,将数据转换成模型结构和动画显示。
1、数据读取
Code
//数据读取函数
void CMD2Loader::ReadMD2Data()
{
//定义存储帧信息的缓冲区
unsigned char buffer[MD2_MAX_FRAMESIZE];
//为纹理名称申请空间
m_pSkins = new tMd2Skin[m_Header.numSkins];
//为纹理坐标申请空间
m_pTexCoords = new tMd2TexCoord[m_Header.numTexCoords];
//为面结构申请空间
m_pTriangles = new tMd2Face[m_Header.numTriangles];
//为帧结构申请空间
m_pFrames = new tMd2Frame[m_Header.numFrames];
//读取纹理名称
fseek(m_FilePointer,m_Header.offsetSkins,SEEK_SET);
fread(m_pSkins,sizeof(tMd2Skin),m_Header.numSkins,m_FilePointer);
//读取纹理坐标
fseek(m_FilePointer,m_Header.offsetTexCoords,SEEK_SET);
fread(m_pTexCoords,sizeof(tMd2TexCoord),m_Header.numTexCoords,m_FilePointer);
//读取面信息
fseek(m_FilePointer,m_Header.offsetTriangles,SEEK_SET);
fread(m_pTriangles,sizeof(tMd2Face),m_Header.numTriangles,m_FilePointer);
fseek(m_FilePointer,m_Header.offsetFrames,SEEK_SET);
//循环读取每一个关键帧信息
for(int i=0; i<m_Header.numFrames; i++)
{
//将缓冲区转换为帧结构
tMd2AliasFrame *pFrame = (tMd2AliasFrame*)buffer;
//为帧的顶点分配空间
m_pFrames[i].pVertices = new tMd2Triangle[m_Header.numVertices];
//读取帧信息
fread(pFrame,1,m_Header.frameSize,m_FilePointer);
//拷贝帧名称
strcpy(m_pFrames[i].strName,pFrame->name);
//获取顶点指针
tMd2Triangle *pVertices = m_pFrames[i].pVertices;
//循环对关键帧的顶点信息进行解压,注意,要交换y,z轴,并将z轴反向。
for(int j=0; j<m_Header.numVertices; j++)
{
pVertices[j].vertex[0] = pFrame->aliasVertices[j].vertex[0] * pFrame->scale[0] + pFrame->translate[0];
pVertices[j].vertex[2] = -1 * (pFrame->aliasVertices[j].vertex[1] * pFrame->scale[1] + pFrame->translate[1]);
pVertices[j].vertex[1] = pFrame->aliasVertices[j].vertex[2] * pFrame->scale[2] + pFrame->translate[2];
}
}
}
2、数据结构转换
未完待续...