Collada Parser своими руками.

KazGameDever 2011 M03 26
5139
3
6
0

В первой статье я б хотел поведать о том с чего впервую очередь начинает gamedever, а именно с вопроса: "Какой выбрать формат файла, для экспорта 3d модлей в свою игру ?". К примеру 3ds max...

В первой статье я б хотел поведать о том с чего впервую очередь начинает 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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
  3. <asset>
  4. <contributor>
  5. <author>%CC%E0%F0%E0%F2</author>
  6. <authoring_tool>3dsMax 9 - Feeling …</authoring_tool>
  7. <comments>ColladaMax Export Options…;</comments>
  8. <source_data>file:///E:/My%...</source_data>
  9. </contributor>
  10. <created>2010-09-12T13:44:09Z</created>
  11. <modified>2010-09-12T13:44:10Z</modified>
  12. <unit meter="1" name="meter"/>
  13. <up_axis>Z_UP</up_axis>
  14. </asset>
  15. <library_animations>
  16. <animation id="Bip01_L_UpperArm-node-transform">
  17. <... и т.д.>
  18. </library_animations>
  19.  
  20. </COLLADA>
В самом начале файла идет заголовок <хml> , который гласит о версии файла, и его кодировке, далее начинается иерархия "блоков данных" файла Collada. Самый главный и самый корневой блок это <СOLLADA> , он является основным и не имеет не родителей не родных, только детей, а именно блок <аsset> , и блок <librаry_аnimations> . Которые в свою очередь имеют одинакового родителя это блок <СOLLADA> , они являются родственниками, и имеют своих детей. Так к примеру блок <аsset> имеет детей: <contributor>, <created>, <modified>, <unit>, <up_axis>.

До конца статьи я буду называть все блоки именно родителями, детми, и родственниками. :)

Так же из выше изложенного кода мы видим, что в файле Collada помимо стандартных блоков, присутствует блок <unit> который является одинарным ! так как у него отсутствует конец блока. Это нужно будет учесть при программировании нашего парсера.

Определения некторых терминов используемых мною в статье:

Пример: <СOLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">

  1. «Хидер» - это полностью выше указанная строка.
  2. «Имя хидера» - это слово COLLADA в выше указанной строке.
  3. «Поля блока» - это xmlns="…" или version="…".
  4. «Блок» - это информация между хидером <Collada> и концом </Collada>

Итак, этого достаточно, что бы приступить к программированию нашего парсера Collada.

Алгоритм:

Как мы уже видели файл состоит из блоков родителей детей и родственников, поэтому самый оптимальный вариант для считывания файла это "рекурсия". Выглядит это примерно так: мы взываем функцию CreateHierarhy(*block) коротая вначале записывает данные блока *block который мы передаем в параметрах, потом проверяет на наличие детей, если есть то вызывает саму себя CreateHierarhy(*block_child), если нет то проверяет наличие родственников и если есть то считывает их CreateHierarhy(*block_sibling), если нет то возвраещается на более высшую ступень вызывая return.

Всё ! 
Но это если вкратце. А если детально то смотрим код и читаем комментарии:

Открываем файл *.dae:

  1. string buf;//глобальный массив данных !!!
  2. string FileName=”SomeAlseFile.dae”;
  3. /*Получаем размер файла в байтах*/
  4. FILE *an;
  5. if (!(an=fopen(FileName.c_str(),"rb")))
  6. {MessageBox(0,"Ошибка открытия файла 1!",0,0); return false;}
  7. fseek(an,0,SEEK_END);
  8. eof=ftell(an); //Найдем конец файла
  9. fclose(an);
  10. /*-------------------------------*/
  11. /*Читаем файл*/
  12. FILE *in;
  13. if (!(in=fopen(FileName.c_str(),"r")))
  14. {MessageBox(0,"Ошибка открытия файла 2!",0,0); return false;}
  15. char* Buffer;
  16. Buffer=new char[eof];
  17. fread(Buffer,eof,1,in);//Считывам в буффер все данные файла.
  18. buf=string(Buffer);
  19. fclose(in);
  20. delete Buffer;

Немного странное нахождение конца файла, однако у меня всё работает. так как UTF-8 - это 1 символ - 1 байт. Так же хочу отметить, что я работаю с std::string так как это очень удобно, и практично, хоть и занимает больше процессорного времени. Но скорость здесь нам особой помехи не представляет мы же не рендерим ? :)

Выше мы открываем файл и считываем всё, что там есть, в память, или в переменную типа string “buf” - это глобальный массив, в котором будут храниться все символьные данные файла.

