Загрузка...
 
Печать

DirectX Graphics. Меши (полигональные сетки; meshes)

Вообще, ядро DirectX не работает с мешами, а только с полигонами1 Поддержку работы с мешами вновь "оставили на откуп" вспомогательной библиотеке D3DX (DirectX 8), которая предоставляет ряд объектов, предназначенных для хранения и рендеринга полигональных сеток (далее - мешей).
На самом низком уровне меши состоят из сотен (часто тысяч) вершин (vertices) и полигонов (polygons), каждые из которых требуют весьма сложных манипуляций. К счастью, в DirectX для хранения полигональных сеток предусмотрен свой собственный формат файлов с расширением .X, описывающий 3D-модель а также все её вершины, грани, нормали, текстуры и т.д.

Содержание

Формат файлов .X

  • Это проприетарный формат от Майкрософт, предназначенный для хранения 3D-моделей.
  • Основан на шаблонах (templates).
  • Расширяемый. Т.е. можно добавлять для хранения свои собственные атрибуты.
  • Хранит в себе много возможностей для хардкорных программеров.
  • .X-файлы обычно создаются в специальных программах по работе с 3D-графикой (Milkshape3D, TrueSpace, 3DSMax и др.).
  • .X-файлы могут сохранены/загружены как в двоичном, так и в текстовом формате (с возможностью "ручной" корректировки в любом текстовом редакторе).
  • Полностью построен на т.н. шаблонах (templates), которые очень схожи с обычными структурами (structures) в языке C.

Шаблоны (templates) .X-файлов

В .X-файлах шаблоны применяются для хранения массива данных. В основном в шаблонах .X-файла хранится информация:

  • о вершинах меша,
  • его полигонах,
  • текстурных картах (texture maps),
  • нормалях (normals).

Каждый из этих узкоспециализированных шаблонов могут объединяться в иерархию шаблонов (frame hierarchy). Например, шаблон, определяющий нормали вершин может быть включён в состав шаблона описания меша (mesh definition template). А тот, в свою очередь, часто входит в состав шаблона ключевого фрейма (frame of reference template; читай о нём ниже).
Игрокодер может создавать инстансы (instances) своих собственных шаблонов точно также, как это делается в C/C++. Техника "ссылки на шаблон" (template referencing) позволяет определить (define) в .X-файле базовый шаблон и затем многократно использовать данные из него без необходимости переопределять другие шаблоны. Представь, что ты определил один меш, который хочешь использовать в рамках одного .X-файла несколько раз (например: человек стоит, человек сидит, бежит и т.д.). Вместо того, чтобы вновь и вновь определять новые шаблоны, достаточно просто определить меш 1 раз и затем просто использовать ссылки на исходный шаблон.
Работа с большинством шаблонов .X-файлов для игрокодера "прозрачна". При загрузке .X-файла ему не придётся иметь с ними дело напрямую. Единственные исключения здесь:

  • шаблоны меша,
  • шаблоны фрейма (frame).

Рис. 1 Иерархия фреймов. Источник: http://netlib.narod.ru/library/book0051/ch02_12.htm
Рис. 1 Иерархия фреймов. Источник: http://netlib.narod.ru/library/book0051/ch02_12.htm

Иерархия фреймов (Frame Hierarchy)

  • Лежит в основе применения продвинутых (advanced) мешей и их анимации.
  • Без неё никуда при использовании скинированных (skinned) мешей (об этом ниже).
  • Применяется для изоляции отдельных объектов сцены друг от друга.

То есть иерархия фреймов позволяет модифицировать (тонко настраивать) небольшую секцию сцены, указав отдельные фреймы. При этом остальные фреймы остаются нетронутыми. К примеру, если в твоей сцене есть фрейм дома (house frame) и двери (door frame), иерархия фреймов позволит работать с фреймом двери изолировано, не "беспокоя" фрейм дома.
Шаблон фрейма (frame tamplate), также известный как ключевой фрейм (frame of reference), используют для группировки одного или нескольких шаблонов (обычно это шаблоны меша) для более удобной работы с ними. Часто создают один (базовый) меш, после чего используют ещё несколько фреймов, содержащих ссылки на фрейм с базовым мешем. Это позволяет применять один и тот же меш несколько раз в пределах одного .X-файла.
Возьмём, к примеру, меш бильярдного шара. Так как в наборе 15 шаров, мы создаём 15 фреймов, каждый из которых содержит ссылку на исходный меш шара. Далее каждый фрейм шара можно ориентировать в пространстве (путём применения шаблона матрицы трансформации фрейма (frame transformation matrix template)) на площади бильярдного стола. Это заставит каждый инстанс меша перемещаться по столу вместе с собственным фреймом. Таким образом из одного меша мы получим 15 бильярдных шаров.
Помимо возможности множить инстансы меша, фреймы также позволяют создавать иерархию фреймов (frame hierarchy). Именно она определяет (defines) структуру сцены или группы мешей.
К примеру возьмём человеческий скелет. Представь, что он представляет собой набок фреймов (set of frames). На вершине иерархии находится грудная клетка (chest), к которой присоединены другие кости (см. Рис.1), к тем, в свою очередь, другие и т.д. Здесь фрейм грудной клетки является исходным (корневым фреймом, root frame). У корневого фрейма нет родительского фрейма. То есть он расположен в самом верху иерархического списка и не принадлежит никакому другому фрейму. Фреймы, присоединённые к другому фрейму (фреймам) называют потомками (child frames; nodes). Например предплечье (upper arm) является родителем (parent) для руки, а рука - родителем для ладони (hand). Предплечье, рука и ладонь являются потомками фрейма "грудная клетка".
При движении фрейма вместе с ним также движутся все его потомки. Это логично, ведь при движении предплечья присоединённые к нему рука и кисть (= ладонь) также двигаются вместе с ним. При движении одной только кисти руки, двигаться будет только она, т.к. у неё нет фреймов потомков (если, конечно, каждому пальцу не назначен свой фрейм).
У каждого фрейма есть своя отдельная ориентация в пространстве, которая в терминах .X-формата называется трансформация фрейма (frame transformation). Трансформация любого дочернего фрейма (= фрейма-потомка) применяется (apply) к т.н. топ-трансформации (top transformation) т.е. трансформации фрейма-родителя (parent frame). Каждая трансформация транслируется вниз, от самого верхнего уровня иерархии до самого последнего фрейма-потомка (child frame). Например, когда ты вращаешь предплечьем (upper arm) трансформация вращения также действует вниз по иерархии, ко всем фреймам-потомкам (рука, кисть). При этом трансформация родителя комбинируется с трансформациями всех его потомков.

Создание .X-мешей (Creating .X Meshes)

Наиболее популярный способ создания .X-файлов - экспорт готовой модели из 3D-редактора. Для этого 3D-редактор должен поддерживать экспорт .X-моделей, что встречается довольно редко. На помощь приходят плагины от сторонних разработчиков. В таблице ниже представлены наиболее популярные 3D-редакторы + кратко описан способ экспорта .X-файла:

