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

DirectX 9 Graphics. Скелетная анимация


Язык программирования: С++
Платформа: Win32

  • Есть в любой современной игре жанра 3D Action/FPS1
  • Более эффективна с точки зрения экономии памяти.
  • Подходит для анимации двуногих мешей (biped), кибернетических механизмов, колёс автомобиля и других объектов.
  • Требует всего 1 меш объекта.
  • Кодируется с применением матриц.



Что собой представляет скелетная анимация?

Представь себе модель игрового персонажа. Скажем, женщину. Мы хотим показать её анимацию ходьбы. Для этого надо прописать изменение положения её ног, рук, корпуса, головы и т.д.
Закрыть
noteОбрати внимание

У любой модели есть анимация "ничегонеделанья" (idle). Как правило, при её проигрывании персонаж стоит, опустив руки вниз. При этом его грудная клетка (торс) периодически едва заметно приподнимается и опускается, имитируя акт дыхания. Существует много других idle-анимаций, вроде почёсывания стволом пистолета головы, позирования, "нервного" потоптывания носком ступни и т.д.

По идее, ходьбу можно анимировать методом интерполяции (путём вызова функции D3DXVec3Lerp). Для этого необходимо сохранить несколько версий меша в разных "стадиях" шага. Но такой метод очень не эффективен с точки зрения производительности. Поэтому был придуман другой - скелетная анимация. В терминологии DirectX анимированный меш называют скинированным (skinned mesh).
Закрыть
noteОбрати внимание

Внутри .x-файла со скинированным мешем в самом начале всегда содержится неанимированная версия меша. А уже затем следуют данные о скелете. В .x-файлах с нескинированными мешами содержится только неанимированный меш, а данные о скелете отсутствуют.

Скелетные данные представлены в виде т.н. иерархии фреймов. Каждый фрейм (вообще это структура данных, но графически её представляют в виде "проволочной" рамки, похожей на ограничивающий объём), во время подготовки меша к рендерингу, "накладывается" на определённую движимую часть меша. Анимация осуществляется путём матричных преобразований этих самых фреймов, хотя в конечной игре сами они игроку не видны. Это также позволяет перемещать руки, ноги и другие движущиеся части меша, при этом не затрагивая весь меш вцелом.
Меш, с которым мы будем работать в данной статье, хранится в файле Tiny.x, который можно найти в одной из папок установленного DirectX SDK 9. В нашем случае (Win7 x64) его можно найти по следующему пути: C:\Program Files x86\Microsoft DirectX SDK\Samples\Media .

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

Tiny.x - это файл, содержащий скинированный меш. В любом подобном файле содержатся следующие данные:
  • 3D-меш
Собственно меш. Он экспортируется из 3D-редактора вместе с вершинами, материалами и текстурами. Для работы с ним в Direct3D предусмотрен интерфейс ID3DXMesh.
  • Информацию о скине (Skin info)
Все .x-файлы со скинированными мешами, помимо самого меша, обязательно содержат специальную структуру данных skin info. Она хранит информацию о специальных объектах - так называемых "костях" (bones). Каждая кость представляет одну из "частей тела" меша (руки, ноги, торс и т.д.). Для работы со структурой костяного скелета меша в Direct3D предусмотрен интерфейс ID3DXSkinInfo.
  • Иерархию фреймов (Frame Hyerarchy)
Фреймы - это специальные объекты, каждый из которых оснащён собственной матрицей трансформаций. Обычно фреймы "размещаются" на месте соответствующих костей меша и отвечают за трансформации и позиционирование его определённых участков. То есть каждой кости сопоставлен один фрейм.
В DirectX каждый фрейм хранит свои данные в специальной структуре D3DXFRAME. Имена фреймов совпадают с именами соответствующих костей скелета меша. Каждый фрейм имеет собственную матрицу трансформаций (об этом ниже), хранящую инфу по его (фрейма) положению и ориентации в пространстве. Изменяя матрицы трансформаций фреймов во времени, выполняют анимацию костного скелета, а вместе с ним и всего меша. Для конечного игрока фреймы на анимированных объектах игровой сцены, как правило, всегда невидимы. Но при тестировании игрокодеры иногда включают (через специальный флаг) их видимость, и тогда на меше становятся видны каркасные (wireframe) "габаритные контейнеры" фреймов.

