Please pm me any corrections and suggestions, and I'll edit this (I'm open to criticism)... Don't be shy.
there seems to be a lot of questions, regarding 3ds loading and since Ranger_One has started writing a tutorial on obj loading I thought I might as well do the same.
first of all I would recommend
wavefront's obj format over 3ds, for several reasons
obj reasons
- Its in a human readable format, so you can use a simple text editor to make changes to the file.
- Its a lot more logical, compared to the irratic structure of a 3ds file
- There is no redundant data.
3ds reasons
- Binary format, therefore it is faster to load, and is smaller.
But there are always people who want to prehaps convert a 3ds file, to their own personal format and 3ds is also a VERY popular format (prehaps even more so than max file format!).
The first and foremost thing to remember about the 3ds file, is that everything is ordered hierachially like in xml.
<root>
<materials>
<brickwall>
// ...
</brickwall>
<mutantskin>
// ...
</mutantskin>
</materials>
<meshes>
<wall>
<texture name="brickwall">
// face list
</texture>
<facelist>
<face a="10" b="20" c="8" />
// other faces
</facelist>
<vertexlist>
<vertex x="120" y="320" z="2" />
// other vertices
</vertexlist>
// other details like uv coords .etc
</wall>
// other meshes
</meshes>
</root>
above you will see a highly simplified version of what I mean.
in 3ds however the tags are replaced by chunk headers.
Everything in 3ds is organized into chunks. In the example above a chunk would be root, meshes, vertexlist .etc
A chunk header takes the form
chunk_beginning_adress:
[Chunk ID - 2 bytes]
[next Chunk offset from 'chunk_beginning_adress' - 4 bytes]
// ... data
second_chunk_beginning_adress:
[Chunk ID - 2 bytes]
[next Chunk offset from 'second_chunk_beginning_adress' - 4 bytes]
// ... data
not only that, but chunks can be nested.
c00:
[Chunk ID - 2 bytes]
[next Chunk offset from c00 - 4 bytes]
// data before sub-chunks
c01:
[Chunk ID - 2 bytes]
[next Chunk offset from c01 - 4 bytes]
c02:
[Chunk ID - 2 bytes]
[next Chunk offset from c02 - 4 bytes]
// ... c02 data, then sub-chunks
c10:
[Chunk ID - 2 bytes]
[next Chunk offset from c10 - 4 bytes]
// ...
reading chunks is trivial. lets say, we opened the 3ds file, read the first 2 bytes and realized its not the chunk we want to parse? well, we simply read the next 4 bytes and jump c00 + offset (this is why it is important to store your offset in the file before reading the chunk id's).
Even better lets say we read the first chunk under c00, and realized that c01 is not a chunk we are parsing? well we simply read the next 4 bytes, and jump c01 + offset, placing us right before c02's chunk id (at the point its good to store your new offset within the file).
Chunk ID's are the equivalent to tag names under xml.
Also a side note, the data under chunks
ALWAYS comes before the sub-chunks.
OK, so lets parse a 3ds file :)
lets give you the part of the 3ds file you will parse (you will of course need a 3ds file).
we are just going to read a few chunk headers.
<main3ds>
// no data under root chunk
<version />
<editor3d />
</main3ds>
we are going to read main3ds chunk, then start reading version sub-chunk, then skip to editor 3d chunk.
#include <fstream>
#include <iostream>
using namespace std;
int main(int argc , char **argv)
{
if(argc < 2)
return -1;
ifstream file(argv[1] , ios::binary);
short id; // hopefully short is 2 bytes on your system
int length; // and an int is 4 bytes :)
int chunk_beginning;
// main3ds
file.read((char*)&id , 2);
cout << "id = (" << hex << id << ")\n";
file.read((char*)&length , 4);
cout << "length = (" << dec << length << ")\n\n";
// version
chunk_beginning = file.tellg();
file.read((char*)&id , 2);
cout << "id = (" << hex << id << ")\n";
file.read((char*)&length , 4);
cout << "length = (" << dec << length << ")\n\n";
// 3d editor chunk
cout << "(chunk_beginning + offset) = ("
<< chunk_beginning + length << ")\n";
file.seekg(chunk_beginning + length);
file.read((char*)&id , 2);
cout << "id = (" << hex << id << ")\n";
file.read((char*)&length , 4);
cout << "length = (" << dec << length << ")\n";
return 0;
}
the output is:
genjix@linux:~/media/tmp> ./notniceparser heavy.3ds
id = (4d4d)
length = (37624)
id = (2)
length = (10)
(chunk_beginning + offset) = (16)
id = (3d3d)
length = (37608)
i should mention that I spent a long time, searching for a bug when i ported my parser to win32 - tellg() returns strange results without the ios::binary flag :S
some 3ds files may have different output with the above program, so get a different 3ds file (all the 3ds files ive tested so far, have given same result, but they may have a different chunk ordering).
No this is all well and good, but its not a nice way to parse a 3ds file :/
Taking our comparison with xml, most xml parsers work like this
<main3ds>
// no data under root chunk
<version />
<editor3d />
</main3ds>
Xml::Document doc("doc.xml");
Xml::Element root = doc.Child();
if(root.Name() == "main3ds")
{
for(Xml::Element child = root.Child() ;
child ; child = child.Sibling())
{
// i know switches only work on integral types
// this is semi-pseudo code
switch(child.Name())
{
case("version"):
cout << "id = (" << child.ID() << ")\n";
cout << "length = ("
<< child.Length() << ")\n";
// do stuff
break;
case("editor3d"):
cout << "id = (" << child.ID() << ")\n";
cout << "length = ("
<< child.Length() << ")\n";
// do stuff
break;
}
}
}
else
{
// parse error!
}
this is how the above example could be coded in our imaginary world.
You may complain how that is longer and more lines, but that sure as hell, is easier to understand.
Now imagine that instead of strings and names we had shorts and ID's. That would then become.
Model3DSFile doc("doc.xml");
Model3DSChunk root = doc.Child();
if(root.ID() == 0x4d4d) // main3ds
{
for(Model3DSChunk child = root.Child() ;
child ; child = child.Sibling())
{
// i know switches only work on integral types
// this is semi-pseudo code
switch(child.ID())
{
case(0x0002): // version
cout << "id = (" << child.ID() << ")\n";
cout << "length = ("
<< child.Length() << ")\n";
// do stuff
break;
case(0x3d3d): // editor3d
cout << "id = (" << child.ID() << ")\n";
cout << "length = ("
<< child.Length() << ")\n";
// do stuff
break;
}
}
}
else
{
// parse error!
}
to keep you happy, here are the header files for Model3DSFile and Model3DSChunk, until my next tutorial.
#ifndef FILE_H
#define FILE_H
#include <fstream>
#include "chunk.h"
/***/
class Model3DSFile
{
public:
/***/
Model3DSFile(const char *src);
/***/
~Model3DSFile();
Model3DSChunk Child();
private:
static int FileSize(std::ifstream &file);
std::ifstream file;
};
#endif
#ifndef CHUNK_H
#define CHUNK_H
#include <fstream>
#include <string>
/***/
class Model3DSChunk
{
public:
/***/
Model3DSChunk(std::ifstream &infile , int csend);
/***/
Model3DSChunk(const Model3DSChunk &chunk);
/***/
~Model3DSChunk();
/**bug : making 2 seperate file stream chunks = each other*/
void operator=(const Model3DSChunk &chunk);
operator bool();
int ID();
short Short();
int Int();
float Float();
std::string Str();
Model3DSChunk Child();
Model3DSChunk Sibling();
private:
std::ifstream &file
int chunkset_end;
int id , begin , end;
};
#endif
[Edited by - Genjix on April 15, 2005 6:50:55 AM]