3D-редактор Платный/бесплатный Где взять + краткое описание процедуры сохранения модели в .X-формат
3DS Max Платный (прибл. 3500 уе) Не рекомендуем качать последнюю версию, т.к. в базе экспорт .X-файлов не поддерживается, а специальные плагины-экспортёры выходят к конкретной версии пакета с большим опозданием. Каждый плагин "заточен" для работы с конкретной версией 3DS Max. Поэтому на практике сначала находят плагин, загуглив "3ds max directx export plugin". И уже, исходя из того, для какой версии 3DS Max он создан, уже гуглят соответствуюшие версии 3DS Max. Все версии 3DS Max могут создавать полигональные меши. Многие из них имеют триальные (ограниченные по времени), но бесплатные варианты. Один из них есть в разеделе "СОФТ". Напомним, модели для игры Half-Life создавались в 3D Studio Max foe Windows 1.0. Засадой может стать несовместимость скачанного 3DS Max с вашей операционной системой на компе и даже неподходящая разрядность 3DS Max (32 или 64 бит). 3DS Max 3.0 не запустится на компе с Win7/8/10. И наоборот: 3DS Max 2018 не запустится на компе с Win XP. Как вариант, можно поставить старый 3DS Max на отдельный старый комп, купленный по объявлениям за 1к. Плагин экспорта в .X-формат копируем в подкаталог Plugins установленной 3DS Max. Далее разрабатываешь 3D-модель и экспортируешь в .X-формат (File -> Export).
Maya Платный (прибл. 81к руб./мес) Нынешний владелец марки Maya совсем "офонарел", предлагая купить за деньги по сути триальную (ограниченную по времени) версию программы! (Подробности здесь: https://www.autodesk.ru/products/maya/overview(external link).) Каждый плагин "заточен" для работы с конкретной версией Maya. Поэтому на практике сначала находят плагин, загуглив "Maya directx export plugin". И уже, исходя из того, для какой версии Maya он создан, уже гуглят соответствуюшие версии Maya. Все версии Maya могут создавать полигональные меши. Многие из них имеют триальные (ограниченные по времени), но бесплатные варианты. Плагин экспорта в .X-формат копируем в подкаталог Plugins установленной Maya. Далее разрабатываешь 3D-модель и экспортируешь в .X-формат (File -> Export).
Milkshape 3D Платный (35 уе) Небольшой (6 Мб) 3D-редактор, умеющий экспортировать .X-файлы уже в базовой комплектации. Впрочем, не только их. Работает в любой ОС семейства MS Windows. Из минусов - нетривиальный интерфейс, к которому сложно привыкнуть. Сам экспорт сводится к разработке 3D-модели и её экспорту в .X-формат (File -> Export). Качаем здесь: http://www.milkshape3d.com(external link) . Триальная версия работает не более 30 дней и потом просит зарегистрироваться (= заплатить). Прога хранит свои настройки и данные о запусках в каталоге C:\Users\<Имя пользователя>\AppData\Roaming\MilkShape 3D 1.x.x\ (Win7/8/10).
GMax Бесплатный Официально бесплатная (для некоммерческого использования) программа для создания и редактирования 3D-моделей. На английском. В 2004 году создатели 3DS Max выпустили бесплатный вариант 3DS Max 4, о чём довольно быстро пожалели. Прога имела огромную популярность, даже несмотря на сильно урезанный функционал и напрочь вырезанные функции рендеринга. Ведь настоящие игрокодеры рендерят сами, с помощью 3D-движков. Последняя версия GMax датируется 2006 годом. Несмотря на это, GMax Работает на любой ОС семейства Windows не старше XP. Берём здесь: https://www.turbosquid.com/gmax(external(external link) link). После установки запускаем приложение с помощью ярлыка на Рабочем столе. При первом запуске запрашивает идентификатор пользователя. Ссылка для его получения на офсайте устарела. Поэтому архив с необходимыми reg-файлами ищем в Гугле по запросу "Gmax v1.2 Registration Utility" либо забираем отсюда: https://neverwintervault.org/project/nwn1/other/tool/gmax-12-registration-workaround(external link). После скачивания открываем архив и последовательно запускаем два файла с расширением .reg, согласившись на добавление информации из них в реестр вашей ОС. В базе .X-экспортер в GMax отсутствует. Поэтому читаем статью на Игрокодере Gmax Установка и настройка экспорта .X-файлов, докачав недостающие "костыли".

Начиная где-то с 9-й версии в DirectX SDK также размещают исходные коды .X-экспортера. В теории .X-файлы можно создавать даже в Блокноте (тестовая версия), вручную вбивая координаты вершин и прочие данные. Но эффективность такого "моделирования" крайне низка.

Парсинг .X-файлов (Parsing .X Files)

Сразу после загрузки .X-файла в игру, его начинают парсить, т.е. разбирать его внутреннюю структуру "по косточкам". А точнее - по строкам. Для этого применяют семейство объектов IDirectXFile, в котором есть объекты открывающие файл и энумерирующие его шаблоны, что заметно упрощает работу.
Парсинг .X-файла на деле не так сложен, как может показаться. Его суть заключается в сканировании иерархии шаблонов (template hierarchy) в поисках нужных шаблонов (в основном это шаблоны мешей и фреймов). Самое сложное здесь - запомнить, какой шаблон, в какую группу шаблонов включён. Вместо самих шаблонов при сканировании .X-файла можно настолкнуться на ссылки на шаблоны (template references), которые должны быть корректно обработаны для доступа к данным актуального шаблона.

Парсинг в DirectX 8

Вот пример кода, который открывает .X-файл и парсит шаблоны, содержащиеся в нём:

Закрыть
noteОбрати внимание

Для использования компонентов семейства IDirectXFile и успешной компиляции нижеприведённого кода потребуется:

  • Подключить (директивой #include) заголовки dxfile.h, rmxfguid.h, rmxftmpl.h.
  • Добавить в Проект библиотеки dxguid.lib и d3dxof.lib .

Все эти файлы расположены в каталоге с установленным DirectX SDK 8.

BOOL ParseXFile(char *Filename)
{
 IDirectXFile  *pDXFile = NULL;
 IDirectXFileEnumObject  *pDXEnum = NULL;
 IDirectXFileData  *pDXData = NULL;

 // Созадём программный объект на основе .X-файла
 if(FAILED(DirectXFileCreate(&pDXFile)))
 return FALSE;

 // Регистрируем требуемые шаблоны.
 // Применяем шаблоны стандартного содержимого, из базовой поддержки Direct3D (standart retained mode templates).
 if(FAILED(pDXFile->RegisterTemplates((LPVOID) D3DRM_XTEMPLATE_BYTES)))
 {
  pDXFile->Release();
  return FALSE;
 }

 // Создаём объект энумерации (enumeration object).
 if(FAILED(pDXFile->CreateEnumObject((LPVOID)Filename, DXFILELOAD_FROMFILE, &pDXEnum)))
 {
  pDXFile->Release();
  return FALSE;
 }

 // Ищем ключевые шаблоны (=шаблоны верхнего уровня; top-level templates).
 while(SUCCEEDED(pDXEnum->GetNextDataObject(&pDXData)))
 {
  ParseXFileData(pDXData);
  ReleaseCOM(pDXData);
 }

 // Освобождаем память, ранее выделенную под объекты.
 ReleaseCOM(pDXEnum);
 ReleaseCOM(pDXFile);

 // В случае успеха возвращаем TRUE.
 return TRUE;
}

void ParseXFileData(IDirectXFileData *pData)
{
 IDirectXFileObject *pSubObj = NULL;
 IDirectXFileData  *pSubData = NULL;
 IDirectXFileDataReference *pDataRef = NULL;
 const GUID *pType = NULL;
 char *pName = NULL;
 DWORD dwSize;
 char *pBuffer;

 // Получаем тип шаблона.
 if(FAILED(pData->GetType(&pType)))
 return;

 // Получаем имя шаблона (если есть).
 if(FAILED(pData->GetName(NULL, &dwSize)))
 return;
 if(dwSize)
 {
  if((pName = new char[swSize]_ != NULL)
   pData->GetName(pName, &dwSize);
 }

 // Если шаблон не найден, присваиваем топ-шаблону имя по умолчанию.
 if(pName == NULL)
 {
  if((pName = new char[9]) == NULL)
  return;
  strcpy(pName, "Template");
 }

 // Смотрим, что за шаблон.
 // Сюда переходим из своего кода.

 // Сканируем в поисках вложенных (embedded) шаблонов.
 while(SUCCEEDED(pData->GetNextObject(&pSubObj)))
 {
  // Обрабатываем вложенные ссылки.
  if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileDataReference, (void**)ApDataRef)))
  {
   ParseXFileData(pSubData);
   ReleaseCOM(pSubData);
  }
 
  ReleaseCOM(pDataRef);
 }

  // Обрабатываем вложенные шаблоны без ссылок (non-referenced).
  if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileData, (void**)&pSubData)))
  {
   ParseXFileData(pSubData);
   ReleaseCOM(pSubData);
  }

  ReleaseCOM(pSubObj);
 }
 
 // Очищаем буфер с именем.
 delete pName; 
}

В коде видим реализацию всего двух больших функций: ParseXFile и ParseXFileData. Они работают вместе, парся каждый шаблон, найденный в .X-файле.

  • ParseXFile открывает .X-файл и энумерирует (=сканирует) его в поиске самых верхних шаблонов (topmost templates) в иерархии. Каждый найденный шаблон передаётся на обработку в функцию ParseXFileData.
  • ParseXFileData обрабатывает данные переданного шаблона. Её выполнение начинается с запроса типа шаблона и имени инстанса (если есть). После этого переходит к обработке данных шаблона и с помощью рекурсии (=вызова самой себя) начинает энумерацию в поисках шаблонов-потомков (child templates). Этот процесс продолжается, пока все шаблоны не будут обработаны.

При вызове ParseXFile ей в качестве параметра передаётся имя загружаемого .X-файла. Далее две другие функции позаботятся обо всём остальном. При изучении темы "Скинированные меши" (Skinned meshs) они не раз нам помогут.

Работа с мешами с применением вспомогательной библиотеки d3dx.lib (DirectX 8)

Напомним, данная библиотека расположена в каталоге с установленным DirectX SDK 8. В нашем случае путь к ней такой: c:\DXSDK8\lib .
Вообще, в Direct3D все меши делятся на 2 вида:

Вид меша Описание
Стандартный (standart; статичный, static) Не меняет свою геометрию при рендеринге. Для улучшения своего внешнего вида может использовать только текстурирование (texture-mapping).
Скинированный (skinned; анимированный, animated) Данный вид мешей уникален, т.к. способен менять свою форму в процессе рендеринга (=deformable). Для подготовки меша к деформации, как правило, (в 3D-редакторе) его вершины присоединяют (attach) к т.н. костям (bones; здесь служебные объекты, помогающие в анимировании меша). Всякий раз при движении костей присоединённые вершины меша двигаются вместе с ними.

Объект ID3DXBuffer (DirectX 8)

  • Специальный объект, который используют оба вида мешей для хранения своих данных.
  • Имеет всего две функции: GetBufferPointer и GetBufferSize.

GetBufferPointer применяется для запроса указателя на данные буфера объекта. Возвращает указатель типа void, который может быть оттранслирован (cast) в любой тип данных:

void *ID3DXBuffer::GetBufferPointer();

GetBufferSize возвращает число байт, задействованных под буфер объекта:

DWORD ID3DXBuffer::GetBufferSize();

