VS 2005 problems...?
Hello! Well after installing VS 2005 and eventually getting my code to compile without all the depricated warnings with fopen(), strupr() and annoying scoped out int i = 0; i < whatever etc etc inside a alot of functions im getting memory leaks being picked up using _CrtDumpMemoryLeaks() where as before hand using .NET 2003 I wasn't...
I've narrowed it down to this bit of code:
void Model3DS::DeleteModel( t3DModel* pModel )
{
int i = 0;
for( i = 0; i < pModel->numOfObjects; i++ )
{
if( pModel->pObject.pFaces != NULL )
{
delete [] (pModel->pObject.pFaces);
pModel->pObject.pFaces = NULL;
}
if( pModel->pObject.pNormals != NULL )
{
delete [] (pModel->pObject.pNormals);
pModel->pObject.pNormals = NULL;
}
if( pModel->pObject.pVerts != NULL )
{
delete [] (pModel->pObject.pVerts);
pModel->pObject.pVerts = NULL;
}
if( pModel->pObject.pTexVerts != NULL )
{
delete [] pModel->pObject.pTexVerts;
pModel->pObject.pTexVerts = NULL;
}
}
pModel->pObject.clear();
pModel->pMaterials.clear();
pModel = NULL;
}
Can anyone think of a reason why VS 2005 would report the .3DS model as not free'd and .NET 2003 says everything is fine?
Cheers!
Answer to question:
It looks like you might be forgetting to delete the model itself (i.e. delete pModel) at the bottom of the function.
Well-intentioned advice which you are free to ignore:
It's OK to pass NULL to delete, so you don't need to do all the "if ( x != NULL )" checks unless you really like them :) This only applies to delete though!
You might also want to encapsulate the ownership of the model internals in the model itself, so that deleting the model does all the other stuff, rather than having external code do it. To do this, put all the "delete pModel->something" code into the model destructor as just "delete something".
You might then also want to use STL containers to store all those internals so you don't have to remember to delete them. Makes looking after memory much easier, but there are pros and cons to STL so this isn't something you should feel you *have* to do.
Hope this helps!
Cheers,
Will
It looks like you might be forgetting to delete the model itself (i.e. delete pModel) at the bottom of the function.
Well-intentioned advice which you are free to ignore:
It's OK to pass NULL to delete, so you don't need to do all the "if ( x != NULL )" checks unless you really like them :) This only applies to delete though!
You might also want to encapsulate the ownership of the model internals in the model itself, so that deleting the model does all the other stuff, rather than having external code do it. To do this, put all the "delete pModel->something" code into the model destructor as just "delete something".
You might then also want to use STL containers to store all those internals so you don't have to remember to delete them. Makes looking after memory much easier, but there are pros and cons to STL so this isn't something you should feel you *have* to do.
Hope this helps!
Cheers,
Will
Hey! Cheers for the help mate!
Well I'm actually passing by reference to the Model3DS::DeleteModel( t3DModel* pModel ) code DeleteModel( &model )...so model isn't a pointer...so I don't actually need to call delete pModel inside of DeleteModel() etc
So I can't really see what else it can be? :S
Cheers
- DB
Well I'm actually passing by reference to the Model3DS::DeleteModel( t3DModel* pModel ) code DeleteModel( &model )...so model isn't a pointer...so I don't actually need to call delete pModel inside of DeleteModel() etc
So I can't really see what else it can be? :S
Cheers
- DB
Quote:Original post by Will Vale
You might also want to encapsulate the ownership of the model internals in the model itself, so that deleting the model does all the other stuff, rather than having external code do it. To do this, put all the "delete pModel->something" code into the model destructor as just "delete something".
QFE.
The object should encapsulate its internals. If I, as a third-party developer, allocate a model, then delete it (either because it had automatic scope and was deleted as the flow of execution left that block or because I've called delete on a pointer to it) then I, quite reasonably, expect exactly that -- that it was deleted. Use the destructor.
It will also help to avoid problems like you're having at the moment because rather than having to delete a whole bunch of stuff and then the model, you need only delete the model.
If your program is deterministic in how it allocates memory, you can use the allocation id reported by the CRT - this should be in your leak report. Add a call to _CrtSetBreakAlloc(long) at the start of your program and pass in this number. You should then break into the debugger on the allocation which is leaking.
If your program isn't deterministic (the order of allocations changes each time you run it, as may happen with leaks during gameplay) you can't do this. If this happens to me I usually use the allocation sizes to give me a clue as to what it is that might be leaking.
If none of this works you might need to post your Model class header so we can see what it allocates and make sure it matches with what's freed. Your code snippet is a bit isolated.
Oh, and if you're also considering making any of the suggested changes to improve your code, I'd make sure to find + fix the memory leak first rather than hoping that the changes will deal with it. Otherwise you might just be covering up a different problem...
Cheers,
Will
If your program isn't deterministic (the order of allocations changes each time you run it, as may happen with leaks during gameplay) you can't do this. If this happens to me I usually use the allocation sizes to give me a clue as to what it is that might be leaking.
If none of this works you might need to post your Model class header so we can see what it allocates and make sure it matches with what's freed. Your code snippet is a bit isolated.
Oh, and if you're also considering making any of the suggested changes to improve your code, I'd make sure to find + fix the memory leak first rather than hoping that the changes will deal with it. Otherwise you might just be covering up a different problem...
Cheers,
Will
Reading comprehension, anyone?
It's unlikely we can help you without more information.
First, use source tags.
Second, share the declaration of t3DModel.
You could be doing something obviously wrong, but we can't tell because we don't know anything about the types involved.
It's unlikely we can help you without more information.
First, use source tags.
Second, share the declaration of t3DModel.
You could be doing something obviously wrong, but we can't tell because we don't know anything about the types involved.
Cheers guys! :) Here the code...just a few notes:
I originally got this source from the net and wrapped up the original functions into a class. Oh, and yes I should move all the deletion into the destructor I know, I was just keeping Destroy() outside the class for testing purposes to start off with. Usually I would use pointers for t3DModel struct inside this class BUT i'm getting a NULL ptr in there somewhere with t3DObject::strName, I'll sort that out later tho...
Cheers guys, appreciate it!
- DB
[Edited by - DrunkenBrit on June 5, 2007 9:11:26 AM]
I originally got this source from the net and wrapped up the original functions into a class. Oh, and yes I should move all the deletion into the destructor I know, I was just keeping Destroy() outside the class for testing purposes to start off with. Usually I would use pointers for t3DModel struct inside this class BUT i'm getting a NULL ptr in there somewhere with t3DObject::strName, I'll sort that out later tho...
/////////////////////////////////////////////////////////////// HEADER//--------------------------------------------------------------------------------------------------/**@file 3ds.h@brief Loads in a .3DS file**///--------------------------------------------------------------------------------------------------#ifndef _3DS_H_#define _3DS_H_//--------------------------------------------------------------------------------------------------/**@includes**///--------------------------------------------------------------------------------------------------#include "TextureManager.h"#include "Math.h"#include <vector>#include <fstream>#pragma warning(disable:4267) //--------------------------------------------------------------------------------------------------/**@defines**///--------------------------------------------------------------------------------------------------#define PRIMARY 0x4D4D#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#define MATERIAL 0xAFFF // This stored the texture info#define OBJECT 0x4000 // This stores the faces, vertices, etc...#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#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//--------------------------------------------------------------------------------------------------/**@forward declarations**///--------------------------------------------------------------------------------------------------class ModelManager;//--------------------------------------------------------------------------------------------------/**@struct tFace@brief Face info for the model**///--------------------------------------------------------------------------------------------------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};//--------------------------------------------------------------------------------------------------/**@struct tMaterialInfo@brief Material info for the model**///--------------------------------------------------------------------------------------------------struct tMaterialInfo{ char strName[255]; // The texture name char strFile[255]; // The texture file name (If this is set it's a texture map) u8 color[3]; // The color of the object (R, G, B) int texureId; // the texture ID f32 uTile; // u tiling of texture (Currently not used) f32 vTile; // v tiling of texture (Currently not used) f32 uOffset; // u offset of texture (Currently not used) f32 vOffset; // v offset of texture (Currently not used)};//--------------------------------------------------------------------------------------------------/**@struct t3DObjectBounds@brief 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 Vector3 *pVerts; // The object's vertices Vector3 *pNormals; // The object's normals Vector2 *pTexVerts; // The texture's UV coordinates tFace *pFaces; // The faces information of the object};//--------------------------------------------------------------------------------------------------/**@enum ELEMENT@brief **///--------------------------------------------------------------------------------------------------enum ELEMENT{ kX = 1, kY = 2, kZ = 3};//--------------------------------------------------------------------------------------------------/**@struct t3DModel@brief 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<tMaterialInfo> pMaterials; // The list of material information (Textures and colors) vector<t3DObject> pObject; // The object list for our model};//--------------------------------------------------------------------------------------------------/**@struct tIndices@brief 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};//--------------------------------------------------------------------------------------------------/**@struct tChunk@brief 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};//--------------------------------------------------------------------------------------------------/**@class Model3DS@brief This class handles all of the loading code**///--------------------------------------------------------------------------------------------------class Model3DS{ friend class ModelManager;public: Model3DS(); //~Model3DS() //{ // Destroy(); //} bool Load( char* strFileName, char* filter, char* wrap, char* texEnv, u8 alphaR = 255, u8 alphaG = 0, u8 alphaB = 255 ); void Destroy(); bool ComputeObjectData(); bool WriteData( const char* fileName ) const; void Render(); inline t3DObject GetObject( uint index ) const; inline t3DModel GetModel() const; inline tMaterialInfo GetModelMaterial( uint index ) const;private: // Will hold collision data and such for each // object in the model struct ObjectData { Vector3 largestVertices; f32 largestVertex; Vector3 largestPositives; Vector3 largestNegatives; f32 width; f32 height; f32 depth; f32 centerX; f32 centerY; f32 centerZ; }; bool Import3DS( t3DModel* pModel, char* strFileName ); void CreateModelTextures( t3DModel* pModel, uint** textureArray, char* filter, char* wrap, char* texEnv, u8 r = 255, u8 g = 0, u8 b = 255 ); void RenderModel( t3DModel* pModel, uint* textureArray ); void DeleteModel( t3DModel* pModel ); int GetString( char* str ); void ReadChunk( tChunk* pChunk ); void ProcessNextChunk( t3DModel *pModel, tChunk* pChunk ); void ProcessNextObjectChunk( t3DModel *pModel, t3DObject* pObject, tChunk* pChunk ); void ProcessNextMaterialChunk( t3DModel* pModel, tChunk* pChunk ); void ReadColorChunk( tMaterialInfo* pMaterial, tChunk* pChunk ); void ReadVertices( t3DObject* pObject, tChunk* pChunk ); void ReadVertexIndices( t3DObject* pObject, tChunk* pChunk ); void ReadUVCoordinates( t3DObject* pObject, tChunk* pChunk ); void ReadObjectMaterial( t3DModel* pModel, t3DObject* pObject, tChunk* pPreviousChunk ); void ComputeNormals( t3DModel* pModel ); f32 FindLargestVertex( t3DObject* pObject ); Vector3 FindLargestVertexXYZ( t3DObject* pObject ); f32 FindLargestPoint( t3DObject* pObject, ELEMENT which ); f32 FindLargestPointNoAbs( t3DObject* pObject, ELEMENT which ); f32 FindLowestPoint( t3DObject* pObject, ELEMENT which ); f32 FindDimensions( t3DObject* pObject, ELEMENT which ); f32 FindCenter( t3DObject* pObject, ELEMENT which ); FILE* m_pFilePointer; t3DModel m_Model; uint* m_pTextureArray; bool m_ModelLoaded; bool m_ObjectDataComputed; vector < ObjectData > m_ObjectData; int m_Buffer[50000]; // Used to read past unwanted data};//--------------------------------------------------------------------------------------------------/**@function GetModel@brief Returns the model struct**///--------------------------------------------------------------------------------------------------inline t3DModel Model3DS::GetModel() const{ assert( m_ModelLoaded && "Model not loaded!" ); return m_Model;}#endif // _3DS_H_ /////////////////////////////////////////// CPP#include "3ds.h"//--------------------------------------------------------------------------------------------------/**@function Ctor @brief Initialise file ptr **///--------------------------------------------------------------------------------------------------Model3DS::Model3DS(){ m_pFilePointer = NULL; m_pTextureArray = NULL; m_ModelLoaded = false; m_ObjectDataComputed = false;}//--------------------------------------------------------------------------------------------------/**@function Import3DS @brief Loads in the 3D model **///--------------------------------------------------------------------------------------------------bool Model3DS::Import3DS(t3DModel *pModel, char *strFileName){ char strMessage[255] = {0}; tChunk currentChunk = {0}; // Open the 3DS file m_pFilePointer = fopen( strFileName, "rb" ); // Make sure we have a valid file pointer (we found the file) if( !m_pFilePointer ) { sprintf(strMessage, "Unable to find the file: %s!", strFileName); MessageBox(NULL, strMessage, "Error", MB_OK); 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(¤tChunk); // Make sure this is a 3DS file if (currentChunk.ID != PRIMARY) { sprintf(strMessage, "Unable to load PRIMARY chuck from file: %s!", strFileName); MessageBox(NULL, strMessage, "Error", MB_OK); return false; } // Now we actually start reading in the data. ProcessNextChunk() is recursive // Begin loading objects, by calling this recursive function ProcessNextChunk(pModel, ¤tChunk); // After we have read the whole 3DS file, we want to calculate our own vertex normals. ComputeNormals(pModel); return true;}//--------------------------------------------------------------------------------------------------/**@function ProcessNextChunk @brief This function reads the main sections of the .3DS file, then dives deeper with recursion **///--------------------------------------------------------------------------------------------------void Model3DS::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 tChunk currentChunk = {0}; // The current chunk to load tChunk tempChunk = {0}; // A temp chunk for holding data // 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(¤tChunk); // Check the chunk ID switch (currentChunk.ID) { case VERSION: // This holds the version of the file // If the file was made in 3D Studio Max, this chunk has an int 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. // However, if the file wasn't made by 3D Studio Max, we don't 100% what the // version length will be so we'll simply ignore the value // Read the file version and add the bytes read to our bytesRead variable currentChunk.bytesRead += fread(m_Buffer, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); // If the file version is over 3, give a warning that there could be a problem if ((currentChunk.length - currentChunk.bytesRead == 4) && (m_Buffer[0] > 0x03)) { MessageBox(NULL, "This 3DS file is over version 3 so it may load incorrectly", "Warning", MB_OK); } 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(&tempChunk); // Get the version of the mesh tempChunk.bytesRead += fread(m_Buffer, 1, tempChunk.length - tempChunk.bytesRead, m_pFilePointer); // Increase the bytesRead by the bytes read from the last chunk currentChunk.bytesRead += tempChunk.bytesRead; // Go to the next chunk, which is the object has a texture, it should be MATERIAL, then OBJECT. ProcessNextChunk(pModel, ¤tChunk); 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, ¤tChunk); 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++; // Add a new tObject node to our list of objects (like a link list) pModel->pObject.push_back(newObject); // Initialize the object and all it's data members memset(&(pModel->pObject[pModel->numOfObjects - 1]), 0, sizeof(t3DObject)); // Get the name of the object and store it, then add the read bytes to our byte counter. currentChunk.bytesRead += GetString(pModel->pObject[pModel->numOfObjects - 1].strName); // Now proceed to read in the rest of the object information ProcessNextObjectChunk(pModel, &(pModel->pObject[pModel->numOfObjects - 1]), ¤tChunk); 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, currentChunk); // Read past this chunk and add the bytes read to the byte counter currentChunk.bytesRead += fread(m_Buffer, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); 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. currentChunk.bytesRead += fread(m_Buffer, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += currentChunk.bytesRead; }}//--------------------------------------------------------------------------------------------------/**@function ProcessNextObjectChunk @brief This function handles all the information about the objects in the file **///--------------------------------------------------------------------------------------------------void Model3DS::ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk){ // The current chunk to work with tChunk currentChunk = {0}; // Continue to read these chunks until we read the end of this sub chunk while (pPreviousChunk->bytesRead < pPreviousChunk->length) { // Read the next chunk ReadChunk(¤tChunk); // Check which chunk we just read switch (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, ¤tChunk); break; case OBJECT_VERTICES: // This is the objects vertices ReadVertices(pObject, ¤tChunk); break; case OBJECT_FACES: // This is the objects face information ReadVertexIndices(pObject, ¤tChunk); 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, ¤tChunk); 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, ¤tChunk); break; default: // Read past the ignored or unknown chunks currentChunk.bytesRead += fread(m_Buffer, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += currentChunk.bytesRead; }}//--------------------------------------------------------------------------------------------------/**@function ProcessNextMaterialChunk @brief This function handles all the information about the material (Texture)**///--------------------------------------------------------------------------------------------------void Model3DS::ProcessNextMaterialChunk(t3DModel *pModel, tChunk *pPreviousChunk){ // The current chunk to work with tChunk currentChunk = {0}; // Continue to read these chunks until we read the end of this sub chunk while (pPreviousChunk->bytesRead < pPreviousChunk->length) { // Read the next chunk ReadChunk(¤tChunk); // Check which chunk we just read in switch (currentChunk.ID) { case MATNAME: // This chunk holds the name of the material // Here we read in the material name currentChunk.bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strName, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); break; case MATDIFFUSE: // This holds the R G B color of our object ReadColorChunk(&(pModel->pMaterials[pModel->numOfMaterials - 1]), ¤tChunk); break; case MATMAP: // This is the header for the texture info // Proceed to read in the material information ProcessNextMaterialChunk(pModel, ¤tChunk); break; case MATMAPFILE: // This stores the file name of the material // Here we read in the material's file name currentChunk.bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strFile, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); break; default: // Read past the ignored or unknown chunks currentChunk.bytesRead += fread(m_Buffer, 1, currentChunk.length - currentChunk.bytesRead, m_pFilePointer); break; } // Add the bytes read from the last chunk to the previous chunk passed in. pPreviousChunk->bytesRead += currentChunk.bytesRead; }}//--------------------------------------------------------------------------------------------------/**@function ReadChunk@brief This function reads in a chunk ID and it's length in bytes**///--------------------------------------------------------------------------------------------------void Model3DS::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_pFilePointer); // 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_pFilePointer);}//--------------------------------------------------------------------------------------------------/**@function GetString@brief This function reads in a string of characters**///--------------------------------------------------------------------------------------------------int Model3DS::GetString(char *pBuffer){ int index = 0; // Read 1 byte of data which is the first letter of the string fread(pBuffer, 1, 1, m_pFilePointer); // 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_pFilePointer); } // Return the string length, which is how many bytes we read in (including the NULL) return strlen(pBuffer) + 1;}//--------------------------------------------------------------------------------------------------/**@function ReadColorChunk@brief This function reads in the RGB color data**///--------------------------------------------------------------------------------------------------void Model3DS::ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk){ tChunk tempChunk = {0}; // Read the color chunk info ReadChunk(&tempChunk); // Read in the R G B color (3 bytes - 0 through 255) tempChunk.bytesRead += fread(pMaterial->color, 1, tempChunk.length - tempChunk.bytesRead, m_pFilePointer); // Add the bytes read to our chunk pChunk->bytesRead += tempChunk.bytesRead;}//--------------------------------------------------------------------------------------------------/**@function ReadVertexIndices@brief This function reads in the indices for the vertex array**///--------------------------------------------------------------------------------------------------void Model3DS::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_pFilePointer); // 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_pFilePointer); if(j < 3) { // Store the index in our face structure. pObject->pFaces.vertIndex[j] = index; } } }}//--------------------------------------------------------------------------------------------------/**@function ReadUVCoordinates@brief This function reads in the UV coordinates for the object**///--------------------------------------------------------------------------------------------------void Model3DS::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_pFilePointer); // Allocate memory to hold the UV coordinates pObject->pTexVerts = new Vector2 [pObject->numTexVertex]; // Read in the texture coodinates (an array 2 f32) pPreviousChunk->bytesRead += fread(pObject->pTexVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_pFilePointer);}//--------------------------------------------------------------------------------------------------/**@function ReadVertices@brief This function reads in the vertices for the object**///--------------------------------------------------------------------------------------------------void Model3DS::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_pFilePointer); // Allocate the memory for the verts and initialize the structure pObject->pVerts = new Vector3 [pObject->numOfVerts]; memset(pObject->pVerts, 0, sizeof(Vector3) * pObject->numOfVerts); // Read in the array of vertices (an array of 3 f32s) pPreviousChunk->bytesRead += fread(pObject->pVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_pFilePointer); // 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 m_Y values with the m_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 f32 fTempY = pObject->pVerts.GetY(); // Set the Y value to the Z value pObject->pVerts.SetY( pObject->pVerts.GetZ() ); // Set the Z value to the Y value, // but negative Z because 3D Studio max does the opposite. pObject->pVerts.SetZ( -fTempY ); }}//--------------------------------------------------------------------------------------------------/**@function ReadObjectMaterial@brief This function reads in the material name assigned to the object and sets the materialID**///--------------------------------------------------------------------------------------------------void Model3DS::ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk){ char strMaterial[255] = {0}; // This is used to hold the objects material name // *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.strName) == 0) { // 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.strFile) > 0) { // Set the material ID to the current index 'i' and stop checking pObject->materialID = i; // Set the object's flag to say it has a texture map to bind. pObject->bHasTexture = true; } break; } else { // Here we check first to see if there is a texture already assigned to this object if(pObject->bHasTexture != true) { // 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(m_Buffer, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_pFilePointer);} //--------------------------------------------------------------------------------------------------/**@function ComputeNormals@brief This function computes the normals and vertex normals of the objects**///--------------------------------------------------------------------------------------------------void Model3DS::ComputeNormals(t3DModel *pModel){ Vector3 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 Vector3 *pNormals = new Vector3 [pObject->numOfFaces]; Vector3 *pTempNormals = new Vector3 [pObject->numOfFaces]; pObject->pNormals = new Vector3 [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.vertIndex[0]]; vPoly[1] = pObject->pVerts[pObject->pFaces.vertIndex[1]]; vPoly[2] = pObject->pVerts[pObject->pFaces.vertIndex[2]]; // Now let's calculate the face normals (Get 2 vectors and find the cross product of those 2) vVector1 = SubVector(vPoly[0], vPoly[2]); // Get the vector of the polygon (we just need 2 sides for the normal) vVector2 = SubVector(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 = vNormal; // Save the un-normalized normal for the vertex normals vNormal = Normalize(vNormal); // Normalize the cross product to give us the polygons normal pNormals = vNormal; // Assign the normal to the list of normals } //////////////// Now Get The Vertex Normals ///////////////// Vector3 vSum(0.0f, 0.0f, 0.0f); Vector3 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 = DivideVectorByScaler(vSum, f32(-shared)); // Normalize the normal for the final vertex normal pObject->pNormals = Normalize(pObject->pNormals); vSum = vZero; // Reset the sum shared = 0; // Reset the shared } // Free our memory and start over on the next object delete [] pTempNormals; delete [] pNormals; }}//--------------------------------------------------------------------------------------------------/**@function FindLargestVertex@brief Finds the largest vertex in the model**///--------------------------------------------------------------------------------------------------f32 Model3DS::FindLargestVertex( t3DObject *pObject ){ Vector3 temp = FindLargestVertexXYZ( pObject ); f32 largest[] = { temp.GetX(), temp.GetY(), temp.GetZ() }; for( int k = 0; k < 2; ++k) for( int j = k + 1; j > 0; --j ) if(largest[j] > largest[j-1]) { f32 temp = largest[j]; largest[j] = largest[j - 1]; largest[j - 1] = temp; } return largest[0];}//--------------------------------------------------------------------------------------------------/**@function FindLargestPoint@brief Finds the largest point in m_X, m_Y and m_Z axis of the model (depending on ELEMENT)**///--------------------------------------------------------------------------------------------------f32 Model3DS::FindLargestPoint( t3DObject* pObject, ELEMENT which ){ int numVerts = pObject->numOfVerts; f32 finalLargest = -99999999.9f; f32* largest = new f32[ numVerts ]; switch( which ) { case kX: for( int i = 0; i < numVerts; i++ ) { largest = abs( pObject->pVerts.GetX() ); if( largest > finalLargest ) finalLargest = largest; } break; case kY: for( int i = 0; i < numVerts; i++ ) { largest = abs( pObject->pVerts.GetY() ); if( largest > finalLargest ) finalLargest = largest; } break; case kZ: for( int i = 0; i < numVerts; i++ ) { largest = abs( pObject->pVerts.GetZ() ); if( largest > finalLargest ) finalLargest = largest; } break; default: break; } SafeDeleteArray( largest ); return finalLargest;}//--------------------------------------------------------------------------------------------------/**@function FindLargestPointNoAbs@brief Finds the largest point in m_X, m_Y and m_Z axis of the model, excludes ABS on all vertices.**///--------------------------------------------------------------------------------------------------f32 Model3DS::FindLargestPointNoAbs( t3DObject* pObject, ELEMENT which ){ int numVerts = pObject->numOfVerts; f32 finalLargest = -99999999.9f; f32* largest = new f32[ numVerts ]; switch( which ) { case kX: for( int i = 0; i < numVerts; i++ ) { largest = pObject->pVerts.GetX(); if( largest > finalLargest ) finalLargest = largest; } break; case kY: for( int i = 0; i < numVerts; i++ ) { largest = pObject->pVerts.GetY(); if( largest > finalLargest ) finalLargest = largest; } break; case kZ: for( int i = 0; i < numVerts; i++ ) { largest = pObject->pVerts.GetZ(); if( largest > finalLargest ) finalLargest = largest; } break; default: break; } SafeDeleteArray( largest ); return finalLargest;}//--------------------------------------------------------------------------------------------------/**@function FindLowestPoint@brief Finds the lowest point in m_X, m_Y and m_Z axis of the model (depending on ELEMENT)**///--------------------------------------------------------------------------------------------------f32 Model3DS::FindLowestPoint( t3DObject* pObject, ELEMENT which ){ int numVerts = pObject->numOfVerts; f32 finalLowest = 99999999.9f; f32* lowest = new f32[ numVerts ]; switch( which ) { case kX: for( int i = 0; i < numVerts; i++ ) { lowest = pObject->pVerts.GetX(); if( lowest < finalLowest ) finalLowest = lowest; } break; case kY: for( int i = 0; i < numVerts; i++ ) { lowest = pObject->pVerts.GetY(); if( lowest < finalLowest ) finalLowest = lowest; } break; case kZ: for( int i = 0; i < numVerts; i++ ) { lowest = pObject->pVerts.GetZ(); if( lowest < finalLowest ) finalLowest = lowest; } break; default: break; } SafeDeleteArray( lowest ); return finalLowest;}//--------------------------------------------------------------------------------------------------/**@function FindCenter@brief **///--------------------------------------------------------------------------------------------------f32 Model3DS::FindCenter( t3DObject* pObject, ELEMENT which ){ f32 lowest = FindLowestPoint( pObject, which ); f32 highest = FindLargestPointNoAbs( pObject, which ); return (lowest + highest) / 2.0f;}//--------------------------------------------------------------------------------------------------/**@function FindLargestPoint@brief Finds the largest point in m_X, m_Y and m_Z axis of the model (depending on ELEMENT)**///--------------------------------------------------------------------------------------------------Vector3 Model3DS::FindLargestVertexXYZ( t3DObject* pObject ){ f32 lX = FindLargestPoint( pObject, kX ); f32 lY = FindLargestPoint( pObject, kY ); f32 lZ = FindLargestPoint( pObject, kZ ); return Vector3( lX, lY, lZ );}//--------------------------------------------------------------------------------------------------/**@function FindDimensions@brief **///--------------------------------------------------------------------------------------------------f32 Model3DS::FindDimensions( t3DObject* pObject, ELEMENT which ){ f32 lowest = abs( FindLowestPoint( pObject, which ) ); f32 highest = FindLargestPointNoAbs( pObject, which ); return lowest + highest;}//--------------------------------------------------------------------------------------------------/**@function DeleteModel@brief Deletes the model data**///--------------------------------------------------------------------------------------------------void Model3DS::DeleteModel( t3DModel* pModel ){ for( int i = 0; i < pModel->numOfObjects; i++ ) { // Free the faces, normals, vertices's, and texture coordinates. if( pModel->pObject.pFaces != NULL ) { delete [] (pModel->pObject.pFaces); pModel->pObject.pFaces = NULL; } if( pModel->pObject.pNormals != NULL ) { delete [] (pModel->pObject.pNormals); pModel->pObject.pNormals = NULL; } if( pModel->pObject.pVerts != NULL ) { delete [] (pModel->pObject.pVerts); pModel->pObject.pVerts = NULL; } if( pModel->pObject.pTexVerts != NULL ) { delete [] pModel->pObject.pTexVerts; pModel->pObject.pTexVerts = NULL; } } pModel->pObject.clear(); pModel->pMaterials.clear(); pModel = NULL;}//--------------------------------------------------------------------------------------------------/**@function CreateModelTextures@brief Takes a ptr to a uint ptr. Finds the amount of textures in the model and creates the uint array inside the function. The number of textures is assigned to arraySize.**///--------------------------------------------------------------------------------------------------void Model3DS::CreateModelTextures( t3DModel* pModel, uint** textureArray, char* filter, char* wrap, char* texEnv, u8 r, u8 g, u8 b ){ uint size = pModel->numOfMaterials; if( size > 0 ) *textureArray = new uint[ size ]; // Go through all the materials for(int i = 0; i < pModel->numOfMaterials; i++) { // Check to see if there is a file name to load in this material if( strlen( pModel->pMaterials.strFile ) > 0 ) GenerateTextures( *textureArray, i, pModel->pMaterials.strFile, filter, wrap, texEnv, r, g, b ); // Set the texture ID for this material pModel->pMaterials.texureId = i; }}//--------------------------------------------------------------------------------------------------/**@function Destroy@brief **///--------------------------------------------------------------------------------------------------void Model3DS::Destroy(){ if( m_Model.numOfMaterials > 0 ) glDeleteTextures( m_Model.numOfMaterials, m_pTextureArray ); DeleteModel( &m_Model ); SafeDeleteArray( m_pTextureArray ); if( !m_ObjectData.empty() ) m_ObjectData.clear();}//--------------------------------------------------------------------------------------------------/**@function RenderModel@brief Renders the 3D model. Takes a texture array created in CreateModelTextures**///--------------------------------------------------------------------------------------------------void Model3DS::RenderModel( t3DModel* pModel, uint* textureArray ){ for(int i = 0; i < pModel->numOfObjects; i++) { // Make sure we have valid objects just in case. (size() is in the vector class) if(pModel->pObject.size() <= 0) break; // Get the current object that we are displaying t3DObject *pObject = &pModel->pObject; // 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, textureArray[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(GL_TRIANGLES); // 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 ].GetX(), pObject->pNormals[ index ].GetY(), pObject->pNormals[ index ].GetZ() ); // 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 ].GetX(), pObject->pTexVerts[ index ].GetY()); } } 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(pModel->pMaterials.size() && pObject->materialID >= 0) { // Get and set the color that the object is, since it must not have a texture u8 *pColor = pModel->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 ].GetX(), pObject->pVerts[ index ].GetY(), pObject->pVerts[ index ].GetZ() ); } } glEnd(); // End the drawing }}//--------------------------------------------------------------------------------------------------/**@function Load@brief Loads the 3DS file and computes each objects bounds (collision) data**///--------------------------------------------------------------------------------------------------bool Model3DS::Load( char* strFileName, char* filter, char* wrap, char* texEnv, u8 alphaR, u8 alphaG, u8 alphaB ){ if( m_ModelLoaded ) { MessageBox( NULL, "Model already loaded!", "Error", MB_OK ); return false; } // Load the model m_ModelLoaded = Import3DS( &m_Model, strFileName ); if( m_pFilePointer ) fclose( m_pFilePointer ); if( !m_ModelLoaded ) { MessageBox( NULL, "Model not loaded!", "Error", MB_OK ); return false; } // If the model was loaded, create the models textures // and the find the objects collision data CreateModelTextures( &m_Model, &m_pTextureArray, filter, wrap, texEnv, alphaR, alphaG, alphaB ); // Compute the collision data // ComputeObjectData(); return m_ModelLoaded;}//--------------------------------------------------------------------------------------------------/**@function ComputeObjectData@brief Computes all the collision data for all objects in the model.**///--------------------------------------------------------------------------------------------------bool Model3DS::ComputeObjectData(){ if( !m_ModelLoaded ) { MessageBox( NULL, "Model not loaded!", "Error", MB_OK ); return false; } if( m_ModelLoaded ) { for( int i = 0; i < m_Model.numOfObjects; i++ ) { ObjectData temp; temp.largestVertices = FindLargestVertexXYZ( &m_Model.pObject ); temp.largestVertex = FindLargestVertex( &m_Model.pObject ); f32 largestX = FindLargestPointNoAbs( &m_Model.pObject, kX ); f32 largestY = FindLargestPointNoAbs( &m_Model.pObject, kY ); f32 largestZ = FindLargestPointNoAbs( &m_Model.pObject, kZ ); temp.largestPositives = Vector3( largestX, largestY, largestZ ); f32 lowestX = FindLowestPoint( &m_Model.pObject, kX ); f32 lowestY = FindLowestPoint( &m_Model.pObject, kY ); f32 lowestZ = FindLowestPoint( &m_Model.pObject, kZ ); temp.largestNegatives = Vector3( lowestX, lowestY, lowestZ ); temp.width = FindDimensions( &m_Model.pObject, kX ); temp.height = FindDimensions( &m_Model.pObject, kY ); temp.depth = FindDimensions( &m_Model.pObject, kZ ); temp.centerX = FindCenter( &m_Model.pObject, kX ); temp.centerY = FindCenter( &m_Model.pObject, kY ); temp.centerZ = FindCenter( &m_Model.pObject, kZ ); m_ObjectData.push_back( temp ); } m_ObjectDataComputed = true; } return m_ObjectDataComputed;}//--------------------------------------------------------------------------------------------------/**@function WriteData@brief Writes each objects collision data to the specified file name**///--------------------------------------------------------------------------------------------------bool Model3DS::WriteData( const char* fileName ) const{ if( !m_ModelLoaded ) { MessageBox( NULL, "Model not loaded!", "Error", MB_OK ); return false; } if( !m_ObjectDataComputed ) { MessageBox( NULL, "Model data not computed!", "Error", MB_OK ); return false; } ofstream data; data.open( fileName ); if( !data ) { MessageBox( NULL, "Can't open stream!", "Error", MB_OK ); return false; } if( data ) { for( uint i = 0; i < m_ObjectData.size(); i++ ) { f32 largestVX = m_ObjectData.largestVertices.GetX(); f32 largestVY = m_ObjectData.largestVertices.GetY(); f32 largestVZ = m_ObjectData.largestVertices.GetZ(); data << "ObjectName=" << m_Model.pObject.strName << endl; data << "LargestVertex=" << m_ObjectData.largestVertex << endl; data << "LargestVertices=" << largestVX << "," << largestVY << "," << largestVZ << endl; data << "Dimensions=" << m_ObjectData.width << "," << m_ObjectData.height << "," << m_ObjectData.depth << endl; data << "Center=" << m_ObjectData.centerX << "," << m_ObjectData.centerY << "," << m_ObjectData.centerZ << endl; } data.close(); } return true;}//--------------------------------------------------------------------------------------------------/**@function Render@brief Renders each object in the model**///--------------------------------------------------------------------------------------------------void Model3DS::Render(){ if( !m_ModelLoaded ) { MessageBox( NULL, "Model not loaded!", "Error", MB_OK ); exit(0); } glEnable( GL_BLEND ); glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); RenderModel( &m_Model, m_pTextureArray ); glDisable( GL_BLEND );}
Cheers guys, appreciate it!
- DB
[Edited by - DrunkenBrit on June 5, 2007 9:11:26 AM]
Quote:Original post by DrunkenBrit
Sorry I tried to do code tags and all I saw on the RHS of page was quote tags...
- DB
[source]your code here[/source]
/\ What you're looking for with that length of code listing.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement