// this code was written by DigiBen and can be accessed at: // http://www.gametutorials.com/Tutorials/OpenGL/OpenGL_Pg4.htm //***********************************************************************// // // // - "Talk to me like I'm a 3 year old!" Programming Lessons - // // // // $Author: DigiBen digiben@gametutorials.com // // // // $Program: 3DS Loader // // // // $Description: Demonstrates how to load a .3ds file format // // // // $Date: 10/6/01 // // // //***********************************************************************// //#include #include #include #include #include #include #include #include // Header File For The OpenGL32 Library #include // Header File For The GLu32 Library //#include //#include #include "MeshModel.h" using namespace std; #define SCREEN_WIDTH 800 // We want our screen width 800 pixels #define SCREEN_HEIGHT 600 // We want our screen height 600 pixels #define SCREEN_DEPTH 16 // We want 16 bits per pixel //////////// *** NEW *** ////////// *** NEW *** ///////////// *** NEW *** //////////////////// // This file includes all of the model structures that are needed to load // in a .3DS file. If you intend to do animation you will need to add on // to this. These structures only support the information that is needed // to load the objects in the scene and their associative materials. #define MAX_TEXTURES 100 // The maximum amount of textures to load // This is our 3D point class. This will be used to store the vertices of our model. class CVector3 { public: float x, y, z; }; // This is our 2D point class. This will be used to store the UV coordinates. class CVector2 { public: float x, y; }; // This is our face structure. This is is used for indexing into the vertex // and texture coordinate arrays. From this information we know which vertices // from our vertex array go to which face, along with the correct texture coordinates. struct tFace { int vertIndex[3]; // indicies for the verts that make up this triangle int coordIndex[3]; // indicies for the tex coords to texture this face }; // This holds the information for a material. It may be a texture map of a color. // Some of these are not used, but I left them because you will want to eventually // read in the UV tile ratio and the UV tile offset for some models. struct tMaterialInfo { char strName[255]; // The texture name char strFile[255]; // The texture file name (If this is set it's a texture map) unsigned char color[3]; // The color of the object (R, G, B) int texureId; // the texture ID float uTile; // u tiling of texture (Currently not used) float vTile; // v tiling of texture (Currently not used) float uOffset; // u offset of texture (Currently not used) float vOffset; // v offset of texture (Currently not used) } ; // This holds all the information for our model/scene. // You should eventually turn into a robust class that // has loading/drawing/querying functions like: // LoadModel(...); DrawObject(...); DrawModel(...); DestroyModel(...); struct t3DObject { int numOfVerts; // The number of verts in the model int numOfFaces; // The number of faces in the model int numTexVertex; // The number of texture coordinates int materialID; // The texture ID to use, which is the index into our texture array bool bHasTexture; // This is TRUE if there is a texture map for this object char strName[255]; // The name of the object CVector3 *pVerts; // The object's vertices CVector3 *pNormals; // The object's normals CVector2 *pTexVerts; // The texture's UV coordinates tFace *pFaces; // The faces information of the object }; // This holds our model information. This should also turn into a robust class. // We use STL's (Standard Template Library) vector class to ease our link list burdens. :) struct t3DModel { int numOfObjects; // The number of objects in the model int numOfMaterials; // The number of materials for the model vector pMaterials; // The list of material information (Textures and colors) vector pObject; // The object list for our model }; //>------ Primary Chunk, at the beginning of each file #define PRIMARY 0x4D4D //>------ Main Chunks #define OBJECTINFO 0x3D3D // This gives the version of the mesh and is found right before the material and object information #define VERSION 0x0002 // This gives the version of the .3ds file #define EDITKEYFRAME 0xB000 // This is the header for all of the key frame info //>------ sub defines of OBJECTINFO #define MATERIAL 0xAFFF // This stored the texture info #define OBJECT 0x4000 // This stores the faces, vertices, etc... //>------ sub defines of MATERIAL #define MATNAME 0xA000 // This holds the material name #define MATDIFFUSE 0xA020 // This holds the color of the object/material #define MATMAP 0xA200 // This is a header for a new material #define MATMAPFILE 0xA300 // This holds the file name of the texture #define OBJECT_MESH 0x4100 // This lets us know that we are reading a new object //>------ sub defines of OBJECT_MESH #define OBJECT_VERTICES 0x4110 // The objects vertices #define OBJECT_FACES 0x4120 // The objects faces #define OBJECT_MATERIAL 0x4130 // This is found if the object has a material, either texture map or color #define OBJECT_UV 0x4140 // The UV texture coordinates // Here is our structure for our 3DS indicies (since .3DS stores 4 unsigned shorts) struct tIndices { unsigned short a, b, c, bVisible; // This will hold point1, 2, and 3 index's into the vertex array plus a visible flag }; // This holds the chunk info struct tChunk { unsigned short int ID; // The chunk's ID unsigned int length; // The length of the chunk unsigned int bytesRead; // The amount of bytes read within that chunk }; // This class handles all of the loading code class CLoad3DS { public: CLoad3DS(); // This inits the data members // This is the function that you call to load the 3DS bool Import3DS(t3DModel *pModel, char *strFileName); private: // This reads in a string and saves it in the char array passed in int GetString(char *); // This reads the next chunk void ReadChunk(tChunk *); // This reads the next large chunk void ProcessNextChunk(t3DModel *pModel, tChunk *); // This reads the object chunks void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *); // This reads the material chunks void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *); // This reads the RGB value for the object's color void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk); // This reads the objects vertices void ReadVertices(t3DObject *pObject, tChunk *); // This reads the objects face information void ReadVertexIndices(t3DObject *pObject, tChunk *); // This reads the texture coodinates of the object void ReadUVCoordinates(t3DObject *pObject, tChunk *); // This reads in the material name assigned to the object and sets the materialID void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk); // This computes the vertex normals for the object (used for lighting) void ComputeNormals(t3DModel *pModel); // This frees memory and closes the file void CleanUp(); // The file pointer FILE *m_FilePointer; // These are used through the loading process to hold the chunk information tChunk *m_CurrentChunk; tChunk *m_TempChunk; }; ///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This constructor initializes the tChunk data ///// ///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* CLoad3DS::CLoad3DS() { m_CurrentChunk = new tChunk; // Initialize and allocate our current chunk m_TempChunk = new tChunk; // Initialize and allocate a temporary chunk } ///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This is called by the client to open the .3ds file, read it, then clean up ///// ///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName) { // char strMessage[255] = {0}; // Open the 3DS file m_FilePointer = fopen(strFileName, "rb"); // Make sure we have a valid file pointer (we found the file) if(!m_FilePointer) { /* sprintf(strMessage, "Unable to find the file: %s!", strFileName); MessageBox(NULL, strMessage, "Error", MB_OK); */ cerr << "Unable to find the file : " << strFileName << endl; return false; } // Once we have the file open, we need to read the very first data chunk // to see if it's a 3DS file. That way we don't read an invalid file. // If it is a 3DS file, then the first chunk ID will be equal to PRIMARY (some hex num) // Read the first chuck of the file to see if it's a 3DS file ReadChunk(m_CurrentChunk); // Make sure this is a 3DS file if (m_CurrentChunk->ID != PRIMARY) { /* sprintf(strMessage, "Unable to load PRIMARY chuck from file: %s!", strFileName); MessageBox(NULL, strMessage, "Error", MB_OK); */ cerr << "Unable to load PRIMARY chuck from file: " << strFileName << "!\n"; return false; } // Now we actually start reading in the data. ProcessNextChunk() is recursive // Begin loading objects, by calling this recursive function ProcessNextChunk(pModel, m_CurrentChunk); // After we have read the whole 3DS file, we want to calculate our own vertex normals. ComputeNormals(pModel); // Clean up after everything CleanUp(); return true; } ///////////////////////////////// CLEAN UP \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function cleans up our allocated memory and closes the file ///// ///////////////////////////////// CLEAN UP \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::CleanUp() { fclose(m_FilePointer); // Close the current file pointer delete m_CurrentChunk; // Free the current chunk delete m_TempChunk; // Free our temporary chunk } ///////////////////////////////// PROCESS NEXT CHUNK\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads the main sections of the .3DS file, then dives deeper with recursion ///// ///////////////////////////////// PROCESS NEXT CHUNK\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk) { t3DObject newObject = {0}; // This is used to add to our object list tMaterialInfo newTexture = {0}; // This is used to add to our material list unsigned int version = 0; // This will hold the file version int buffer[50000] = {0}; // This is used to read past unwanted data m_CurrentChunk = new tChunk; // Allocate a new chunk // Below we check our chunk ID each time we read a new chunk. Then, if // we want to extract the information from that chunk, we do so. // If we don't want a chunk, we just read past it. // Continue to read the sub chunks until we have reached the length. // After we read ANYTHING we add the bytes read to the chunk and then check // check against the length. while (pPreviousChunk->bytesRead < pPreviousChunk->length) { // Read next Chunk ReadChunk(m_CurrentChunk); // Check the chunk ID switch (m_CurrentChunk->ID) { case VERSION: // This holds the version of the file // This chunk has an unsigned short that holds the file version. // Since there might be new additions to the 3DS file format in 4.0, // we give a warning to that problem. // Read the file version and add the bytes read to our bytesRead variable m_CurrentChunk->bytesRead += fread(&version, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); // If the file version is over 3, give a warning that there could be a problem if (version > 0x03) //MessageBox(NULL, "This 3DS file is over version 3 so it may load incorrectly", "Warning", MB_OK); cerr << "Warning: This 3DS file is over version 3 so it may load incorrectly\n"; break; case OBJECTINFO: // This holds the version of the mesh // This chunk holds the version of the mesh. It is also the head of the MATERIAL // and OBJECT chunks. From here on we start reading in the material and object info. // Read the next chunk ReadChunk(m_TempChunk); // Get the version of the mesh m_TempChunk->bytesRead += fread(&version, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer); // Increase the bytesRead by the bytes read from the last chunk m_CurrentChunk->bytesRead += m_TempChunk->bytesRead; // Go to the next chunk, which is the object has a texture, it should be MATERIAL, then OBJECT. ProcessNextChunk(pModel, m_CurrentChunk); break; case MATERIAL: // This holds the material information // This chunk is the header for the material info chunks // Increase the number of materials pModel->numOfMaterials++; // Add a empty texture structure to our texture list. // If you are unfamiliar with STL's "vector" class, all push_back() // does is add a new node onto the list. I used the vector class // so I didn't need to write my own link list functions. pModel->pMaterials.push_back(newTexture); // Proceed to the material loading function ProcessNextMaterialChunk(pModel, m_CurrentChunk); break; case OBJECT: // This holds the name of the object being read // This chunk is the header for the object info chunks. It also // holds the name of the object. // Increase the object count pModel->numOfObjects++; // Initialize the object and all it's data members memset(&(newObject), 0, sizeof(t3DObject)); // Add a new tObject node to our list of objects (like a link list) pModel->pObject.push_back(newObject); // Get the name of the object and store it, then add the read bytes to our byte counter. m_CurrentChunk->bytesRead += GetString(&(pModel->pObject[pModel->numOfObjects - 1].strName[0])); // Now proceed to read in the rest of the object information ProcessNextObjectChunk(pModel, &(pModel->pObject[pModel->numOfObjects - 1]), m_CurrentChunk); break; case EDITKEYFRAME: // Because I wanted to make this a SIMPLE tutorial as possible, I did not include // the key frame information. This chunk is the header for all the animation info. // In a later tutorial this will be the subject and explained thoroughly. //ProcessNextKeyFrameChunk(pModel, m_CurrentChunk); // Read past this chunk and add the bytes read to the byte counter m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; default: // If we didn't care about a chunk, then we get here. We still need // to read past the unknown or ignored chunk and add the bytes read to the byte counter. m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead; } // Free the current chunk and set it back to the previous chunk (since it started that way) delete m_CurrentChunk; m_CurrentChunk = pPreviousChunk; } ///////////////////////////////// PROCESS NEXT OBJECT CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function handles all the information about the objects in the file ///// ///////////////////////////////// PROCESS NEXT OBJECT CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk) { int buffer[50000] = {0}; // This is used to read past unwanted data // Allocate a new chunk to work with m_CurrentChunk = new tChunk; // Continue to read these chunks until we read the end of this sub chunk while (pPreviousChunk->bytesRead < pPreviousChunk->length) { // Read the next chunk ReadChunk(m_CurrentChunk); // Check which chunk we just read switch (m_CurrentChunk->ID) { case OBJECT_MESH: // This lets us know that we are reading a new object // We found a new object, so let's read in it's info using recursion ProcessNextObjectChunk(pModel, pObject, m_CurrentChunk); break; case OBJECT_VERTICES: // This is the objects vertices ReadVertices(pObject, m_CurrentChunk); break; case OBJECT_FACES: // This is the objects face information ReadVertexIndices(pObject, m_CurrentChunk); break; case OBJECT_MATERIAL: // This holds the material name that the object has // This chunk holds the name of the material that the object has assigned to it. // This could either be just a color or a texture map. This chunk also holds // the faces that the texture is assigned to (In the case that there is multiple // textures assigned to one object, or it just has a texture on a part of the object. // Since most of my game objects just have the texture around the whole object, and // they aren't multitextured, I just want the material name. // We now will read the name of the material assigned to this object ReadObjectMaterial(pModel, pObject, m_CurrentChunk); break; case OBJECT_UV: // This holds the UV texture coordinates for the object // This chunk holds all of the UV coordinates for our object. Let's read them in. ReadUVCoordinates(pObject, m_CurrentChunk); break; default: // Read past the ignored or unknown chunks m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead; } // Free the current chunk and set it back to the previous chunk (since it started that way) delete m_CurrentChunk; m_CurrentChunk = pPreviousChunk; } ///////////////////////////////// PROCESS NEXT MATERIAL CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function handles all the information about the material (Texture) ///// ///////////////////////////////// PROCESS NEXT MATERIAL CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ProcessNextMaterialChunk(t3DModel *pModel, tChunk *pPreviousChunk) { int buffer[50000] = {0}; // This is used to read past unwanted data // Allocate a new chunk to work with m_CurrentChunk = new tChunk; // Continue to read these chunks until we read the end of this sub chunk while (pPreviousChunk->bytesRead < pPreviousChunk->length) { // Read the next chunk ReadChunk(m_CurrentChunk); // Check which chunk we just read in switch (m_CurrentChunk->ID) { case MATNAME: // This chunk holds the name of the material // Here we read in the material name m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strName, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; case MATDIFFUSE: // This holds the R G B color of our object ReadColorChunk(&(pModel->pMaterials[pModel->numOfMaterials - 1]), m_CurrentChunk); break; case MATMAP: // This is the header for the texture info // Proceed to read in the material information ProcessNextMaterialChunk(pModel, m_CurrentChunk); break; case MATMAPFILE: // This stores the file name of the material // Here we read in the material's file name m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strFile, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; default: // Read past the ignored or unknown chunks m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead; } // Free the current chunk and set it back to the previous chunk (since it started that way) delete m_CurrentChunk; m_CurrentChunk = pPreviousChunk; } ///////////////////////////////// READ CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in a chunk ID and it's length in bytes ///// ///////////////////////////////// READ CHUNK \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadChunk(tChunk *pChunk) { // This reads the chunk ID which is 2 bytes. // The chunk ID is like OBJECT or MATERIAL. It tells what data is // able to be read in within the chunks section. pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer); // Then, we read the length of the chunk which is 4 bytes. // This is how we know how much to read in, or read past. pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer); } ///////////////////////////////// GET STRING \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in a string of characters ///// ///////////////////////////////// GET STRING \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* int CLoad3DS::GetString(char *pBuffer) { int index = 0; // Read 1 byte of data which is the first letter of the string fread(pBuffer, 1, 1, m_FilePointer); // Loop until we get NULL while (*(pBuffer + index++) != 0) { // Read in a character at a time until we hit NULL. fread(pBuffer + index, 1, 1, m_FilePointer); } // Return the string length, which is how many bytes we read in (including the NULL) return strlen(pBuffer) + 1; } ///////////////////////////////// READ COLOR \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in the RGB color data ///// ///////////////////////////////// READ COLOR \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk) { // Read the color chunk info ReadChunk(m_TempChunk); // Read in the R G B color (3 bytes - 0 through 255) m_TempChunk->bytesRead += fread(pMaterial->color, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer); // Add the bytes read to our chunk pChunk->bytesRead += m_TempChunk->bytesRead; } ///////////////////////////////// READ VERTEX INDECES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in the indices for the vertex array ///// ///////////////////////////////// READ VERTEX INDECES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadVertexIndices(t3DObject *pObject, tChunk *pPreviousChunk) { unsigned short index = 0; // This is used to read in the current face index // In order to read in the vertex indices for the object, we need to first // read in the number of them, then read them in. Remember, // we only want 3 of the 4 values read in for each face. The fourth is // a visibility flag for 3D Studio Max that doesn't mean anything to us. // Read in the number of faces that are in this object (int) pPreviousChunk->bytesRead += fread(&pObject->numOfFaces, 1, 2, m_FilePointer); // Alloc enough memory for the faces and initialize the structure pObject->pFaces = new tFace [pObject->numOfFaces]; memset(pObject->pFaces, 0, sizeof(tFace) * pObject->numOfFaces); // Go through all of the faces in this object for(int i = 0; i < pObject->numOfFaces; i++) { // Next, we read in the A then B then C index for the face, but ignore the 4th value. // The fourth value is a visibility flag for 3D Studio Max, we don't care about this. for(int j = 0; j < 4; j++) { // Read the first vertice index for the current face pPreviousChunk->bytesRead += fread(&index, 1, sizeof(index), m_FilePointer); if(j < 3) { // Store the index in our face structure. pObject->pFaces[i].vertIndex[j] = index; } } } } ///////////////////////////////// READ UV COORDINATES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in the UV coordinates for the object ///// ///////////////////////////////// READ UV COORDINATES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadUVCoordinates(t3DObject *pObject, tChunk *pPreviousChunk) { // In order to read in the UV indices for the object, we need to first // read in the amount there are, then read them in. // Read in the number of UV coordinates there are (int) pPreviousChunk->bytesRead += fread(&pObject->numTexVertex, 1, 2, m_FilePointer); // Allocate memory to hold the UV coordinates pObject->pTexVerts = new CVector2 [pObject->numTexVertex]; // Read in the texture coodinates (an array 2 float) pPreviousChunk->bytesRead += fread(pObject->pTexVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer); } ///////////////////////////////// READ VERTICES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in the vertices for the object ///// ///////////////////////////////// READ VERTICES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadVertices(t3DObject *pObject, tChunk *pPreviousChunk) { // Like most chunks, before we read in the actual vertices, we need // to find out how many there are to read in. Once we have that number // we then fread() them into our vertice array. // Read in the number of vertices (int) pPreviousChunk->bytesRead += fread(&(pObject->numOfVerts), 1, 2, m_FilePointer); // Allocate the memory for the verts and initialize the structure pObject->pVerts = new CVector3 [pObject->numOfVerts]; memset(pObject->pVerts, 0, sizeof(CVector3) * pObject->numOfVerts); // Read in the array of vertices (an array of 3 floats) pPreviousChunk->bytesRead += fread(pObject->pVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer); // Now we should have all of the vertices read in. Because 3D Studio Max // Models with the Z-Axis pointing up (strange and ugly I know!), we need // to flip the y values with the z values in our vertices. That way it // will be normal, with Y pointing up. If you prefer to work with Z pointing // up, then just delete this next loop. Also, because we swap the Y and Z // we need to negate the Z to make it come out correctly. // Go through all of the vertices that we just read and swap the Y and Z values for(int i = 0; i < pObject->numOfVerts; i++) { // Store off the Y value float fTempY = pObject->pVerts[i].y; // Set the Y value to the Z value pObject->pVerts[i].y = pObject->pVerts[i].z; // Set the Z value to the Y value, // but negative Z because 3D Studio max does the opposite. pObject->pVerts[i].z = -fTempY; } } ///////////////////////////////// READ OBJECT MATERIAL \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* ///// ///// This function reads in the material name assigned to the object and sets the materialID ///// ///////////////////////////////// READ OBJECT MATERIAL \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\* void CLoad3DS::ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk) { char strMaterial[255] = {0}; // This is used to hold the objects material name int buffer[50000] = {0}; // This is used to read past unwanted data // *What is a material?* - A material is either the color or the texture map of the object. // It can also hold other information like the brightness, shine, etc... Stuff we don't // really care about. We just want the color, or the texture map file name really. // Here we read the material name that is assigned to the current object. // strMaterial should now have a string of the material name, like "Material #2" etc.. pPreviousChunk->bytesRead += GetString(strMaterial); // Now that we have a material name, we need to go through all of the materials // and check the name against each material. When we find a material in our material // list that matches this name we just read in, then we assign the materialID // of the object to that material index. You will notice that we passed in the // model to this function. This is because we need the number of textures. // Yes though, we could have just passed in the model and not the object too. // Go through all of the textures for(int i = 0; i < pModel->numOfMaterials; i++) { // If the material we just read in matches the current texture name if(strcmp(strMaterial, pModel->pMaterials[i].strName) == 0) { // Set the material ID to the current index 'i' and stop checking pObject->materialID = i; // Now that we found the material, check if it's a texture map. // If the strFile has a string length of 1 and over it's a texture if(strlen(pModel->pMaterials[i].strFile) > 0) { // Set the object's flag to say it has a texture map to bind. pObject->bHasTexture = true; } break; } else { // Set the ID to -1 to show there is no material for this object pObject->materialID = -1; } } // Read past the rest of the chunk since we don't care about shared vertices // You will notice we subtract the bytes already read in this chunk from the total length. pPreviousChunk->bytesRead += fread(buffer, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer); } // *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 CLoad3DS::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++) { int i; // 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(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; } } bool MeshModel::Init(char *filename) { // First we need to actually load the .3DS file. We just pass in an address to // our t3DModel structure and the file name string we want to load ("face.3ds"). (*_Load3ds).Import3DS(_3DModel, filename); // Load our .3DS file into our model structure /* // Depending on how many textures we found, load each one (Assuming .BMP) // If you want to load other files than bitmaps, you will need to adjust CreateTexture(). // Below, we go through all of the materials and check if they have a texture map to load. // Otherwise, the material just holds the color information and we don't need to load a texture. // Go through all the materials for(int i = 0; i < (*_3DModel).numOfMaterials; i++) { // Check to see if there is a file name to load in this material if(strlen((*_3DModel).pMaterials[i].strFile) > 0) { // Use the name of the texture file to load the bitmap, with a texture ID (i). // We pass in our global texture array, the name of the texture, and an ID to reference it. CreateTexture(g_Texture, (*_3DModel).pMaterials[i].strFile, i); } // Set the texture ID for this material (*_3DModel).pMaterials[i].texureId = i; } */ _DisplayList = glGenLists(1); glNewList(_DisplayList, GL_COMPILE); glEnable(GL_COLOR_MATERIAL); glColor3f(1, 1, 1); // Since we know how many objects our model has, go through each of them. for(int i = 0; i < (*_3DModel).numOfObjects; i++) { // Make sure we have valid objects just in case. (size() is in the vector class) if((*_3DModel).pObject.size() <= 0) break; // Get the current object that we are displaying t3DObject *pObject = &((*_3DModel).pObject[i]); // Check to see if this object has a texture map, if so bind the texture to it. /* if(pObject->bHasTexture) { // Turn on texture mapping and turn off color glEnable(GL_TEXTURE_2D); // Reset the color to normal again glColor3ub(255, 255, 255); // Bind the texture map to the object by it's materialID glBindTexture(GL_TEXTURE_2D, _Texture[pObject->materialID]); } else { */ // Turn off texture mapping and turn on color glDisable(GL_TEXTURE_2D); // Reset the color to normal again glColor3ub(255, 255, 255); /* } */ // This determines if we are in wireframe or normal mode glBegin(_Viewmode); // Begin drawing with our selected mode (triangles or lines) // Go through all of the faces (polygons) of the object and draw them for(int j = 0; j < pObject->numOfFaces; j++) { // Go through each corner of the triangle and draw it. for(int whichVertex = 0; whichVertex < 3; whichVertex++) { // Get the index for each point of the face int index = pObject->pFaces[j].vertIndex[whichVertex]; // Give OpenGL the normal for this vertex. glNormal3f(pObject->pNormals[ index ].x, pObject->pNormals[ index ].y, pObject->pNormals[ index ].z); // If the object has a texture associated with it, give it a texture coordinate. if(pObject->bHasTexture) { // Make sure there was a UVW map applied to the object or else it won't have tex coords. if(pObject->pTexVerts) { glTexCoord2f(pObject->pTexVerts[ index ].x, pObject->pTexVerts[ index ].y); } } else { // Make sure there is a valid material/color assigned to this object. // You should always at least assign a material color to an object, // but just in case we want to check the size of the material list. // if the size is at least one, and the material ID != -1, // then we have a valid material. if((*_3DModel).pMaterials.size() && pObject->materialID >= 0) { // Get and set the color that the object is, since it must not have a texture unsigned char *pColor = (*_3DModel).pMaterials[pObject->materialID].color; // Assign the current color to this model glColor3ub(pColor[0], pColor[1], pColor[2]); } } // Pass in the current vertex of the object (Corner of current face) glVertex3f(pObject->pVerts[ index ].x, pObject->pVerts[ index ].y, pObject->pVerts[ index ].z); } } glEnd(); // End the drawing } glDisable(GL_COLOR_MATERIAL); glEndList(); return true; } MeshModel::~MeshModel() { // When we are done, we need to free all the model data // We do this by walking through all the objects and freeing their information // Go through all the objects in the scene for(int i = 0; i < (*_3DModel).numOfObjects; i++) { // Free the faces, normals, vertices, and texture coordinates. delete [] (*_3DModel).pObject[i].pFaces; delete [] (*_3DModel).pObject[i].pNormals; delete [] (*_3DModel).pObject[i].pVerts; delete [] (*_3DModel).pObject[i].pTexVerts; } } void MeshModel::Render(float scale) const { glPushMatrix(); glScalef(scale, scale, scale); glCallList(_DisplayList); glPopMatrix(); } void MeshModel::Render() const { glCallList(_DisplayList); } MeshModel::MeshModel() : _Viewmode(GL_TRIANGLES) { _Load3ds = new CLoad3DS; _3DModel = new t3DModel; memset(_3DModel, 0, sizeof(t3DModel)); }