Структура костей скинированного меша

  • Состоит из фреймов.
  • Её можно просмотреть с помощью программы mview, входящей в состав DirectX SDK 8.1 .
Подробнее об mview читаем здесь: Anim8or. Моделинг и экспорт в .x формат .
  • Найди в Интернете и скачай DirectX SDK 8.1. Найди среди его установочных файлов программу mview.exe и запусти.
  • Открой в mview файл Tiny.x (File->Open) или любой другой, со скинированным мешем.
Меш отобразится в главном окне.
  • В программе mview вызови доп. окно просмотра иерархии фреймов (View->Hierarchy) Tiny.x .
Появится окно, отображающее древовидную структуру иерархии фреймов. Изначально оно свёрнуто (collapsed). Для развёртывания узлов дерева...
  • последовательно жми символ + у каждого из родительских фреймов.
Все узлы и ветви фреймов (читай костей) соответствуют определённым "частям тела" меша (руки, запястья, кисти, ноги, ступни, голова, шея и т.д.), которые (в перспективе) будут анимированы. По сути здесь представлен костяной скелет, размещаемый внутри меша, по аналогии со скелетом любого позвоночного животного. Все кости скелета находятся в отношениях "родитель-потомок". Есть кости без потомков (окончание ветви), есть без родителя (вершина иерархии; сам меш). Это обеспечивает связность нижестоящих костей с вышестоящими. К примеру, когда человек поднимает руку, он поднимает предплечье. Остальные нижестоящие части руки (запястье, ладонь) тоже вовлекаются в это движение и перемещаются вслед за предплечьем. В скелетной анимации всё устроено аналогичным образом: при перемещении/вращении предплечья (upper arm) скелета, рука, запястье и ладонь также будут вовлечены в это преобразование и их положение/ориентация тоже изменятся. Само собой это также изменит геометрию конечности меша. Ведь кость предплечья не сможет переместиться, оставив мышцы и кожу (под которыми она скрыта) в исходном положении. Всё как в реальной жизни.

Загрузка скинированного меша из .x-файла

Первым делом загружают собственно сам меш + данные по его скину (skin info). Уже затем отдельно загружают и парсят структуру иерархии фреймов костей. А всё из-за того, что скелетов у одного меша может быть несколько: ходячий, стоячий, сидячий и т.д. Согласно замыслу, при проигрывании запрошенной анимации в меш "вставляется" выбранный фрейм-скелет, фреймы которого изменяют этот меш как надо.
В общих чертах работа с .x-файлом, содержащим скинированный меш строится следующим образом. Сначала он загружается в память, затем с помощью циклов его "прочёсывают" в поисках требуемых объектов, относящихся к данному мешу.
Закрыть
noteЛюбопытно

Формат .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


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

По теме .x-файлов на Игрокодере есть хорошая статья Формат файлов X (DirectX).


Итак, сперва находим в .x-файле меш (который, напомним, отличается от нескинированного только наличием доп. шаблона skin info) + данные skin info. Затем сразу вызываем функцию D3DXLoadSkinMeshFromXof. Вот её прототип:
Прототип функции 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, выглядит так:
Перечисление Перечисление D3DXMESH
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, в каждом из которых хранится имя фрейма, отдельная матрица трансформаций + некоторые другие данные.
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).
  • Каждый фрейм содержит одну матрицу трансформаций.
  • Каждый фрейм может иметь одного и более дочерних фреймов. Может вообще их не иметь. Дочерние фреймы представляют кости, соединённые с костью-родителем (т.е. стоящей выше по иерархии). При движении кость-родитель "увлекает" за собой все свои кости-потомки.