Далее нам необходима функция поиска символа или слова в buf. В библиотеке std есть функция поиска в массиве string но всё же я предпочел немного её откорректировать. Для нас так удобней.

  1. int ParseDae2::find(string str,int point_of_start,int point_of_end)
  2. {
  3. int i=buf.find(str,point_of_start);//::std функция поиска в string-e
  4. if (i<0 || i>point_of_end)
  5. {
  6. return 0;
  7. }
  8. else
  9. {
  10. return i;
  11. }
  12. }

Функция в параметрах принимает строку/символ который необходимо найти, номер символа с которого нужно начать поиск, номер символа на котором нужно остановить поиск (эти номера символов в массиве buf). А возвращает номер символа в массиве buf с которого, начинается искомое слово.

Пример: "Мамы мыла раму"  - find("мыла", 0, 12); - вернет 6, искать будет с "М" до "м".

Дальше нам необходимо каждый блок в файле представить в виде структуры данных. Что бы потом с ней можно было работать.

Назовем эту структуру - Block:

  1. //Это моя структура блока на сегодняшний день
  2. //в будущем, я думаю, она будет побольше, но на данный
  3. //момент это все поля, которые мне необходимы
  4. struct Block
  5. {
  6. //Точка начала данных блока,
  7. //или позиция указателя на начало данных
  8. int point_start_data;
  9. //Точка конца блока, именно БЛОКА !,
  10. //или позиция указателя на конец блока
  11. int point_end_block;
  12. bool IsEndble;//Флаг описывающий блок (конечный/одинарный) это такой </block> или такой </>
  13. //…
  14. //Поля блока:
  15. string Header; //Хидер блока или его название к примеру <Collada>
  16. string texture; //поле - Текстура..
  17. string Name;//поле - Имя
  18. string id;//поле – id
  19. string sid;
  20. string material;// материал и т.д. …
  21. //…
  22. //Указатели
  23. Block* bSibling; //Указатель на родственника
  24. Block* bChild; //Указатель на дитя
  25. Block* bParent; //Указатель на родителя
  26. };

При считывании блока, функция описание, которой я приведу ниже, будет заполнять поля выше указанной структуры в зависимости от их (полей) наличия.

Следующая функция самая главнаяfindblock(); Функция не ищет какой-то определенный блок в файле ! Она находит/инициализирует первый попавшийся ей по пути блок. В параметрах данной функции мы передаем позицию начала поиска и конца поиска (позиции массива buf). На основании этих параметров функция находит первый блок в данном диапазоне буфера данных buf, и заполняет все поля данных выше указанной структуры.

