3DS parsing tutorial

Started by
16 comments, last by Genjix 18 years, 12 months ago
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

  1. Its in a human readable format, so you can use a simple text editor to make changes to the file.
  2. Its a lot more logical, compared to the irratic structure of a 3ds file
  3. There is no redundant data.

3ds reasons

  1. 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]
Advertisement
if anyone thinks its worth me finishing this tutorial, tell me otherwise ill just post source code to parser.
I belive you should finish it. And some mod should add this (and OBJ) thread to FAQ.

And just one note: There are also upsides compared to .OBJ files. The main one would be the fact that data in 3ds files is stored in binary format and this makes them much smaller and alot faster to load.
You should never let your fears become the boundaries of your dreams.
ok, so here goes then (certainly spurred me on ).

now, when we create a chunk we need to know where the end of the parent is, so we know once we've stopped parsing sibling chunks.

<root>	<teacup />	<nine />	<farah /></root>


once we reach farah, we also reach the end of root.

so we open up our 3ds file. When we start parsing, we pass a reference of the current ifstream to the root chunk (stored as a const reference), and the filesize in bytes.
The end of the file is also the end of the child chunks - although a 3ds file should only have one root chunk (rather like xml).

the code for this goes something like this
/*open file stream*/ifstream file(src , ios::in|ios::binary);/*get child node*/file.seekg(0);Chunk c(file , FileSize(file));


