Создание 3ds loader’a.

Содержание статьи:

Введение.
Структура формата 3ds.
MAGIC = 0x4000.
MAGIC = 0x4110.
MAGIC = 0x4120.
MAGIC = 0x4140.
MAGIC = 0xAFFF.
MAGIC = 0xA200.
Структура класса.
Загрузка.
Завершение создания лоадера

Введение.

к началу статьи
Приветствую всех, кто посетил эту страничку. В данном уроке я расскажу, как создать простенький загрузчик 3ds файлов. Наверняка многим из Вас уже надоело наблюдать в своих программах разноцветные, но от этого не становящиеся менее унылыми, квадратики, треугольнички и прочую примитивную дребедень. Настало время изобразить что-нибудь высокохудожественное, что бы не было стыдно друзьям показать. Итак, поехали!

Структура формата 3ds.

к началу статьи
Все данные, хранящиеся в этом формате, расположены в чанках. Чанк – это некоторый блок данных, который имеет более-менее стандартную структуру (в рамках данного формата, разумеется), хотя могут быть некоторые девиации (о них я скажу ниже). Как правило, в начале чанка хранится его тип (в формате обозначенный как некий magic, и занимающий 2 байта) и размер (4 байта). Так же надо отметить, что внутри одного чанка могут располагаться другие чанки. Теперь рассмотрим структуру каждого чанка в отдельности (в скобках указывается размер поля в байтах):

MAGIC = 0x4000 – именованный объект

magic(2)
size(4) – размер чанка.
name(20 включая нуль-символ) – название объекта.
type(2) – тип (пояснения смотрите ниже).
Этот как раз пример чанка с нестандартной структурой. Поле type может принимать одно из двух значений – 0x4100 или 0x4600 (треугольная сетка и источник света). Поскольку лоадер у нас простенький, то позвольте мне рассмотреть только 0x4100. В состав чанка входят координаты вершин, текстурные координаты, индексы, материалы.

MAGIC = 0x4110 – чанк с координатами вершин

magic(2)
size(4) – размер чанка.
count(2) – количество вершин.
data – вершины, по три float’а в каждой.

MAGIC = 0x4120 – чанк с гранями

magic(2)
size(4) – размер чанка.
count(2) – количество граней.
data – грани, по 4 2-х байтовых целых в каждой:
a номер первой вершины в массиве вершин, содержащийся в предыдущем чанке
b номер второй вершины в массиве вершин, содержащийся в предыдущем чанке
с номер третьей вершины в массиве вершин, содержащийся в предыдущем чанке
d флаги

MAGIC = 0x4140 – чанк с текстурными координатами.

magic(2)
size(4) – размер чанка.
count(2) – количество пар текстурных координат.
data – текстурные координаты. Хранится в виде пар:
u 4-х байтовое вещественное
v 4-х байтовое вещественное

MAGIC = 0xAFFF – чанк материалов.

Как и чанк именованного объекта, он не содержит ничего кроме других чанков.
magic(2)
size(4) – размер чанка.

MAGIC = 0xA200 – чанк с названием текстуры для материала.

magic(2)
size(4) – размер чанка.
unknown(14) – неизвестно
name(20 включая нуль-символ) – название текстуры
Думаю этого нам должно хватить. Теперь давайте возьмемся за код.

Структура класса.

к началу статьи
Как всегда делаем класс:
class	cModelLoader{
	cArray<cD3DMesh> 		Meshes;
	cArray<cMaterial> 	Materials;
	void	ProcessObjectChunk( unsigned char ** , int& );
	void	ProcessVertexChunk( unsignedchar ** , int& );
	void	ProcessFaceChunk( LPDIRECT3DDEVICE8 , unsigned char ** , int& );
void ProcessTexCoordsChunk( unsigned char ** , int& ); void ProcessMeshChunk( unsigned char ** , int& ); void ProcessTextureMapChunk( LPDIRECT3DDEVICE8 , unsigned char ** , int& ); cChunkHeader GetChunkHeader( unsigned char ** , int &i );
cD3DIVertexBuffer <cD3DVertex> VertexBuffer; public: cModelLoader( void ){} void Render( LPDIRECT3DDEVICE8 ); void Load3ds( LPDIRECT3DDEVICE8 ,char * );
void Release( void ); };
С точки зрения использования это очень простой класс. Всего три функции – создание, удаление и рендерринг.