При написании это функции нужно учесть все подводные камни это:

  1. То что в блок могут входить одноименные блоки, т.е. с тем же именем хидера только внутри искомого.
  2. Блоки бывают 2 видов "одинарные" и "конеченые".
  1. Block* findblock (int pOfStart,int pOfEnd)
  2. {
  3. int s4=1;//счетчик
  4. int Num;//счетчик кол-ва
  5. Block* block;//объявим блок
  6. block= new Block();//выделим память
  7. //Начнем с того что всё путем
  8. block->find=true;
  9. string header;
  10. //Получим header
  11. int tmp=find("<",pOfStart,pOfEnd);
  12. if (tmp==0) {block->find=false; return block;}
  13. //Если найдено то переместим курсор с “<” на первый
  14. //символ header-a
  15. tmp++;
  16. //Получим количество символов в header-e
  17. Num=GetNumSimbols(tmp);//Возвращает кол-во символов (опис. см. ниже)
  18. header=GetStr(tmp,Num);//Возвращает строку типа string (опис. см. ниже)
  19. if (header[0]=='/') {return block;}//Проверим на ошибки
  20. block->Header=header;
  21. header.insert(0,"<");
  22. bool t=true;
  23. tmp=pOfStart;
  24. int posOfStartHeader=tmp--;
  25. if (posOfStartHeader==0) {block->find=false;return block;}
  26. //Перемещаем указатель на символ после header-a.
  27. tmp=posOfStartHeader+header.length();
  28. //Найдем конец header-a
  29. int posOfEndHeader=find(">",posOfStartHeader+1,pOfEnd);
  30. //проверка
  31. if (posOfEndHeader==0)
  32. {
  33. MessageBox(0,"posOfEndHeader=0","function findblock2",0);
  34. block->find=false; return block;
  35. }
  36. //Получим point_start_data т.е. позицию указателя
  37. //за “>”-концом header-a, а так же проверим «одинарный» ли
  38. //блок или «конечный» ?
  39. if (buf[posOfEndHeader-1]=='/')
  40. {
  41. block->point_start_data=posOfEndHeader;
  42. block->IsEndble=false;//одинарный
  43. }
  44. else
  45. {
  46. block->point_start_data=posOfEndHeader+1;
  47. block->IsEndble=true;//конечный
  48. }
  49. //Получим id
  50. int pos=find("id=",posOfStartHeader,posOfEndHeader);
  51. if (pos==0)
  52. {block->id="NULL";}
  53. else
  54. {pos+=4;
  55. Num=GetNumSimbols(pos);
  56. block->id=GetStr(pos,Num-1);}//id
  57. //Получим sid
  58. pos=find("sid=",posOfStartHeader,posOfEndHeader);
  59. if (pos==0)
  60. {block->sid="NULL";}
  61. else
  62. {pos+=5;
  63. Num=GetNumSimbols(pos);
  64. block->sid=GetStr(pos,Num-1);}//sid
  65. //Получим material
  66. pos=find("material=",posOfStartHeader,posOfEndHeader);
  67. if (pos==0)
  68. {block->material="NULL";}
  69. else
  70. {pos+=10;
  71. Num=GetNumSimbols(pos);
  72. block->material=GetStr(pos,Num-1).c_str();} //material
  73. //Получим point_end_block – позицию указателя
  74. //на конец блока. По правде говоря тут по сложнее
  75. //так как в блок может входить не определенное
  76. //кол-во одинаковых блоков ! к примеру такие блоки
  77. //как <asset> и т.д. я реализовал обход
  78. //даннной проблемы так:
  79. if (block->IsEndble) //если блок закан-ся на </sample> то тогда:
  80. {
  81. int NumStarts;//количество «стартов» :)
  82. int endpos;//позиция конца
  83. string StartBlock;//Имя хидера начального блока
  84. StartBlock=header;//Запомним наш хидер
  85. header.insert(1,"/");//Доб 2 символ
  86. header.append(">");//Доб посл символ
  87. pos=find(header,block->point_start_data,pOfEnd);
  88. //Следующая функция находит количество блоков с
  89. //одноименным названием, которые входят в этот
  90. //блок. Функция возвращает значение только дойдя
  91. //до конца заданного диапозоноа.
  92. //Получаем кол-во «стартов»
  93. NumStarts=getNumBlocks(StartBlock,block->point_start_data,pos);
  94. pos+=header.length()-1;
  95. //Если есть одноименные блоки
  96. if (NumStarts!=0)
  97. {
  98. while (NumStarts!=0)//Запускаем цикл
  99. {
  100. //уменьшаем кол-во стартов (блока)
  101. NumStarts--;
  102. //Ищем конец блока
  103. endpos=find(header,pos,pOfEnd);
  104. //Проверяем были ли старты ? и
  105. //прибовляем к общему количеству
  106. NumStarts+=getNumBlocks(StartBlock,pos,endpos);
  107. //устанавливаем pos в позицию
  108. //за последним найденным концом блока
  109. endpos+=header.length()-1;
  110. pos=endpos;
  111. }
  112. }
  113. //В конце концов присваем позицию конца блока.
  114. block->point_end_block=pos-(header.length());
  115. }
  116. else //Иначе если блок одниарный:
  117. {
  118. block->point_end_block=posOfEndHeader;
  119. }
  120. block->find=true;
  121. return block;
  122. }

В функции выше, встречаются функции getNumBlocks(),GetNumSimbols(),и GetStr() - описание этих функций смотрим ниже:

  1. //Функция возвращает количество блоков с хидером NameBlock
  2. //в диапозоне с pOfStart до pOfEnd.
  3. int getNumBlocks(string NameBlock,int pOfStart,int pOfEnd)
  4. {
  5. int flag=1;
  6. int NumBlocks=0,pos;
  7. pos=pOfStart;
  8. while (flag)
  9. {
  10. pos=find(NameBlock.c_str(),pos,pOfEnd);
  11. if (pos>0) {NumBlocks++;}
  12. else
  13. {
  14. flag=0;
  15. break;
  16. }
  17. pos+=NameBlock.length();
  18. }
  19. return NumBlocks;
  20. }
  21. //Функция возвращает количество символов
  22. //с позиции pos, до пробела, и знаков
  23. //больше или равно см ниже.
  24. int GetNumSimbols(int i)
  25. {
  26. int Num=0, t=1;
  27. while(t==1)
  28. {
  29. if (buf[i]==' ' || buf[i] == '<' || buf[i] == '>')
  30. {
  31. t=0;
  32. break;
  33. }
  34. i++;
  35. Num++;
  36. }
  37. if (t!=0) {return 0;}
  38. return Num;
  39. }
  40. //Функция возвращает строку типа string
  41. //из нашего глобального буфера buf.
  42. //С позиции “i”.
  43. string GetStr(int i,int NumSimbols)
  44. {
  45. string str;
  46. for (int a=0;a<NumSimbols;a++)
  47. {
  48. str.push_back(buf[i]);
  49. i++;
  50. }
  51. return str;
  52. }

Теперь когда мы имеем функцию findblock нам осталось только создать рекурсивную функцию парсинга нашего файла. Так же в этой функции надо будет установить наши указатели родителя, дитя и соседа.

Рекурсивная функция парсинга Collada:

Описание: 
В параметре функция принимает указатель на структуру Block (*mb), точку начала данных этого блока, и точку конца даннах этого блока. Далее см. ниже:

  1. void CreateHierarchy(ParseDae2::Block* mb,int pOfStart,int pOfEnd)
  2. {
  3. //Вызываем поиск блока дитя для mb
  4. mb->bChild=findblock(mb->point_start_data,mb->point_end_block);
  5. if (mb->bChild->find) //если найдено то рекурсивно вызываем функцию
  6. {
  7. CreateHierarchy(mb->bChild,mb->bChild->point_start_data,mb->point_end_block);
  8. }
  9. else //Иначе блок «дитя» не существует.
  10. {mb->bChild=NULL;}
  11. //Определим точку конца блока:
  12. int pointostart;
  13. //Если блок «конечный» то точка поиск родителя надо начинать
  14. //с point_end_block+длина имени хидреа+3 символа (это “/”,”<”,”>”)
  15. //проще говоря point_end_block указывает на первый символ, конца блока:
  16. //</COLLADA> то point_end_block указывает на символ “<”.
  17. //Надеюсь теперь понятней. :)
  18. if (mb->IsEndble) {pointostart=mb->point_end_block+mb->Header.length()+3;}
  19. //Иначе если он «одинарный» то:
  20. else {pointostart=mb->point_end_block+1;}
  21. //А теперь вызовем поиск блока родственника
  22. mb->bSibling=findblock(pointostart,pOfEnd);
  23. //и то же самое что и с дитем:
  24. if (mb->bSibling->find) //если найдено то рекурсивно вызываем функцию
  25. {
  26. CreateHierarchy(mb->bSibling,mb->bSibling->point_start_data,pOfEnd);
  27. }
  28. else //Иначе блок «родственника» не существует.
  29. {
  30. mb->bSibling=NULL;
  31. return;//Возвращем.
  32. }
  33. }

Вот вобщемто и всё ! Все основные функции готовы осталось только, найти главный блок Collada и вызвать функцию CreateHierarhy(), передав в параметре этот блок. После чего функция рекурсивно переберет весь файл, и разложит его пополочкам, в виде древа: дитя сосед родитель.

Опять вернемся к началу:

  1. //-------------------------------------это вы видели в начале-----
  2. string buf;//глобальный массив данных !!!
  3. string FileName=”SomeAlseFile.dae”;
  4. /*Получаем размер файла в байтах*/
  5. FILE *an;
  6. if (!(an=fopen(FileName.c_str(),"rb")))
  7. {MessageBox(0,"Ошибка открытия файла 1!",0,0); return false;}
  8. fseek(an,0,SEEK_END);
  9. eof=ftell(an); //Найдем конец файла
  10. fclose(an);
  11. /*-------------------------------*/
  12. /*Читаем файл*/
  13. FILE *in;
  14. if (!(in=fopen(FileName.c_str(),"r")))
  15. {MessageBox(0,"Ошибка открытия файла 2!",0,0); return false;}
  16. char* Buffer;
  17. Buffer=new char[eof];
  18. fread(Buffer,eof,1,in);//Считывам в буффер все данные файла.
  19. buf=string(Buffer);
  20. fclose(in);
  21. delete Buffer;
  22. //-------------------------------------это вы видели в начале-----
  23. //далее..
  24. Block *bCollada;
  25. bCollada=new Block();
  26. bCollada->point_start_data=find(<Collada>,0, eof);
  27. CreateHierarchy(bCollada,bCollada->point_start_data,eof);

Дальше - больше к примеру вы можете создать функцию поиска в только, что созданном древе, блока по имени хидера:

  1. Block* GetBlockH(string header, ParseDae2::Block* mb)
  2. {
  3. Block* bl;
  4. if (!strcmp(header.c_str(),mb->Header.c_str()))
  5. {return mb;}
  6. if (mb->bChild!=NULL)
  7. {
  8. bl=GetBlockH(header,mb->bChild);
  9. if (bl!=NULL) {return bl;}
  10. }
  11. if (mb->bSibling!=NULL)
  12. {
  13. bl=GetBlockH(header,mb->bSibling);
  14. if (bl!=NULL) {return bl;}
  15. }
  16. return NULL;
  17. }

Или тоже самое, но по id блока и т. д.

Вот теперь, я думаю, действительно ВСЁ ! Надеюсь мои старанья в написании этой статьи действительно окажутся для Вас продуктивными.

Копипаст тут никак не поможет. Дело в том что этой статей я хотел поведать читателю более оптимальный вариант для реализации Parsera.

Спасибо за то, что зашли ;) 
С Уважением m21448 !

     

Оцените пост

6

Комментарии

0
Совершенно верно! Идея хорошая, поддерживаю.
Signat H15
Показать комментарии