Создание 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<cVector3> 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<cD3DVertex>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->Vertexes.SetLength( vs.Length ); VertexBuffer->Vertexes.Cursor = 0;
for( int i( 0 ) ; i < vs.Length ; i++ ) VertexBuffer->AddVertex( vs[ i ] );
VertexBuffer->Vertexes.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->Vertexes[ i ].u = TCArray[ 2 * i + 0 ]; VertexBuffer->Vertexes[ 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<cFace> &is ){ if( !VertexBuffer ){ MessageBox( 0 , "Память для вершинного буффера не была выделена! \ cD3DMesh::AddVertexes " , "Ошибка" , 0 ); exit( 0 ); }
VertexBuffer->Indexes.SetLength( is.Length ); VertexBuffer->Indexes.Cursor = 0;
for( int i( 0 ) ; i < is.Length ; i++ ){ VertexBuffer->Indexes.AddEnd( is[ i ].c1 ); VertexBuffer->Indexes.AddEnd( is[ i ].c2 ); VertexBuffer->Indexes.AddEnd( is[ i ].c3 ); }
VertexBuffer->Indexes.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
к началу статьи
Исходные
коды.
Дополнительные
материалы. |