Loading 3ds files

From GPWiki

Contents

Preface

Sample code - This example code loads and displays the points out of a file called "bull.3ds" (not included, download it from 3D Cafe in the free stuff/animals section.) The files C3dsParser.cpp C3dsParser.h and 3dschunknames.h define a basic file wrapper class that should be capable of parsing any valid .3ds file. (if you find any bugs email me.) As usual if anyone has any questions email me.

.3ds format

Everyone will be relieved to know that .3ds files are not hard to load (due in no small part to the very simple and elegant chunk design, kudos to the engineer who thought it up). .3ds files are organized into sections called chunks each of which has a header that contains a 2-byte unsigned integer chunkname and a 4-byte unsigned integer of its length. The header length field includes the 6 bytes for the header itself. All fields in a .3ds are stored in little-endian byte ordering, so be sure to convert all you Mac users. The chunk name defines the format of what's contained in the chunk, which is potentially some data and/or some subchunks, in that order. If you don't know what a particular chunk is or don't care you can simply jump forward by length bytes from the beginning of the chunks header and arrive at the next one. If you're using C++ you might want to download the above example code and reuse the C3dsParser class (more on its use later). As far as game programming goes, we're probably not interested in what's selected in the views of the editor or where the editor places the lights. We're interested in geometry. Geometry is stored as one or more object chunks. The 3dschunknames.h header (borrowed with minor modification from this spec) defines these chunk names of interest:

MAIN3DS 
0x4D4D - This is the main chunk in the .3ds file, it contains all other chunks, the subchunk we're most interested in is EDIT3DS
EDIT3DS 
0x3D3D - This chunk contains EDIT_OBJECT chunks, which define the objects in the .3ds scene (lights, geometry, etc...)
EDIT_OBJECT 
0x4000 - This chunk contains OBJ_TRIMESH, but first it contains data! The chunk header is immediately followed by an object name, which is a NULL terminated string.
OBJ_TRIMESH 
0x4100 - This chunk contains the geometric information about the object: the vertices, triangles, and texture coordinate chunks.
TRI_VERTEXL 
0x4110 - This chunk contains a list of vertices, its format is a 2-byte unsigned integer vertex count followed by vertex count float triplets that define the x, y, z coordinates. (or you might want to think of it as x, z, y because artists tend to use the third axis as "up" I've discovered whereas we geeks tend to use the second). If you intend to display more than simple points from the file it is important to preserve the order of the vertices in the list for reasons that will be clear shortly.
TRI_FACEL1 
0x4120 - This list defines the triangles of the model, similar to the vertex list, it starts out with a 2-byte unsigned integer that gives a count of how many triangles there are, following this there are sets of four 2-byte unsigned integers, the first three are indices to one of the vertices in the vertex list, the last is contains a few bit-flags. Three vertices equals one triangle, and they're provided in counter-clockwise order so feel free to cull the back faces and calculate normals based on the given order.
TRI_TEXCOORD 
0x4140 - (Note: I added this one to the header file) This one gives a series texture coordinates, it starts out with a 2-byte unsigned integer like the last couple lists which defines how many entries are in this list. After that you find a series of two floating point numbers which define the two dimensional texture coordinates of the cooresponding vertex.

Example

For example consider the following 22 bytes taken from the beginning of a .3ds file:

4D 4D 69 C5 00 00 02 00   0A 00 00 00 03 00 00 00
3D 3D 7B C4 00 00 ...
  • The first two form a chunk name (0x4D4D), MAIN3DS
  • The next four bytes form a chunk length for the main chunk: 0x0000C569 = 50537 bytes (little-endian remember...), the chunk length of the main chunk is the total length of the file because it contains all other chunks

Following that header we have another header: a subchunk (the main hasn't ended according to the length and main chunks only contain subchunks, see one of the specs listed in the .3ds page for more info on what specific chunk types contain)

  • The subchunk's name is 0x0002
  • The subchunk's length is 0x0000000A = 10 bytes, meaning after reading the 6-byte header there are still four bytes left that belong to it. Given that we don't know what this chunk is we'll skip those four bytes.

Following our unknown subchunk we have a sibling chunk (the unknown chunk's length is over, but its parent's isn't so its a sibling)

  • This sibling subchunk carries name (0x3D3D), EDIT3DS
  • This sibling subchunk carries length 0x0000C47B = 50299 bytes

class C3dsParser

C3dsParser is a file wrapper class I wrote to handle all the moving around in the .3ds file. It functions as follows:

C3dsParser(void); - Default constructor, use LoadFile() to load a .3ds file after using this constructor.

C3dsParser(char *filename); - constructor that loads a .3ds file right at the beginning. Equivalent to using the default constructor then calling LoadFile().

~C3dsParser(void); - Destructor

unsigned short GetChunkName(void); - Returns the name field from the currently selected chunk

void SkipChunk(void); - Skips the currently selected chunk and selects the one directly after it

void EnterChunk(long datalength = 0); - Enters the currently selected chunk so you can retrieve subchunks, if any data follows the header of the currently selected chunk, pass its size (in bytes) to this function.

unsigned long GetChunkLength(void); - Returns the length field of the currently selected chunk

void GetChunkData(void *buffer, unsigned long buffersize, unsigned long skip = 0); - Retrieves a number of bytes (maximum buffersize or length of the chunk - 6, whichever is smaller) after the header of the current chunk and puts them in buffer, skipping the first skip bytes

void SkipStrData(void); - Skips the string data following any chunk header, such as EDIT_OBJECT, entering the chunk directly after the string data ends.

unsigned long GetStrDataLength(void); - Returns the length of the string data right after a chunk header such as EDIT_OBJECT.

void Rewind(void); - Rewinds the .3ds file to the beginning.

BOOL LoadFile(char *filename); - Loads a .3ds file to be parsed

BOOL Eof(void); - Returns true if you've recently hit the end of file marker.

void CloseFile(void); - Closes the .3ds file (done automatically by the destructor)

Example use of C3dsParser

From the example code:

float *GetVerts(C3dsParser &modelfile, unsigned short &numverts)
{
	float *verts = NULL;
	unsigned short chunkname;
	unsigned long mem = 0;
	while(!modelfile.Eof()) {
		chunkname = modelfile.GetChunkName();
		switch (chunkname) {
			case MAIN3DS:
				modelfile.EnterChunk();
				break;
			case EDIT3DS:
				modelfile.EnterChunk();
				break;
			case EDIT_OBJECT:
				modelfile.SkipStrData();
				modelfile.EnterChunk();
				break;
			case OBJ_TRIMESH:
				modelfile.EnterChunk();
				break;
			case TRI_VERTEXL:
				modelfile.GetChunkData(&numverts, 2);
				// numverts vertices * 3 floats per vertex * 4 bytes per float
				mem = numverts * 3 * 4;
				verts = (float *)malloc(mem);	
				if (verts == NULL)
					return NULL;
				modelfile.GetChunkData(verts, mem, 2);
				return verts;
			default:
				modelfile.SkipChunk();
				break;
		}
	}
}

This function parses through a .3ds file and retrieves the vertices from the first object chunk it finds and returns a buffer with numverts vertices in it. The function skips all chunks except MAIN3DS, EDIT3DS, EDIT_OBJECT, OBJ_TRIMESH, and TRI_VERTEXL. In a properly formed .3ds file MAIN3DS contains EDIT3DS, which contains EDIT_OBJECT, which contains OBJ_TRIMESH, which contains TRI_VERTEXL, the vertex list. (Well, they won't always contain those chunks, an object for instance might be a camera which contains no geometry, but if we enter such a chunk we simply skip all its subchunks and end up at the chunk directly after the camera, just as if we'd skipped it.)

Screenshots

All models in these screenshots are from 3D Cafe.

.3ds example code output
Enlarge
.3ds example code output
High poly count Gouraud-shaded .3ds model
Enlarge
High poly count Gouraud-shaded .3ds model
The bull from the example code Gouraud shaded
Enlarge
The bull from the example code Gouraud shaded
A Gouraud shaded, textured Palace
Enlarge
A Gouraud shaded, textured Palace


TODO

  • Learn more about the animation capabilities of the format (it supports inverse kinematics, skeletal, motion captured animations, keyframe and maybe more). Add info on loading animations.

-Doug Sheets Nov 26, 2004