DirectX 9 Graphics. Скелетная анимация
Язык программирования: С++
Платформа: Win32
- Есть в любой современной игре жанра 3D Action/FPS1
- Более эффективна с точки зрения экономии памяти.
- Подходит для анимации двуногих мешей (biped), кибернетических механизмов, колёс автомобиля и других объектов.
- Требует всего 1 меш объекта.
- Кодируется с применением матриц.
Содержание
Что собой представляет скелетная анимация?
Представь себе модель игрового персонажа. Скажем, женщину. Мы хотим показать её анимацию ходьбы. Для этого надо прописать изменение положения её ног, рук, корпуса, головы и т.д.Обрати внимание
У любой модели есть анимация "ничегонеделанья" (idle). Как правило, при её проигрывании персонаж стоит, опустив руки вниз. При этом его грудная клетка (торс) периодически едва заметно приподнимается и опускается, имитируя акт дыхания. Существует много других idle-анимаций, вроде почёсывания стволом пистолета головы, позирования, "нервного" потоптывания носком ступни и т.д.
По идее, ходьбу можно анимировать методом интерполяции (путём вызова функции D3DXVec3Lerp). Для этого необходимо сохранить несколько версий меша в разных "стадиях" шага. Но такой метод очень не эффективен с точки зрения производительности. Поэтому был придуман другой - скелетная анимация. В терминологии DirectX анимированный меш называют скинированным (skinned mesh).
Обрати внимание
Внутри .x-файла со скинированным мешем в самом начале всегда содержится неанимированная версия меша. А уже затем следуют данные о скелете. В .x-файлах с нескинированными мешами содержится только неанимированный меш, а данные о скелете отсутствуют.
Скелетные данные представлены в виде т.н. иерархии фреймов. Каждый фрейм (вообще это структура данных, но графически её представляют в виде "проволочной" рамки, похожей на ограничивающий объём), во время подготовки меша к рендерингу, "накладывается" на определённую движимую часть меша. Анимация осуществляется путём матричных преобразований этих самых фреймов, хотя в конечной игре сами они игроку не видны. Это также позволяет перемещать руки, ноги и другие движущиеся части меша, при этом не затрагивая весь меш вцелом.
Меш, с которым мы будем работать в данной статье, хранится в файле Tiny.x, который можно найти в одной из папок установленного DirectX SDK 9. В нашем случае (Win7 x64) его можно найти по следующему пути: C:\Program Files x86\Microsoft DirectX SDK\Samples\Media .
Скинированный меш (Skinned Mesh)
Tiny.x - это файл, содержащий скинированный меш. В любом подобном файле содержатся следующие данные:- 3D-меш
- Информацию о скине (Skin info)
- Иерархию фреймов (Frame Hyerarchy)
В DirectX каждый фрейм хранит свои данные в специальной структуре D3DXFRAME. Имена фреймов совпадают с именами соответствующих костей скелета меша. Каждый фрейм имеет собственную матрицу трансформаций (об этом ниже), хранящую инфу по его (фрейма) положению и ориентации в пространстве. Изменяя матрицы трансформаций фреймов во времени, выполняют анимацию костного скелета, а вместе с ним и всего меша. Для конечного игрока фреймы на анимированных объектах игровой сцены, как правило, всегда невидимы. Но при тестировании игрокодеры иногда включают (через специальный флаг) их видимость, и тогда на меше становятся видны каркасные (wireframe) "габаритные контейнеры" фреймов.
Структура костей скинированного меша
- Состоит из фреймов.
- Её можно просмотреть с помощью программы mview, входящей в состав DirectX SDK 8.1 .
- Найди в Интернете и скачай DirectX SDK 8.1. Найди среди его установочных файлов программу mview.exe и запусти.
- Открой в mview файл Tiny.x (File->Open) или любой другой, со скинированным мешем.
- В программе mview вызови доп. окно просмотра иерархии фреймов (View->Hierarchy) Tiny.x .
- последовательно жми символ + у каждого из родительских фреймов.
Загрузка скинированного меша из .x-файла
Первым делом загружают собственно сам меш + данные по его скину (skin info). Уже затем отдельно загружают и парсят структуру иерархии фреймов костей. А всё из-за того, что скелетов у одного меша может быть несколько: ходячий, стоячий, сидячий и т.д. Согласно замыслу, при проигрывании запрошенной анимации в меш "вставляется" выбранный фрейм-скелет, фреймы которого изменяют этот меш как надо.В общих чертах работа с .x-файлом, содержащим скинированный меш строится следующим образом. Сначала он загружается в память, затем с помощью циклов его "прочёсывают" в поисках требуемых объектов, относящихся к данному мешу.
Любопытно
Формат .x-файлов изначально разрабатывался как максимально универсальный и расширяемый. А всё из-за того, что он построен на основе шаблонов. Помимо предустановленных (т.н. стандартных) шаблонов данных, программер может создавать свои (пользовательские, кастомные) и хранить в них в принципе любые данные и в любой форме, пусть даже они никак не связаны с мешем. Наличие меша при этом внутри .x-файла совсем не обязательно. Теоретически .x-файл может вообще быть без него!
В данной статье мы работаем с .x-файлами моделей и пользуемся стандартными (созданными в Майкрософт) шаблонами данных для работы с ними. В нашем случае наличие меша внутри .x-файла обязательно.
Вот список стандартных шаблонов, поддерживаемых DirectX:
Animation
AnimationKey
AnimationOptions
AnimationSet
AnimTicksPerSecond
Boolean
Boolean2d
ColorRGB
ColorRGBA
Coords2d
DeclData
EffectDWord
EffectFloats
Effectlnstance
EffectParamDWord
EffectParamFloats
Effect ParamString
EffectString
FaceAdjacency
FloatKeys
Frame
FrameTransformMatrix
FVFData
Guid
IndexedColor
Material
MaterialWrap
Matrix4x4
Mesh
MeshFace
MeshFaceWraps
MeshMaterialList
MeshNormals
MeshTextureCoords
MeshVertexColors
Patch
PatchMesh
PatchMesh9
PMAttributeRange
PMInfo
PMVSplitRecord
SkinWeights
TextureFilename
TimedFloatKeys
Vector
VertexDuplicationlndices
VertexElement
XSkinMeshHeader
Примечание
Итак, сперва находим в .x-файле меш (который, напомним, отличается от нескинированного только наличием доп. шаблона skin info) + данные skin info. Затем сразу вызываем функцию D3DXLoadSkinMeshFromXof. Вот её прототип:
HRESULT WINAPI D3DXLoadSkinMeshFromXof ( LPD3DXFILEDATA pxOfMesh, // Указатель на объект .x-файла меша в памяти. DWORD Options, // Опции. Представляют собой один или несколько элементов перечисленияD3DXMESH. // Обычно здесь стоит D3DXMESH_SYSTEMMEM. // Полный список возможных опций см. ниже в Таблице 1. LPDIRECT3DDEVICE9 pD3DDevice, // Указатель на действующий объект устройства Direct3D. LPD3DXBUFFER *ppAdjacency, // Указатель на т.н. "подстроечный" буфер. LPD3DXBUFFER *ppMaterials, // Указатель на буфер материалов. LPD3DXBUFFER *ppEffectInstances, // Указатель на буфер эффектов. Можно выставлять в NULL. DWORD *pMatOut, // Кол-во полученных материалов. LPD3DXSKININFO *ppSkinInfo, // Указатель на (предварительно созданную) структуру по данным скина. LPD3DXMESH *ppMesh // Указатель на загружаемый скинированный меш. );
Перечисление (enumeration) D3DXMESH, содержащее все возможные значения второго параметра функции D3DXLoadSkinMeshFromXof, выглядит так:
typedef enum D3DXMESH { D3DXMESH_32BIT = 0x001, D3DXMESH_DONOTCLIP = 0x0 02, D3DXMESH_POINTS = 0x004, D3DXMESH_RTPATCHES = 0x008, D3DXMESH_NPAT CHES = 0x4 000 , D3DXMESH_VB_SYSTEMMEM = 0x010, D3DXMESH_VB_MANAGED = 0x020, D3DXMESH_VB_WRITEONLY = 0x040, D3DXMESH_VB_DYNAMIC = 0x080, D3DXMESH_VB_SOFTWARE PROCESSING = 0x8000 , D3DXMESH_IB_SYSTEMMEM = 0x100, D3DXMESH_IB_MANAGED = 0x200, D3DXMESH_IB_WRITEONLY = 0x400, D3DXMESH_IB_DYNAMIC = 0x800, D3DXMESH_IB_SOFTWARE PROCESSING = 0x10000, D3DXMESH_VB_S HARE = 0x1000 , D3DXMESH_USEHWONLY = 0x2000, D3DXMESH_SYSTEMMEM = 0x110, D3DXMESH_MANAGED = 0x220, D3DXMESH_WRITEONLY = 0x4 40, D3DXMESH_DYNAMIC = 0x880, D3DXMESH_SOFTWAREPROCESSING = 0x18000 } D3DXMESH, *LPD3DXMESH;
Таблица 1. Описание элементов перечисления D3DXMESH
ЭЛЕМЕНТ | ОПИСАНИЕ |
---|---|
D3DXMESH_32BIT | Меш имеет 32-битные индексы. Теоретически такой меш может состоять максимум из 2 в 32-й степени -1 полигонов. На деле (в случае 32-битной ОС) такие большие меши не применяются. |
D3DXMESH_DONOTCLIP | Отключает клипинг для трансформированных вершин. |
D3DXMESH_POINTS | Указывает на использование меша в качестве системы точечных спрайтов (point sprites; снег, дождь и т.д.). Если видеокарта их не поддерживает, они будут сэмулированы программно. |
D3DXMESH_RTPATCHES | Полигоны меша будут рендериться как примитивы приоритетного порядка (Higher-Order Primitives). Это повышает эффективность интерполяционных фильтров. Часто применяется для анимации и рендеринга персонажей, а также при отрисовке земной или водной поверхности. |
D3DXMESH_NPATCHES | Применяется при рендеринге меша средствами N-patch-расширения Direct3D. |
D3DXMESH_VB_SYSTEMMEM | Вершинный буфер хранится в системной памяти (RAM). |
D3DXMESH_VB_MANAGED | Вершинный буфер хранится в системной памяти (RAM), но автоматически копируется в память видеокарты при необходимости. |
D3DXMESH_VB_WRITEONLY | Вершинный буфер будет доступен только для записи. Увеличивает производительность. При попытке чтения выдаст ошибку. |
D3DXMESH_VB_DYNAMIC | Вершинный буфер выбирает память динамически. Полезно при разработке драйверов устройств, т.к. позволяет им выбирать, где хранить буфер данных. Нельзя применять совместно с флагом D3DPOOL_MANAGED. |
D3DXMESH_VB_SOFTWAREPROCESSING | Программная обработка вершин вершинного буфера. Медленнее, но проделает даже то, что видеокарта не поддерживает. |
D3DXMESH_IB_SYSTEMMEM | Индексный буфер хранится в системной памяти (RAM). |
D3DXMESH_IB_MANAGED | Индексный буфер автоматически копируется в видеопамять при необходимости. |
D3DXMESH_IB_WRITEONLY | Индексный буфер доступен только для записи. |
D3DXMESH_IB_DYNAMIC | Индексный буфер выбирает память динамически. Нельзя применять совместно с флагом D3DPOOL_MANAGED. |
D3DXMESH_IB_SOFTWAREPROCESSING | Программная обработка вершин индексного буфера. Медленнее, но проделает даже то, что видеокарта не поддерживает. |
D3DXMESH_VB_SHARE | Командует клонированным мешам использовать один вершинный буфер. |
D3DXMESH_USEHWONLY | Использовать только аппаратную обработку вершин. Быстрее, но выполнит только те обработки, которые поддерживает видеокарта. |
D3DXMESH_SYSTEMMEM | Аналогично одновременному указанию флагов D3DXMESH_VB_SYSTEMMEM и D3DXMESH_IB_SYSTEMMEM. |
D3DXMESH_MANAGED | Аналогично одновременному указанию флагов D3DXMESH_VB_MANAGED и D3DXMESH_IB_MANAGED. |
D3DXMESH_WRITEONLY | Аналогично одновременному указанию флагов D3DXMESH_VB_WRITEONLY и D3DXMESH_IB_WRITEONLY. |
D3DXMESH_DYNAMIC | Аналогично одновременному указанию флагов D3DXMESH_VB_DYNAMIC и D3DXMESH_IB_DYNAMIC. |
D3DXMESH_SOFTWAREPROCESSING | Аналогично одновременному указанию флагов D3DXMESH_VB_SOFTWAREPROCESSING и D3DXMESH_IB_SOFTWAREPROCESSING. |
Вот пример применения функции D3DXLoadSkinMeshFromXof:
HRESULT Res = D3DXLoadSkinMeshFromXof( m_FileDataObject, D3DXMESH_DYNAMIC, m_pDevice, &m_pAdjacencyBuffer, &m_pMaterialBuffer, NULL, &NumMaterials, &pSkinInfo, &pMesh ); if(SUCCEEDED(Res)) { pMesh->CloneMeshFVF(0, pMesh->GetFVF(), m_pDevice, &pClonedSkinMesh); } pMaterials = (D3DXMATERIAL*)m_pMaterialBuffer->GetBufferPointer(); pTextures = new LPDIRECT3DTEXTURE9[NumMaterials]; for(unsigned int Counter = 0; Counter < NumMaterials; Counter++) { pMaterials[Counter].MatD3D.Ambient = pMaterials[Counter].MatD3D.Diffuse; pTextures[Counter] = NULL; D3DXCreateTextureFromFile(m_pDevice, pMaterials[Counter].pTextureFilename, pTextures[Counter]); } // Если к этому моменту pSkinInfo = NULL, то загружаемый меш не является скинированным // (= не содержит данные по анимации). return pSkinInfo;
В этом фрагменте кода также загружаем материалы и текстуры.
Создаём клон (=дубликат) меша из .x-файла путём вызова функции CloneMeshFVF. Согласно замысла, скелет "вживляется" именно в копию меша. При этом исходный меш, даже будучи загруженным в память, не трогается и служит источником для новых копий (в случае обработки новых анимаций).
Иерархия фреймов
- Третий шаблон данных, содержащийся в .x-файле с анимированным мешем. Первые два - сам меш и его данные скина (skin info) - мы рассмотрели выше.
- Иногда называют "иерархией костей". С точки зрения DirectX это одно и то же.
- Состоит из объектов типа D3DXFRAME, в каждом из которых хранится имя фрейма, отдельная матрица трансформаций + некоторые другие данные.
typedef struct D3DXFRAME { LPSTR Name; // Строка, хранящая имя фрейма. D3DXMATRIX TransformationMatrix; // Матрица трансформации фрейма. // У каждого фрейма своя. LPD3DXMESHCONTAINER pMeshContainer; // Редко, но некоторые фреймы могут заключать // в себе более одного меша, объединённых в один меш-контейнер. // В нашем случае ставим здесь NULL. D3DXFRAME *pFrameSibling; D3DXFRAME *pFrameFirstChild; } D3DXFRAME, *LPD3DXFRAME;
Последние два элемента применяются для отслеживания текущего положения при итерации иерархии фреймов. Обычно иерархию фреймов обслуживает предварительно созданный связный список (Linked List). Это означает, что у каждого фрейма должен быть указатель на следующий фрейм одного с ним уровня (sibling; если есть) + на его первый дочерний фрейм (child frame; если есть).
Проинициализировав структуру D3DXFRAME, можно начинать итерирование (= "прочёсывание" в поисках нужной инфы) иерархии фреймов .x-файла в поисках самих фреймов. Каждый найденный фрейм ставится на определённое место в иерархии. Так выстраивается вся иерархия фреймов.
На практике игрокодерам часто не хватает полей данных, которые способна хранить структура D3DXFRAME, из-за чего её просто переопределяют. Так же поступим и мы, создав на базе структуры D3DXFRAME класс D3DXFRAME2, расширенный и углублённый. Так как в структурах все члены имеют статус public (т.е. доступны из других классов), в нашем классе все члены тоже будут public:
class D3DXFRAME2 : public D3DXFRAME { private: protected: public: D3DXMATRIX matCombined; D3DXMATRIX matOriginal; D3DXFRAME2() { Name = NULL; D3DXMatrixIdentity(&TransformationMatrix); D3DXMatrixIdentity(&matOriginal); D3DXMatrixIdentity(&matCombined); pMeshContainer = NULL; pFrameSibling = pFrameFirstChild = NULL; } ~D3DXFRAME2() { if(pMeshContainer) { delete pMeshContainer; } if(pFrameSibling) { delete pFrameSibling; } if(pFrameFirstChild) { delete pFrameFirstChild; } } D3DXFRAME2* Find(const char* FrameName) { D3DXFRAME2 *pFrame, *pFramePtr; if(Name && FrameName && !strcmp(FrameName, Name)) return this; if(pFramePtr = (D3DXFRAME2*)pFrameSibling) { if(pFrame = pFramePtr->Find(FrameName)) return pFrame; } if(pFramePtr = (D3DXFRAME2*)pFrameFirstChild) { if(pFrame = pFramePtr->Find(FrameName)) return pFrame; } return NULL; } void Reset() { TransformationMatrix = matOriginal; D3DXFRAME2 *pFramePtr = NULL; if(pFramePtr = (D3DXFRAME2*)pFrameSibling) { pFramePtr->Reset(); } if(pFramePtr = (D3DXFRAME2*)pFrameFirstChild) { pFramePtr->Reset(); } } void UpdateHierarchy(D3DXMATRIX *matTransformation = NULL) { D3DXFRAME2 *pFramePtr = NULL; D3DXMATRIX matIdentity; if(!matTransformation) { D3DXMatrixIdentity(&matIdentity); matTransformation = &matIdentity; } matCombined = TransformationMatrix * (*matTransformation); if(pFramePtr = (D3DXFRAME2*)pFrameSibling) { pFramePtr->UpdateHierarchy(matTrans formation); } if(pFramePtr = (D3DXFRAME2*)pFrameFirstChild) { pFramePtr->UpdateHierarchy(matCombined); } } void AddChildFrame(D3DXFRAME2 *Frame) { if(Frame) { if(!pFrameFirstChild) { pFrameFirstChild = Frame; } else { D3DXFRAME* FramePtr = pFrameFirstChild; while(pFramePtr->pFrameSibling) { FramePtr = FramePtr->pFrameSibling; FramePtr->pFrameSibling = Frame; } } } } };
Воу! Наш новый класс теперь может хранить исходную (original) и комбинированную (combined) матрицы трансформаций.
Извлекаем данные об иерархии фреймов из .x-файла
Уточним пару моментов:- В любой иерархии фреймов есть корневой фрейм (top frame). Обычно он ссылается на ключевую кость меша вроде головы (head) или тазовой кости (pelvis).
- Каждый фрейм содержит одну матрицу трансформаций.
- Каждый фрейм может иметь одного и более дочерних фреймов. Может вообще их не иметь. Дочерние фреймы представляют кости, соединённые с костью-родителем (т.е. стоящей выше по иерархии). При движении кость-родитель "увлекает" за собой все свои кости-потомки.
HRESULT ProcessObject(LPD3DXFILEDATA DataObject, D3DXFRAME2 *Parent) { // Если объект данных = NULL, то выходим. if(!DataObject) return E_FAIL; D3DXFRAME2 *Frame = NULL; // Если объект данных - ссылка (reference), то выходим. if(DataObject->IsReference()) return E_FAIL; // Является ли объект данных фреймом (frame)? GUID Type; DataObject->GetType(&Type); if(Type == TID_D3DRMFrame) { Frame = new D3DXFRAME2(); // Получаем имя фрейма. SIZE_T Size=0; DataObject->GetName(NULL, &Size); Frame->Name = new char[Size]; DataObject->GetName(Frame->Name, &Size); // Есть ли у текущего фрейма родитель (Parent Frame)? if(!Parent) { if(m_RootFrame) { // Если фрейм корневой (root), то добавляем к нему фрейм того же уровня (sibling). D3DXFRAME* FramePtr = m_RootFrame; while(FramePtr->pFrameSibling) { FramePtr = FramePtr->pFrameSibling; FramePtr->pFrameSibling = Frame; } } else { m_RootFrame = Frame; // Делаем этот фрейм корневым (root frame). } } else { Parent->AddChildFrame(Frame); // Добавляем текущий фрейм в качестве дочернего (Child frame); } } // Если объект данных - матрица трансформаций. if(Type == TID_D3DRMFrameTransformMatrix) { // Проверяем наличие родительского фрейма, к которому будем добавлять матрицу трансфорамций. if(Parent) { D3DXMATRIX *Matrix = NULL; SIZE_T Size = 0; // Блокируем данные матрицы. DataObject->Lock(&Size, (const void**)&Matrix); if(Size == sizeof(D3DXMATRIX)) { // Копируем матрицу. Parent->TransformationMatrix = *(Matrix); Parent->matOriginal = Parent->TransformationMatrix; } DataObject->Unlock(); } } // Обрабатываем дочерние объекты. SIZE_T ChildCount = 0; DataObject->GetChildren(&ChildCount); for(SIZE_T Counter = 0; Counter < ChildCount; Counter++) { LPD3DXFILEDATA ChildObject = NULL; DataObject->GetChild(&ChildObject); ProcessObject(ChildObject, Frame); } return S_OK; };
В коде выше выясняется, является ли объект фреймом. Для этого его т.н. GUID-имя (текстовый алиас уникального номера, "прописанный" в определённом шаблоне данных .x-файла) сравнивается со стандартным ( = предопределённым в шаблоне Mesh) GUID-именем фрейма - TID_D3DRMFrame. Данное GUID-имя является одним из многих, прописанных в спецификации .x-файла для объекта шаблона данных Mesh (там они всегда прописаны в паре: GUID - GUID-имя). Подробнее о стандартных шаблонах DirectX читай статью Формат файлов X (DirectX?).
Одна из основных переменных в коде выше это m_RootFrame, представляющая собой глобальный указатель типа *D3DXFRAME, указывающий на корневой (самый верхний в иерархии) фрейм. Изначально она установлена в NULL. Функция ProcessObject является рекурсивной (т.к. в конце фрагмента она вызывает саму себя). Для каждого обрабатываемого фрейма результат её выполнения будет передаваться в неё же в качестве второго параметра (Parent), добавляя следующий обрабатываемый фрейм в качестве его потомка (Child). Цикл повторяется вновь для каждого фрейма, обнаруженного в .x-файле.
Примерный алгоритм извлечения иерархии фреймов выглядит так:
- Создаём объект (инстанс класса) D3DXFRAME2.
- Получаем имя фрейма и присваиваем его объекту фрейма (frame object).
- Проверяем, был ли назначен объект фрейма корневому фрейму иерархии (m_RootFrame). Если m_RootFrame=NULL, то в настоящий момент корневой фрейм отсутствует (= пока не обнаружен). Значит его место (обычно временно) займёт фрейм, обрабатываемый в данный момент. Если m_RootFrame не равен NULL, значит статус корневого фрейма уже назначен одному из фрейм-объектов (frame object), обработанных до этого. В этом случае текущий обрабатываемый фрейм назначается в качестве дочернего (child) фрейму, указанному во втором параметре функции ProcessObject (Parent).
- Проверяем наличие родительского фрейма, которому данная матрица принадлежит. Если флаг Parent = NULL, то родительский фрейм отсутствует и присвоение матрицы можно пропустить. Если параметру Parent присвон корректный фрейм, то матрица обрабатывается.
- Блокируем (Lock) данные и проверяем их размер.
- Копируем данные матрицы во фрейм.
- Разблокируем (Unlock) данные и проверяем их размер.
Применение иерархии фреймов к мешу
К этому моменту у нас готовы два отдельных блока скинированного меша:- сам меш,
- иерархия фреймов костей, ассоциированная с ним.
VOID MapFramesToBones() { if(IsSkinnedMesh()) { DWORD NumBones = m_pMeshContainer->pSkinInfo->GetNumBones(); m_pMeshContainer->ppFrameMatrices = new D3DXMATRIX*[NumBones]; m_pMeshContainer->pBoneMatrices = new D3DXMATRIX[NumBones]; for(DWORD Counter = 0; Counter < NumBones; Counter++) { const char* BoneName = m_pMeshContainer->pSkinInfo->GetBoneName(Counter); D3DXFRAME2* FramePtr = m_pFrames->Find(BoneName); if(FramePtr) { m_pMeshContainer->ppFrameMatrices[Counter] = &FramePtr->matCombined; } else { m_pMeshContainer->ppFrameMatrices[Counter] = NULL; } } } }
Код выше для получения кол-ва костей в меше вызывает функцию GetNumBones, экспонированную интерфейсом ID3DXSkinInfo. Затем создаются матрицы для хранения трансформаций ксотей. Далее идёт итерирование через список костей, в ходе которого выясняется имя кости и затем ищется фрейм с тем же именем. В случае успеха сохраняется указатель на матрицу трансформаций данного фрейма для последующего применения.
Обновление меша (Updating the Mesh)
- Выполняется в каждом кадре.
К примеру, в анимации у человекоподобного меша поднимается рука. Это означает, что фрейм руки содержит матрицу, которая корректно спозиционирует эту руку в пространстве. В коде это может выглядеть так:
Update() { m_pFrames->UpdateHierarchy(m_Transform); DWORD NumBones = m_pMeshContainer->pSkinInfo->GetNumBones(); for(DWORD Counter = 0; Counter < NumBones; Counter++) { m_pMeshContainer->pBoneMatrices[Counter] = (*m_pMeshContainer->pSkinInfo->GetBoneOffsetMatrix(Counter)); if(m_pMeshContainer->ppFrameMatrices[Counter]) { m_pMeshContainer->pBoneMatrices[Counter] *= (*m_pMeshContainer->ppFrameMatrices[Counter]); } } void *SrcPtr, *DesPtr; m_pMeshContainer->MeshData.pMesh->LockVertexBuffer(D3DLOCK_READONLY, (void**) &SrcPtr); m_pMeshContainer->pSkinMesh->LockVertexBuffer(0, (void**) &DesPtr); m_pMeshContainer->pSkinInfo->UpdateSkinnedMesh(m_pMeshContainer->pBoneMatrices, NULL, SrcPtr, DesPtr); m_pMeshContainer->MeshData.pMesh->UnlockVertexBuffer(); m_pMeshContainer->pSkinMesh->UnlockVertexBuffer(); }
В коде выше первым делом вызываем функцию Update на корневом фрейме (Root frame) иерархии, передавая в параметре его (фрейма) матрицу трансформаций. Корневой фрейм обновляется и рекурсивно передаёт его трансформацию всем нижестоящим фреймам.
Далее вызываем функцию GetNumBones для получения общего кол-ва костей в меше. После этого итерируем через все кости меша, выявляя в каждой её матрицу смещения (Offset Matrix) путём вызова функции GetBoneOffsetMatrix (оба метода экспонированы интерфейсом ID3DXSkinInfo). Эти матрицы указывают на места "соединения" костей друг с другом. Далее на каждой кости применяется матрица трансформаций.
На данном этапе своё положение (+ориентацию) обновили только кости. Но не меш! Для обновления позы меша (согласно обновлённому положению его костей) сперва блокируют вершинный буфер для обоих мешей (загруженного оригинала и его клона) и затем вызывается функция UpdateSkinnedMesh (экспонированы интерфейсом ID3DXSkinInfo). Вот её прототип:
HRESULT UpdateSkinnedMesh( const D3DXMATRIX *pBoneTransforms, // Указатель на массив матриц трансформаций. // Это матрицы трансформаций костей. const D3DXMATRIX *pBoneInvTransposeTransforms, // Инвертированное преобразование матрицы // трансформации костей. Обычно NULL. LPCVOID pVerticesSrc, // Указатель на вершины меша-источника. PVOID pVerticesDst // Указатель на вершины меша-получателя. );
Рендеринг меша
- Выполняется в каждом кадре. Здесь меш отрисовывается на экране. Скинированный меш рендерится точно так же, как и нескинированный (=статичный):
bool CXSkinnedMesh::Render() { if(m_pMeshContainer->pSkinMesh) { Update(); for(unsigned int Counter = 0; Counter < m_pMeshContainer->NumMaterials; Counter++) { m_pDevice->SetMaterial(&m_pMeshContainer->pMaterials[Counter].MatD3D); m_pDevice->SetTexture(0, m_pMeshContainer->pTextures[Counter]); m_pMeshContainer->pSkinMesh->DrawSubset(Counter); } return true; } return false; }
Анимация. Приводим меш в движение
Меш приходит в движение вслед за своим скелетом. Поэтому задача сводится к указанию скелету нужных поз в определённые промежутки времени.Анимация по ключевым кадрам (Keyframe animation)
Анимация по ключевым кадрам - это эффективный способ создания и воспроизведения анимаций. В общих чертах она заключается в сохранении поз скелета на различных промежутках временной шкалы. При проигрывании анимации все промежуточные положения скелета просчитываются автоматически (методом интерполяции). То есть не требуется указывать и сохранять позу скелета в каждои кадре. Можно лишь в некоторых. Остальные просчитает компьютер.Анимируем скелет
ОК, меш со своей иерархией загружен и показан на экране. Но сейчас он статичен. Застыл на экране в одной позе. В теории можно вручную рассчитать изменение матриц трансформаций всех фреймов. Но это долго и неэффективно. Куда удобнее применить анимацию по ключевым кадрам (keyframe animation). Обычно они создаются в 3D-редакторах и экспортируются вместе с мешем в одном .x-файле. Это значит, что для одного меша (и его скелета) можно создать несколько разных анимаций, которые затем можно запрашивать и воспроизводить в игре. Например, можно создать анимацию по ключевым кадрам, в которой скелет меша прыгает. А затем другую, где скелет бежит. Позднее любую из них можно вызывать при необходимости и воспроизводить на загруженном меше. Такой подход позволяет менять анимации меша буквально "на лету", например по нажатию кнопки на клавиатуре.До начала анимации нашего меша создадим несколько служебных классов для хранения данных анимации (animation data). Например тех же самых ключевых кадров (keyframes).
Отдельно мы должны разработать класс, хранящие все данные по ключевым кадрам (keyframe data), извлечённые из .x-файла.
Любопытно
Физически меш и его ключевые кадры могут храниться как в одном .x-файле, так и в разных.
Кроме того мы закодим контроллер анимации (animation controller). Это будет нечто вроде медиапроигрывателя, которые включает и останавливает воспроизведение анимации по команде. В классах анимации ключевых кадров будут инкапсулированы следующие данные:
Ключевой кадр (Keyframe)
- Записывает состояние (state) определённой части меша в указанный момент анимации.
- состояние (положение) кости предплечья,
- отметка времени, в котором кость приняла данное положение.
class CMatrixKey { pivate: protected: public: DWORD m_Time; // Время смены позы. D3DXMATRIX m_Matrix; // Зафиксированное положение. }
Анимация
В контексте скелетной анимации, анимация представляет собой набор относительных (related) ключевых кадров (keyframes). В нашем случае - это набор ключевых кадров не всего тела меша, а лишь определённой его части. Поэтому отдельный набор ключевых кадров формирует анимацию, допустим, кости предплечья. Другой - отдельную анимацию кости бедра и т. д.Авторский класс CAnimation служит для инкапсуляции данных по одной отдельно взятой анимации:
class CAnimation { private: protected: public: CAnimation *m_Next; D3DXFRAME *m_AnimBone; DWORD m_Length; CMatrixKey *m_Keys; DWORD m_NumKeys; char* m_Name; CAnimation() { m_Next = NULL; m_AinmBone = NULL; m_Length = m_NumKeys = 0; m_Keys = NULL; m_Name = NULL; } ~CAnimation() { delete [] m_Keys; } };
Класс CAnimation хранит указатель на массив экземпляров классов ключевых кадров CMatrixKey.
Набор анимации (Animation Set)
- Представляет собой набор (collection), содержащий одну или несколько анимаций. Вот авторский класс, инкапсулирующий набор анимаций:
class CAnimationSet { private: protected: public: CAnimationSet *m_Next; CAnimation *m_Animations; DWORD m_NumAnimations; char* m_Name; CAnimationSet() { m_Next = NULL; m_Name = NULL; m_Animations = NULL; m_NumAnimations = 0; } ~CAinmationSet() HRESULT AddAnimationObject(CAnimation *Anim) { if(!Anim) return E_FAIL; if(!m_Ainmations) { m_Animations = Anim; } else { CAnimation *Anims = m_Animations; while(Anims->m_Next) { Anims = Anims->m_Next; } Anims->m_Next = Anim; } m_NumAnimations++; return S_OK; } };
Загрузка анимаций
- Представляет собой процесс поиска в .x-файле шаблонов ключевых файлов и анимаций + построение иерархии анимаций (animation hierarchy) с помощью авторского класса, который рассмотрим в данной главе.
Вот пример реализации функции загрузки анимаций:
HRESULT LoadFromFile(char *File) { if(!FileExists(File)) { return E_FAIL; } LPD3DXFILE pFile = NULL; LPD3DXFILEENUMOBJECT Enum = NULL; if(SUCCEEDED(D3DXFileCreate(&pFile))) { pFile->RegisterTemplates(D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES); if(SUCCEEDED(pFile->CreateEnumObject(File, D3DXF_FILELOAD_FROMFILE, &Enum))) { SIZE_T Size=0; Enum->GetChildren(&Size); for(SIZE_T Counter = 0; Counter < Size; Counter++) { LPD3DXFILEDATA DataObject = NULL; Enum->GetChild(Counter, &DataObject); ProcessItems(DataObject); DataObject->Release(); } Enum->Release(); } pFile->Release(); } return S_OK; }
Код выше открывает .x-файл и поочерёдное просматривает все объекты верхнего уровня (top level objects) путём вызова функции ProcessItems. Авторская функция ProcessItems методом рекурсии создаёт иерархию анимаций:
HRESULT ProcessItems(LPD3DXFILEDATA Object, CAnimationSet *AnimSet = NULL, CAnimation *Anim = NULL) { if(!Object) return E_FAIL; CAnimationSet *AnimationSet = AnimSet; CAnimation *Animation = Anim; GUID guid; Object->GetType(&guid); if((guid == TID_D3DRMAnimationSet) && (!Object->IsReference())) { SIZE_T Length = 0; AnimationSet = new CAnimationSet(); Object->GetName(NULL, &Length); AnumationSet->m_Name = new char[Length]; Object->GetName(AnimationSet->m_Name, &Length); // Добавляем набор анимации. if(!m_AnimationSets) { m_AninmationSets = AnimationSet; } else { CAnimationSet *AnimSets = m_AnimationSets; while(AnimSets->m_Next) { AnimSets = AnimSets->m_Next; } AnimSets->m_Next = AnimationSet; } m_AnimationSets++; } if(guid == TID_D3DRMAnimation) && (!Object->IsReference())) { if(AnimSet) { SIZE_T Length = 0; Animation = new CAnimation(); Object->GetName(NULL, &Length); Animation->m_Name = new char[Length]; Object->GetName(Animation->m_Name, &Length); AnimSet->AddAnimationOnject(Animation); ProcessKeyFrames(Object, AnimationSet, Animation); } } SIZE_T size = 0; Object->GetChildren(&Size); for(SIZE_T Counter = 0; Counter < Size; Counter++) { LPD3DXFILEDATA DataObject = NULL; Object->GetChild(Counter, &DataObject); ProcessItems(DataObject, AnimationSet, Animation); DataObject->Release(); } return S_OK; }
Функция ProcessItems выполняет следующие операции:
- Циклично просматривает каждый объект данных (data object) .x-файла в поисках набора анимации (animation set; в качестве шаблона поиска указано стандартное GUID-имя TID_D3DRMAnimationSet).
- Раз уж нашли набор анимации, то настало время искать в нём объекты анимации (animation objects).
HRESULT ProcessKeyFrames(LPD3DXFILEDATA Object, CAnimationSet *AnimSet = NULL, CAnimation = *Anim = NULL) { if((!Object) || (!AnimSet) || (!Anim)) return E_FAIL; SIZE_T size = 0; Object->GetChildren(&Size); for(SIZE_T Counter = 0; Counter < Size; Counter++) { LPD3DXFILEDATA DataObject = NULL; Object->GetChild(Counter, &DataObject); GUID guid; DataObject->GetType(&guid); if((guid == TID_D3DRMFrame) && (!Object->IsReference())) { SIZE_T Length = 0; char *Name = NULL; DataObject->GetName(NULL, &Length); Name = new char[Length]; DataObject->GetName(Name, &Length); Anim->m_AnimBone = m_Frames->Find(Name); delete [] Name; } if((guid == TID_D3DRMAnimationKey) && (!Object->IsReference())) { DWORD *DataPtr = NULL; SIZE_T BufferSize = 0; if(SUCCEEDED(DataObject->Lock(&BufferSize, (const void**) &DataPtr))) { DWORD Type = *DataPtr++; DWORD NumKeys = *DataPtr++; if(Type == 4) // Если ключ матрицы { CMatrixKey *Key = new CMatrixKey[NumKeys]; for(DWORD Counter = 0; Counetr < Numkeys; Counter++) { Key[Counter].m_Time = *DataPtr++; if(Key[Counter].m_Time > Anim->m_Length) { Anim->m_Length = Key[Counter].m_Time; } DataPtr++; D3DXMATRIX *mPtr = (D3DXMATRIX*) DataPtr; Key[Counter].m_Matrix = *mPtr; DataPtr+=16; } Anim->m_Keys = Key; Anim->m_NumKeys = NumKeys; } } DataObject->Unlock(); ProcessKeyFrames(DataObject, AnimSet, Anim); } DataObject->Release(); } return S_OK; }
Функция ProcessKeyFrames получает объект данных из .x-файла (в нашем случае - это шаблон анимации) и циклически обрабатывает все его дочерние объекты. При каждой итерации происходит следующее:
- Проверяется, найден ли объект типа фрейм (frame).
Далее мы просматриваем (предварительно загруженную) иерархию фреймов меша и находим меш с данным именем, после чего сохраняем ссылку (reference) на него (фрейм) в нашем объекте анимации (animation object). Это позволит затем обращаться к нему при обновлении матриц в процессе анимации.
- В случае обнаружения ключей анимации (= ключевых кадров), первым делом запрашиваем их общее количество (ассоциированное с данной костью и анимацией).
По завершении рекурсивного выполнения функции ProcessKeyFrames все данные анимации считаны из .x-файла в иерархию. На данном этапе анимация полностью загружена в память. Осталось её лишь воспроизвести.
Воспроизведение анимаций (Playing animations)
- Относительно несложная операция.
- наборов анимаций,
- анимаций,
- ключевых кадров (данной) анимации.
- (относительное от начала анимации) временное значение т.е. временная отметка, когда кость окончательно примет данное положение,
- матрица трансформаций, описывающая конкретные трансформации на указанной временной отметке.
VOID Update() { m_CurrentTime = timeGetTime() - m_StartTime; if(!m_AnimationSets) return; CAnimationSet *Sets = m_AnimationSets; while(Sets) { CAnimation *Animation = Sets->m_Animations; while(Animation) { CMatrixKey *Keys = Animation->m_Keys; CMatrixKey *StartKey = NULL; CMatrixKey *EndKey = NULL; for(DWORD Counter = 0; Counter < Animation->m_NumKeys; Counter++) { if(m_CurrentTime >= Keys[Counter].m_Time) { StartKey = &Keys[Counter]; } else { EndKey = &Keys[Counter]; } } if(!EndKey) { Animation->m_AnimBone->TransformationMatrix = StartKey->m_Matrix; } else { DWORD TimeDifference = EndKey->m_Time - StartKey->m_Time; float Scalar = (float) (m_CurrentTime - StartKey->m_Time)/TimeDifference; D3DXMATRIX Matrix = EndKey->m_Matrix - StartKey->m_Matrix; Matrix *= Scalar; Matrix += StartKey->m_Matrix; Animation->m_AnimBone->TransformationMatrix = Matrix; } Animation = Animation->m_Next; } Sets = Sets->m Next; } }
Функция Update вызывается в каждом кадре, обновляя скинированный меш, согласно текущей временной отметке анимации. Сперва функция итерирует через наборы анимаций (animation sets; в нашем случае такой набор всего один). Затем итерирует через объекты анимации (animation objects). Затем через ключевые кадры (keyframes). Выяснив значения временных отметок каждого ключевого кадра, функция определяет (используя отметку текущей позиции анимации), между какими двумя ключевыми кадрами расположен "курсор" воспроизведения в данный момент. Сразу после этого временной интервал между двумя ключевыми кадрами представляется в виде скалярной величины, изменяющейся от 0 до 1. Данный показатель эффективно определяет величину интерполяции двух матриц (= матриц двух ключевых кадров). На основе двух матриц высчитывается третья, которая и применяется к скинированному мешу. Данный процесс повторяется для каждой кости меша.
После этого меш принял нужную позу и готов к рендерингу.
Источники
1. Thorn A. DirectX 9 graphics: the definitive guide to Direct3D. - Wordware Publishing, 2005
Последние изменения страницы Понедельник 13 / Июнь, 2022 12:06:24 MSK
Последние комментарии wiki