Применив созданный выше класс D3DXRAME2, создадим иерархию фреймов, на основе данных, полученных из .x-файла:
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-файле.
Примерный алгоритм извлечения иерархии фреймов выглядит так:
  1. Создаём объект (инстанс класса) D3DXFRAME2.
  2. Получаем имя фрейма и присваиваем его объекту фрейма (frame object).
  3. Проверяем, был ли назначен объект фрейма корневому фрейму иерархии (m_RootFrame). Если m_RootFrame=NULL, то в настоящий момент корневой фрейм отсутствует (= пока не обнаружен). Значит его место (обычно временно) займёт фрейм, обрабатываемый в данный момент. Если m_RootFrame не равен NULL, значит статус корневого фрейма уже назначен одному из фрейм-объектов (frame object), обработанных до этого. В этом случае текущий обрабатываемый фрейм назначается в качестве дочернего (child) фрейму, указанному во втором параметре функции ProcessObject (Parent).
Другой важной особенностью функции ProcessObject является то, что она назначает матрицу обрабатываемому фрейму. Из кода видно, что матрица фрейма хранится в качестве его отдельного дочернего объекта. При парсинге .x-файла у каждого фрейма также проверяется наличие матрицы. В случае обнаружения матрицы, функция ProcessObject делает следующее:
  1. Проверяем наличие родительского фрейма, которому данная матрица принадлежит. Если флаг Parent = NULL, то родительский фрейм отсутствует и присвоение матрицы можно пропустить. Если параметру Parent присвон корректный фрейм, то матрица обрабатывается.
  2. Блокируем (Lock) данные и проверяем их размер.
  3. Копируем данные матрицы во фрейм.
  4. Разблокируем (Unlock) данные и проверяем их размер.

Применение иерархии фреймов к мешу

К этому моменту у нас готовы два отдельных блока скинированного меша:
  • сам меш,
  • иерархия фреймов костей, ассоциированная с ним.
В коде они пока никак не связаны друг с другом. Пробил час начать итерацию через иерархию фреймов и ассоциировать каждый фрейм с определённой костью меша. Другими словами, необходимо обработать каждый фрейм и убедиться, что он сопоставлен определённой кости меша. Так мы свяжем меш и предварительно подготовленную иерархию фреймов. Для доступа к данным о скелете меша мы обращаемся к интерфейсу ID3DXSkinInfo, который загружается вместе с мешем и содержит полный набор данных о строении скелета и его расположении внутри меша. Рассмотрим авторскую функцию, сопоставляющую фреймы костям меша:
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). Вот её прототип:
Прототип функции UpdateSkinnedMesh
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-файла.
Закрыть
noteЛюбопытно

Физически меш и его ключевые кадры могут храниться как в одном .x-файле, так и в разных.

Кроме того мы закодим контроллер анимации (animation controller). Это будет нечто вроде медиапроигрывателя, которые включает и останавливает воспроизведение анимации по команде. В классах анимации ключевых кадров будут инкапсулированы следующие данные:

Ключевой кадр (Keyframe)


Image
Рис.1 Структура будущего класса контроллера анимации


  • Записывает состояние (state) определённой части меша в указанный момент анимации.
Например, если во время анимации поднимается кость предплечья, то для этого в исходной и конечной точках временной шкалы будут созданы ключевын кадры, в которых записаны:
  • состояние (положение) кости предплечья,
  • отметка времени, в котором кость приняла данное положение.
Класс может хранить в себе инфу о ключевом кадре, его временной отметке (значение типа DWORD) и указатель на специальную матрицу, которая кодирует трансформацию данной кости в данный момент времени. Его примерная структура показана на Рис.1 . Вот его код:
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) с помощью авторского класса, который рассмотрим в данной главе.
Как только в памяти окажется сконструирована корректная иерархия анимаций, можно смело начинать процесс анимирования скинированного меша. В нашем случае код загрузки анимаций и код для их воспроизведения будут содержаться в одном классе, который мы назовём CAnimationController.
Вот пример реализации функции загрузки анимаций:
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).
Всякий раз, когда находим, создаём объект типа AnimationSet. Далее, созданному на базе найденного, набору анимации присваиваются имя и он добавляется в общий список наборов анимаций m_AnimationSets. В нашем случае в этом списке будет всего один набор анимации, т.к. в .x-файле он также один, анимирующий ходьбу. Но вообще обычно их больше. Далее обработка продолжается и у найденного набора анимации ищутся дочерние объекты, содержащие в свою очередь объекты ключевых кадров (keyframe objects). В конце реализации функция ProcessItems вызывает саму себя (т.е. выполняется рекурсивно). В качестве одного из параметров она в саму себя передаёт указатель на только что подготовленный набор анимации. Таким образом каждый "прогон" функции ProcessItems имеет доступ к ранее созданным наборам анимации. В будущем это ощутимо поможет при добавлении в (родительский) набор анимации дочерних объектов.
  • Раз уж нашли набор анимации, то настало время искать в нём объекты анимации (animation objects).