Библиотека D3DX использует объект ID3DXBuffer для хранения информации о конкретном меше. Например это могут быть списки (lists) материалов и текстурных карт.
ID3DXBuffer создаётся путём вызова функции D3DXCreateBuffer:

Прототип функции D3DXCreateBuffer
HRESULT D3DXCreateBuffer
(
 DWORD NumBytes,  // Размер создаваемого буфера.
 ID3DXBuffer **ppvBuffer  // Указатель на объект буфера.
);

Вот пример, в котором создаётся объект буфера размером 1024 байт и заполняется нулями:

ID3DXBuffer *pBuffer;

// Создаём буфер
if(SUCCEEDED(D3DXCreateBuffer(1024, &pBuffer)))
{
 // Получаем указатель на буфер.
 char *pPtr = pBuffer->GetBufferPointer();

 // Заполняем буфер нулями.
 memset(pPtr, 0, pBuffer->GetBufferSize());

 // Освобождаем буфер.
 pBuffer->Release();
}

Стандартные меши (Standart Meshes; DirectX 8)

  • Содержит определение одного меша.
  • Самый простой случай меша, с которого лучше всего начать работу.

Применение вспомогательной библиотеки D3DX делает работу со стандартными мешами ещё проще, т.к. заметно уменьшает объём кода для загрузки и рендеринга.
Для примера создадим объект меша ID3DXMesh (подробнее об этом см. ниже).
После создания инстанса ID3DXMesh вызываем функцию D3DXLoadMeshFromX, загружающую меш из .X-файла. Вот её прототип:

HRESULT D3DXLoadMeshFromX
(
 LPSTR pFilename,  // Имя загружаемого файла .X-файла
 DWORD Options,  // Обычно здесь указывают D3DXMESH_SYSTEMMEM.
 IDirect3DDevice8 *pDevice,  // Предварительно проинициализированный объект устройства Direct3D.
 ID3DXBuffer **Adjacency,  // NULL
 ID3DXBuffer **pMaterials,  // Указатель на готовый объект буфера ID3DXBuffer, содержащий инфу по материалам меша.
 DWORD pNumMaterials,  // Кол-во материалов у меша.
 ID3DXMesh **ppMesh  // Указатель на создаваемый D3DX-объект меша.
);

Многие аргументы здесь библиотека D3DX заполняет автоматически. Программер предоставляет только следующие параметры:

  • Имя .X-файла;
  • Указатель на готовый объект буфера ID3DXBuffer, содержащий инфу по материалам меша;
  • Кол-во материалов меша;
  • Указатель на создаваемый D3DX-объект меша.

При попытке загрузки с помощью функции D3DXLoadMeshFromX .X-файла, содержащего несколько мешей, она (функция) автоматически объединит их в один меш. Часто это как раз то, что нужно.
Вот пример кода, загружающего меш:

// g_pD3DDevice - предварительно созданный и проинициализированный объект устройства Direct3D.
ID3DXBuffer *pD3DXMaterials;
DWORD g_dwNumMaterials;
ID3DXMesh *g_pD3DXMesh;

if(FAILED(D3DXLoadMeshFromX("mesh.x", D3DXMESH_SYSTEMMEM, g_pD3DDevice, NULL, &pD3DXMaterials, &g_dwNumMaterials, &g_pD3DXMesh)))
{
 // Ошибка.
}


Сразу после (успешной) загрузки меша из .X-файла запрашиваем инфу о его материалах (materials) и текстурных картах (texture-maps):

D3DXMATERIAL *pMaterials = NULL;
D3DMATERIAL8 *g_pMaterialList = NULL;
IDirect3DTexture8 **g_pTextureList;

// Получаем указатель на список материалов (material list).
pMaterials = (D3DXMATERIAL*)pD3DXMaterials->GetBufferPointer();
if(pMaterials != NULL)
{
 // Выделяем структуру для копирования туда данных.
 g_pMaterialList = new D3DMATERIAL8(dwNumMaterials);

 // Создаём массив указателей на объекты текстур (texture object pointers array).
 g_pTextureList = new IDirect3DTexture8(dwNumMaterials);

 // Копируем инфу по материалам меша.
 for(DWORD i=0; i<dwNumMaterials; i++)
 {
  g_pMaterialList[i] = pMaterials[i].MatD3D;

  // Делаем цвет тени (ambient color) такой же, как рассеяный (diffuse color).
  g_pMaterialList[i].Ambient = g_pMaterialList[i].Diffuse;

  // Загружаем текстуры (если есть).
  if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice, g_pMaterials[i]->pTextureFilename, &g_pTextureList[i])))
  g_pTextureList[i] = NULL;
 }

 // Освобождаем буфер материалов по завершении их загрузки.
 pD3DXMaterials->Release();
} else
{
 // Если материалы не загружены, создаём один материал по умолчанию.
 g_dwNumMaterials = 1;

 // Создаём белый (white) материал.
 g_pMaterialList = new D3DMATERIAL8[1];
 g_pMaterialList[i].Diffuse.r = 1.0f;
 g_pMaterialList[i].Diffuse.g = 1.0f;
 g_pMaterialList[i].Diffuse.b = 1.0f;
 g_pMaterialList[i].Diffuse.a = 1.0f;
 g_pMaterialList[i].Ambient = g_pMaterialList[i].Diffuse;

 // Создаём пустую ссылку на текстуру.
 g_pTextureList = new IDirect3DTexture8[i];
 g_pTextureList[0] = NULL;
}

После выполнения данного фрагмента у нас в распоряжении окажется список материалов и текстур, полностью готовый к применению в сцене. Следующий шаг - рендеринг меша.

Рендерим меши (Rendering Meshes; DirectX 8)

В "сердце" объекта ID3DXMesh лежит функция рендеринга DrawSubset. Субсетом (subset) называют часть (portion) меша, которая отделена на случай изменения условий рендеринга. Самый типичный такой случай - смена материала или текстуры. Меш можно разбивать на множество субсетов. Здесь главное - помнить, что представляет собой каждый из них.
Сразу после загрузки .X-файла, ты остаёшься один на один с объектом меша и его материалами. Число субсетов совпадает с числом материалов. То есть, если объект меша содержит 5 материалов, меш содержит 5 субсетов, готовых к рендерингу. Такая зависимость упрощает процесс рендеринга меша. Мы просто сканируем каждый материал, назначаем его субсету и затем просто этот самый субсет рендерим. Весь процесс повторяется, пока весь меш не будет отрисован.
Для позиционирования меша в мировом пространстве (world space), до начала рендеринга назначаем мешу мировую матрицу трансформации (world transformation matrix).
Вот пример рендеринга меша, загруженного и подготовленного в предыдущих абзацах:

// g_pD3DDevice - предварительно созданный и проинициализированный объект устройства Direct3D.
// pD3DXmesh - готовый инстанс объекта ID3DXMesh.
// matWorld - мировая матрица трансформации меша.

// Начало сцены
if(SUCCEEDED(g_pD3DDevice->BeginScene()))
{
 // Назначаем мировую матрицу трансформации меша.
 g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);

 // Проходим цикл по каждому материалу меша.
 for(DWORD i=0; i<g_dwNumMaterials; i++)
 {
  // Назначаем материал и текстуру.
  g_pD3DDevice->SetMaterial(&g_pMaterialList[i]);
  g_pD3DDevice->SetTexture(0, g_pTextureList[i]);

  // Отрисовываем субсет меша.
  pD3DXMesh->DrawSubset(i);
 }

 // Завершаем отрисовку сцены.
 g_pD3DDevice->EndScene();
}

Прежде чем рендерить меш, сперва необходимо установить (set) его мировую матрицу трансформации (world transformation matrix). Она необходима для корректного позиционирования меша в мировом пространстве. При загрузке нескольких мешей их можно объединить, создав анимированный объект путём простого изменения положения его частей в пространстве. Именно эта концепция лежит в основе real-time 3D-анимации.

Скинированные меши (Skinned Meshes)

  • Могут динамически изменяться во время рендеринга.

Это достигается путём:

  • объединения отдельных вершин в специальные структуры, "присоединяемые" к т.н. костям (bones);
  • перемещения отдельных фреймов иерархии.

Скинированный меш использует кости для определения (и сохранения в процессе анимации) своей формы. При движении костей, "насаженный" на них, меш перемещается вместе с ними.
Внутри .X-файла кости представлены в виде иерархии фреймов (frame hierarchy). При моделировании меша фреймы объединяют по принципу родитель (parent) - потомок (child). При повороте родительского фрейма, все, присоединённые к нему, дочерние фреймы также наследуют трансформацию фрейма-родителя, которая, к тому же, комбинируется с их собственными трансформациями. Это заметно упрощает процесс анимирования для программера: двигаем родительский фрейм, а все дочерние двигаются вместе с ним.
Работу с анимированным мешем начинают с загрузки .X-файла и парсинга его шаблонов (аналогично процессу описанному для стандартных мешей). Далее формируем список фреймов (list of frames) и иерархию фреймов.

Загрузка скинированных мешей (Load Skinned Meshes; DirectX 8)

Выше мы рассматривали процесс загрузки стандартного меша из .X-файла. Там мы вызывали несколько функций из библиотеки D3DX для обработки шаблонов (templates). При загрузке скинированных мешей принцип тот же. Но для загрузки применяют другую функцию - D3DXLoadSkinMeshFromXof. В результате её выполнения считываются шаблоны из .X-файла и на основе интерфейса ID3DXSkinMesh создаётся объект скинированного меша. Весь процесс проще представить, имея перед глазами исходный код примера. Поэтому создадим пример.

