本文介紹MD2文件的格式,並介紹使用OpenGL顯示MD2文件的方法。git
首先,咱們必需要搞清幾個問題:
一、動畫的實現原理
二、MD2文件的數據存儲格式
三、OpenGL顯示動畫的方法數據結構
1、動畫的原理
動畫就是連續出現的畫面,在3D動畫中,在一個在兩個差異很大的動做之間進行插值,使得3D模型的各個部分連續運動而獲得動畫的效果。好比:將手臂在左邊時的3D模型和手臂在右邊時的3D模型進行保留,而後根據時間在這兩個模型之間進行插值,讓其在某個時刻顯示其在中間的模型,如此連續的顯示便構成了動畫的效果。
所以,MD2文件中便存儲了動畫的各個關鍵幀,只不過可能某些動做的完成須要多個關鍵幀,另外,咱們瞭解了動畫的原理,咱們便知道,在動畫的運動過程當中,模型的頂點個數和紋理是相同的,只是在某個時刻模型的頂點座標有差別。app
2、MD2文件數據的格式
要搞清楚MD2文件的格式必需要知道其中都存儲了那些數據,MD2動畫由兩個文件組成,一個是以.MD2爲後綴的文件,其中保留了動畫模型的各個點的信息,包括:頂點座標、紋理座標、紋理名稱、三角形索引等信息。另外一個是一個圖片文件,能夠是多種格式的圖片,本文中使用的是BMP文件。ide
一、文件頭
要搞清楚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; /**< 文件結尾偏移位置 */文件結束的位置,這個參數能夠用來檢查該文件的完整性。 };
二、頂點結構
MD2文件中的頂點是通過壓縮的,它包含的這樣的一個結構:動畫
/** 幀中的頂點結構 */ 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。this
二、紋理名稱
MD2文件中紋理名稱是長度爲64的字符序列,咱們這樣表示:spa
/** 紋理名字 */ typedef char tMd2Skin[64];
三、紋理座標
MD2文件中的紋理座標也是通過壓縮的,它的結構以下:scala
/** 紋理座標結構 */ struct tMd2TexCoord { short u, v; };
在讀取紋理座標後須要對其進行解壓,公式爲:U = u / skinWidth; V = v / skinHeight。指針
四、面結構
咱們說過了,MD2文件中的使用面結構組成一個三角形,面結構保存了該三角形的三個頂點在幀頂點中的索引,和三個頂點所對應的紋理座標在紋理座標序列中的索引。
/** 面結構 */ struct tMd2Face { short vertexIndices[3];//頂點索引 short textureIndices[3];//紋理索引 };
3、輔助結構
由於MD2文件數據自己是壓縮過的,所以爲了獲得真正能有的信息,咱們必需要定義一些輔助結構來存儲轉換後的數據。
一、頂點結構
頂點結構用來存儲解壓後的頂點信息。
/** 解壓後的頂點結構 */ struct tMd2Triangle { float vertex[3];//頂點座標 float normal[3];//法向量 };
二、面結構
面結構用來存儲每一個三角形面的三個點的頂點座標索引和紋理座標索引。
/** 面信息 */ struct tFace { int vertIndex[3]; /**< 頂點索引 */ int coordIndex[3]; /**< 紋理座標索引 */ };
三、關鍵幀結構
關鍵幀結構用來存儲關鍵幀的名稱和它包含的全部的頂點信息。
/** 關鍵幀結構 */ struct tMd2Frame { char strName[16];//關鍵幀名稱 tMd2Triangle *pVertices;//幀中頂點信息 };
四、動做信息結構
動做信息結構用來存放該動做的名稱和該動做包含的起始關鍵幀索引和結束關鍵幀索引。
/** 動做信息結構體 */ struct tAnimationInfo { char strName[255]; /**< 幀的名稱 */ int startFrame; /**< 開始幀 */ int endFrame; /**< 結束幀 */ };
五、關鍵幀結構
關鍵幀結構用來存儲當前幀中頂點、面、紋理座標信息。
/** 對象信息結構體 */ 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; /**< 對象的面信息 */ };
六、模型信息結構
模型信息結構用來存放動畫的所有信息,包括:關鍵幀鏈表,材質鏈表和動做信息鏈表等。
/** 模型信息結構體 */ struct t3DModel { int numOfObjects; /**< 模型中對象的數目 */ int numOfMaterials; /**< 模型中材質的數目 */ int numOfAnimations; /**< 模型中動做的數目 */ int currentAnim; /**< 幀索引 */ int currentFrame; /**< 當前幀 */ vector<tAnimationInfo> pAnimations; /**< 幀信息鏈表 */ vector<tMaterialInfo> pMaterials; /**< 材質鏈表信息 */ vector<t3DObject> pObject; /**< 模型中對象鏈表信息 */ };
4、實現過程
咱們構建好了用於存儲數據的結構,下面介紹實現動畫的過程,咱們將整個過程分爲三個部分:讀取原始數據,將數據轉換成模型結構和動畫顯示。
一、數據讀取
//數據讀取函數 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]; } } }
二、數據結構轉換
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); }
三、動畫顯示
// *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; } }