Загрузка.

к началу статьи
Загрузка осуществляется следующей функцией:
void	cModelLoader::Load3ds( LPDIRECT3DDEVICE8 D3DDevice ,  char *path ){
	FILE 		*f;
	unsigned int	FileLength;
unsigned char *buffer;

f = fopen( path , "rb" );
if( !f ){ MessageBox( 0 , "Не найден файл 3ds" , "Ошибка" , 0 ); exit( 0 ); } fseek( f , 0 , SEEK_END ); FileLength = ftell( f ); fseek( f , 0 , SEEK_SET ); buffer = new unsigned char [ FileLength ]; fread( buffer , FileLength , 1 , f ); unsigned short int CHUNK_ID;
for( int i( 0 ) ; i < FileLength ; ){ CHUNK_ID = * ( ( short int * ) buffer ); switch( CHUNK_ID ){ case( 0x4000 ): ProcessObjectChunk( &buffer , i ); break; case( 0x4110 ): ProcessVertexChunk( &buffer , i ); break; case( 0x4120 ): ProcessFaceChunk( D3DDevice , &buffer , i ); break; case( 0xafff ): ProcessMaterialChunk( &buffer , i ); break; case( 0x4140 ): ProcessTexCoordsChunk( &buffer , i ); break; case( 0xa200 ): ProcessTextureMapChunk( D3DDevice , &buffer , i ); break; default: i += 1; buffer += 1; break; } } buffer -= FileLength; delete [] buffer; }
В ней сначала проверяем, существует ли файл. Если существует, то сразу читаем его в буфер, и в цикле начинаем его сканировать, в котором в зависимости от CHUNK_ID (он же magic) вызываем необходимую функцию. В каждой из них в самом начале есть вызов функции GetChunkHeader(), которая просто читает magic и размер чанка, а затем сдвигает указатель на буфер:
cChunkHeader cModelLoader:: GetChunkHeader ( unsigned char **buffer ,  int &i ){
	cChunkHeader Header;

	Header.magic = * ( ( unsigned int * ) ( * buffer ) );
	Header.chunk_size = * ( ( unsigned int * ) ( ( * buffer ) + 2 ) );
( * buffer ) += 6;
i += 6; return( Header ); }
Обработка файла начинается с нахождения чанка именованного объекта (в этой функции ничего дельного не считывается):
void	cModelLoader::ProcessObjectChunk( unsigned char **buffer , int &i ){
	char	ObjectName[ 21 ];


	int	type( 0 );
	cChunkHeader Header( GetChunkHeader( buffer , i ) );
	//имя
memset( ObjectName , '/0' , 20 );
memcpy( ObjectName , *buffer , 19 );

( * buffer ) += strlen( ObjectName ) + 1;
i += strlen( ObjectName ) + 1; //тип
type = * ( ( unsigned short int * ) ( * buffer ) );

( * buffer ) += 6;
i += 6;
}
Потом читаем вершины:
void	cModelLoader::ProcessVertexChunk( unsigned char ** buffer , 
int &i ){

cArray&ltcVector3> XYZArray;

cChunkHeader Header( GetChunkHeader( buffer , i ) );

XYZArray.SetLength( * ( ( unsigned short int * ) ( * buffer ) ) );

( * buffer ) += 2;
i += 2;

memcpy( XYZArray.Array , *buffer , XYZArray.Length * sizeof( cVector3 ) );

Meshes.AddEnd( NULL );
Meshes[ Meshes.Cursor - 1 ].CreateMesh();
Meshes[ Meshes.Cursor - 1 ].AddVertexes( XYZArray );

XYZArray.Release();

( * buffer ) += Header.chunk_size - 8;
i += Header.chunk_size - 8;
}
В этой функции сначала читаем заголовок чанка, затем переписываем в заранее созданный буфер вершины (вы мне сразу же укажите на лишние операции выделения/копирования/удаления памяти... ну что ж, оптимизируйте, если очень хочется). Для большего понимания этой функции рассмотрим код класса cD3Dmesh:
class	cD3DMesh{
	int				MaterialCursor;
	cD3DIVertexBuffer<cD3DVertex>	*VertexBuffer;
public:
	cD3DMesh( float ){VertexBuffer=NULL;MaterialCursor=-1;}
	cD3DMesh( void ){VertexBuffer=NULL;MaterialCursor=-1;}