Пример приложения, показывающего меш (Win32, MSVC++2010, DirectX 8)

Создадим пример, который:

  • загружает .X файл,
  • грамотно его парсит,
  • "достаёт" шаблоны фреймов (frame templates; о них читай чуть ниже),
  • готовит текстуру и наносит на 3D-модель.

Перед началом проверь, что у тебя установлены следующие программные компоненты:

  • MS Visual C++ 2010;
  • DirectX SDK 8.1
  • Windows SDK (для программирования под Win7/8/10).

Все эти штуки:

  • + инструкции по их установке ты найдёшь в разделе "Софт" нашего сайта;
  • Бесплатны;
  • Без труда гуглятся.

Создаём Проект приложения

  • Создай пустой Проект с именем XFile01. Проект автоматически разместится внутри Решения с таким же именем.

Весь процесс подробно расписан в статье Настройка MS Visual C plus plus 2010 и DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта XFile01.
  • Во всплывающем меню Добавить->Создать элемент...
Добавляем исходный файл
Добавляем исходный файл
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp.
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 6 XFile/Skinned Mesh Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)

Required libraries:
  WINMM.LIB, D3D8.LIB, D3DX8.LIB, and DXGUID.LIB
**************************************************/

// Macro to release COM objects
#define ReleaseCOM(x) if(x) { x->Release(); x = NULL; }

// Include files
#include <windows.h>
#include <stdio.h>
#include "d3d8.h"
#include "d3dx8.h"
#include "dxfile.h"
#include "rmxfguid.h"
#include "rmxftmpl.h"

// Window handles, class and caption text
HWND          g_hWnd;
HINSTANCE     g_hInst;
static char   g_szClass[]   = "XFileClass";
static char   g_szCaption[] = "XFile Demo by Jim Adams";

// The Direct3D and Device object
IDirect3D8       *g_pD3D       = NULL;
IDirect3DDevice8 *g_pD3DDevice = NULL;

// A mesh definition structure
typedef struct sMesh
{
  char               *m_Name;            // Name of mesh

  ID3DXMesh          *m_Mesh;            // Mesh object
  ID3DXSkinMesh      *m_SkinMesh;        // Skin mesh object

  DWORD               m_NumMaterials;    // # materials in mesh
  D3DMATERIAL8       *m_Materials;       // Array of materials
  IDirect3DTexture8 **m_Textures;        // Array of textures

  DWORD               m_NumBones;        // # of bones
  ID3DXBuffer        *m_BoneNames;       // Names of bones
  ID3DXBuffer        *m_BoneTransforms;  // Internal transformations

  D3DXMATRIX         *m_BoneMatrices;    // X file bone matrices

  sMesh              *m_Next;            // Next mesh in list

  sMesh()
  { 
    m_Name           = NULL;  // Clear all structure data
    m_Mesh           = NULL;
    m_SkinMesh       = NULL;
    m_NumMaterials   = 0;
    m_Materials      = NULL;
    m_Textures       = NULL;
    m_NumBones       = 0;
    m_BoneNames      = NULL;
    m_BoneTransforms = NULL;

    m_Next           = NULL;
  }

  ~sMesh()
  {
    // Free all used resources
    delete [] m_Name;
    ReleaseCOM(m_Mesh);
    ReleaseCOM(m_SkinMesh);
    delete [] m_Materials;
    if(m_Textures != NULL) {
      for(DWORD i=0;i<m_NumMaterials;i++) {
        ReleaseCOM(m_Textures[i]);
      }
      delete [] m_Textures;
    }
    ReleaseCOM(m_BoneNames);
    ReleaseCOM(m_BoneTransforms);

    delete m_Next;  // Delete next mesh in list
  }
} sMesh;

// Structure to contain frame information
typedef struct sFrame
{
  char       *m_Name;      // Frame's name

  sMesh      *m_Mesh;      // Linked list of meshes

  sFrame     *m_Sibling;   // Sibling frame
  sFrame     *m_Child;     // Child frame

  sFrame() 
  {
    // Clear all data
    m_Name = NULL;
    m_Mesh = NULL;
    m_Sibling = m_Child = NULL;
  }

  ~sFrame()
  {
    // Delete all used resources, including linked list of frames
    delete m_Name;
    delete m_Mesh;
    delete m_Child;
    delete m_Sibling;
  }
} sFrame;

// Parent frame for .X file
sFrame *g_pParentFrame = NULL;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);
long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam);

BOOL DoInit();
BOOL DoShutdown();
BOOL DoFrame();
BOOL SetupMesh();

sFrame *LoadFile(char *Filename);
sFrame *ParseXFile(char *Filename);
void ParseXFileData(IDirectXFileData *pDataObj, sFrame *ParentFrame);
void DrawFrame(sFrame *Frame);

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  WNDCLASSEX wcex;
  MSG        Msg;

  g_hInst = hInst;

  // Create the window class here and register it
  wcex.cbSize        = sizeof(wcex);
  wcex.style         = CS_CLASSDC;
  wcex.lpfnWndProc   = WindowProc;
  wcex.cbClsExtra    = 0;
  wcex.cbWndExtra    = 0;
  wcex.hInstance     = hInst;
  wcex.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wcex.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wcex.hbrBackground = NULL;
  wcex.lpszMenuName  = NULL;
  wcex.lpszClassName = g_szClass;
  wcex.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
  if(!RegisterClassEx(&wcex))
    return FALSE;

  // Create the Main Window
  g_hWnd = CreateWindow(g_szClass, g_szCaption,
        WS_CAPTION | WS_SYSMENU,
        0, 0, 400, 400,
        NULL, NULL,
        hInst, NULL );
  if(!g_hWnd)
    return FALSE;
  ShowWindow(g_hWnd, SW_NORMAL);
  UpdateWindow(g_hWnd);

  // Run init function and return on error
  if(DoInit() == FALSE)
    return FALSE;

  // Start message pump, waiting for signal to quit
  ZeroMemory(&Msg, sizeof(MSG));
  while(Msg.message != WM_QUIT) {
    if(PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&Msg);
      DispatchMessage(&Msg);
    }
    if(DoFrame() == FALSE)
      break;
  }

  // Run shutdown function
  DoShutdown();
  
  UnregisterClass(g_szClass, hInst);

  return Msg.wParam;
}

long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam)
{
  switch(uMsg) {
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

  }

  return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

BOOL DoInit()
{
  D3DPRESENT_PARAMETERS d3dpp;
  D3DDISPLAYMODE        d3ddm;
  D3DXMATRIX matProj, matView;

  // Do a windowed mode initialization of Direct3D
  if((g_pD3D = Direct3DCreate8(D3D_SDK_VERSION)) == NULL)
    return FALSE;
  if(FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
    return FALSE;
  ZeroMemory(&d3dpp, sizeof(d3dpp));
  d3dpp.Windowed = TRUE;
  d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  d3dpp.BackBufferFormat = d3ddm.Format;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
  if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, g_hWnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, &g_pD3DDevice)))
    return FALSE;

  // Set the rendering states
  g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
  g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);

  // Create and set the projection matrix
  D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4.0f, 1.33333f, 1.0f, 1000.0f);
  g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);

  // Create and set the view matrix
  D3DXMatrixLookAtLH(&matView,                                \
                     &D3DXVECTOR3(0.0f, 50.0f, -150.0f),     \
                     &D3DXVECTOR3(0.0f, 50.0f, 0.0f),          \
                     &D3DXVECTOR3(0.0f, 1.0f, 0.0f));
  g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);

  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");

  return TRUE;
}

BOOL DoShutdown()
{
  // Release frames and meshes
  if(g_pParentFrame != NULL)
    delete g_pParentFrame;

  // Release device and 3D objects
  if(g_pD3DDevice != NULL)
    g_pD3DDevice->Release();

  if(g_pD3D != NULL)
    g_pD3D->Release();

  return TRUE;
}

BOOL DoFrame()
{
  D3DXMATRIX matWorld;

  // Clear device backbuffer
  g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
                      D3DCOLOR_RGBA(0,64,128,255), 1.0f, 0);

  // Begin scene
  if(SUCCEEDED(g_pD3DDevice->BeginScene())) {

    // Set world transformation to rotate on Y-axis
    D3DXMatrixRotationY(&matWorld, (float)timeGetTime() / 1000.0f);
    g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);

    // Draw frames
    DrawFrame(g_pParentFrame);

    // Release texture
    g_pD3DDevice->SetTexture(0, NULL);

    // End the scene
    g_pD3DDevice->EndScene();
  }

  // Display the scene
  g_pD3DDevice->Present(NULL, NULL, NULL, NULL);

  return TRUE;
}


sFrame *LoadFile(char *Filename)
{
  return ParseXFile(Filename);
}

