INTRODUCTION

After this long wait it's time to say goodbye to our dear cube! In this lesson we will develop a routine to load 3ds objects, a file format very famous on the net and supported by various 3d modelers. For the one that doesn't know yet what a 3d modeler is, I immediately say that, thanks to it, it's possible to create any type of object in a more intuitive and human way rather than to define by hand the coordinates of the vertices, it's an impossible thing for objects only just a little bit more complexes than a cube. To say the truth I am sorry to throw away the cube, a so simple and perfect figure, but, until proved otherwise, the spaceships, the planets, the missiles and everything that has something to do with a space simulator is completely different from the cube!
Before starting to write code it's necessary to analyze the 3ds file structure. Ok, take a chamomile and get ready...

 

THE 3DS FILE STRUCTURE

A 3ds file contains a series of useful information to describe in every minimum detail a 3d scene composed by one or more objects. Internally a 3ds file is constituted by a series of blocks called Chunks. What is it contained in these blocks? Everything necessary to describe the scene: for each object is stored the name, the vertices coordinates, the mapping coordinates, the list of the polygons, the faces colors, the animation keyframes and so on...

The chunks don't have a linear structure, this means that some are closely dependent from others and therefore they can be read only if their relative fathers are read as well.  Of course it's not necessary to read all the chunks, we will consider only the most important ones!
To describe the 3ds format I will base myself on the file 3dsinfo.txt of Jochen Wilhelmy in which is explained in detail the structure of all the chunks. 
A chunk is composed of 4 fields: 
-Identifier: a hexadecimal number of two byte of length that identify the chunk. With this information we can immediately realize if the chunk is useful for our purpose. If we need the chunk we extrapolate the contained information in it and, if necessary, in its children, if instead the chunk is useless we jump it using the following information... 
-Length of the chunk: another number, this time of 4 byte, that is the sum of the chunk length and all the lengths of every contained sub-chunks.
-Chunk data: this field has a variable length. The real data of the chunk are contained in this field.

 
In this table we can see the offset (in byte) and the length (in byte) of each field in a typical chunk: 

 

Offset Length  
0 2 Chunk identifier
2 4 Chunk length: chunk data + sub-chunks(6+n+m)
6 n Data
6+n m Sub-chunks

From the last line we can easily realize how it is possible to make dependent some chunks from others: each chunk child is in fact entirely contained inside the field "Sub-chunks" of the father.  
These are the most important chunks in a file 3ds, 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

Obviously if we want to read a particular chunk we needs to be careful to read always its fathers! To understand better let's imagine the 3ds file as a tree and the chunk that we need a leaf... of course we are a little ant in the ground! To reach the leaf it is necessary to walk from the trunk to all the branches up to it. If we for example want to reach the chunk VERTICES LIST we have to read the MAIN CHUNK, the 3D EDITOR CHUNK, the OBJECT BLOCK and finally the TRIANGULAR MESH. The other chunks can quietly be jumped... 
Now let's prune again our tree and leave only the branches that contain the information: "vertices", "faces", "mapping coordinates" and their relative fathers: we are going to use in this tutorial...

MAIN CHUNK 0x4D4D
   3D EDITOR CHUNK 0x3D3D
      OBJECT BLOCK 0x4000
         TRIANGULAR MESH 0x4100

            VERTICES LIST 0x4110
            FACES DESCRIPTION 0x4120

            MAPPING COORDINATES LIST 0x4140

Let's describe in detail these chunks:

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 format is enough clear we are going to analyze the code of this tutorial... What? You have understood nothing? =D Let's continue anyway! The chunks structure will be surely clearer to you continuing with the lesson, after all we are programmer and we understand better the C language rather than the usual chatters! ;)

A SHORT BRIEFING

The necessary steps to load a 3ds object and save it in our structure are: 
-In the same way we did for the texture loader we must implement a "while" loop that continues its execution until the end of file is reached. 
-For each cycle we read the chunk_id and the chunk_length. 
-Through a switch we analyze the content of the chunk_id . 
-If the chunk is a section of the tree in which we don't need to pass then we jump the whole length of the chunk moving the file pointer to the position calculated using the length of the chunk added to the current position. In this way we jump the chunk and all the contained sub-chunks. If we want to use other words: let's jump to another branch! Are we ancestors of the monkeys or not? =) 
-Instead if the chunk allows us to reach another chunk that we need, or maybe it contains data that we need, then we must read its data, then we read the next chunk. 
 

FINALLY... CODE!

