Reading Simple Data From a Caligari TrueSpace File

The 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:

  1. Read the file header, make sure it is valid.
  2. Iterate through the chunk headers, looking for the “PolH” (Polygon) chunks.
  3. Ignore all others, but read data from the PolH chunks, adding them to a list of simple structures.
  4. Make sure the chunk isn't a 'End' chunk, if it is, close the file.
  5. (Optional) Create a new file, go through your object structures, and write them in your own way.

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 '"<&ltszFilePath<<"'. . .\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.

Dave Kerr - http://www.focus.esmartweb.com