sFrame *ParseXFile(char *Filename)
{
  IDirectXFile           *pDXFile = NULL;
  IDirectXFileEnumObject *pDXEnum = NULL;
  IDirectXFileData       *pDXData = NULL;
  sFrame                 *Frame;

  // Create the file object
  if(FAILED(DirectXFileCreate(&pDXFile)))
    return FALSE;

  // Register the templates
  if(FAILED(pDXFile->RegisterTemplates((LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES))) {
    pDXFile->Release();
    return FALSE;
  }

  // Create an enumeration object
  if(FAILED(pDXFile->CreateEnumObject((LPVOID)Filename, DXFILELOAD_FROMFILE, &pDXEnum))) {
    pDXFile->Release();
    return FALSE;
  }

  // Allocate a frame that becomes root
  Frame = new sFrame();

  // Loop through all objects looking for the frames and meshes
  while(SUCCEEDED(pDXEnum->GetNextDataObject(&pDXData))) {
    ParseXFileData(pDXData, Frame);
    ReleaseCOM(pDXData);
  }

  // Release used COM objects
  ReleaseCOM(pDXEnum);
  ReleaseCOM(pDXFile);

  // Return root frame
  return Frame;
}

void ParseXFileData(IDirectXFileData *pDataObj, sFrame *ParentFrame)
{
  IDirectXFileObject *pSubObj  = NULL;
  IDirectXFileData   *pSubData = NULL;
  IDirectXFileDataReference *pDataRef = NULL;
  const GUID *Type = NULL;
  char       *Name = NULL;
  DWORD       Size;

  sFrame     *Frame = NULL;
  sFrame     *SubFrame = NULL;

  sMesh         *Mesh = NULL;
  ID3DXBuffer   *MaterialBuffer = NULL;
  D3DXMATERIAL  *Materials = NULL;
  ID3DXBuffer   *Adjacency = NULL;
  DWORD         *AdjacencyIn = NULL;
  DWORD         *AdjacencyOut = NULL;

  DWORD i;

  // Get the template type
  if(FAILED(pDataObj->GetType(&Type)))
    return;

  // Get the template name (if any)
  if(FAILED(pDataObj->GetName(NULL, &Size)))
    return;
  if(Size) {
    if((Name = new char[Size]) != NULL)
      pDataObj->GetName(Name, &Size);
  }

  // Give template a default name if none found
  if(Name == NULL) {
    if((Name = new char[9]) == NULL)
      return;
    strcpy(Name, "Template");
  }

  // Set sub frame
  SubFrame = ParentFrame;

  // Process the templates

  // Frame
  if(*Type == TID_D3DRMFrame) {
    // Create a new frame structure
    Frame = new sFrame();

    // Store the name
    Frame->m_Name = Name;
    Name = NULL;

    // Add to parent frame
    Frame->m_Sibling = ParentFrame->m_Child;
    ParentFrame->m_Child = Frame;

    // Set sub frame parent
    SubFrame = Frame;
  }

  // Load a mesh
  if(*Type == TID_D3DRMMesh) {
    // Create a new mesh structure
    Mesh = new sMesh();

    // Store the name
    Mesh->m_Name = Name;
    Name = NULL;

    // Load mesh data (as a skinned mesh)
    if(FAILED(D3DXLoadSkinMeshFromXof(pDataObj, 0,
                g_pD3DDevice,
                &Adjacency,
                &MaterialBuffer, &Mesh->m_NumMaterials, 
                &Mesh->m_BoneNames, &Mesh->m_BoneTransforms,
                &Mesh->m_SkinMesh))) {
      delete Mesh;
      return;
    }

    // Convert to regular mesh if no bones
    if(!(Mesh->m_NumBones = Mesh->m_SkinMesh->GetNumBones())) {
    
      // Convert to a regular mesh
      Mesh->m_SkinMesh->GetOriginalMesh(&Mesh->m_Mesh);
      ReleaseCOM(Mesh->m_SkinMesh);

    } else {

      // Get a pointer to bone matrices
      Mesh->m_BoneMatrices = (D3DXMATRIX*)Mesh->m_BoneTransforms->GetBufferPointer();

      // Allocate buffers for adjacency info
      AdjacencyIn  = (DWORD*)Adjacency->GetBufferPointer();
      AdjacencyOut = new DWORD[Mesh->m_SkinMesh->GetNumFaces() * 3];

      // Generate the skin mesh object
      if(FAILED(Mesh->m_SkinMesh->GenerateSkinnedMesh(
                  D3DXMESH_WRITEONLY, 0.0f, 
                  AdjacencyIn, AdjacencyOut, NULL, NULL, 
                  &Mesh->m_Mesh))) {
        // Convert to a regular mesh if error
        Mesh->m_SkinMesh->GetOriginalMesh(&Mesh->m_Mesh);
        ReleaseCOM(Mesh->m_SkinMesh);
        Mesh->m_NumBones = 0;
      }

      delete [] AdjacencyOut;
      ReleaseCOM(Adjacency);
    }

    // Load materials or create a default one if none
    if(!Mesh->m_NumMaterials) {
      // Create a default one
      Mesh->m_Materials = new D3DMATERIAL8[1];
      Mesh->m_Textures  = new LPDIRECT3DTEXTURE8[1];

      ZeroMemory(Mesh->m_Materials, sizeof(D3DMATERIAL8));
      Mesh->m_Materials[0].Diffuse.r = 1.0f;
      Mesh->m_Materials[0].Diffuse.g = 1.0f;
      Mesh->m_Materials[0].Diffuse.b = 1.0f;
      Mesh->m_Materials[0].Diffuse.a = 1.0f;
      Mesh->m_Materials[0].Ambient   = Mesh->m_Materials[0].Diffuse;
      Mesh->m_Materials[0].Specular  = Mesh->m_Materials[0].Diffuse;
      Mesh->m_Textures[0] = NULL;

      Mesh->m_NumMaterials = 1;
    } else {
      // Load the materials
      Materials = (D3DXMATERIAL*)MaterialBuffer->GetBufferPointer();
      Mesh->m_Materials = new D3DMATERIAL8[Mesh->m_NumMaterials];
      Mesh->m_Textures  = new LPDIRECT3DTEXTURE8[Mesh->m_NumMaterials];

      for(i=0;i<Mesh->m_NumMaterials;i++) {
        Mesh->m_Materials[i] = Materials[i].MatD3D;
        Mesh->m_Materials[i].Ambient = Mesh->m_Materials[i].Diffuse;
     
        // Build a texture path and load it
        if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice,
                                            Materials[i].pTextureFilename,
                                            &Mesh->m_Textures[i]))) {
          Mesh->m_Textures[i] = NULL;
        }
      }
    }
    ReleaseCOM(MaterialBuffer);

    // Link in mesh
    Mesh->m_Next = ParentFrame->m_Mesh;
    ParentFrame->m_Mesh = Mesh;
  }

  // Skip animation sets and animations
  if(*Type == TID_D3DRMAnimationSet || *Type == TID_D3DRMAnimation || *Type == TID_D3DRMAnimationKey) {
    delete [] Name;
    return;
  }

  // Release name buffer
  delete [] Name;

  // Scan for embedded templates
  while(SUCCEEDED(pDataObj->GetNextObject(&pSubObj))) {

    // Process embedded references
    if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileDataReference, (void**)&pDataRef))) {
      if(SUCCEEDED(pDataRef->Resolve(&pSubData))) {
        ParseXFileData(pSubData, SubFrame);
        ReleaseCOM(pSubData);
      }
      ReleaseCOM(pDataRef);
    }

    // Process non-referenced embedded templates
    if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileData, (void**)&pSubData))) {
      ParseXFileData(pSubData, SubFrame);
      ReleaseCOM(pSubData);
    }
    ReleaseCOM(pSubObj);
  }

  return;
}

void DrawFrame(sFrame *Frame)
{
  sMesh *Mesh;
  D3DXMATRIX *Matrices;
  DWORD i;

  // Return if no frame
  if(Frame == NULL)
    return;
  
  // Draw meshes if any in frame
  if((Mesh = Frame->m_Mesh) != NULL) {

    // Generate mesh from skinned mesh to draw with
    if(Mesh->m_SkinMesh != NULL) {
      // Allocate an array of matrices to orient bones
      Matrices = new D3DXMATRIX[Mesh->m_NumBones];

      // Set all bones orientation to identity
      for(i=0;i<Mesh->m_NumBones;i++)
        D3DXMatrixIdentity(&Matrices[i]);

      // Update skinned mesh
      Mesh->m_SkinMesh->UpdateSkinnedMesh(Matrices, NULL, Mesh->m_Mesh);

      // Render the mesh
      for(i=0;i<Mesh->m_NumMaterials;i++) {
        g_pD3DDevice->SetMaterial(&Mesh->m_Materials[i]);
        g_pD3DDevice->SetTexture(0, Mesh->m_Textures[i]);
        Mesh->m_Mesh->DrawSubset(i);
      }

      // Free array of matrices
      delete [] Matrices;
    }

    // Go to next mesh
    Mesh = Mesh->m_Next;
  }

  // Draw child frames
  DrawFrame(Frame->m_Child);

  // Draw sibling frames
  DrawFrame(Frame->m_Sibling);
}
  • Сохрани Решение (Файл -> Сохранить все).

Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). Автор прогал на MS Visual C++ 6.0 (вышла в свет в 1998 г.) c установленным DirecX SDK 8.0. И если в нашем случае в качестве Graphic API мы юзаем практически идентичный DirectX SDK 8.1, то IDE у нас совсем другая (MS Visual C++ 2010 Express Edition). Нет, не с целью усложнения задачи. Просто MS Visual C++ 6.0 в своё время стоила 500 USD. Сейчас её, в принципе, можно поискать в торрентах. (За всё, что ты там скачаешь, администрация Igrocoder.ru ответственности не несёт!) Но использование платного/пиратского софта не соответствует концепции сайта Igrocoder.ru .
Поэтому наш выбор:

  • Бесплатная IDE MS Visual C++ 2010 Express edition;
  • Бесплатный DirectX SDK 8.1 .