	void	CreateMesh( void ){VertexBuffer = 
			new cD3DIVertexBuffer&ltcD3DVertex>MaterialCursor=0;}

	void	Release( void ){VertexBuffer->Release();
			delete [] VertexBuffer;MaterialCursor=0;}

	__forceinline void	CreateBuffers( LPDIRECT3DDEVICE8 );

	void	AddVertexes( cArray <cVector3> & );
	void	AddIndexes( cArray <cFace> & );
	void	AddTexCoords( cArray<float> & );

__forceinline void Render( LPDIRECT3DDEVICE8 ); bool vb_null( void ){return( VertexBuffer == NULL );} int GetMaterial( void ){return( MaterialCursor );} };
Комментариев заслуживает только одно поле этого класса – MaterialCursor. Дело в том, что все материалы хранятся в массиве Materials класса cModelLoader. Так вот, MaterialCursor это курсор на элемент этого массива. Пользуясь случаем хочу рассказать (или даже покаяться) о том, что наш загрузчик может справиться со сколь угодно большим числом сеток в файле, но работать может только с ОДНИМ материалом... Мне не удалось найти в созданных мною файлах 3ds чанка с magic’ом 0x4130 – именно он отвечает за хранение информации о том какой материал, к какой сетке применяется. Вот.
void	cD3DMesh::AddVertexes( cArray< cVector3 > & vs ){
if( !VertexBuffer ){
MessageBox( 0 , "Память для вершинного буффера не была выделена! \
cD3DMesh::AddVertexes " , "Ошибка" , 0 );
exit( 0 );
}

VertexBuffer-&gtVertexes.SetLength( vs.Length );
VertexBuffer-&gtVertexes.Cursor = 0;

for( int i( 0 ) ; i < vs.Length ; i++ )
VertexBuffer-&gtAddVertex( vs[ i ] );

VertexBuffer-&gtVertexes.Cursor = vs.Length;
}
Теперь приступим к загрузке текстурных координат:
void	cModelLoader::ProcessTexCoordsChunk( unsigned char **buffer , int &i ){
cChunkHeader Header( GetChunkHeader( buffer , i ) );

//количество текстурных координат
int tc_number( * ( ( unsigned short int * )( * buffer ) ) );

cArray<float> TCArray;
TCArray.SetLength( 2 * tc_number );

( * buffer ) += 2;
i += 2;

memcpy( TCArray.Array , *buffer , 2 * tc_number * sizeof( float ) );

Meshes[ Meshes.Cursor - 1 ].AddTexCoords( TCArray );

TCArray.Release();

( * buffer ) += Header.chunk_size - 8;
i += Header.chunk_size - 8;
}

