|
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 (port
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>>
|