К концу данной статьи мы вновь докажем, что игрокодинг (под ОС Windows) в домашних условиях возможен. И для него нужны только компьютер с ОС Windows и доступ к Интернету.
Но вернёмся к нашему Проекту XFile01. С момента выхода MS Visual C++ 6.0 прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки. Даже с настроенными путями к DirectX SDK.
Но мы это исправим.

Готовим Проект XFile01 к компиляции

Для успешной компиляции изменим настройки (=свойства) текущего Проекта XFile01, созданного в MSVC++2010. При этом сам код из книги 2002 года останется нетронутым. Или почти нетронутым. В ходе тестирования выяснилось, что косячный автор в своём коде вызывает функцию GenerateSkinnedMesh с пятью параметрами, вместо положенных семи. Вот её прототип из документации DirectX SDK 8:

Прототип функции GenerateSkinnedMesh (DirectX 8)
HRESULT GenerateSkinnedMesh(
  DWORD Options,
  FLOAT minWeight,
  CONST LPDWORD pAdjacencyIn,
  LPDWORD pAdjacencyOut,
  DWORD* pFaceRemap,  
  LPD3DXBUFFER* ppVertexRemap,   
  LPD3DXMESH* ppMesh
  );

Также функцию UpdateSkinnedMesh он вызывает с двумя параметрами, вместо положенных трёх. Вот её прототип из документации DirectX SDK 8:

Прототип функции UpdateSkinnedMesh (DirectX 8)
HRESULT UpdateSkinnedMesh(
CONST D3DXMATRIX* pBoneTransforms,
CONST D3DXMATRIX* pBoneInvTransforms,
LPD3DXMESH pMesh
);

Конечно, для учебных целей эти параметры не так важны. Но за числом параметров в своих функциях DirectX следит строго, в противном случае вызывая многочисленные ошибки в ходе компиляции. В приведённом выше исходном коде мы доставили на месте недостающих параметров значения NULL и всё пошло как по маслу.

Закрыть
noteОбрати внимание

Напомним, что такую настройку необходимо повторно проделывать при создании каждого нового Проекта. Ниже представлен алгоритм действий по настройке Проекта XFile01, созданного в MSVC++2010 Express с применением DirectX SDK 8.1 . При создании приложений под платформы, отличные от Win32, либо применении более новых версий DirectX SDK, процесс конфигурирования Проекта может отличаться от приведённого ниже.

Указываем пути к DirectX SDK 8.1

Если попытаться скомпилировать Проект XFile01 в таком виде, то ничего не выйдет.
В единственном файле исходного кода WinMain.cpp можно увидеть инклуды различных заголовочных файлов DirectX (весии 8).

Фрагмент WinMain.cpp
...
// Include files
#include <windows.h>
#include <stdio.h>
#include "d3d8.h"
#include "d3dx8.h"
#include "dxfile.h"
...

На данном этапе MSVC++2010 ничего не знает об их местоположении. В статье Настройка MS Visual C plus plus 2010 и DirectX SDK мы указывали пути к DirectX SDK (версии 9 и выше). Для DirectX SDK 8 это делается аналогично. Начнём.

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • Убедись, что DirectX SDK 8.1 установлен на компьютере и ты уверенно можешь назвать полный путь к его каталогу.
  • В Обозревателе решений видим: "Решение "XFile01"", а строкой ниже жирным шрифтом название Проекта (тоже XFile01).
  • Жмём правой кнопкой мыши по названию Проекта XFile01. Во всплывающем меню выбираем пункт "Свойства". Или в Главном меню выбираем Проект->Свойства. Или нажимаем Alt+F7.

Image

  • В появившемся меню свойств проекта выбираем Свойства конфигурации -> Каталоги VC++. В правой части этой страницы расположены пути ко всевозможным каталогам. Здесь нас интересуют только 2 строки: Каталоги включения и Каталоги библиотек.


Указываем каталог включений (include) DirectX SDK 8.1

  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги включения. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."

Image

  • В появившемся меню "Каталоги включения" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к заголовочным файлам DirectX SDK 8.1 (include). В нашем случае это C:\DXSDK8\include. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.

Image

  • Жмём "ОК".


Указываем каталог библиотек (lib) DirectX SDK 8.1

  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги библиотек. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
  • В появившемся меню "Каталоги библиотек" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к 32-разрядным версиям файлов библиотек DirectX SDK 8.1 (lib). В нашем случае это c:\DXSDK8\lib\. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
  • Жмём "ОК".

Image

  • На Странице свойств тоже жмём "ОК".

  • Сохрани Решение (File -> Save All).

Готово.

Указываем пути к Windows SDK

Помимо указания путей к заголовкам (include) и библиотекам (lib) DirectX SDK, для любого DirectX-Проекта также необходимо казать пути к заголовкам (include) и библиотекам (lib) Windows SDK. Самое смешное, что в MS Visual C++ 2010 эти пути указываются по умолчанию для каждого создаваемого Проекта. В этом нетрудно убедиться, если ещё раз открыть Проект -> Свойства -> Свойства конфигурации - >Каталоги VC++ -> Каталоги включения -> Изменить. В окне "Каталоги включения" в нижней (недоступной для редактирования) части видим список "Унаследованные значения", где в третьей строке стоит значение:

$(WindowsSdkDir)include

В результате видим, что добавленные пути к DirectX SDK (в обоих окнах: include и lib) расположены вверху списка, а пути к Windows SDK - в недоступной области, на несколько строк ниже.
Но заголовочным файлам DirectX SDK 8.1 "жизненно важно", чтобы они включались после включений заголовков Windows SDK.

Закрыть
noteВажно!

Каталоги, пути к которым указаны в окнах "Каталоги включения" и "Каталоги библиотек" при компиляции считываются один за другим по списку сверху вниз. Поэтому для корректного указания путей надо разместить пути к Windows SDK выше, а к DirectX SDK 8.1 - ниже по списку (чтобы они считывались последними).

В данной ситуации мы не можем поднять пути к Windows SDK, прописанные по умолчанию при создании Проекта, т.к. они расположены в специальной нередактируемой области (ограничение бесплатной версии MSVC++2010 Express).
Но можем схитрить и добавить ещё раз пути к тем же самым каталогам Windows SDK, подняв эти строки выше строк DirectX SDK.
Выше мы указывали пути к DirectX SDK 8.1. Для путей к Windows SDK это делается аналогично. Начнём.

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • Убедись, что Windows SDK (в нашем случае версия 7) установлен на компьютере и ты уверенно можешь назвать полный путь к его каталогу.
  • В Обозревателе решений видим: "Решение "XFile01"", а строкой ниже жирным шрифтом название Проекта (тоже XFile01).
  • Жмём правой кнопкой мыши по названию Проекта XFile01. Во всплывающем меню выбираем пункт "Свойства". Или в Главном меню выбираем Проект->Свойства. Или нажимаем Alt+F7.

Image

  • В появившемся меню свойств проекта выбираем Свойства конфигурации -> Каталоги VC++. В правой части этой страницы расположены пути ко всевозможным каталогам. Здесь нас интересуют только 2 строки: Каталоги включения и Каталоги библиотек.


Указываем каталог включений (include) Windows SDK

  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги включения. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."

Image

  • В появившемся меню "Каталоги включения" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к заголовочным (include) файлам Windows SDK. В нашем случае (Win7 x64) это C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.

Image

  • Меняй порядок считывания каталогов включений с помощью кнопок с чёрными стрелками в верхней части окна "Каталоги вложений".

Каталог включений DirectX SDK 8.1 должен всегда стоять в самом конце списка, как на этом скриншоте:
Image

  • Жмём "ОК".


Указываем каталог библиотек (lib) Windows SDK

  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги библиотек. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
  • В появившемся меню "Каталоги библиотек" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к папке с файлами библиотек (lib) Windows SDK. В нашем случае (Win7 x64) это C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.

Image

  • Меняй порядок считывания каталогов вложений с помощью кнопок с чёрными стрелками в верхней части окна "Каталоги вложений".

Каталог библиотек DirectX SDK 8.1 должен всегда стоять в самом конце списка, как на этом скриншоте:
Image

  • Жмём "ОК".
  • На Странице свойств тоже жмём "ОК".

  • Сохрани Решение (File -> Save All).

Готово.

Выбираем многобайтовую кодировку

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию только что созданного Проекта XFile01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".

Image

  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все)

Отключаем инкрементную компоновку (incremental linking)

Инкрементная компоновка призвана сократить время компилирования. Но на деле её присутствие часто вызывает ошибки вроде этой:

Error LNK1123: сбой при преобразовании в COFF: файл недопустим или поврежден

Отключается в свойствах открытого Проекта. Для MS Visual C++ 2010 порядок следующий:

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение "Нет (/INCREMENTAL:NO)".
  • Жмём ОК.

Прописываем библиотеки d3dx8.lib, d3d8.lib, dxguid.lib, d3dxof.lib и winmm.lib в окне "Дополнительные зависимости" (Additional dependencies) компоновщика (Linker)