so adding creating the file loader (you will notice I've now wrapped it in a namespace)
#ifndef FILE_H#define FILE_H#include <fstream>#include "chunk.h"namespace Model3DS{/***/class File{    public:	/***/	File(const char *src);	/***/	~File();	Model3DS::Chunk Child();    private:	static int FileSize(std::ifstream &file);		std::ifstream file;};}#endif


and the source
#include "file.h"using namespace Model3DS;#include <iostream>using namespace std;File::File(const char *src): file(src , ios::in|ios::binary)	// open the file stream{}File::~File(){	file.close();			// close the file stream}Chunk File::Child(){	file.seekg(0);			// seek to beginning before parse	return Chunk(file , FileSize(file));// give current fstream and chunk end}/*calculates the file size by seeking to end and finding offset from beginningof the file stream*/int File::FileSize(ifstream &file){	int curr_pos = file.tellg();	file.seekg(0);	int beginning = file.tellg();	file.seekg(0 , ios::end);	int ending = file.tellg();	file.seekg(curr_pos);	return ending - beginning;	// ;)}

now what do we need for a chunk parser?

  • get ID() of current chunk for identifying chunk (rather like name of xml element)

  • get Child() chunk

  • move to Sibling() chunk

  • check whether current chunk is valid (whether reached end of parent)

the last point you may not understand.
	<root>		<child />		<child />		<child />	</root>		for(Xml::Element child = root("child") ; child ; child = child.Sibling("child"))	{		// ... do stuff	}

that piece of code will loop 3 times. On the fourth time round the boolean test on child will be false, because there are no more <child /> tags underneath <root /&gt.
	<root>		<child />		<child />		<child />	</root> <child />	<root1>		

its like if you have the size of the whole <root /> chunk, then at offset of root in file in bytes (p), and size of root in bytes (q), then
	p + q = c4 = r1

where c4 is the poisiton of the fourth child chunk and r1 is the position of the next root chunk (<root1 />).

so when we create a sibling, and the test on the sibling is done we must test
	return (begin < chunkset_end) && (begin >= 0);

the (begin >= 0), tests if we have reached end of file (as begin will = -1 when on eof and you call tellg()).

here is the watered down header for chunk.h
#ifndef CHUNK_H#define CHUNK_H#include <fstream>#include <string>namespace Model3DS{/***/class Chunk{    public:	/***/	Chunk(std::ifstream &infile , int csend);	/***/	Chunk(const Chunk &chunk);	/***/	~Chunk();	/**bug : making 2 seperate file stream chunks = each other*/	void operator=(const Chunk &chunk);	operator bool();	int ID();	Chunk Child();	Chunk Sibling();    private:	std::ifstream &file		int chunkset_end;	int id , begin , end;};}#endif


everything else, may be safely ignored for time being.
Nice work! I would also suggest to you making a .DOC/.HTML version formated and colored [wink]. I have yet to read though it, but new tutorials are always good. When I get a chance to read it, I'll give you some feedback.
so lets start filling in the methods.

first of all when we create a chunk, we want to know where we are.
Chunk::Chunk(ifstream &infile , int csend): file(infile) , chunkset_end(csend){	//cout << "Chunk::Chunk()\n";	begin = file.tellg();		// get chunk beginning position	id = 0;				// zero out upper bytes	file.read((char*)&id , 2);	// read chunk id	file.read((char*)&end , 4);	// read chunk end offset	end += begin;			// compute absolute position	// debugging info :)	/*cout << "id = (" << hex << id << ")\t" << dec		<< "begin = (" << begin << ")\t"		<< "end = (" << end << ")\t"		<< "chunkset_end = (" << chunkset_end << ")\n";*/}Chunk::Chunk(const Chunk &chunk): file(chunk.file) ,  chunkset_end(chunk.chunkset_end) ,  id(chunk.id) , begin(chunk.begin) , end(chunk.end){	//cout << "Chunk::Chunk(const Chunk &chunk)\n";}Chunk::~Chunk(){}

they are simple and easy enough to understand. notice we zero out id before filling in the bottom 2 bytes.
for those who may not know much low level stuff, this is what happens
[ 255 ][ 10 ][ 211 ][ 3 ]	// before zeroing, it has random garbage[   0 ][  0 ][   0 ][ 0 ]	// zero whole thing out[   0 ][  0 ][   X ][ X ]	// read info into bottom 2 bytes

otherwise what will happen is that the first 2 bytes will be filled with garbage.

my void Chunk::operator=(const Chunk &chunk); is not so clean -
void Chunk::operator=(const Chunk &chunk){	//cout << "Chunk::operator=(const Chunk &chunk)\n";	/*if chunk.file is opened on a seperate file...*/	file.seekg(chunk.file.tellg());	chunkset_end = chunk.chunkset_end;	id = chunk.id;	begin = chunk.begin;	end = chunk.end;}

anyone spot the bug? what do you think would happen were you to have open 2 seperate 3ds chunk file streams and called operator=() on them? well since the first file stream wouldn't be changed to the second, you'd end up positioned where the second chunk is in their file stream, in this file stream!
This is ok, and poses no real threat however when we write the parser wrapper (as it opens a the file stream, parses the chunks and when the function exits, Model3DS::File is destroyed and hence the file stream is destroyed.

the test, to see whether whether this chunk, is at the end of the parent chunk (i.e this sibling and all the next siblings are invalid), we saw above
Chunk::operator bool(){	/*cout << "Chunk::operator bool() == "		<< ((begin < chunkset_end) && (begin >= 0)) << "\n"		<< "(" << begin << " < " << chunkset_end << ")\n";*/	return (begin < chunkset_end) && (begin >= 0);}


we parsed the id, at the chunk headers and stored it in the variable id, so the only thing to do in the ID() method is to return id.
int Chunk::ID(){	return id;}


you remember in the constructor we passed the end of the parent chunk as an absolute position in the constructor above, where it is used in the boolean test (also above)?
Chunk Chunk::Child(){	//cout << "end = (" << end << ")\n";	return Chunk(file , end);	// pass end of this 'father' chunk}Chunk Chunk::Sibling(){	file.seekg(end);		// travel to beginning of next chunk	//cout << "seek = (" << end << ")\n";	return Chunk(file , chunkset_end);// pass this chunks fathers end position}
arlier on I mentioned that data comes before child chunks. "But whats data?" I hear you ask.
<root>	<littleperson>		string name = "jessica simpson\0";		float age = 13.3141592654;		int normal = 2;		float x = 120;		float y = 230;		float z = 480;			<toys />		<toys />		<toys />	</littleperson>		<vertices>		short number_of_vertices = 4;		float x = ...;		float y = ...;		float z = ...;		float x = ...;		float y = ...;		float z = ...;		float x = ...;		float y = ...;		float z = ...;		float x = ...;		float y = ...;		float z = ...;	</vertices></root>

under littleperson, the data is name , age , normal , x , y and z;
the size of the data before we get to start parsing the child chunks in this case would be
	sizeof(data) = sizeof(name) + sizeof(age) + sizeof(normal) + sizeof(x) 				+ sizeof(y) + sizeof(z)	             = sizeof(16*sizeof(char)) + sizeof(float) + sizeof(int)				+ sizeof(float) + sizeof(float) + sizeof(float)		     = 16 + 4 + 4 + 4 + 4 + 4		     = 38

so we have to parse 38 bytes when we begin in littleperson before we start reading the child chunks no matter what. Even if we don't want that info.

in the vertices node we'd read that short, and know to loop 4 times, read 3 sets of floats.

have you also noticed how the string was terminated by \0? strings in a 3ds file are simple c style nul terminated strings.

here is our new header file with our new data getting functions.
#ifndef CHUNK_H#define CHUNK_H#include <fstream>#include <string>namespace Model3DS{/***/class Chunk{    public:	/***/	Chunk(std::ifstream &infile , int csend);	/***/	Chunk(const Chunk &chunk);	/***/	~Chunk();	/**bug : making 2 seperate file stream chunks = each other*/	void operator=(const Chunk &chunk);	operator bool();	int ID();	short Short();	int Int();	float Float();	std::string Str();	Chunk Child();	Chunk Sibling();    private:	std::ifstream &file		int chunkset_end;	int id , begin , end;};}#endif

How complicated was that!!!11!1!!11 4 new functions?
Also remember this 2 handy pieces of advice -

  • always parse the data first

  • you need to know how much data to read before parsing the child chunks,
    otherwise things will go haywire because the child chunk you'll be reading
    will be offset backwards incorrectly


short Chunk::Short(){	short s = 0;	// zero all variables if worried about 64 bit	file.read((char*)&s , 2);	return s;}int Chunk::Int(){	int i;	file.read((char*)&i , 4);	return i;}float Chunk::Float(){	float f;	file.read((char*)&f , 4);	return f;}string Chunk::Str(){	string s;	char c;	do	{		file.read(&c , 1);		s += c;	}while(c != '\0');	return s;}

if you are worried about the sizes of ints and other data types on different machines (64 bit), just do what i have done with the Short() method, otherwise its fine.

[Edited by - Genjix on April 15, 2005 6:51:51 AM]
Time to take a step back, and look at what we have so far.
#ifndef FILE_H#define FILE_H#include <fstream>#include "chunk.h"namespace Model3DS{/***/class File{    public:	/***/	File(const char *src);	/***/	~File();	Model3DS::Chunk Child();    private:	static int FileSize(std::ifstream &file);		std::ifstream file;};}#endif

#include "file.h"using namespace Model3DS;#include <iostream>using namespace std;File::File(const char *src): file(src , ios::in|ios::binary){}File::~File(){	cout << "File::~File()\n";	file.close();}Chunk File::Child(){	file.seekg(0);	return Chunk(file , FileSize(file));}int File::FileSize(ifstream &file){	int curr_pos = file.tellg();	file.seekg(0);	int beginning = file.tellg();	file.seekg(0 , ios::end);	int ending = file.tellg();	file.seekg(curr_pos);	return ending - beginning;	// ;)}


#ifndef CHUNK_H#define CHUNK_H#include <fstream>#include <string>namespace Model3DS{/***/class Chunk{    public:	/***/	Chunk(std::ifstream &infile , int csend);	/***/	Chunk(const Chunk &chunk);	/***/	~Chunk();	/**bug : making 2 seperate file stream chunks = each other*/	void operator=(const Chunk &chunk);	operator bool();	int ID();	short Short();	int Int();	float Float();	std::string Str();	Chunk Child();	Chunk Sibling();    private:	std::ifstream &file		int chunkset_end;	int id , begin , end;};}#endif

#include "chunk.h"using namespace Model3DS;#include <iostream>using namespace std;Chunk::Chunk(ifstream &infile , int csend): file(infile) , chunkset_end(csend){	//cout << "Chunk::Chunk()\n";	begin = file.tellg();	id = 0;		// zero out upper bytes	file.read((char*)&id , 2);	file.read((char*)&end , 4);	end += begin;	// compute absolute position	/*cout << "id = (" << hex << id << ")\t" << dec		<< "begin = (" << begin << ")\t"		<< "end = (" << end << ")\t"		<< "chunkset_end = (" << chunkset_end << ")\n";*/}Chunk::Chunk(const Chunk &chunk): file(chunk.file) ,  chunkset_end(chunk.chunkset_end) ,  id(chunk.id) , begin(chunk.begin) , end(chunk.end){	//cout << "Chunk::Chunk(const Chunk &chunk)\n";}Chunk::~Chunk(){}void Chunk::operator=(const Chunk &chunk){	//cout << "Chunk::operator=(const Chunk &chunk)\n";	file.seekg(chunk.file.tellg());	chunkset_end = chunk.chunkset_end;	id = chunk.id;	begin = chunk.begin;	end = chunk.end;}Chunk::operator bool(){	int curr_pos = file.tellg();	/*cout << "Chunk::operator bool() == "		<< ((begin < chunkset_end) && (begin >= 0)) << "\n"		<< "(" << begin << " < " << chunkset_end << ")\n";*/	return (begin < chunkset_end) && (begin >= 0);}int Chunk::ID(){	return id;}short Chunk::Short(){	short s;	file.read((char*)&s , 2);	return s;}int Chunk::Int(){	int i;	file.read((char*)&i , 4);	return i;}float Chunk::Float(){	float f;	file.read((char*)&f , 4);	return f;}string Chunk::Str(){	string s;	char c;	do	{		file.read(&c , 1);		s += c;	}while(c != '\0');	return s;}Chunk Chunk::Child(){	//cout << "end = (" << end << ")\n";	return Chunk(file , end);}Chunk Chunk::Sibling(){	file.seekg(end);// travel to next chunk	//cout << "seek = (" << end << ")\n";	return Chunk(file , chunkset_end);}


and for copy-pasting
chunk/file.h
#ifndef FILE_H#define FILE_H#include <fstream>#include "chunk.h"namespace Model3DS{/***/class File{    public:	/***/	File(const char *src);	/***/	~File();	Model3DS::Chunk Child();    private:	static int FileSize(std::ifstream &file);		std::ifstream file;};}#endif

chunk/file.cpp
#include "file.h"using namespace Model3DS;#include <iostream>using namespace std;File::File(const char *src): file(src , ios::in|ios::binary){}File::~File(){	cout << "File::~File()\n";	file.close();}Chunk File::Child(){	file.seekg(0);	return Chunk(file , FileSize(file));}int File::FileSize(ifstream &file){	int curr_pos = file.tellg();	file.seekg(0);	int beginning = file.tellg();	file.seekg(0 , ios::end);	int ending = file.tellg();	file.seekg(curr_pos);	return ending - beginning;	// ;)}

chunk/chunk.h
#ifndef CHUNK_H#define CHUNK_H#include <fstream>#include <string>namespace Model3DS{/***/class Chunk{    public:	/***/	Chunk(std::ifstream &infile , int csend);	/***/	Chunk(const Chunk &chunk);	/***/	~Chunk();	/**bug : making 2 seperate file stream chunks = each other*/	void operator=(const Chunk &chunk);	operator bool();	int ID();	short Short();	int Int();	float Float();	std::string Str();	Chunk Child();	Chunk Sibling();    private:	std::ifstream &file		int chunkset_end;	int id , begin , end;};}#endif

chunk/chunk.cpp
#include "chunk.h"using namespace Model3DS;#include <iostream>using namespace std;Chunk::Chunk(ifstream &infile , int csend): file(infile) , chunkset_end(csend){	//cout << "Chunk::Chunk()\n";	begin = file.tellg();	id = 0;		// zero out upper bytes	file.read((char*)&id , 2);	file.read((char*)&end , 4);	end += begin;	// compute absolute position	/*cout << "id = (" << hex << id << ")\t" << dec		<< "begin = (" << begin << ")\t"		<< "end = (" << end << ")\t"		<< "chunkset_end = (" << chunkset_end << ")\n";*/}Chunk::Chunk(const Chunk &chunk): file(chunk.file) ,  chunkset_end(chunk.chunkset_end) ,  id(chunk.id) , begin(chunk.begin) , end(chunk.end){	//cout << "Chunk::Chunk(const Chunk &chunk)\n";}Chunk::~Chunk(){}void Chunk::operator=(const Chunk &chunk){	//cout << "Chunk::operator=(const Chunk &chunk)\n";	file.seekg(chunk.file.tellg());	chunkset_end = chunk.chunkset_end;	id = chunk.id;	begin = chunk.begin;	end = chunk.end;}Chunk::operator bool(){	int curr_pos = file.tellg();	/*cout << "Chunk::operator bool() == "		<< ((begin < chunkset_end) && (begin >= 0)) << "\n"		<< "(" << begin << " < " << chunkset_end << ")\n";*/	return (begin < chunkset_end) && (begin >= 0);}int Chunk::ID(){	return id;}short Chunk::Short(){	short s = 0;	file.read((char*)&s , 2);	return s;}int Chunk::Int(){	int i;	file.read((char*)&i , 4);	return i;}float Chunk::Float(){	float f;	file.read((char*)&f , 4);	return f;}string Chunk::Str(){	string s;	char c;	do	{		file.read(&c , 1);		s += c;	}while(c != '\0');	return s;}Chunk Chunk::Child(){	//cout << "end = (" << end << ")\n";	return Chunk(file , end);}Chunk Chunk::Sibling(){	file.seekg(end);// travel to next chunk	//cout << "seek = (" << end << ")\n";	return Chunk(file , chunkset_end);}
before we get onto the full blown parser lets have a quick test
#include <iostream>using namespace std;#include "3ds/3ds.h"namespace Level5{void FacesMaterials(Model3DSChunk c){	cout << "\t\t\t\tmaterial name = (" << c.Str() << ")\n";		short n_faces = c.Short();	for(short i = 0 ; i < n_faces ; i++)	{		cout << "\t\t\t\tmaterial assigned to face " << c.Short() << "\n";	}}}namespace Level4{void LocalCoordinateSystem(Model3DSChunk c){	float matrix[4][3];	for(int i = 0 ; i < 4 ; i++)	{		cout << "\t\t\t(";		for(int j = 0 ; j < 3 ; j++)		{			matrix[j] = c.Float();			cout << matrix[j];						if(j < 2)				cout << " , ";		}		cout << ")\n";	}}void Vertices(Model3DSChunk c){	short n_vertices = c.Short();	float x , y , z;	cout << dec << "\t\t\tn_vertices = (" << n_vertices << ")\n";	for(short i = 0 ; i < n_vertices ; i++)	{		x = c.Float();		y = c.Float();		z = c.Float();		cout << "\t\t\tv[" << i << "] = ("			<< x << "," << y << "," << z << ")\n";	}}void TextureCoords(Model3DSChunk c){	short n_texcoords = c.Short();	float u , v;	cout << dec << "\t\t\tn_texcoords = (" << n_texcoords << ")\n";	for(short i = 0 ; i < n_texcoords ; i++)	{		u = c.Float();		v = c.Float();				cout << "\t\t\ttexcoord[" << i << "] = ("			<< u << "," << v << ")\n";	}}void TriangleFaces(Model3DSChunk c){	short n_faces = c.Short();		short fa , fb , fc , flag;	cout << dec << "\t\t\tn_faces = (" << n_faces << ")\n";	for(short i = 0 ; i < n_faces ; i++)	{		fa = c.Short();		fb = c.Short();		fc = c.Short();		flag = c.Short();		cout << "\t\t\tface[" << i << "] = ("			<< fa << "," << fb << "," << fc << ",{" << flag << "})\n";	}		for(Model3DSChunk cc = c.Child() ; cc ; cc = cc.Sibling())	{		switch(cc.ID())		{			case(0x4130):				cout << "\t\t\tfaces materials\n";				Level5::FacesMaterials(cc);			break;			default:				cout << "\t\t\terror : unknown chunk "					<< hex << cc.ID() << dec << "\n";			break;		}	}}}namespace Level3{void ObjectTriangleMesh(Model3DSChunk c){	for(Model3DSChunk cc = c.Child() ; cc ; cc = cc.Sibling())	{		switch(cc.ID())		{			case(0x4160):				cout << "\t\tlocal coordinate system\n";				Level4::LocalCoordinateSystem(cc);			break;			case(0x4110):				cout << "\t\tvertices\n";				Level4::Vertices(cc);			break;						case(0x4140):				cout << "\t\tuv vertices\n";				Level4::TextureCoords(cc);			break;						case(0x4120):				cout << "\t\ttriangles\n";				Level4::TriangleFaces(cc);			break;						default:				cout << "\t\terror : unknown chunk " << cc.ID() << "\n";			break;		}	}}}namespace Level2{void MeshVersion(Model3DSChunk c){	cout << "\tmesh version = (" << c.Int() << ")\n";}void EditMaterial(Model3DSChunk c){	string name;	for(Model3DSChunk mat_name = c.Child() ; mat_name ;		mat_name = mat_name.Sibling())	{		switch(mat_name.ID())		{			case(0xa000):				name = mat_name.Str();				cout << "\tmaterial_name = (" << name << ")\n";			break;			default:				cout << "\terror : unknown chunk "					<< mat_name.ID() << "\n";			break;		}	}}void EditObject(Model3DSChunk c){	cout << "\tobject name = (" << c.Str() << ")\n";	for(Model3DSChunk cc = c.Child() ; cc ; cc = cc.Sibling())	{		switch(cc.ID())		{			case(0x4100):				cout << "\tobject tri mesh\n";				Level3::ObjectTriangleMesh(cc);			break;						case(0x4600):				cout << "\tobject light\n";			break;						case(0x4700):				cout << "\tobject camera\n";			break;						default:				cout << "\terror : unknown object "					<< hex << cc.ID() << dec << "\n";			break;		}	}}}namespace Level1{void Version(Model3DSChunk c){	cout << "version = (" << c.Int() << ")\n";}void Editor3D(Model3DSChunk c){	for(Model3DSChunk cc = c.Child() ; cc ; cc = cc.Sibling())	{		switch(cc.ID())		{			case(0x3d3e):				cout << "mesh version\n";				Level2::MeshVersion(cc);			break;			case(0xafff):				cout << "edit material\n";				Level2::EditMaterial(cc);			break;						case(0x4000):				cout << "edit object\n";				Level2::EditObject(cc);			break;						default:				cout << "error : unknown chunk " << cc.ID() << "\n";			break;		}	}}}int main(int argc , char **argv){	if(argc < 2)		return -1;	Model3DSFile file(argv[1]);	Model3DSChunk root = file.Child();	cout << "id = (" << hex << root.ID() << ")\n";	for(Model3DSChunk cc = root.Child() ; cc ; cc = cc.Sibling())	{		switch(cc.ID())		{			case(0x0002):				Level1::Version(cc);			break;						case(0x3d3d):				Level1::Editor3D(cc);			break;						default:				cout << "error : unknown chunk in main\n";			break;		}	}	return 0;}

please not, I know the above main.cpp is not a good example of coding, but I wrote it in one sitting as a demonstration of the using the above classes to parse a 3ds file

This topic is closed to new replies.

Advertisement