В первой статье я б хотел поведать о том с чего впервую очередь начинает gamedever, а именно с вопроса: "Какой выбрать формат файла, для экспорта 3d модлей в свою игру ?". К примеру 3ds max экспортирует сцену в такие форматы как 3ds, obj, wvf и т. д. ... Но перепробывав все форматы макса я понял что с ними работать начинающему gamedever-y очень сложно. В сети я нашел очень удобный и распростарненный формат Collada - его простота в первую очередь вызвана тем что этот формат хранит данные по 3d модели в текстовом варианте - в XML.
Plugin Collada для 3ds Max 2009 32 bit + Спецификация Collada (ENG) можете скачать здесь.
Итак: сегодня я расскажу как удобней и проще прочитать файл collada, и приведу примеры своего кода.
Collada Parser:
Как сделать Collada Parser своими руками ? Какой алгоритм самый оптимальный ? Что представляет собой файл Collada *.dae ?
Вот о чем сейчас я хочу более подробно рассказать.
Конечно же, уже существуют такие парсеры как Collada DOM и т.д., но покопавшись в мануале этого парсера я понял, что нужно убить как минимум 5-6 дней что бы понять как работает тот же DOM. В связи с этим решил написать свой парсер конечно же менее функциональный, но зато он экспортирует те данные, которые мне действительно неободимы, а не все то что выплёвывает 3ds max при экспорте в Colllada.
Однако я не стану объяснять как и каким образом хранятся данные в файле коллада(в этом вам поможет спецификация), как храниться та же скелетная анимация, что за матрицы предоставляет нам формат Collada и как их перемножать на вершины… всё это выходит за рамки данной статьи, я обязательно постараюсь обо всем этом написать в следующих статьях ! Однако в этой статье пойдет речь именно о формате Collada и о том, как проще и оптимальней с него считать данные и использовать в работе.
Я упомянул о том, что мой двиг имеет возможность экспортирования данных из файла *.dae (Collada) – это и был парсер встроенный в мой двиг. Однако всё дальше углубляясь в программирование своей игры, я понял, что наличие данной возможности в двиге игры совсем не обязательно, и даже неудобно. Хотя на начальных стадиях разработки движка было очень удобно вносить корректировки и добавлять новые функции для моего парсера, но чем дальше тем больше он начал жрать памяти, так как появлялись данные, которые я просто боялся удалить ! А вдруг чего-нить перестанет работать и тогда будет ой как сложно найти дырку в коде, да и ещё её и залатать. :(
Что бы этого избежать я советую готовиться к тому, что Вам все-таки придется перенести Ваш парсер в отдельный софт, который парсит файл *.dae и сохраняет их в Вашем формате файла. Который, уже потом читает Ваш двиг и использует в работе.
Collada *.dae - это открытый стандарт файлов для интерактивных 3D приложений базирующийся на формате XML.
Из этих слов, важными для нас являются «базирующийся на формате XML».
Что такое XML - (англ. eXtensible Markup Language — расширяемый язык разметки; произносится [экс-эм-э́л]) — рекомендованный Консорциумом Всемирной паутины язык разметки, фактически представляющий собой свод общих синтаксических правил. XML — текстовый формат, предназначенный для хранения структурированных данных (взамен существующих файлов баз данных), для обмена информацией между программами, а также для создания на его основе более специализированных языков разметки (например, XHTML), иногда называемых словарями. XML является упрощённым подмножеством языка SGML. (Wikipedia.ru)
Кому нравиться так, а по мне так Collada *.dae – Это текстовый файл, легко читаемый (ибо текстовый :) ), состоящий в виде иерархии, где имеються родительские блоки родственные блоки и блоки дети, которые в свою очередь имеют родителей, детей, родственников и т. д. (более подробно см. далее). Блоки имеют заголовок (хидер) и конец к примеру главный и самый корневой блок Collada, который начинается с заголовка (хидера) <Collada> и конца </Collada>. Я думаю комментарии излишне, сразу же видно в чем разница между заголовком и концом. Однако очень часто в заголовке содержаться дополнительные поля, в которых описано, что храняться между началом и концом данного блока.
Вот такой вид у файла Collada *.dae
- <?xml version="1.0" encoding="utf-8"?>
- <COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
- <asset>
- <contributor>
- <author>%CC%E0%F0%E0%F2</author>
- <authoring_tool>3dsMax 9 - Feeling …</authoring_tool>
- <comments>ColladaMax Export Options…;</comments>
- <source_data>file:///E:/My%...</source_data>
- </contributor>
- <created>2010-09-12T13:44:09Z</created>
- <modified>2010-09-12T13:44:10Z</modified>
- <unit meter="1" name="meter"/>
- <up_axis>Z_UP</up_axis>
- </asset>
- <library_animations>
- <animation id="Bip01_L_UpperArm-node-transform">
- <... и т.д.>
- </library_animations>
- </COLLADA>
До конца статьи я буду называть все блоки именно родителями, детми, и родственниками. :)
Так же из выше изложенного кода мы видим, что в файле Collada помимо стандартных блоков, присутствует блок <unit> который является одинарным ! так как у него отсутствует конец блока. Это нужно будет учесть при программировании нашего парсера.
Определения некторых терминов используемых мною в статье:
Пример: <СOLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
- «Хидер» - это полностью выше указанная строка.
- «Имя хидера» - это слово COLLADA в выше указанной строке.
- «Поля блока» - это xmlns="…" или version="…".
- «Блок» - это информация между хидером <Collada> и концом </Collada>
Итак, этого достаточно, что бы приступить к программированию нашего парсера Collada.
Алгоритм:
Как мы уже видели файл состоит из блоков родителей детей и родственников, поэтому самый оптимальный вариант для считывания файла это "рекурсия". Выглядит это примерно так: мы взываем функцию CreateHierarhy(*block) коротая вначале записывает данные блока *block который мы передаем в параметрах, потом проверяет на наличие детей, если есть то вызывает саму себя CreateHierarhy(*block_child), если нет то проверяет наличие родственников и если есть то считывает их CreateHierarhy(*block_sibling), если нет то возвраещается на более высшую ступень вызывая return.
Всё !
Но это если вкратце. А если детально то смотрим код и читаем комментарии:
Открываем файл *.dae:
-
string buf;//глобальный массив данных !!!
-
string FileName=”SomeAlseFile.dae”;
-
/*Получаем размер файла в байтах*/
-
{MessageBox(0,"Ошибка открытия файла 1!",0,0); return false;}
-
/*-------------------------------*/
-
/*Читаем файл*/
-
{MessageBox(0,"Ошибка открытия файла 2!",0,0); return false;}
-
char* Buffer;
-
Buffer=new char[eof];
-
buf=string(Buffer);
-
delete Buffer;
Немного странное нахождение конца файла, однако у меня всё работает. так как UTF-8 - это 1 символ - 1 байт. Так же хочу отметить, что я работаю с std::string так как это очень удобно, и практично, хоть и занимает больше процессорного времени. Но скорость здесь нам особой помехи не представляет мы же не рендерим ? :)
Выше мы открываем файл и считываем всё, что там есть, в память, или в переменную типа string “buf” - это глобальный массив, в котором будут храниться все символьные данные файла.
Далее нам необходима функция поиска символа или слова в buf. В библиотеке std есть функция поиска в массиве string но всё же я предпочел немного её откорректировать. Для нас так удобней.
-
int ParseDae2::find(string str,int point_of_start,int point_of_end)
-
{
-
int i=buf.find(str,point_of_start);//::std функция поиска в string-e
-
if (i<0 || i>point_of_end)
-
{
-
return 0;
-
}
-
else
-
{
-
return i;
-
}
-
}
Функция в параметрах принимает строку/символ который необходимо найти, номер символа с которого нужно начать поиск, номер символа на котором нужно остановить поиск (эти номера символов в массиве buf). А возвращает номер символа в массиве buf с которого, начинается искомое слово.
Пример: "Мамы мыла раму" - find("мыла", 0, 12); - вернет 6, искать будет с "М" до "м".
Дальше нам необходимо каждый блок в файле представить в виде структуры данных. Что бы потом с ней можно было работать.
Назовем эту структуру - Block:
-
//Это моя структура блока на сегодняшний день
-
//в будущем, я думаю, она будет побольше, но на данный
-
//момент это все поля, которые мне необходимы
-
struct Block
-
{
-
//Точка начала данных блока,
-
//или позиция указателя на начало данных
-
int point_start_data;
-
//Точка конца блока, именно БЛОКА !,
-
//или позиция указателя на конец блока
-
int point_end_block;
-
bool IsEndble;//Флаг описывающий блок (конечный/одинарный) это такой </block> или такой </>
-
//…
-
//Поля блока:
-
string texture; //поле - Текстура..
-
string Name;//поле - Имя
-
string id;//поле – id
-
string sid;
-
string material;// материал и т.д. …
-
//…
-
//Указатели
-
Block* bSibling; //Указатель на родственника
-
Block* bChild; //Указатель на дитя
-
Block* bParent; //Указатель на родителя
-
};
При считывании блока, функция описание, которой я приведу ниже, будет заполнять поля выше указанной структуры в зависимости от их (полей) наличия.
Следующая функция самая главная – findblock(); Функция не ищет какой-то определенный блок в файле ! Она находит/инициализирует первый попавшийся ей по пути блок. В параметрах данной функции мы передаем позицию начала поиска и конца поиска (позиции массива buf). На основании этих параметров функция находит первый блок в данном диапазоне буфера данных buf, и заполняет все поля данных выше указанной структуры.
При написании это функции нужно учесть все подводные камни это:
- То что в блок могут входить одноименные блоки, т.е. с тем же именем хидера только внутри искомого.
- Блоки бывают 2 видов "одинарные" и "конеченые".
-
Block* findblock (int pOfStart,int pOfEnd)
-
{
-
int s4=1;//счетчик
-
int Num;//счетчик кол-ва
-
Block* block;//объявим блок
-
block= new Block();//выделим память
-
//Начнем с того что всё путем
-
block->find=true;
-
string header;
-
//Получим header
-
int tmp=find("<",pOfStart,pOfEnd);
-
if (tmp==0) {block->find=false; return block;}
-
//Если найдено то переместим курсор с “<” на первый
-
//символ header-a
-
tmp++;
-
//Получим количество символов в header-e
-
Num=GetNumSimbols(tmp);//Возвращает кол-во символов (опис. см. ниже)
-
bool t=true;
-
tmp=pOfStart;
-
int posOfStartHeader=tmp--;
-
if (posOfStartHeader==0) {block->find=false;return block;}
-
//Перемещаем указатель на символ после header-a.
-
//Найдем конец header-a
-
int posOfEndHeader=find(">",posOfStartHeader+1,pOfEnd);
-
//проверка
-
if (posOfEndHeader==0)
-
{
-
MessageBox(0,"posOfEndHeader=0","function findblock2",0);
-
block->find=false; return block;
-
}
-
//Получим point_start_data т.е. позицию указателя
-
//за “>”-концом header-a, а так же проверим «одинарный» ли
-
//блок или «конечный» ?
-
if (buf[posOfEndHeader-1]=='/')
-
{
-
block->point_start_data=posOfEndHeader;
-
block->IsEndble=false;//одинарный
-
}
-
else
-
{
-
block->point_start_data=posOfEndHeader+1;
-
block->IsEndble=true;//конечный
-
}
-
//Получим id
-
{block->id="NULL";}
-
else
-
//Получим sid
-
{block->sid="NULL";}
-
else
-
//Получим material
-
{block->material="NULL";}
-
else
-
//Получим point_end_block – позицию указателя
-
//на конец блока. По правде говоря тут по сложнее
-
//так как в блок может входить не определенное
-
//кол-во одинаковых блоков ! к примеру такие блоки
-
//как <asset> и т.д. я реализовал обход
-
//даннной проблемы так:
-
if (block->IsEndble) //если блок закан-ся на </sample> то тогда:
-
{
-
int NumStarts;//количество «стартов» :)
-
int endpos;//позиция конца
-
string StartBlock;//Имя хидера начального блока
-
//Следующая функция находит количество блоков с
-
//одноименным названием, которые входят в этот
-
//блок. Функция возвращает значение только дойдя
-
//до конца заданного диапозоноа.
-
//Получаем кол-во «стартов»
-
//Если есть одноименные блоки
-
if (NumStarts!=0)
-
{
-
while (NumStarts!=0)//Запускаем цикл
-
{
-
//уменьшаем кол-во стартов (блока)
-
NumStarts--;
-
//Ищем конец блока
-
//Проверяем были ли старты ? и
-
//прибовляем к общему количеству
-
//устанавливаем pos в позицию
-
//за последним найденным концом блока
-
}
-
}
-
//В конце концов присваем позицию конца блока.
-
}
-
else //Иначе если блок одниарный:
-
{
-
block->point_end_block=posOfEndHeader;
-
}
-
block->find=true;
-
return block;
-
}
В функции выше, встречаются функции getNumBlocks(),GetNumSimbols(),и GetStr() - описание этих функций смотрим ниже:
-
//Функция возвращает количество блоков с хидером NameBlock
-
//в диапозоне с pOfStart до pOfEnd.
-
int getNumBlocks(string NameBlock,int pOfStart,int pOfEnd)
-
{
-
int flag=1;
-
while (flag)
-
{
-
if (pos>0) {NumBlocks++;}
-
else
-
{
-
flag=0;
-
break;
-
}
-
}
-
return NumBlocks;
-
}
-
//Функция возвращает количество символов
-
//с позиции pos, до пробела, и знаков
-
//больше или равно см ниже.
-
int GetNumSimbols(int i)
-
{
-
int Num=0, t=1;
-
while(t==1)
-
{
-
if (buf[i]==' ' || buf[i] == '<' || buf[i] == '>')
-
{
-
t=0;
-
break;
-
}
-
i++;
-
Num++;
-
}
-
if (t!=0) {return 0;}
-
return Num;
-
}
-
//Функция возвращает строку типа string
-
//из нашего глобального буфера buf.
-
//С позиции “i”.
-
string GetStr(int i,int NumSimbols)
-
{
-
string str;
-
for (int a=0;a<NumSimbols;a++)
-
{
-
str.push_back(buf[i]);
-
i++;
-
}
-
return str;
-
}
Теперь когда мы имеем функцию findblock нам осталось только создать рекурсивную функцию парсинга нашего файла. Так же в этой функции надо будет установить наши указатели родителя, дитя и соседа.
Рекурсивная функция парсинга Collada:
Описание:
В параметре функция принимает указатель на структуру Block (*mb), точку начала данных этого блока, и точку конца даннах этого блока. Далее см. ниже:
-
void CreateHierarchy(ParseDae2::Block* mb,int pOfStart,int pOfEnd)
-
{
-
//Вызываем поиск блока дитя для mb
-
mb->bChild=findblock(mb->point_start_data,mb->point_end_block);
-
if (mb->bChild->find) //если найдено то рекурсивно вызываем функцию
-
{
-
CreateHierarchy(mb->bChild,mb->bChild->point_start_data,mb->point_end_block);
-
}
-
else //Иначе блок «дитя» не существует.
-
{mb->bChild=NULL;}
-
//Определим точку конца блока:
-
int pointostart;
-
//Если блок «конечный» то точка поиск родителя надо начинать
-
//с point_end_block+длина имени хидреа+3 символа (это “/”,”<”,”>”)
-
//проще говоря point_end_block указывает на первый символ, конца блока:
-
//</COLLADA> то point_end_block указывает на символ “<”.
-
//Надеюсь теперь понятней. :)
-
if (mb->IsEndble) {pointostart=mb->point_end_block+mb->Header.length()+3;}
-
//Иначе если он «одинарный» то:
-
else {pointostart=mb->point_end_block+1;}
-
//А теперь вызовем поиск блока родственника
-
mb->bSibling=findblock(pointostart,pOfEnd);
-
//и то же самое что и с дитем:
-
if (mb->bSibling->find) //если найдено то рекурсивно вызываем функцию
-
{
-
CreateHierarchy(mb->bSibling,mb->bSibling->point_start_data,pOfEnd);
-
}
-
else //Иначе блок «родственника» не существует.
-
{
-
mb->bSibling=NULL;
-
return;//Возвращем.
-
}
-
}
Вот вобщемто и всё ! Все основные функции готовы осталось только, найти главный блок Collada и вызвать функцию CreateHierarhy(), передав в параметре этот блок. После чего функция рекурсивно переберет весь файл, и разложит его пополочкам, в виде древа: дитя сосед родитель.
Опять вернемся к началу:
-
//-------------------------------------это вы видели в начале-----
-
string buf;//глобальный массив данных !!!
-
string FileName=”SomeAlseFile.dae”;
-
/*Получаем размер файла в байтах*/
-
{MessageBox(0,"Ошибка открытия файла 1!",0,0); return false;}
-
/*-------------------------------*/
-
/*Читаем файл*/
-
{MessageBox(0,"Ошибка открытия файла 2!",0,0); return false;}
-
char* Buffer;
-
Buffer=new char[eof];
-
buf=string(Buffer);
-
delete Buffer;
-
//-------------------------------------это вы видели в начале-----
-
//далее..
-
Block *bCollada;
-
bCollada=new Block();
-
bCollada->point_start_data=find(“<Collada>”,0, eof);
-
CreateHierarchy(bCollada,bCollada->point_start_data,eof);
Дальше - больше к примеру вы можете создать функцию поиска в только, что созданном древе, блока по имени хидера:
-
{
-
Block* bl;
-
{return mb;}
-
if (mb->bChild!=NULL)
-
{
-
if (bl!=NULL) {return bl;}
-
}
-
if (mb->bSibling!=NULL)
-
{
-
if (bl!=NULL) {return bl;}
-
}
-
return NULL;
-
}
Или тоже самое, но по id блока и т. д.
Вот теперь, я думаю, действительно ВСЁ ! Надеюсь мои старанья в написании этой статьи действительно окажутся для Вас продуктивными.
Копипаст тут никак не поможет. Дело в том что этой статей я хотел поведать читателю более оптимальный вариант для реализации Parsera.
Спасибо за то, что зашли ;)
С Уважением m21448 !