TUTORIAL 4: 3DS LOADER

  Home  
  Tutorials  
    The 3d engine    
    OpenGL and GLUT    
    Texture mapping    
    3ds File Loader    
    Vectors and lighting    
  Projects  
  Useful links  
  Store  
  About  
 

 
     
 

Subscribe to know
 about the latest updates

 
     
 

INTRODUCTION

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 4 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  
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 byte hexadecimal number that tells us the chunk's id.
-unsigned int l_chunk_length; a 4 byte 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. 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 0x3d3d:
         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 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;

This next chunk is simply another empty node that is the parent node of the next chunks that we must read.

         case 0x4100:
         break;

Finally, here are the vertices! The chunk VERTICES LIST contains all the vertices of the model. 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.

         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 a list of the object's polygons. 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.

         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, we read the MAPPING COORDINATES LIST. 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? ;)

         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;

The default case! 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

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

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.  The next tutorial (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

To compile and execute this project you need the GLUT libraries that can be found at:www.opengl.org/developers/documentation/glut.html

Download the C/C++ source code and executable in zip format: 
Windows version
Linux version (port by Panteleakis Ioannis)
SDL version (p
ort by Afrasinei Alexandru)
MAC OS version (port by Martin Williams)

UTILITIES

Mingw32 makefile (To use the Linux version on Mingw32 for Windows - by Jose Ortega)
Printer friendly version of this tutorial (by Steve Bruce)

 

<< PREVIOUS TUTORIAL   NEXT TUTORIAL>>

 

 

©2000-2003 Damiano Vitulli. All Rights Reserved. No Portion Of This Site May Be Reproduced Without Permission. Best viewed at 1024x768.