Данный этап проходил даже автор кода Jim Adams в 2002 году, прогая на MSVC++6.0. В начальных комментариях листинга WinMain.cpp он намекнул, что библиотеки d3dx8.lib, d3d8.lib, winmm.lib и dxguid.lib необходимо явно указывать в списке дополнительных зависимостей линкера (=компоновщика):

Фрагмент WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 6 XFile/Skinned Mesh Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)

Required libraries:
  WINMM.LIB, D3D8.LIB, D3DX8.LIB, and DXGUID.LIB
...

Библиотека WINMM.LIB расположена в папке с установленным Windows SDK (в нашем случае по пути C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib\), пути к которой мы уже прописали выше.
Библиотеки D3D8.LIB, D3DX8.LIB и DXGUID.LIB расположены в папке с установленным DirectX SDK 8 (в нашем случае по пути C:\DXSDK8\lib\), пути к которой мы также прописали выше.
На деле выяснилось, что одна из функций примера также юзает библиотеку d3dxof.lib. Поэтому её тоже добавим. Она также расположена в папке с установленным DirectX SDK 8.
Т.к. мы создаём исполняемое приложение (исполняемый .exe-файл), а не библиотеку, то после компиляции полученный объектный модуль сразу линкуется путём вызова компоновщика (=linker). Так вот, этот самый компоновщик по ранее прописанным каталогам данные библиотеки не ищет. Поэтому их необходимо указывать отдельно в окне настроек Проекта, в разделе "Компоновщик".
ОК, начинаем.

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Ввод (Configuration Properties -> Linker -> Input).
  • В правой части, напротив строки "Дополнительные зависимости" жмём кнопку с чёрным треугольником.
  • В всплывающем списке жмём "Изменить".
  • В появившемся окне "Дополнительные зависимости" в верхнем поле ввода прописываем в столбик (один под другим) имена файлов пяти библиотек:

d3dx8.lib
d3d8.lib
dxguid.lib
d3dxof.lib
Winmm.lib
Image

  • Жмём ОК, ОК.
  • Сохрани Решение (Файл->Сохранить все).

Отключаем использование компоновщиком библиотеки libci.dll

Да, даже на данном этапе при компиляции Проект XFile01 выдаст ошибку. Библиотека использовалась в VisualStudio когда-то очень давно. И в современных версях IDE её давно нет. Тем не менее компоновщик почти всегда вызывает её при компиляции, ругаясь на её отсутствие. Самый простой способ это исправить - запретить использовать libci.dll по умолчанию.
ОК, начнём.

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Командная строка (Configuration Properties -> Linker -> Command Promt).

В правой части, внизу, видим поле ввода "Дополнительные параметры".

  • Пишем в него строку: /NODEFAULTLIB:libci
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

Компилируем Проект XFile01

Наконец, наш тестовый Проект готов к компиляции.

  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 нак лавиатуре.

После компилирования приложение XFile01 автоматически запустится и покажет окно с голубым фоном. Это нормально, т.к. .X-файл и его текстуру пока никто не готовил.

Готовим .X файл

В конце реализации авторской функции DoInit видим функцию загрузки .X-файла warror.x:

Фрагмент WinMain.cpp
...
BOOL DoInit()
{
  D3DPRESENT_PARAMETERS d3dpp;
  D3DDISPLAYMODE        d3ddm;
  D3DXMATRIX matProj, matView;

  // Do a windowed mode initialization of Direct3D
  if((g_pD3D = Direct3DCreate8(D3D_SDK_VERSION)) == NULL)
    return FALSE;
  if(FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
    return FALSE;
  ZeroMemory(&d3dpp, sizeof(d3dpp));
  d3dpp.Windowed = TRUE;
  d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  d3dpp.BackBufferFormat = d3ddm.Format;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
  if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, g_hWnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, &g_pD3DDevice)))
    return FALSE;

  // Set the rendering states
  g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
  g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);

  // Create and set the projection matrix
  D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4.0f, 1.33333f, 1.0f, 1000.0f);
  g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);

  // Create and set the view matrix
  D3DXMatrixLookAtLH(&matView,                                \
                     &D3DXVECTOR3(0.0f, 50.0f, -150.0f),     \
                     &D3DXVECTOR3(0.0f, 50.0f, 0.0f),          \
                     &D3DXVECTOR3(0.0f, 1.0f, 0.0f));
  g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);

  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");

  return TRUE;
...
}
  • Создай в любом 3D-редакторе или скачай с Интернета любой файл модели с расширением .x .

Приложение XFile01 откроет далеко не каждый .X-файл, даже если тот переименовать в warrior.x. Например, файл tiger.x, расположенный в папке с примерами DirectX SDK 8 (в нашем случае c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\) не загружается и призапуске XFile01.exe мы видим всё то же окно с голубым фоном.
В целях экономии времени возьми файл warrior.x из архива с примером от Jim Adams: https://yadi.sk/d/pyoDHEdm1j514g(external link). К сожалению, автор книги не рассказал, с какими сеттингами он создавал данный .X-файл.

  • Помести его в папку с исполняемым файлом Проекта XFile01.

В нашем случае путь до него такой: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\XFile01\Debug

  • Повторно запусти XFile01.exe.

Видим в окне вращающуюся модель без текстуры (модель окрашена в белый цвет).

Готовим текстуру

По умолчанию DirectX-приложения ищут сопутствующую текстуру в том же каталоге, где расположен .X-файл. Причём их имена должны совпадать.

  • Создай в любом графическом редакторе (проще всего в обычном Paint) файл растрового изображения warrior.bmp. Или найди в Интернете.

Желательно небольшого разрешения (например 256х256) и с 256-цветной палитрой.

  • Помести его в папку с исполняемым файлом Проекта XFile01.

В нашем случае путь до него такой: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\XFile01\Debug

  • Повторно запусти XFile01.exe.

Видим в окне вращающуюся модель с нанесённой текстурой.

Ссылка на готовый Проект XFile01

https://yadi.sk/d/pyoDHEdm1j514g(external link) (ZIP-архив)
В архив также включены .X-файл с мешем и .BMP-файл текстуры.

Задание на дом

В папке с примерами (samples) DirectX SDK 8 есть пример "Tut06_Meshes", также демонстрирующий загрузку меша из .X-файла. В нашем случае путь до него такой: c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\.

  • Найди в папке с данным примером файл исходного кода Meshes.cpp.
  • Создай новый Проект Meshes01 в MSVC++2010 и скомпилируй исходный код, содержащийся в Meshes.cpp.

Смотри выше инструкции по подготовке Проекта к компиляции в MSVC++2010 Express Edition.

  • Зрительно сравни исходные коды примеров Джима Адамса и MS DirectX SDK 8, выяви сходства и различия.
  • Подумай, почему модель тигра из данного примера (tiger.x), "подсунутая" приложению XFile01.exe под именем warrior.x не хочет открываться.
  • Проведи обратный эксперимент, "подсунув" скачанные модели приложению, полученному в результате компиляции Проекта Meshes01.

В папке с установленным DirectX SDK 8 есть чудная программа mview.exe, загружающая .X-файлы и показывающая меш в окне. В нашем случае путь до неё такой: c:\DXSDK8\bin\DXUtils\

  • Запусти программу mview.exe.
  • Открой в ней файл warrior.x из примера к данному уроку. (В главном меню жми File->Open Mesh File...)
  • Открой окно иерархии фреймов. (В главном меню жми View->Hierarchy.)

В появившемся окне видим свёрнутое дерево фреймов с ключевым фреймом Unnamed Frame. Слева от его имени видим символ +.

  • Щёлкни левой кнопкой мыши по символу +.

Там будет ещё один дочерний фрейм Bip01 со своим символом +.

  • Последовательно нажимая на символы +, раскрой все ветви иерархии фреймов. Изучи их "говорящие" имена и расположение в иерархии.
  • Проделай те же операции с файлом tiger.x, расположенном в папке с примерами установленного DiretcX SDK 8.

В нашем случае путь до него такой: c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\.
Вероятно, решающее значение для корректного открытия .X файла имеет метод его сохранения из 3D-редактора. Один из примеров корректных сеттингов в окне импорта 3DS Max представлен здесь: http://www.cgdev.net/axe/download.php(external link)

Исследуем код примера

В начале листинга видим пару структур:

  • sMesh - описывает загружаемый меш.
  • sFrame - содержит иерархию фреймов + матрицы трансформаций каждого фрейма.

Загрузка скинированного меша

Функция-обёртка LoadMesh вызывает функцию ParseXFile:

Фрагмент WinMain.cpp
...
sFrame *LoadFile(char *Filename)
{
  return ParseXFile(Filename);
}

