本文介绍MD2文件的格式,并介绍使用OpenGL显示MD2文件的方法。
首先,我们必须要搞清几个问题:
1、动画的实现原理 2、MD2文件的数据存储格式 3、OpenGL显示动画的方法 一、动画的原理 动画就是连续出现的画面,在3D动画中,在一个在两个差别很大的动作之间进行插值,使得3D模型的各个部分连续运动而得到动画的效果。比如:将手臂在左边时的3D模型和手臂在右边时的3D模型进行保留,然后根据时间在这两个模型之间进行插值,让其在某个时刻显示其在中间的模型,如此连续的显示便构成了动画的效果。 因此,MD2文件中便存储了动画的各个关键帧,只不过可能某些动作的完成需要多个关键帧,另外,我们了解了动画的原理,我们便知道,在动画的运动过程中,模型的顶点个数和纹理是相同的,只是在某个时刻模型的顶点坐标有差异。 二、MD2文件数据的格式 要搞清楚MD2文件的格式必须要知道其中都存储了那些数据,MD2动画由两个文件组成,一个是以.MD2为后缀的文件,其中保留了动画模型的各个点的信息,包括:顶点坐标、纹理坐标、纹理名称、三角形索引等信息。另一个是一个图片文件,可以是多种格式的图片,本文中使用的是BMP文件。 1、文件头 要搞清楚MD2文件中各种数据的大小和存储位置就必须要先分析文件头,我们使用下面的结构体来描述文件头:/** MD2文件头 */struct tMd2Header{ int magic; /**< 文件标志 */是表明该文件是MD2文件的标志,它必须等于"IPD2",不然就不是一个MD2文件。 int version; /**< 文件版本号 */:表明该文件的版本,本文中,它的值为8。 int skinWidth; /**< 纹理宽度 */纹理的宽度,我们用这个参数来对纹理坐标进行解压。当然,因为纹理是与MD2文件分离的,你也可以到文件中去获取。 int skinHeight; /**< 纹理高度 */纹理的高度,它的用途同上。 int frameSize; /**< 每一帧的字节数 */表明每个关键帧的大小,它决定了我们每次读取关键帧时的数据读取量。 int numSkins; /**< 纹理数目 */表明纹理的个数,本文中只有一个纹理。 int numVertices; /**< 顶点数目(每一帧中) */每帧中顶点的个数,我们用这个参数决定读取顶点信息时的数据读取量。 int numTexCoords; /**< 纹理坐标数目 */纹理坐标的个数,我们用这个参数决定读取纹理坐标时的数据读取量。 int numTriangles; /**< 三角行数目 */三角形个数,在动画模型中,使用三角形索引来绘制一个面。 int numGlCommands; /**< gl命令数目 */OpenGL命令的条数,本文中未使用这个参数。 int numFrames; /**< 总帧数 */总帧数,它决定了我们需要读取的帧信息量。 int offsetSkins; /**< 纹理的偏移位置 */纹理名称在文件中的偏移量,读取纹理名称从它指定的地方开始。 int offsetTexCoords; /**< 纹理坐标的偏移位置 */纹理坐标在文件中的偏移量,读取纹理坐标从它指定的地方开始。 int offsetTriangles; /**< 三角形索引的偏移位置 */面顶点索引在文件中的偏移量,读取面顶点索引从它指定的地方开始。 int offsetFrames; /**< 第一帧的偏移位置 */第一帧的位置,读取帧信息时从它指定的地方开始。 int offsetGlCommands; /**< OPenGL命令的偏移位置 */OpenGL命令在文件中的偏移量,文中未使用这个参数。 int offsetEnd; /**< 文件结尾偏移位置 */文件结束的位置,这个参数可以用来检查该文件的完整性。};
/** 帧中的顶点结构 */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、关键帧结构
关键帧结构用来存储当前帧中顶点、面、纹理坐标信息。/** 对象信息结构体 */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、模型信息结构
模型信息结构用来存放动画的全部信息,包括:关键帧链表,材质链表和动作信息链表等。/** 模型信息结构体 */struct t3DModel { int numOfObjects; /**< 模型中对象的数目 */ int numOfMaterials; /**< 模型中材质的数目 */ int numOfAnimations; /**< 模型中动作的数目 */ int currentAnim; /**< 帧索引 */ int currentFrame; /**< 当前帧 */ vectorpAnimations; /**< 帧信息链表 */ vector pMaterials; /**< 材质链表信息 */ vector pObject; /**< 模型中对象链表信息 */};
四、实现过程
我们构建好了用于存储数据的结构,下面介绍实现动画的过程,我们将整个过程分为三个部分:读取原始数据,将数据转换成模型结构和动画显示。 1、数据读取//数据读取函数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; iname); //获取顶点指针 tMd2Triangle *pVertices = m_pFrames[i].pVertices; //循环对关键帧的顶点信息进行解压,注意,要交换y,z轴,并将z轴反向。 for(int j=0; j 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、数据结构转换
void CLoadMD2::ConvertDataStructures(t3DModel *pModel) { int j = 0, i = 0; // Assign the number of objects, which is 1 since we only want 1 frame // of animation. In the next tutorial each object will be a key frame // to interpolate between. pModel->numOfObjects = 1; // Create a local object to store the first frame of animation's data t3DObject currentFrame = { 0}; // Assign the vertex, texture coord and face count to our new structure currentFrame.numOfVerts = m_Header.numVertices; currentFrame.numTexVertex = m_Header.numTexCoords; currentFrame.numOfFaces = m_Header.numTriangles; // Allocate memory for the vertices, texture coordinates and face data. currentFrame.pVerts = new CVector3 [currentFrame.numOfVerts]; currentFrame.pTexVerts = new CVector2 [currentFrame.numTexVertex]; currentFrame.pFaces = new tFace [currentFrame.numOfFaces]; // Go through all of the vertices and assign them over to our structure for (j=0; j < currentFrame.numOfVerts; j++) { currentFrame.pVerts[j].x = m_pFrames[0].pVertices[j].vertex[0]; currentFrame.pVerts[j].y = m_pFrames[0].pVertices[j].vertex[1]; currentFrame.pVerts[j].z = m_pFrames[0].pVertices[j].vertex[2]; } // We can now free the old vertices stored in this frame of animation delete m_pFrames[0].pVertices; // Go through all of the uv coordinates and assign them over to our structure. // The UV coordinates are not normal uv coordinates, they have a pixel ratio of // 0 to 256. We want it to be a 0 to 1 ratio, so we divide the u value by the // skin width and the v value by the skin height. This gives us our 0 to 1 ratio. // For some reason also, the v coodinate is flipped upside down. We just subtract // the v coordinate from 1 to remedy this problem. for (j=0; j < currentFrame.numTexVertex; j++) { currentFrame.pTexVerts[j].x = m_pTexCoords[j].u / float(m_Header.skinWidth); currentFrame.pTexVerts[j].y = 1 - m_pTexCoords[j].v / float(m_Header.skinHeight); } // Go through all of the face data and assign it over to OUR structure for(j=0; j < currentFrame.numOfFaces; j++) { // Assign the vertex indices to our face data currentFrame.pFaces[j].vertIndex[0] = m_pTriangles[j].vertexIndices[0]; currentFrame.pFaces[j].vertIndex[1] = m_pTriangles[j].vertexIndices[1]; currentFrame.pFaces[j].vertIndex[2] = m_pTriangles[j].vertexIndices[2]; // Assign the texture coord indices to our face data currentFrame.pFaces[j].coordIndex[0] = m_pTriangles[j].textureIndices[0]; currentFrame.pFaces[j].coordIndex[1] = m_pTriangles[j].textureIndices[1]; currentFrame.pFaces[j].coordIndex[2] = m_pTriangles[j].textureIndices[2]; } // Here we add the current object (or frame) to our list object list pModel->pObject.push_back(currentFrame); }
3、动画显示
// *Note* // // Below are some math functions for calculating vertex normals. We want vertex normals // because it makes the lighting look really smooth and life like. You probably already // have these functions in the rest of your engine, so you can delete these and call // your own. I wanted to add them so I could show how to calculate vertex normals. // Math Functions ////* // This computes the magnitude of a normal. (magnitude = sqrt(x^2 + y^2 + z^2) #define Mag(Normal) (sqrt(Normal.x*Normal.x + Normal.y*Normal.y + Normal.z*Normal.z)) // This calculates a vector between 2 points and returns the result CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2) { CVector3 vVector; // The variable to hold the resultant vector vVector.x = vPoint1.x - vPoint2.x; // Subtract point1 and point2 x's vVector.y = vPoint1.y - vPoint2.y; // Subtract point1 and point2 y's vVector.z = vPoint1.z - vPoint2.z; // Subtract point1 and point2 z's return vVector; // Return the resultant vector } // This adds 2 vectors together and returns the result CVector3 AddVector(CVector3 vVector1, CVector3 vVector2) { CVector3 vResult; // The variable to hold the resultant vector vResult.x = vVector2.x + vVector1.x; // Add Vector1 and Vector2 x's vResult.y = vVector2.y + vVector1.y; // Add Vector1 and Vector2 y's vResult.z = vVector2.z + vVector1.z; // Add Vector1 and Vector2 z's return vResult; // Return the resultant vector } // This divides a vector by a single number (scalar) and returns the result CVector3 DivideVectorByScaler(CVector3 vVector1, float Scaler) { CVector3 vResult; // The variable to hold the resultant vector vResult.x = vVector1.x / Scaler; // Divide Vector1's x value by the scaler vResult.y = vVector1.y / Scaler; // Divide Vector1's y value by the scaler vResult.z = vVector1.z / Scaler; // Divide Vector1's z value by the scaler return vResult; // Return the resultant vector } // This returns the cross product between 2 vectors CVector3 Cross(CVector3 vVector1, CVector3 vVector2) { CVector3 vCross; // The vector to hold the cross product // Get the X value vCross.x = ((vVector1.y * vVector2.z) - (vVector1.z * vVector2.y)); // Get the Y value vCross.y = ((vVector1.z * vVector2.x) - (vVector1.x * vVector2.z)); // Get the Z value vCross.z = ((vVector1.x * vVector2.y) - (vVector1.y * vVector2.x)); return vCross; // Return the cross product } // This returns the normal of a vector CVector3 Normalize(CVector3 vNormal) { double Magnitude; // This holds the magitude Magnitude = Mag(vNormal); // Get the magnitude vNormal.x /= (float)Magnitude; // Divide the vector's X by the magnitude vNormal.y /= (float)Magnitude; // Divide the vector's Y by the magnitude vNormal.z /= (float)Magnitude; // Divide the vector's Z by the magnitude return vNormal; // Return the normal }
/ COMPUTER NORMALS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* / / This function computes the normals and vertex normals of the objects / / COMPUTER NORMALS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*
void CLoadMD2::ComputeNormals(t3DModel *pModel) { CVector3 vVector1, vVector2, vNormal, vPoly[3]; // If there are no objects, we can skip this part if(pModel->numOfObjects <= 0) return; // What are vertex normals? And how are they different from other normals? // Well, if you find the normal to a triangle, you are finding a "Face Normal". // If you give OpenGL a face normal for lighting, it will make your object look // really flat and not very round. If we find the normal for each vertex, it makes // the smooth lighting look. This also covers up blocky looking objects and they appear // to have more polygons than they do. Basically, what you do is first // calculate the face normals, then you take the average of all the normals around each // vertex. It's just averaging. That way you get a better approximation for that vertex. // Go through each of the objects to calculate their normals for(int index = 0; index < pModel->numOfObjects; index++) { // Get the current object t3DObject *pObject = &(pModel->pObject[index]); // Here we allocate all the memory we need to calculate the normals CVector3 *pNormals = new CVector3 [pObject->numOfFaces]; CVector3 *pTempNormals = new CVector3 [pObject->numOfFaces]; pObject->pNormals = new CVector3 [pObject->numOfVerts]; // Go though all of the faces of this object for(int i=0; i < pObject->numOfFaces; i++) { // To cut down LARGE code, we extract the 3 points of this face vPoly[0] = pObject->pVerts[pObject->pFaces[i].vertIndex[0]]; vPoly[1] = pObject->pVerts[pObject->pFaces[i].vertIndex[1]]; vPoly[2] = pObject->pVerts[pObject->pFaces[i].vertIndex[2]]; // Now let's calculate the face normals (Get 2 vectors and find the cross product of those 2) vVector1 = Vector(vPoly[0], vPoly[2]); // Get the vector of the polygon (we just need 2 sides for the normal) vVector2 = Vector(vPoly[2], vPoly[1]); // Get a second vector of the polygon vNormal = Cross(vVector1, vVector2); // Return the cross product of the 2 vectors (normalize vector, but not a unit vector) pTempNormals[i] = vNormal; // Save the un-normalized normal for the vertex normals vNormal = Normalize(vNormal); // Normalize the cross product to give us the polygons normal pNormals[i] = vNormal; // Assign the normal to the list of normals } //// Now Get The Vertex Normals ///// CVector3 vSum = { 0.0, 0.0, 0.0}; CVector3 vZero = vSum; int shared=0; for (i = 0; i < pObject->numOfVerts; i++) // Go through all of the vertices { for (int j = 0; j < pObject->numOfFaces; j++) // Go through all of the triangles { // Check if the vertex is shared by another face if (pObject->pFaces[j].vertIndex[0] == i || pObject->pFaces[j].vertIndex[1] == i || pObject->pFaces[j].vertIndex[2] == i) { vSum = AddVector(vSum, pTempNormals[j]);// Add the un-normalized normal of the shared face shared++; // Increase the number of shared triangles } } // Get the normal by dividing the sum by the shared. We negate the shared so it has the normals pointing out. pObject->pNormals[i] = DivideVectorByScaler(vSum, float(-shared)); // Normalize the normal for the final vertex normal pObject->pNormals[i] = Normalize(pObject->pNormals[i]); vSum = vZero; // Reset the sum shared = 0; // Reset the shared } // Free our memory and start over on the next object delete [] pTempNormals; delete [] pNormals; } }