void cD3DMesh::AddTexCoords( cArray<float> &TCArray ){
if( !VertexBuffer ){
MessageBox( 0 , "Память для вершинного буффера не была выделена! \
cD3DMesh::AddVertexes " , "Ошибка" , 0 );
exit( 0 );
}
for( int i( 0 ) ; i < TCArray.Length / 2 ; i++ ){
VertexBuffer-&gtVertexes[ i ].u = TCArray[ 2 * i + 0 ];
VertexBuffer-&gtVertexes[ i ].v = TCArray[ 2 * i + 1 ];
}
}
Тут тоже вроде бы все стандартно. Чтение информации о материале:
void	cModelLoader::ProcessTextureMapChunk( LPDIRECT3DDEVICE8 D3DDevice , 
unsigned char **buffer , int &i ){
cChunkHeader Header( GetChunkHeader( buffer , i ) );

char TextureName[1000];

*buffer += 14;
i += 14;

memset( TextureName , '/0' , 20 );
memcpy( TextureName , *buffer , 19 );

Materials.AddEnd( NULL );
Materials[ Materials.Cursor - 1 ].CreateMaterial( D3DDevice , TextureName );

( * buffer ) += Header.chunk_size - 20;
i += Header.chunk_size - 20;
}
Приступаем к чтению индексов:
void	cModelLoader::ProcessFaceChunk( LPDIRECT3DDEVICE8 D3DDevice , 
unsigned char ** buffer , int &i ){
cArray<cFace> FArray;

cChunkHeader Header( GetChunkHeader( buffer , i ) );

FArray.SetLength( * ( ( unsigned short int * ) ( * buffer ) ) );

( * buffer ) += 2;
i += 2;

memcpy( FArray.Array , *buffer , FArray.Length * sizeof( cFace ) );

Meshes[ Meshes.Cursor - 1 ].AddIndexes( FArray );
Meshes[ Meshes.Cursor - 1 ].CreateBuffers( D3DDevice );
FArray.Release();

( * buffer ) += Header.chunk_size - 8;
i += Header.chunk_size - 8;
}

void cD3DMesh::AddIndexes( cArray&ltcFace> &is ){
if( !VertexBuffer ){
MessageBox( 0 , "Память для вершинного буффера не была выделена! \
cD3DMesh::AddVertexes " , "Ошибка" , 0 );
exit( 0 );
}

VertexBuffer-&gtIndexes.SetLength( is.Length );
VertexBuffer-&gtIndexes.Cursor = 0;

for( int i( 0 ) ; i < is.Length ; i++ ){
VertexBuffer-&gtIndexes.AddEnd( is[ i ].c1 );
VertexBuffer-&gtIndexes.AddEnd( is[ i ].c2 );
VertexBuffer-&gtIndexes.AddEnd( is[ i ].c3 );
}

VertexBuffer-&gtIndexes.Cursor = 3 * is.Length;
}
Эта функция вызывается последней, поэтому для сетки дополнительно вызывается CreateBuffers() – в которой создаются вершинные буферы.

Завершение создания лоадера.

к началу статьи
Теперь всю загруженную красоту (я надеюсь) неплохо бы отрендеррить:
void	cModelLoader::Render( LPDIRECT3DDEVICE8 D3DDevice ){
	for( int i( 0 ) ; i < Meshes.Cursor ; i++ ){
		Materials[ Meshes[ i ].GetMaterial() ].SetMaterial( D3DDevice );

		Meshes[ i ].Render( D3DDevice );

		Materials[ Meshes[ i ].GetMaterial() ].ResetMaterial( D3DDevice );
	}
}
А затем удалить:
void	cModelLoader::Release( void ){
	for( int i( 0 ) ; i < Meshes.Cursor ; i++ )
		Meshes[ i ].Release();
	Meshes.Release();

	for( int j( 0 ) ; j < Materials.Cursor ; j++ )
		Materials[ j ].Release();
	Materials.Release();
}
На сегодня это все. Если захотите расширить функциональность, то загляните в прилагающийся к статье файл (честно говоря, не помню с какого сайта я его утащил). А если не захотите, то дождитесь статьи об основах плагиностроения для 3DS MAX. До встречи.

PS: связь как всегда по почте dodonov_a_a (__AT) inbox.ru

к началу статьи


Исходные коды.
Дополнительные материалы.
© 2004-2005 Savardge.ru
Hosted by uCoz