sFrame *ParseXFile(char *Filename)
{
  IDirectXFile           *pDXFile = NULL;
  IDirectXFileEnumObject *pDXEnum = NULL;
  IDirectXFileData       *pDXData = NULL;
  sFrame                 *Frame;

  // Create the file object
  if(FAILED(DirectXFileCreate(&pDXFile)))
    return FALSE;

  // Register the templates
  if(FAILED(pDXFile->RegisterTemplates((LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES))) {
    pDXFile->Release();
    return FALSE;
  }
...

Обе функции авторские. Шаблоны фремов (Frame Tamplates), по мере считывания (=энумерации) функцией LoadFile, добавляются в общую иерархию фреймов. Все остальные шаблоны (не являющиеся шаблонами фреймов) также энумерируются в виде серии дочерних (точнее, не имеющих своих потомков) шаблонов.
При обнаружении шаблона меша (Mesh Template) во время энумерирования .X-файла функция LoadFile загружает меш путём вызова DirectX-функции D3DXLoadSkinnedMeshFromXof. Сразу после этого объект меша добавляется в связный список (linked list) загруженных мешей. У каждого фрейма есть указатель на тот или иной загруженный меш. Это позволяет на основе одного меша создавать несколько фреймов.
Как только меш окажется загруженным, функция загрузки автоматически сопоставит кости меша с их соответствующими фреймами и затем загрузит материалы. Если меш не содержит костей, он автоматически конвертируется в стандартный меш (standart mesh).
Таким образом для загрузки скинированного меша (или серии таких мешей) вызывают функцию LoadFile, указав в единственном параметре имя загружаемого .X-файла:

Фрагмент WinMain.cpp
...
  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");

  return TRUE;
}
...

В свою очередь функция LoadFile вызывает функцию ParseXFile, которая возвращает в первую функцию указатель на фрейм, являющийся ключевым (=корневым; root frame).
Сразу после загрузки меша. обновляем (Update) его и рендерим (Render). Обновление меша запрашивает указатель на объект стандартного меша (standart mesh object), который затем также используется для рендеринга (меша).

Обновление и рендеринг скинированного меша (Updating and Rendering a Skinned Mesh)

Прежде чем рендерить скинированный меш его надо сперва обновить (update). Ведь он, на самом деле, не знает, как именно отрисовывать самого себя. Для этого применяют все (родительскую + собственную) матрицы трансформации к каждой из костей и вызывают функцию ID3DXSkinMesh::UpdateSkinnedMesh, передавая ей в качестве аргумента указатель на инстанс объекта ID3DXMesh, представляющий собой итоговую форму меша с применением всех нужных деформаций.
Для изменения трансформации фрейма мы модифицируем её матрицу вращения (frame's rotation transformation matrix). Данной операции часто оказывается достаточно, т.к. дочерние фреймы не могут быть оттрансллированы. Только кключевой фрейм может.
Сперва определяем, какой из фремов будем модифицировать. Для этого сканируем список фреймов и затем путём вызова функции D3DXMatrix* изменяют вращение каждого из них.
Как только вращение фреймов окончено и у каждого установлена нужная ориентация в пространстве, всё готово к обновлению всех трансформаций в иерархии фреймов. Начинают с ключевого (=корневого; root) фрейма, который сориентирован в мире именно там, где будет рендериться меш. Затем матрица трансформации ключевого фрейма передаётся следующему дочернему фрейму и комбинируется с его собственной. Этот процесс продолжается до тех пор, пока матрицы трансформаций всех фреймов не будут обновлены.

3D-анимации меша, хранимые в .X-файлах

3D-анимация:

  • Может храниться в самом .X-файле.

Причём в одном .X-файле может храниться несколько анимаций.

  • Сильно отличается от своей 2D-сестрёнки.

Здесь не прокатит ручная отрисовка каждого кадра с созданием последующей анимации путём быстрой смены кадров. В мире 3D объект может быть виден с любого ракурса.
В основе 3D-анимации лежит изменение матриц трансформации фреймов, используемых для переориентирования мешей с течением времени. Таким образом меши, расположенные во фреймах могут вращаться и изменять своё положение прямо у игрока на глазах. Такое перемщение мешей и является анимацией. Меши в реальном времени могут быть:

  • перемещены (при изменении матрицы трансляции);
  • повёрнуты (при изменении матрицы вращения);
  • увеличены или уменьшены в размерах (при изменении матрицы масштабирования; scal matrix).

При работе со скинированными мешами применение трансформации фреймов - единственный способ анимировать их (меши). Так как скинированный меш является одним (single) сплошным мешем (т.е. не составленным из нескольких мешей), для деформации вершин необходимо изменять фреймы. Самый простой способ изменения матриц трансформации фрейма - техника ключевых кадров (Key Frame Techniques).
Каждая анимация в .X-файле представлена в виде набора специальных шаблонов анимации (animation templates). Загрузка данных из них происходит примерно тем же образом, что и загрузка скинированного меша.
Загружать анимацию из .X-файла весьма непросто. У каждой анимации надо обрабатывать шаблон анимации, шаблон набора анимации (animation set template), временные шаблоны (time templates), шаблоные ключевых кадров (key-frame templates) и т.д.
Шаблоны анимации содержат т.н. ключи (keys), которые применяются в анимации по ключевым кадрам (key frame ainamtion). Каждый ключ представляет собой одну трансформацию:

  • вращение (rotation);
  • изменение масштаба (scaling);
  • translation (изменение положения, перемещение).

Эти ключи представлены в том числе матрицами, которые можно комбинировать (=перемножать), объединяя несколько трансформаций в одну (например для тех случаев, когда 3D-объект двигается + вращается одновременно).
Каждый такой ключ содержит временную отметку, в течение которого он активен. Например, если в ключе вращения (rotation key) проставлена отметка time=0, то на временной отметке 0 будет применено данное значение вращения. А если второй ключ вращения активируется на отметке 200, то, исходя из начального и конечного временных ключей вращения, рассчитываются все промежуточные ориентации объекта в процессе анимации. При достижении анимацией отметки 200, значение вращения будет равно второму ключу вращения (second rotation key). Схожим образом работают все остальные виды анимации.

Закрыть
noteОбрати внимание

Когда мы говорим про время, в нашем случае оно не привязано к какой-либо системе измерения. Игрокодер сам решает, в чём его измерять. Например отрезки времени можно измерять в секундах, прошедших с определённого момента. Временем также может быть прошедшее число кадров. Для упрощения задачи работы со временем его интервалы обычно привязываются к системному времени компьютера, позволяющему отсчитывать равные промежутки времени.

Группы анимаций (Animation sets)

В пределах одного .X-файла несколько анимаций объединяются в группы или наборы. Каждому набору "приписан" определённый фрейм. Возможна ситуация, когда одному набору назначено более одного ключа анимации. Например фрейм может быть изменён набором ключей вращения (set of rotation-keys) и набором ключей трансляции (set of translation-keys) одновременно. К счастью, 3D-редакторы (чаще всего) позволяют не иметь дело с данными анимаций .X-файла напрямую.

Рис.2 Промежуточные ориентации фреймов автоматический интерполируются исходя из данных о начальном и конечном положении 3D-объекта
Рис.2 Промежуточные ориентации фреймов автоматический интерполируются исходя из данных о начальном и конечном положении 3D-объекта

Анимация по ключевым кадрам (Key Frame аnimation)

  • Берутся два различных положения объекта (два разных кадра анимации) и интерполируются (плавно переходят) из одного в другой с течением времени.

При этом компьютер автоматически рассчитывает все промежуточные кадры. В результате зритель видит на экране плавное перемещение/вращение 3D-объекта.

  • Метод широко распространён в 3D-редакторах для создания CG-анимаций (т.е. создании 3D-видеороликов, например, рекламного характера) и даже программах видеомонтажа.
Закрыть
noteЛюбопытный факт

Метод интерполяции широко применяется в математике. Он представляет собой способ расчёта промежуточных значений между двумя числами, в течение определённого промежутка времени. Например, если от дома до твоей работы 2 километра и ты ежедневно добираешься туда за 30 минут, то по этим данным можно определить пройденную дистанцию на каждом конкретном временном участке по следующей формуле:
Distance = (DistanceToWork / TravelTime) * CurrentTime
Путём несложных вычислений определяем, что, спустя 26 минут после начала пути, ты пройдёшь (2 / 30) * 26 = 1,73 км.

Как ты знаешь, иерархия фреймов формируется по принципу присоединения одних фреймов к другим. Добавим сюда вычисления интерполяции промежуточных кадров и получим плавную анимацию. В данной статье мы применяем технику матричных ключевых кадров (matrix key framing). Благодаря применению матричных объектов из библиотеки D3DX техника матричных ключевых кадров реализуется достаточно просто. Допустим, у нас есть две матрицы: Mat1 и Mat2, представляющие собой начальную (starting) и конечную (ending) матрицы соответственно. Временной отрезок представлен как Length, а текущее время - Time (т.е. промежуток от отметки 0 до Length; См. Рис.2). Интерполированная матрица (interpolated matrix) вычисляется так:

// D3DXMATRIX Mat1, Mat2;
// DWORD Length, Time;
D3DXMATRIX MatInt; // Результирующая матрица интерполяции.

// Вычисляем матрицу интерполяции.
MatInt = (Mat2 - Mat1) / Length;

// Умножаем на время.
MatInt *= Time;

// Прибавляем начальную матрицу и готово!
MatInt += Mat1;

В результате получаем матрицу, содержащую ориентацию/положение 3D-объекта на любом промежуточном отрезке времени.

Источники:


1. Jim Adams. Programming Role Playing Games with DirectX 8.0. - Premier Press. 2002

Contributors to this page: slymentat .
Последнее изменение страницы Понедельник 04 / Январь, 2021 14:20:27 MSK автор slymentat.

Помочь проекту

Яндекс-деньги: 410011791055108