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! |