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:

OffsetLengthDescription
02Chunk identifier
24Chunk length: chunk data + sub-chunks(6+n+m)
6nData
6+nmSub-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
Identifier0x4d4d
Length0 + sub-chunks length
Chunk fatherNone
Sub chunks3D EDITOR CHUNK
DataNone
3D EDITOR CHUNK
Identifier0x3D3D
Length0 + sub-chunks length
Chunk fatherMAIN CHUNK
Sub chunksOBJECT BLOCK, MATERIAL BLOCK, KEYFRAMER CHUNK
DataNone
OBJECT BLOCK
Identifier0x4000
LengthObject name length + sub-chunks length
Chunk father3D EDITOR CHUNK
Sub chunksTRIANGULAR MESH, LIGHT, CAMERA
DataObject name
TRIANGULAR MESH
Identifier0x4100
Length0 + sub-chunks length
Chunk fatherOBJECT BLOCK
Sub chunksVERTICES LIST, FACES DESCRIPTION, MAPPING COORDINATES LIST
DataNone
VERTICES LIST
Identifier0x4110
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksNone
DataVertices number (unsigned short)
Vertices list: x1,y1,z1,x2,y2,z2 etc. (for each vertex: 3*float)
FACES DESCRIPTION
Identifier0x4120
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksFACES MATERIAL
DataPolygons 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
Identifier0x4140
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksSMOOTHING GROUP LIST
DataVertices 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:

  1. implement a "while" loop (as we did for the texture loader) that continues its execution until the end of file is reached.
  2. read the chunk_id and the chunk_length each iteration of the loop.
  3. analyze the content of the chunk_id using a switch .
  4. 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? =)
  5. 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