As we have already done for the last tutorial the first thing to do is to create the files that will contain the new routines.
Till now we have used the file tutorialN.cpp to contain the main data types of the engine. Now it's clear that our data structures are becoming bigger, therefore it's better to insert the declarations of our data types already present in tutorial3.cpp (renamed tutorial4.cpp for this lesson) in a file header that we will call tutorial4.h. The changes that we are going to do are:

#define MAX_VERTICES 8000
#define MAX_POLYGONS 8000

We must increase the number of vertices and polygons that our engine is able to manage.
The other change to be done concerns the structure obj_type in which we will insert the field char name[20]; that will contain the name of the loaded object. 
We will also 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 other file to create is 3dsloader.cpp. In this file we will insert this 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 as entry parameters the pointer to the object data structure and the name of the file to open. Then it returns "0" if the file has not been found or "1" if the file has been found and readed. 
As you can notice there aren't so much variables to initialize: we have the usual counter i, the pointer to the file  *l_file and the support variable to extrapolate byte data l_char.
The other variables are: 
-unsigned short l_chunk_id; the identifier of the chunk, a hexadecimal number of 2 byte of length.
-unsigned int l_chunk_length; 4 byte of length instead to specify the dimension of the chunk.
-unsigned short l_qty; this is only a support variable that will be useful to know the quantity of information to read. 
-unsigned short l_face_flags; This variable memorizes some information regarding the current polygon (visible, not visible etc.) useful only for the 3d editors scene visualization, we will only read them to move correctly 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 cycle is performed for the whole length of the file. The ftell function allows us to acquire the current file pointer position while filelength returns us 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

We have extrapolated the identifier and the length of the chunk and have respectively saved them in l_chunk_id and l_chunk_length. 
Now we must 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? Simple... nothing! In fact the MAIN CHUNK has not data, what is interesting are its sub-chunks, for this reason we have included this line of code. In fact, if we didn't include this "case", the whole chunk have been jumped! Mmmm why? The explanation is found in the "default case" at the end of this lesson... For now please don't worry, we will arrive to it soon... let's only say that jumping the length of the MAIN CHUNK would have meant to move the file pointer at the end! We didn't want this... or yes? ;)

The same approach for the 3D EDITOR CHUNK: this is the secondary branch that is useful to reach the information that we need, it hasn't own data. So let's pretend to read it =) he will bring us to its child... the Object Block!

         case 0x3d3d:
         break;

Here is the chunk OBJECT BLOCK, this chunk finally has some interesting information: the name of the object, that we immediately store in the field name of the object structure. The while cycle exit if there is the character '\0' or the number of characters are more than 20. But... be careful! We have had to read all the data of this chunk because this has allowed us to move the pointer of the file to the next chunk.

         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;

Another empty branch that however is the father of chunks that we must read...

         case 0x4100:
         break;

So here are the vertices! The chunk VERTICES LIST contains all the vertices of the object. We read first the value "quantity" and then we use it to create a for cycle to read all the vertices. We save all the information inside the object structure.

         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;

The chunk FACES DESCRIPTION contains the list of the object's polygons. As it was explained in the tutorial 1 in this structure we don't memorize coordinates but only numbers that  point to elements of the vertices list. To read this chunk we can exactly do the same we have already done for the vertices chunk: at first we read the number of faces and create a for cycle to read all the faces.
Each face has also another field of 2 byte, the face flags, that contains some information useful only for the 3d editors: face visible sides and so on. We read it only to move the file pointer...

         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;

Finally let's read the MAPPING COORDINATES LIST, as usual we read first the quantity and then the list of coordinates, this time however one point has two coordinates, in fact the mapping coordinates are bidimensional, u and v do you remember? No?? What are you doing here then? ;) Go back to the first tutorial!

         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;

Great! The default case! This means that we are at the end of the routine! When we meet chunks that we don't want to read the fseek function help us, using the chunk_length information, it moves the file pointer to the beginning of the next chunk.   

         default:
            fseek(l_file, l_chunk_length-6, SEEK_CUR);
      } 
   }

We have finished! Very little remains to be done: let's 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 is the start point to realize more complex readers. Keep in mind however that our routine can read only a 3ds scene if in it there is only one object and it is exactly at the center. In the next tutorials (the matrices tutorial), we will add the possibility to load other objects. In fact we must necessarily include other spaceships, in the opposite case we have not objectives to destroy! 
This lesson was really funny, don't you think? It wasn't so hard! After all the big work has already been done with the last lessons. We can use the saved energies for the next tutorial, in which we will learn how to add lighting in the scene using some OpenGL functions. Bye bye happy coders!