Всякий раз, при обнаружении объекта по GUID-имени TID_D3DRMAnimation, создаётся локальный объект анимации. Ему также присваивается имя и он добавляется в набор анимации. После этого вызывается авторская функция ProcessKeyFrames, которая берёт шаблон из .x-файла, находит все дочерние шаблоны ключевых кадров (child keyframe templates) и для каждого из них создаёт локальный объект:
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).
Напомним, каждый фрейм сопоставлен определённой кости (выбранного) скелета меша. Если найден фрейм, то по логике не найдены ключевые кадры (keyframes). Но каждый найденный фрейм говорит нам о том, какой кости скелета предназначена данная анимация. К примеру, если мы обнаружили фрейм с именем "предплечье" (forearm), то в этом случае мы знаем, что данная анимация и все её ключевые кадры относятся к данному фрейму предплечья. Именно поэтому при обнаружении любого фрейма и добавлении его в общую иерархию первым делом извлекают его имя.
Далее мы просматриваем (предварительно загруженную) иерархию фреймов меша и находим меш с данным именем, после чего сохраняем ссылку (reference) на него (фрейм) в нашем объекте анимации (animation object). Это позволит затем обращаться к нему при обновлении матриц в процессе анимации.
  • В случае обнаружения ключей анимации (= ключевых кадров), первым делом запрашиваем их общее количество (ассоциированное с данной костью и анимацией).
Далее инициализируем новый указатель ключевых кадров (keys pointer; m_Keys) данного объекта анимации для их (ключевых кадров) хранения. Затем блокируем (Lock) буфер даннх и копируем туда ключевые кадры из .x-файла.
По завершении рекурсивного выполнения функции ProcessKeyFrames все данные анимации считаны из .x-файла в иерархию. На данном этапе анимация полностью загружена в память. Осталось её лишь воспроизвести.

Воспроизведение анимаций (Playing animations)

  • Относительно несложная операция.
Напомним, что анимация скинированного меша состоит из трёх основных элементов:
  • наборов анимаций,
  • анимаций,
  • ключевых кадров (данной) анимации.
Каждый объект анимации (animation object) сопоставлен (relates) определённой кости меша. Каждый ключевой кадр объекта анимации ссылается на определённое положение кости на некоторой временной отметке. Другими словами, в каждом ключевом кадре анимации хранится:
  • (относительное от начала анимации) временное значение т.е. временная отметка, когда кость окончательно примет данное положение,
  • матрица трансформаций, описывающая конкретные трансформации на указанной временной отметке.
В то же время в любой анимации будут временные отметки, несовпадающие ни с одной из тех, что хранятся в ключевых кадрах. Но будут расположены между ними. Например, в анимации может быть ключевой кадр на отметке 0 (секунд), а другой - на отметке 100 миллисекунд. Допустим, текущая отметка воспроизведения анимации - 50 миллисекунд. То есть воображаемый "курсор" воспроизведения находится как раз посередине между двумя ключевыми кадрами. В этом случае в расчёт принимаются обе матрицы из ключевоых кадров 0 и 100 (миллисекунд) и положение между двумя ключевыми кадрами просто интерполируется. По ходу (= с течением времени) анимации доля матрицы ключевого кадра на отметке 0 уменьшается. Ключевого кадра на отметке 100 миллисекунд - напротив, увеличивается. Подобным образом обрабатываются все ключевые кадры на каждом временной мпромежутке. Причём это делается строго перед обновлением анимации. Авторская функция Update обновляет данные меша, готовя его к рендерингу:
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

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++