Формат файлов .x (икс). Часть первая
Дата создания: 2010-02-03 12:18:21
Последний раз редактировалось: 2012-02-08 08:33:40
Самые сложные модели, которые мы использовали в своих программах, состояли максимум из восьми вершин (кубы и пирамиды). При этом все модели создавались во время выполнения программы. Таким способом довольно трудно сделать сколько-нибудь сложную модель. Открою вам страшную тайну: во всех современных играх модели персонажей, зданий, ландшафта, создаются в специальных программах трёхмерного моделирования. Для хранения моделей используются различные форматы файлов. Сегодня мы начнём знакомство с одним из таких форматов - форматом файлов x.
Формат файлов x (читается как икс) был разработан компанией Microsoft для хранения моделей. Именно формат x используется в DirectX.
Формат x достаточно сложный. А если сравнивать с уже известным нам форматом bmp, то можно сказать, что он очень сложный. Чтобы изучение этого формата прошло максимально безболезненно, мы не будем сразу же пытаться загрузить сложные модели с текстурами и эффектами, а начнём с примитивных примеров. По моим подсчётам, по формату x будет четыре урока.
Описание формата x
Все файлы формата x начинаются с заголовка:
xof 0303txt 0032
Первые три буквы обозначают, что это файл формата x.
Далее указывается версия: старшее число и младшее. В данном случае - 3.3.
После этого указывается тип формата. В данный момент используется только текстовый (txt). Файлы x этого типа можно открыть в любом текстовом редакторе. Другие типы: двоичный (или бинарный - bin) и сжатый (cmp - от compressed).
Последнее число - количество бит в переменных вещественного типа (возможен вариант - 64).
Шаблоны в файлах .x (templates)
После заголовка определяются шаблоны. Шаблоны в файлах .x - это примерно то же самое, что и классы/структуры в C++. Т.е. в шаблонах описывается, какая информация должна храниться в экземпляре данного шаблона.
В шаблонах можно создать поля следующих типов данных:
WORD 16 бит DWORD 32 бит FLOAT Вещественное число стандарта IEEE DOUBLE 64 бита CHAR 8 бит UCHAR 8 бит BYTE 8 бит STRING Строка заканчивающаяся нулём CSTRING Форматированная C строка (не поддерживается) UNICODE Строка кодировки UNICODE (не поддерживается)
Отдельные переменные объявляются, как и в C++ - имя типа, идентификатор, точка с запятой:
DWORD variable; // объявление поля variable в шаблоне
Обратите внимание, что для комментариев используется двойная косая - //. Комментарии действуют до конца строки.
Помимо отдельных полей, в шаблонах можно определить массивы. Например, вот так будет выглядеть определение массива из 10 чисел:
array DWORD a[10];
Сначала указывается ключевое слово array. Далее, имя типа и идентификатор массива. В конце, в квадратных скобочках указывается количество элементов в массиве.
Помимо встроенных типов перечисленных выше, в определениях шаблонов можно использовать экземпляры других шаблонов. Все шаблоны имеют следующий вид:
template Идентификатор { < XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX> ; // uuid (guid) // список полей // в конце каждого объявления нужно ставить // точку с запятой }
В заголовке шаблона указывается ключевое слово template. После него пишется идентификатор (имя шаблона).
Определение шаблона располагается между фигурными скобочками.
Обязательная часть любого определения шаблона - UUID (Universally Unique Identifier - универсальный уникальный идентификатор). Иногда вместо UUID встречается название GUID (Globally Unique Identifier - глобальный уникальный идентификатор).
UUID - это 128-мибитное число. В определении шаблона UUID должен находиться в угловых скобках. Пока что мы не будем подробно обсуждать UUID/GUID, остановимся на паре моментов: у каждого шаблона свой UUID. Если вы хотите создать свой шаблон, вам понадобится программа для генерации UUID. В полных версиях Visual C++ генератор UUID уже есть. Если вы используете Express Edition, то найти генератор UUID можно в интернете.
В DirectX уже определено несколько шаблонов. Здесь я приведу определения только двух:
template Frame { < 3D82AB46-62DA-11CF-AB39-0020AF71E433 > [...] } template Mesh { <3D82AB44-62DA-11CF-AB39-0020AF71E433> DWORD nVertices; array Vector vertices[nVertices]; DWORD nFaces; array MeshFace faces[nFaces]; [...] }
В определении шаблона Frame (frame - несущая конструкция, каркас) стоит конструкция [...]. Она означает, что данный шаблон является открытым, а в его экземплярах можно использовать любые типы данных. Обычно в экземплярах Frame встречаются экземпляры трёх шаблонов: Frame, Mesh и FrameTransformMatrix. Заметьте, экземпляры Frame могут содержать другие экземпляры Frame. Это позволяет создавать сложные иерархические объекты, например, автомобиль и колёса.
Шаблон Mesh также является открытым, но в нём есть и обязательные поля:
Переменная для хранения количества вершин nVertices.
Список вершин (экземпляры шаблона Vector) в виде массива vertices.
Переменная для хранения количества граней (face - грань) nFaces.
Массив индексов faces, образующих грани в модели.
После задания в экземпляре Mesh обязательных полей, можно добавить любые свои данные. Обычно к перечисленным выше полям добавляют текстуры, нормали.
Полный список всех шаблонов определённых в DirectX можно найти в документации (и конечно же в MSDN): Direct3D → Reference → X File Reference (Legacy) → X File Templates.
После определения всех шаблонов, следуют данные трёхмерной модели.
Во всех файлах .x трёхмерные модели хранятся в экземпляре шаблона Frame. Для описания экземпляра шаблона используется идентификатор шаблона, идентификатор экземпляра (можно пропустить) и фигурные скобочки, между которыми располагаются данные. Следующие два экземпляра Frame одинаковые:
Frame RootFrame { // данные } Frame { // данные }
Во втором варианте опущен идентификатор экземпляра шаблона.
Напоследок приведу содержимое одного файла x (немного сокращённого):
xof 0303txt 0032 template VertexDuplicationIndices { <b8d65549-d7c9-4995-89cf-53a9a8b031e3> DWORD nIndices; DWORD nOriginalVertices; array DWORD indices[nIndices]; } template XSkinMeshHeader { <3cf169ce-ff7c-44ab-93c0-f78f62d172e2> WORD nMaxSkinWeightsPerVertex; WORD nMaxSkinWeightsPerFace; WORD nBones; } template SkinWeights { <6f0d123b-bad2-4167-a0d0-80224f25fabb> STRING transformNodeName; DWORD nWeights; array DWORD vertexIndices[nWeights]; array float weights[nWeights]; Matrix4x4 matrixOffset; } Frame RootFrame { FrameTransformMatrix { 1.000000,0.000000,0.000000,0.000000, 0.000000,-0.000000,-1.000000,0.000000, 0.000000,1.000000,-0.000000,0.000000, 0.000000,0.000000,0.000000,1.000000;; } Frame Cube { FrameTransformMatrix { 1.000000,0.000000,0.000000,0.000000, 0.000000,1.000000,0.000000,0.000000, 0.000000,0.000000,1.000000,0.000000, 0.000000,0.000000,0.000000,1.000000;; } Mesh { // начало Mesh 24; // поле nVertices 1.000000; 1.000000; -1.000000;, // массив vertices 1.000000; -1.000000; -1.000000;, -1.000000; -1.000000; -1.000000;, -1.000000; 1.000000; -1.000000;, // оставшиеся 20 вершин 6; // поле nFaces 4; 0, 1, 2, 3;, // массив Faces 4; 4, 5, 6, 7;, 4; 8, 9, 10, 11;, 4; 12, 13, 14, 15;, 4; 16, 17, 18, 19;, 4; 20, 21, 22, 23;; // Здесь начинается [...] MeshMaterialList { 1; 6; 0, 0, 0, 0, 0, 0;; Material Material { 0.800000; 0.800000; 0.800000;1.0;; 0.500000; 1.000000; 1.000000; 1.000000;; 0.0; 0.0; 0.0;; } //End of Material } //End of MeshMaterialList MeshNormals { 24; 0.000000; 0.000000; -1.000000;, 0.000000; 0.000000; -1.000000;, 0.000000; 0.000000; -1.000000;, 0.000000; 0.000000; -1.000000;, // Оставшиеся 20 нормалей 6; 4; 0, 1, 2, 3;, 4; 4, 5, 6, 7;, 4; 8, 9, 10, 11;, 4; 12, 13, 14, 15;, 4; 16, 17, 18, 19;, 4; 20, 21, 22, 23;; } //End of MeshNormals } } }
Я добавил немного комментариев (выделено зелёным), чтобы показать поля шаблона Mesh. Также был немного сокращён код.
Здесь в файле формата .x сохранён куб, созданный мной в программе Blender и экспортированный в формат .x.
В начале файла определены три шаблона: VertexDuplicationIndices, XSkinMeshHeader, SkinWeights. Эти шаблоны вставил экспортёр Blender. Они тут не нужны (можно удалить, ничего не изменится) так как уже определены в DirectX.
Думаю, ничего сложного здесь нет. Есть некоторые моменты, которые мы не рассмотрели (посмотрите, как задаётся массив Faces). Но мы не будем работать с файлами x напрямую, поэтому это не критично.
Последний момент, на который нужно обратить внимание: в файле модель хранится в полигональном виде (об этом ниже). Помимо этого в файле .x много избыточных данных.
Теперь осталось рассмотреть загрузку файлов .x в программу.
Загрузка файлов .x
Как видите, файлы .x довольно сложны. У нас не получится загрузить содержимое .x файла также, как мы загружали содержимое файла .bmp. Для загрузки файлов со сложной структурой используются парсеры (мы их скоро будем изучать в разделе Алгоритмы и структуры данных). Написать свой парсер для формата x мы пока не в состоянии, да и смысла это не имеет. Поэтому воспользуемся теми средствами, которые уже есть в DirectX.
Для загрузки файла .x в DirectX предусмотрено несколько способов. Раньше использовался интерфейс IDirectXFile (устарел). Затем интерфейс ID3DXFile. Эти интерфейсы позволяли представить файл в виде дерева. Вся обработка данных лежала на плечах программиста.
Также для доступа к содержимому .x файлов можно воспользоваться функциями и интерфейсами D3DX. Напоминаю, что D3DX - дополнительная библиотека к Direct3D. Помимо прочего в D3DX есть мощные средства для обработки файлов формата x.
Трёхмерные модели из файлов .x в программе можно представить интерфейсом ID3DXMesh. Этот интерфейс является наследником другого интерфейса - ID3DXBaseMesh. Т.е. экземплярам ID3DXMesh доступны методы ID3DXBaseMesh. Поэтому, когда работаете с ID3DXMesh, в документации смотрите методы обоих интерфейсов - и базового, и производного.
Meshes - сетки
По поводу слова mesh:
У нас оно используется как меш или сетка. Я сторонник второго варианта.
Теперь, что такое mesh:
Когда в трёхмерном редакторе создаётся модель, она состоит из полигонов. Многие не догадываются, что по-русски полигон (polygon) - многоугольник. Многоугольник - грань из трёх и более вершин (в определении есть ошибка, но это не важно).
DirectX может работать только с треугольниками, так как это самый простой вид многоугольников. Для того чтобы можно было выводить полигональные модели средствами DirectX, все полигоны нужно раздробить на треугольники. Трёхмерная модель, состоящая только из треугольников, как раз и называется сеткой (mesh). Практически во всех трёхмерных редакторах есть возможность представить модель как в виде полигонов, так и в виде сетки.
Функция D3DXLoadMeshFromX и интерфейс ID3DXMesh
Допустим, мы уже создали трёхмерную модель в каком-нибудь редакторе и экспортировали её в файл dog.x (dog - собака; я назвал так файл, потому как сваял прекрасную реалистичную модель собаки, чуть ниже её покажу). Теперь нам нужно получить доступ к этой модели во время выполнения программы. Для этого можно воспользоваться функцией D3DXLoadMeshFromX (load - загрузить, from - из):
HRESULT D3DXLoadMeshFromX( LPCTSTR pFilename, DWORD Options, LPDIRECT3DDEVICE9 pD3DDevice, LPD3DXBUFFER * ppAdjacency, LPD3DXBUFFER * ppMaterials, LPD3DXBUFFER * ppEffectInstances, DWORD * pNumMaterials, LPD3DXMESH * ppMesh );
Первый аргумент - имя файла.
Options - дополнительные параметры. Здесь указываются флаги из перечисления D3DXMESH (полный список всех флагов можно посмотреть в документации). Эти флаги предназначены для размещения в памяти индексного и вершинного буфера. Мы пока будем передавать ноль.
pD3DDevice - устройство IDirect3DDevice9.
Далее идут три буфера (смежные данные, материалы, эффекты) и параметр, хранящий количество материалов. В данный момент нам всё это не нужно.
Последний аргумент ppMesh - указатель на ID3DXMesh.
Загружаем сетку из файла dog.x:
ID3DXMesh* mesh = NULL; D3DXLoadMeshFromX(L"dog.x",0,videocard,NULL,NULL,NULL,NULL,&mesh);
Всё, теперь у нас есть доступ к сетке модели.
Вывод сетки (mesh) модели на экран
Мы рассмотрим самый простой вариант вывода сетки модели на экран. Для этого есть два способа (работают они одинаково). Но перед этим нужно установить состояния рендеринга:
// включить освещение videocard->SetRenderState(D3DRS_LIGHTING, true); // отображать только рёбра треугольников videocard->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); // показывать задние грани сетки videocard->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
Мы ещё не обсуждали состояния рендеринга (скоро будем). Не буду вдаваться в их подробное объяснение, просто скопируйте этот код и поместите его где-нибудь перед основным циклом. Замечу только, что мы используем именно эти состояния рендеринга из-за того, что ещё не умеем работать с освещением и буфером глубины.
Теперь первый способ вывода сетки:
Тут всё просто. Достаточно вызвать метод ID3DXMesh::DrawSubset с аргументом ноль между вызовами BeginScene и EndScene:
mesh->DrawSubset(0);
Всё!!! Ну разве это не просто? Смотрите, что мы сделали: получили экземпляр видеокарты, загрузили файл .x, определили матрицы преобразования (здесь я не стал приводить этот код, используйте матрицы D3DMATRIX), вывели сетку на экран. Заметьте, никаких индексных и вершинных буферов!
В метод DrawSubset передаётся один аргумент - идентификатор атрибутов. Этот аргумент предназначен для того, чтобы выводить треугольники с разными текстурами. Пока что он нам не нужен.
Теперь второй способ. Вот здесь как раз используются и индексный, и вершинный буфер. Но эти буферы не нужно создавать, они уже есть. Нужно только получить доступ к интересующим нас данным.
Давайте вспомним, что необходимо знать при использовании индексных/вершинных буферов:
- Гибкий формат вершин - FVF. Используется в методе SetFVF.
- Количество вершин. Используется в методе DrawIndexedPrimitive.
- Количество треугольников (граней). Используется в методе DrawIndexedPrimitive.
- Размер одной вершины. Используется в методе SetStreamSource.
- Указатель на вершинный буфер. Используется в методе SetStreamSource.
- Указатель на индексный буфер. Используется в методе SetIndices.
Все упомянутые выше методы (SetFVF, SetStreamSource, SetIndices, DrawIndexedPrimitive) мы уже неоднократно использовали. Осталось только узнать аргументы для этих методов. У интерфейса ID3DXBaseMesh есть очень простые методы для получения нужных нам данных (get - получить):
ID3DXMesh* mesh = NULL; D3DXLoadMeshFromX(L"dog.x",0,videocard,NULL,NULL,NULL,NULL,&mesh); IDirect3DVertexBuffer9* vb = NULL; IDirect3DIndexBuffer9* ib = NULL; mesh->GetVertexBuffer(&vb); // получение вершинного буфера mesh->GetIndexBuffer(&ib); // получение индексного буфера unsigned long fvf = mesh->GetFVF(); // FVF вершин unsigned long numVertices = mesh->GetNumVertices(); // Количество вершин unsigned long numFaces = mesh->GetNumFaces(); // количество треугольников D3DVERTEXBUFFER_DESC desc; vb->GetDesc(&desc); // description - описание videocard->SetStreamSource(0,vb,0,desc.Size/numVertices); videocard->SetFVF(fvf); videocard->SetIndices(ib);
В первой части кода происходит вытаскивание нужной нам информации (методы настолько простые, что не нуждаются в пояснениях), во второй мы используем знакомые нам методы.
Единственное, что мы здесь ещё не обсуждали - структура D3DVERTEXBUFFER_DESC (description - описание). При использовании метода IDirect3DVertexBuffer9::GetDesc в экземпляр этой структуры сохраняется информация о вершинном буфере. Метод GetDesc (и подобные методы других интерфейсов) как раз и предназначен для таких ситуаций: мы получили уже созданный буфер, и нам понадобились информация о данных в нём.
В следующем операторе, используя поле Size, в котором хранится размер всего буфера, мы выясняем размер одной вершины, разделив Size на количество вершин.
Теперь в основном цикле осталось вызвать один метод:
videocard->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0, numVertices,0,numFaces);
Как я уже писал выше, оба способа работают одинаково, только во втором случае мы выполняем все действия явно, а в первом они скрыты от нас.
3D Studio Max и Blender. Бесплатное программное обеспечение.
Наверное, самая популярная программа для трёхмерного моделирования - 3d Studio Max. В то же время, стоимость этой программы - более ста тысяч рублей. Многие тут же кинутся искать кейгены и ключи, но в связи с последними известиями с фронтов борьбы с пиратством, я бы не рекомендовал этого делать. Тем более программе 3D Studio Max есть бесплатная альтернатива.
Для всех программ, которые мы будем рассматривать, я буду создавать модели в бесплатной программе Blender. Скачать Blender можно
В Blender есть встроенная поддержка русского языка. Перевод, конечно, неважный (и неполный), но хоть что-то. Включить русский язык можно следующим образом: навести курсор мышки на разделительную полосу верхнего меню и трёхмерного редактора (на картинке показано красным цветом), нажать левую кнопку мыши и протащить курсор вниз:
Далее нужно выбрать вкладку Language & font, щёлкнуть на кнопку International fonts. Далее находите выпадающий список с выбором языка. Также не забудьте щёлкнуть на все три кнопки под списком:
Напоминаю, что нам нужно созданные модели экспортировать в формат x. Чтобы в Blender был доступен соответствующий скрипт, нужно
После того как вы создали в Blender модель, нужно выбрать пункт меню File → Export → DirectX (.x)... появится вот такое окошко:
Установите все кнопки так, как показано на картинке и щёлкните Export All (экспортировать всё) или Export Sel (экспортировать выбранное). Появится окошко, в котором необходимо ввести путь к папке сохранения и имя файла. Жмёте Export DirectX. Па-рам! Теперь можно загрузить созданную модель в вашу программу. Не забывайте, что файл .x должен находиться в папке с исполняемым файлом (.exe), или нужно будет указывать путь при загрузке файла.
Ну а теперь я покажу своё гениальное творенье. Первая картинка - модель в Blender'е:
Вторая - загруженная в программу. Немножко неудачный ракурс, ну да ладно, главное, есть возможность оценить всю гениальность модели:
На сегодня всё!
Упражнения
1. Создайте в любом трёхмерном редакторе пару моделей и экспортируйте их в отдельные .x файлы. Загрузите эти файлы в свою программу.
2. Напишите мне письмо (адрес на главной странице), понятен ли материал урока (структура файлов .x, загрузка сеток в программу) и удалось ли загрузить .x файл без мучений. Обычно, изучение формата x превращается в изуверскую средневековую пытку. Я старался сделать всё максимально простым и понятным. Удалось ли? Хммм... Заодно можно и на рассылку подписаться. :)
To be continued...