Reading Simple Data From a Caligari TrueSpace FileThe Basics: There are two types of trueSpace file, *.scn and *.cob. These are Caligari Scenes, and Caligari Objects, respectively. The actual data in the files is not too complicated, because each file is a series of "chunks". Each chunk represents a different type of information. This allows us to scan through every chunk, only reading in the ones that are relevant. What We Need: The official specification for trueSpace files is 82 pages long. The font is 10pts. For anyone wanting to be able to import simple objects, this is very daunting. Even looking through the file for a while won't get you very far, there are masses of chunks, each more confusing than the last. But don't give up! It is actually very simple to read the objects, it is done in a few steps:
Getting Started: Create a new *.h file, call it something like "CaligariObjects.h". The first part is the inclusion guard. Then I typedef the BYTE symbol, which I like (although feel free to use the actual type name. The same goes for LPCTSTR). #ifndef __CALIGARI_OBJECTS_H__ #define __CALIGARI_OBJECTS_H__ #include <vector> // We Need This Later typedef unsigned char BYTE; // My Strange Ways typedef const char* LPCTSTR; // Microsoft Made Me Strange static const F_HOLE = 0x08; // (bit 3) Flags Used By Caligari Objects (Irrelevant) static const F_BACKCULL = 0x10; // (bit 4) Flags Used By Caligari Objects (Irrelevant) typedef struct tagCALIGARI_HEADER { char szIdentifier[9]; // Always "Caligari " char szVersion[6]; // V00.01 Or Whatever Version The File Comes From char chMode; // A: ASCII, B: BINARY. We Will Only Read Binary Files char szBitMode[2]; // LH: Little Endian, HL: Big Endian (Irrelevant) char szBlank[13]; // Blank Space char chNewLine; // '\n' Char } CALIGARI_HEADER; Next you'll need a 'chunk header'. This comes at the top of every chunk in the file. typedef struct tagCHUNK_HEADER { char szChunkType[4]; // Identifies The Chunk Type short sMajorVersion; // Version short sMinorVersion; // Version long lChunkID; // Identifier: Each Chunk Is Unique long lParentID; // Parent, Some Chunks Own Each Other long lDataBytes; // Number Of Bytes In Data } CHUNK_HEADER; A stream of data follows each chunk header. Sometimes this is ints, and floats etc, but lots of chunks contain the same type of data, such as position and axis data. Here is the structure that is used to give a chunk it's name (not all chunks have names). typedef struct tagCHUNK_NAME { short sNameDupecount; // Dupecount short sStringLen; // Length Of String char* szName; // Name } CHUNK_NAME; A lot of chunks that represent actual objects in the scenes can define their own axies. typedef struct tagCHUNK_AXES { float fCentre[3]; // x,y,z, Center float fDirectionX[3]; // x,y,z, Coords Direction Of Local x float fDirectionY[3]; // x,y,z, Coords Direction Of Local y float fDirectionZ[3]; // x,y,z, Coords Direction Of Local z } CHUNK_AXES; A four by four matrix defines the position of chunks, but only the first three lines are saved, the last line is always assumed to be [0, 0, 0, 1]. typedef struct tagCHUNK_POSITION { float fFirstRow[4]; float fSecondRow[4]; float fThirdRow[4]; } CHUNK_POSITION; Before we actually create an object class, we need to define a few simple data types. A face is basically a series of long ints, which represent positions in the vertex and UV arrays, there is usually only 3 or four pairs. struct FACE { BYTE byFlags; // Flags short sCount; // Number Of Vertices short sMatIndex; // Material Index long* pVertexUVIndex; // long Index To Vertex, long Index To UV }; Some might say that representing a vertex in a struct is overkill, but it doesn't incur any memory cost, and due to the added simplicity you can actually get a significant speed increase. The same applies to the UV coord structure. This approach means that if you make a huge change, like adding the w coordinate to all vertices (for homogenous coordinates) you can easily change the code. struct VERTEX { float x, y, z; }; struct UV { float u, v; }; Now we create the actual TrueSpace object class. Later you could include the name and position, but this serves well as a starting point. class FC_CaligariObject { public: FC_CaligariObject() {m_pFaces = NULL, m_pVertices = NULL, m_pUV = NULL;} virtual ~FC_CaligariObject() {delete [] m_pFaces; delete [] m_pVertices; delete [] m_pUV;} int m_nFaceCount; FACE* m_pFaces; int m_nVertexCount; VERTEX* m_pVertices; int m_nUVCount; UV* m_pUV; BYTE m_byDrawFlags[4]; BYTE m_byRadiositySetting[2]; }; We've included the vector template, now typedef it for readability, and declare the big function we will use. using namespace std; typedef vector<FC_CaligariObject*> cob_vector; bool ReadObjects(LPCTSTR lpszFileName, cob_vector* pDestination); #endif Now we create the loading function. Any file can contain a number of objects, so the best way to manage this is to use some sort of container for each object. Containers are a touchy subject, some people like the standard libraries, some people like MFC ones, and others use their own. In my opinion the standard library containers are that fastest and best. Now create a new *.cpp file, call it something like "CaligariObjects.cpp", and the following, including the header we've created, and starting the implementation of the function we've declared. #include <CaligariObjects.h> // Or Whatever Your Header Is bool ReadObjects(LPCTSTR lpszFileName, cob_vector* pDestination) { We first try to open the specified file. FILE* pFile = fopen(lpszFileName, "rb"); if(!pFile) return false; Now we move the file pointer to the beginning of the chunk data. // Get To The Beginning Of Real Data fseek(pFile, sizeof(CALIGARI_HEADER), SEEK_SET); Now we loop though every chunk, storing the data temporarily in 'ch'. CHUNK_HEADER ch; char chName[5]; do { // Read The Header fread(&ch, sizeof(CHUNK_HEADER), 1, pFile); We only want "PolH" chunks, so here we test to see if we have one. if(strcmp(ch.szChunkType, "PolH") == 0) // Is It A Poly? We will need a new caligari object, and we create it on the heap. It will later be added to the vector. FC_CaligariObject* pOb = new FC_CaligariObject; This section reads the chunk name data into a struct, but doesn't use it. Later on, you may want to name your objects so this has been included for detail. CHUNK_NAME cn; // Read The Name Info fread(&cn, sizeof(short) * 2, 1, pFile); // Get Memory For The String cn.szName = new char[cn.sStringLen + 1]; // Read The String fread(cn.szName, sizeof(char), cn.sStringLen, pFile); // Zero Terminate cn.szName[cn.sStringLen] = '\0'; I cannot find a way to easily implement the local axis system (if anyone can, please e-mail me), but we read the data anyway, just in case you want to use it in your own implementation. CHUNK_AXES ax; // Read The Local Axies fread(&ax, sizeof(ax), 1, pFile); Now we read the position. Try as I might, I am not good enough to translate this into simple data (like a translate x, y and z factor, a scale x, y, and z factor etc) so the objects I load are always at the origin, not rotated or scaled. If you find a way to make this matrix into simple values like those described, please e-mail me. // Read The Position CHUNK_POSITION ps; fread(&ps, sizeof(ps), 1, pFile); Don't be worried if this looks complex. First we read the number of vertices, then we get space for them (using the new operator), and then we get 'fread' to read them into our array, all at once. // Read Number Of Verticies fread(&pOb->m_nVertexCount, sizeof(int), 1, pFile); // Get Space For Them pOb->m_pVertices = new VERTEX[pOb->m_nVertexCount]; // This Reads All The Vertices fread(&pOb->m_pVertices, sizeof(VERTEX), pOb->m_nVertexCount, pFile); Exactly the same as before applies to our UVs. // Read UV Count fread(&pOb->m_nUVCount, sizeof(int),1,pFile); // alloc Space For Them pOb->m_pUV = new UV[pOb->m_nUVCount]; // Read Every UV fread(pOb->m_pUV, sizeof(UV), pOb->m_nUVCount, pFile); Here we get the number of faces and get memory for them, but they are slightly more difficult to read in, so we have to use another loop. // Read Faces fread(&pOb->m_nFaceCount, sizeof(int),1,pFile); // alloc Space pOb->m_pFaces = new FACE[pOb->m_nFaceCount]; for(int i=0; i<pOb->m_nFaceCount; i++) { TrueSpace faces can be of different types. In the interest of simplicity, we will ignore the special 'hole' faces (which are holes in the previous face) however, when we do read these faces, we check their type, as some have extra data. // Read Face Type FACE* pFace = &pOb->m_pFaces[i]; fread(&pFace->byFlags, sizeof(BYTE), 1, pFile); // Read Vertex Count fread(&pFace->sCount, sizeof(short), 1, pFile); // Do We Read A Material Number? if((pFace->byFlags&F_HOLE) == 0) fread(&pFace->sMatIndex, sizeof(short), 1, pFile); This is where we read the actual indices. Each one is actually a pair of 'longs' which are indices into the vertex and UV array, respectively. // Vertex And UV pFace->pVertexUVIndex = new long[pFace->sCount * 2]; fread(pFace->pVertexUVIndex, sizeof(long), pFace->sCount * 2, pFile); } We want to be able to read any version, so we must check the chunk version ID, and depending on the version, we might read extra data. // Any Extra Stuff? if(ch.sMinorVersion > 4) { // We Have Flags To Read fread(&pOb->m_byDrawFlags, sizeof(BYTE) * 4, 1 ,pFile); if(ch.sMinorVersion > 5 && ch.sMinorVersion < 8) fread(&pOb->m_byRadiositySetting, sizeof(BYTE) * 2, 1, pFile); } pDestination->push_back(pOb); } else fseek(pFile, ch.lDataBytes, SEEK_CUR); memcpy(chName, ch.szChunkType, 4); chName[4] = '\0'; } while(strcmp(chName, "END ") != 0); return true; } The easiest way to use this code is in an example. Create a new workspace, call it 'test' or something, and then add the CaligariObjects.h and CaligariObjects.cpp files to it. // Working Example Of TrueSpace Loading Code // Code Created By Dave Kerr #include <iostream> // If You Get An Error, #include <iostream.h> Instead #include "CaligariObjects.h" // The Structures Defined Earlier using namespace std; int main(int argc, char* argv[]) { cout << "Caligari loading code tester.\nPlease enter file path: "; char szFilePath[255]; cin >> szFilePath; cout << "Attempting to load '"<<szFilePath<<"'. . .\n"; cob_vector cobs; if(!ReadObjects(szFilePath, &cobs)) { cout << "Failed to open the file.\n"; return 0; } cout << "Success! Showing object data . . .\n"; for(cob_vector::iterator i = cobs.begin(); i < cobs.end(); i++) { cout << "Object:\n"<< (*i)->m_nFaceCount << " faces.\n"; cout << (*i)->m_nVertexCount << " vertices.\n"; cout << (*i)->m_nUVCount << " UV coords.\n"; } return 0; } Now you have the simple objects. They can very easily be drawn and implemented, but that would add too much to the tutorial. If anyone can find solutions to the problems concerning local axis and (the serious problem) the position/translation/scale matrix, please e-mail me. |