Contents
INTRODUCTION
Original Author: Damiano Vitulli
Translation by: Click here
It's now time to say goodbye to our dear cube! In this lesson we will develop a routine to load 3ds objects, a very popular file format on the internet and supported by various 3d modelers. A 3d modeler allows you to create any type of object in a more intuitive and human way rather than to define by hand the coordinates of the vertices, which can become an impossible task even for simple objects just slightly more complicated than a cube.
Actually, I am very reluctant to throw away the cube, such a simple and perfect figure. However, until proven otherwise, spaceships, planets, missiles and anything that has to do with a space simulator seems to be completely different from the cube.
Before starting to write the code it will be necessary to analyze the 3ds file structure. Ok, prepare your favorite programming drink and get ready...
THE 3DS FILE STRUCTURE
A 3ds file contains a series of information used to describe every detail of a 3d scene composed of one or more objects. A 3ds file contains a series of blocks called Chunks. What is contained in these blocks? Everything necessary to describe the scene: the name of each object, the vertices coordinates, the mapping coordinates, the list of polygons, the faces colors, the animation keyframes and so on.
These chunks don't have a linear structure. This means that some chunks are dependent on others and can only be read if their relative parent chunks have been read first. It's not necessary to read all the chunks and we will only consider the most important ones here.
I will base my description of the 3ds file format on the information contained in the 3dsinfo.txt file written by Jochen Wilhelmy which explains in detail the structure of all the chunks.
A chunk is composed of 3 fields:
- Identifier: a hexadecimal number two bytes in length that identifies the chunk. This information immediately tells us if the chunk is useful for our purpose. If we need the chunk we can then extrapolate the scene information in it and, if necessary, any child chunks it may have. If we don't need the chunk, we jump it using the following information:
- Length of the chunk: a 4 byte number that is the sum of the chunk length and all the lengths of every contained sub-chunk.
- Chunk data: this field has a variable length and conatians all the data for the scene.
This table shows the offset (in bytes) and the length (also in bytes) of each field in a typical chunk:
Offset | Length | Description |
---|---|---|
0 | 2 | Chunk identifier |
2 | 4 | Chunk length: chunk data + sub-chunks(6+n+m) |
6 | n | Data |
6+n | m | Sub-chunks |
We can see from the last line in the table exactly how some chunks are dependent on others: each child chunk is in fact contained inside the field "Sub-chunks" of the parent chunk.
The following are the most important chunks in a 3ds file. Please note the hierarchy among the various elements:
MAIN CHUNK 0x4D4D 3D EDITOR CHUNK 0x3D3D OBJECT BLOCK 0x4000 TRIANGULAR MESH 0x4100 VERTICES LIST 0x4110 FACES DESCRIPTION 0x4120 FACES MATERIAL 0x4130 MAPPING COORDINATES LIST 0x4140 SMOOTHING GROUP LIST 0x4150 LOCAL COORDINATES SYSTEM 0x4160 LIGHT 0x4600 SPOTLIGHT 0x4610 CAMERA 0x4700 MATERIAL BLOCK 0xAFFF MATERIAL NAME 0xA000 AMBIENT COLOR 0xA010 DIFFUSE COLOR 0xA020 SPECULAR COLOR 0xA030 TEXTURE MAP 1 0xA200 BUMP MAP 0xA230 REFLECTION MAP 0xA220 [SUB CHUNKS FOR EACH MAP] MAPPING FILENAME 0xA300 MAPPING PARAMETERS 0xA351 KEYFRAMER CHUNK 0xB000 MESH INFORMATION BLOCK 0xB002 SPOT LIGHT INFORMATION BLOCK 0xB007 FRAMES (START AND END) 0xB008 OBJECT NAME 0xB010 OBJECT PIVOT POINT 0xB013 POSITION TRACK 0xB020 ROTATION TRACK 0xB021 SCALE TRACK 0xB022 HIERARCHY POSITION 0xB030
As mentioned earlier, if we want to read a particular chunk we must always read its parent chunk first. Imagine the 3ds file is a tree and the chunk that we need is a leaf (and we are a little ant on the ground). In order to reach the leaf, we need to start from the trunk and cross any branches that lead to that leaf. For example, if we want to reach the chunk VERTICES LIST, we have to read the MAIN CHUNK first, then the 3D EDITOR CHUNK, the OBJECT BLOCK and finally the TRIANGULAR MESH chunk. The other chunks can safely be skipped.
Now let's prune our tree and leave only the branches we are going to use in this tutorial : vertices, faces, mapping coordinates and their relative parents:
MAIN CHUNK 0x4D4D 3D EDITOR CHUNK 0x3D3D OBJECT BLOCK 0x4000 TRIANGULAR MESH 0x4100 VERTICES LIST 0x4110 FACES DESCRIPTION 0x4120 MAPPING COORDINATES LIST 0x4140
Here are the chunks described in detail:
MAIN CHUNK | |
---|---|
Identifier | 0x4d4d |
Length | 0 + sub-chunks length |
Chunk father | None |
Sub chunks | 3D EDITOR CHUNK |
Data | None |
3D EDITOR CHUNK | |
Identifier | 0x3D3D |
Length | 0 + sub-chunks length |
Chunk father | MAIN CHUNK |
Sub chunks | OBJECT BLOCK, MATERIAL BLOCK, KEYFRAMER CHUNK |
Data | None |
OBJECT BLOCK | |
Identifier | 0x4000 |
Length | Object name length + sub-chunks length |
Chunk father | 3D EDITOR CHUNK |
Sub chunks | TRIANGULAR MESH, LIGHT, CAMERA |
Data | Object name |
TRIANGULAR MESH | |
Identifier | 0x4100 |
Length | 0 + sub-chunks length |
Chunk father | OBJECT BLOCK |
Sub chunks | VERTICES LIST, FACES DESCRIPTION, MAPPING COORDINATES LIST |
Data | None |
VERTICES LIST | |
Identifier | 0x4110 |
Length | varying + sub-chunks length |
Chunk father | TRIANGULAR MESH |
Sub chunks | None |
Data | Vertices number (unsigned short) Vertices list: x1,y1,z1,x2,y2,z2 etc. (for each vertex: 3*float) |
FACES DESCRIPTION | |
Identifier | 0x4120 |
Length | varying + sub-chunks length |
Chunk father | TRIANGULAR MESH |
Sub chunks | FACES MATERIAL |
Data | Polygons number (unsigned short) Polygons list: a1,b1,c1,a2,b2,c2 etc. (for each point: 3*unsigned short) Face flag: face options, sides visibility etc. (unsigned short) |
MAPPING COORDINATES LIST | |
Identifier | 0x4140 |
Length | varying + sub-chunks length |
Chunk father | TRIANGULAR MESH |
Sub chunks | SMOOTHING GROUP LIST |
Data | Vertices number (unsigned short) Mapping coordinates list: u1,v1,u2,v2 etc. (for each vertex: 2*float) |
Now that the 3ds file format is clear enough, we are going to take a look at the code for this tutorial. What? You're completely lost? =D Let's continue anyway. The chunks structure will become clearer to you as you go through the lesson. After all, we are programmers and we understand C better than own chatter ;)
A SHORT BRIEFING
The steps we need to take in order to load a 3ds object and save it in the format defined by our engine are:
- implement a "while" loop (as we did for the texture loader) that continues its execution until the end of file is reached.
- read the chunk_id and the chunk_length each iteration of the loop.
- analyze the content of the chunk_id using a switch .
- if the chunk is a section of the tree we don't need to read, we jump the whole length of that chunk by moving the file pointer to a new position which is calculated by using the length of the current chunk added to the current position. This allows us to jump any chunk we don't need as well as all contained sub-chunks. In other words: let's jump to another branch! Are you starting to feel like a monkey yet? =)
- if the chunk allows us to reach another chunk that we need, or it contains data that we need, then we read its data if needed, and then move to the next chunk.
FINALLY... CODE!
The first thing to do is to create the files that will contain the new routines.
We have used the file tutorial(n).cpp to contain the main data types of the engine in the previous tutotials. However, since our data structures are becoming bigger, we will insert the declarations of the data types in a header file that we will call tutorial4.h
First, we increase the number of vertices and polygons that our engine is able to manage.
#define MAX_VERTICES 8000 #define MAX_POLYGONS 8000
Next, we add the field char name[20]; to the structure obj_type. This field will contain the name of the loaded object.
Lastly, we modify the name of our object variable from obj_type cube; to obj_type object; just to "highlight" the generic nature of our object.
The next file to create is 3dsloader.cpp. In this file, we insert the following routine:
char Load3DS (obj_type_ptr p_object, char *p_filename) { int i; FILE *l_file; unsigned short l_chunk_id; unsigned int l_chunk_length; unsigned char l_char; unsigned short l_qty; unsigned short l_face_flags;
The Load3DS routine accepts two parameters: a pointer to the object data structure and the name of the file to open. It returns "0" if the file has not been found or "1" if the file has been found and read. There aren't too many variables to initialize: we have the usual counter i, a pointer to the file *l_file and a support variable to extrapolate byte data l_char. The other variables are:
- unsigned short l_chunk_id; a 2 bytes hexadecimal number that tells us the chunk's id.
- unsigned int l_chunk_length; a 4 bytes number used to specify the length of the chunk.
- unsigned short l_qty; a support variable that will tell us the quantity of information to read.
- unsigned short l_face_flags; This variable holds various information regarding the current polygon (visible, not visible etc.) which the 3d editor uses to render the scene. We will only use this value to move the file pointer to the next chunk position.
So let's open the file at last!
if ((l_file=fopen (p_filename, "rb"))== NULL) return 0; //Open the file while (ftell (l_file) < filelength (fileno (l_file))) //Loop to scan the whole file {
The while loop is performed for the entire length of the file. The ftell function allows us to acquire the current file pointer position while filelength returns the length of the file.
fread (&l_chunk_id, 2, 1, l_file); //Read the chunk header fread (&l_chunk_length, 4, 1, l_file); //Read the length of the chunk
Here, we have extrapolated the identifier and the length of the chunk and have saved them in l_chunk_id and l_chunk_length respectively. First, we analyze the content of l_chunk_id:
switch (l_chunk_id) { case 0x4d4d: break;
We have found the MAIN CHUNK! Cool! What are we going to do with it? Simple... nothing! In fact, the MAIN CHUNK has no data. However, we are interested in its sub-chunks. We have included this particular "case" statement so that the whole MAIN chunk is not jumped! Jumping the length of the MAIN CHUNK would have meant moving the file pointer to the end of the file due to the "default case" at the end of this switch statement. I will discuss this "default case" more, later in this tutorial
We take the same approach for the 3D EDITOR CHUNK:
case 0x3d3d: break;
This is the next node that we need to navigate through in order to reach the information we need. Once again, this node has no data. So let's pretend to read it =) This will bring us to the child called Object Block:
case 0x4000: i=0; do { fread (&l_char, 1, 1, l_file); p_object->name[i]=l_char; i++; }while(l_char != '\0' && i<20); break;
The chunk OBJECT BLOCK finally has some interesting information: the name of the object. We store this data in the "name" field of the object structure. The while loop exits if the '\0' character is encountered or the number of characters exceeds 20. Be careful! We have just read all the data of this chunk and this has moved the file pointer to the next chunk:
case 0x4100: break;
This last chunk is simply another empty node that is the parent node of the next chunks that we must read.
Finally, here are the vertices! The chunk VERTICES LIST contains all the vertices of the model:
case 0x4110: fread (&l_qty, sizeof (unsigned short), 1, l_file); p_object->vertices_qty = l_qty; printf("Number of vertices: %d\n",l_qty); for (i=0; i<l_qty; i++) { fread (&p_object->vertex[i].x, sizeof(float), 1, l_file); fread (&p_object->vertex[i].y, sizeof(float), 1, l_file); fread (&p_object->vertex[i].z, sizeof(float), 1, l_file); } break;
First, we read the value "quantity" and use it to create a for loop that reads all the vertices. We then save each vertex in the corresponding field of the object structure.
The chunk FACES DESCRIPTION contains a list of the object's polygons:
case 0x4120: fread (&l_qty, sizeof (unsigned short), 1, l_file); p_object->polygons_qty = l_qty; printf("Number of polygons: %d\n",l_qty); for (i=0; i<l_qty; i++) { fread (&p_object->polygon[i].a, sizeof (unsigned short), 1, l_file); fread (&p_object->polygon[i].b, sizeof (unsigned short), 1, l_file); fread (&p_object->polygon[i].c, sizeof (unsigned short), 1, l_file); fread (&l_face_flags, sizeof (unsigned short), 1, l_file); } break;
As explained in tutorial 1, the structure dealing with polygons doesn't contain coordinates, only numbers that correspond to elements containing a list of vertices. In order to read this chunk we do exactly the same procedure we have done for the vertices chunk: first, we read the number of faces then we create a for loop to read all the faces. Each face also has another 2 byte field, the face flags, that contains some information useful only for 3d editors (indicating visible faces and so on). We will only read it to move the file pointer to the next chunk.
Finally, we read the MAPPING COORDINATES LIST:
case 0x4140: fread (&l_qty, sizeof (unsigned short), 1, l_file); for (i=0; i<l_qty; i++) { fread (&p_object->mapcoord[i].u, sizeof (float), 1, l_file); fread (&p_object->mapcoord[i].v, sizeof (float), 1, l_file); } break;
Once again, we read the quantity and use this value to set up a for loop. Each point has two coordinates, u and v do you remember? No?? What are you doing here then? ;P
Finally, the default case:
default: fseek(l_file, l_chunk_length-6, SEEK_CUR); } }
This means that we are at the end of the routine. This case is simple: when we find chunks that we don't want to read, the fseek function moves the file pointer to the beginning of the next chunk using the chunk_length information
We have finished! Very little remains to be done. We close the file and return 1.
fclose (l_file); // Closes the file stream return (1); // Returns ok }
CONCLUSIONS
The 3ds reader that we have developed here is a starting point for more complex readers. Keep in mind however that our routine can only read a 3ds file if there is only one object present and it is positioned at the center. One of the next tutorials (the matrices tutorial), will add the functionality needed to load other objects. This will be the fun part. We have to include other spaceships right? Otherwise we won't have anything to destroy =)
This lesson wasn't so hard was it? After all, we have already done the big work in previous lessons. We can use all the code written so far for the next tutorial, in which we will learn how to add lighting using OpenGL functions. Bye bye for now happy coders!
SOURCE CODE
The Source Code of this lesson can be downloaded from the Tutorials Main Page