Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.14 Полигональные сетки и материалы
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.14 Полигональные сетки (меши) и материалы


В этой Главе:

  • Разработаем систему поддержки материалов, основанную на скриптах.
  • Обсудим полигональные сетки (meshes, меши) и набросаем систему загрузки мешей.
  • Рассмотрим ещё несколько служебных классов (utility classes), призванных помочь в обработке и рендеринге 3D-геометрии.


Содержание

Пробил час сделать всё необходимое, для того, чтобы наш движок мог рендерить полигональные сетки (меши) с текстурами, наложенными на них. Тема непростая. Поэтому рекомендуется дополнительно просмотреть соответствующие разделы документации к DirectX SDK.

Материалы (materials)

Обычно мы начинаем каждую Главу с теоретических сведений. Но здесь мы сделаем исключение и сразу перейдём к разработке системы поддержки материалов.
В Главе 1.5 мы обсуждали текстуры и способы их наложения на трёхмерные объекты. Материалы представляют собой нечто большее, чем просто текстуры. Они также накладываются на грани 3D-модели, но, вместе с тем, несут в себе дополнительные свойства, связанные с данной 3D-поверхностью.

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

Прежде чем двигаться дальше, уточним некоторые моменты. Direct3D также использует материалы в виде структуры D3DMATERIAL9, которая используется Direct3D для определения того, как отражаемый свет распространяется по поверхности грани. В данной структуре могут описываться способы отражения различных источников света, а также свойства собственного свечения объекта. Система поддержки материалов, которую мы разработаем в этой Главе, также будет основана на структуре D3DMATERIAL9, по крайней мере в той части, где дело касается отражательных свойств и собственной светимости материала. Но, в то же время, будет поддерживать некоторые дополнительные свойства. Таким образом, здесь и далее когда мы будем говорить о материале, мы будем подразумевать один из кастомных материалов нашей собственной системы поддержки материалов, а не производную структуру D3DMATERIAL9, к которой мы будем обращаться только для описания свойств освещённости объекта.

Зачем же нужные материалы? Каково их назначение? Почему нельзя просто наложить текстуру на поверхность 3D-объекта и на этом закончить? Причина в том, что помимо структуры нам необходимо указать ещё множество других свойств поверхности, на которую она нанесена. Эти дополнительные сведения необходимы нашему движку для корректной обработки граней. Кроме того, наша игра может предъявлять различные требования к применяемым текстурам. Поэтому нам просто необходмо "обернуть" текстуры в материалы.
Рассмотрим пример. Скажем, в разрабатываемой FPS-игре мы хотим, чтобы всякий раз при движении игрока были слышны звуки ходьбы (footstep sound). Не ограничиваясь этим, мы хотим, чтобы при ходьбе по разным поверхностям (пол/трава) эти звуки были разными. Если мы просто наложим текстуры на каждую из поверхностей, то организовать проигрывание разных звуков при хождении игрока по различным поверхностям будет довольно проблематично. К примеру персонаж идёт по поверхности, к которой применена текстура бетона. Откуда наша игра будет знать, что это именно бетон? В этом случае нам придётся жёстко "привязывать" к игроку проверку текстуры по которой он ходит. А если таких тестур несколько, то и ресурсов такие проверки будут занимать значительно больше, да и объём кода значительно увеличится. Кроме того, если вдруг потребуется добавить новую текстуру с новым звуком, их придётся вновь "жёстко" программировать на уровне ядра...

Рис.1 Принцип действия системы материалов
Рис.1 Принцип действия системы материалов

Но есть способ лучше - создать систему поддержки материалов. Здесь это самое элегантное и оптимальное решение. Согласись, куда проще определить материал, который включает в себя описание всех свойств поверхности (параметры освещения, используемую текстуру и даже звук, воспроизводимый при ходьбе по нему). Кроме того, будет здорово, если мы организуем добавление и удаление материалов в игре динамически, то есть без необходимости перекомпиляции исходного кода. Это будет куда профессиональнее. Это можно легко организовать, взяв на вооружение систему поддержки скриптов, разработанную нами ранее. Мы создадим систему материалов, которая позволяет определить небольшой скрипт для каждого материала в игре. При запуске игры (как вариант, загрузке уровня) система просто считывает скрипты материалов и загружает для каждого из них соответствующие свойства (См. Рис.1).

Скрипты материалов

Прежде чем начинать разработку системы материалов, необходимо определить общую структуру их скриптов. Это не значит, что мы должны немедленно создать скрипт для каждого материала, планируемого к использованию. Вместо этого мы просто определим шаблон который затем будем использовать для создания скриптов материалов. Наш шаблон скрипта материала будет выглядеть так:

Шаблон скрипта материала
#begin

texture   string
ignore_face   bool
ignore_fog   bool
ignore_ray   bool

transparency   colour

diffuse   colour
ambient   colour
specular   colour
emissive   colour
power   float

#end

Данный шаблон будет использоваться при создании всех новых скриптов материалов. Он определяет все основные свойства, которые необходимы движку для корректной обработки материала. Конечно, ты можешь добавлять свои собственные свойства для реализации функционала, специфичного для конкретной игры, как например звук шагов при ходьбе по поверхности с данным материалом. Мы рассмотрим эту тему более подробно во второй части курса, при разработке игры.
Пробежимся по параметрам представленного шаблона скрипта.

Параметр Описание
texture Значение типа string. Здесь мы указываем имя файла текстуры, который ассоциирован с данным материалом. Данный параметр также должен включать в себя путь до этого файла, причём он должен быть указан относительно местоположения файла скрипта.
ignore_face Указывает движку рендерить или нет поверхность (грани) с данным материалом. Это бывает полезно для размещения объектов (граней), которые игрок не должен видеть.
ignore_fog Указывает движку применять или нет эффект тумана к данному материалу.
ignore_ray Указывает движку будут ли грани с данным материалом участвовать в детектировании столкновений. При значении TRUE грани с данным материалом не будут участвовать в детектировании столкновений. Детектирование (определение) столкновений - тема одной из будущих глав данного курса.
transparency Служит для указания цвета прозрачности (transparent color) текстуры. Direct3D позволяет указать один любой цвет в качестве цветового ключа (color key). Цветовой ключ представляет собой 32-битное ARGB (alpha, red, green, blue) значение цвета и даёт команду Direct3D игнорировать все пиксели текстуры с данным цветом. Это означает, что участки с данным цветом просто не будут рендериться, обозначая прозрачность (см. Рис 2). Это позволяет создавать поверхности с (воображаемыми) отверстиями в ней. При указании цветового ключа alpha-компонент всегда должен иметь значение 1.0, в то время как остальные могут принимать значение от 0.0 до 1.0 (включительно). Например значение 1.0 0.0 0.0 0.0 назначает в качестве цветового ключа чёрный цвет. В этом случае все чёрные пиксели текстуры не будут рендерится.
Рис. 2 Принцип действия прозрачности с ключевым цветом
Рис. 2 Принцип действия прозрачности с ключевым цветом

Последние 5 параметров используются для определения свойств освещения/освещённости материала. Они напрямую указывают на структуру D3DMATERIAL9. Вот её определение:

Структура D3DMATERIAL9
typedef struct _D3DMATERIAL9 {
  D3DCOLORVALUE Diffuse;
  D3DCOLORVALUE Ambient;
  D3DCOLORVALUE Specular;
  D3DCOLORVALUE Emissive;
  float Power;
} D3DMATERIAL9;

Из этого определения видно, как наши свойства указывают на её члены. Каждый наш материал содержит в себе внутреннюю копию структуры D3DMATERIAL9 (размещённую внутри скрипта материала). Значения, размещённые в скрипте, также сохраняются в локальной копии данной структуры, которая устанавливается в Direct3D до того, как грани с материалами начнут рендериться.
Если не указывать эти свойства, то все поверхности с данным материалом будут выглядеть на экране абсолютно чёрными, даже при наличии в сцене источников света. Сейчас мы не будем использовать параметры освещения материалов, но подробно рассмотрим как они работают.
Первые 3 параметра определяют, в каком количестве падающий свет будет отражаться (reflect) от поверхности с данным материалом. Точно также как и в реальном мире, когда свет отражается от разных объектов, их поверхности поглощают определённые диапазоны цветового спектра и отражают остальные. Это придаёт объекту (предмету) определённый цвет. Direct3D работает по схожему принципу.

  • Рассеянный (diffuse) свет используется для отражения света от источников света, размещённых в сцене. Если задать параметр Diffuse материала равным 0.0, 0.0, 1.0, 1.0 (red, green, blue, alpha), то данный материал будет отражать 100% синего (blue) компонента и 0% красного (red) и зелёного (green) компонентов. В этом случае все поверхности с данным материалом будут рендериться в синем цвете.
Закрыть
noteОбрати внимание

Цветовой ключ (Color key) использует ARGB порядок указания компонентов, в то время как структура D3DMATERIAL9 указывает цветовые компоненты своих членов в порядке RGBA. Постарайся не запутаться в них, иначе рискуешь получить грани с очень странными цветами. Если ты хочешь, чтобы порядок указания цветов в структуре D3DMATERIAL9 соответствовал оному в скрипте материалов, DirectX предлагает несколько макросов для указания цветовых значений в различном порядке.

  • Приглушённый (Ambient) свет. Отражение материала, освещаемого приглушённым светом определяется свойством ambient colour скрипта материала. Приглушённым считается любой свет с низкой интенсивностью, который присутствует в данной сцене. Он представляет собой самые тёмные грани объекта, не освещённые направленным (direct) источником света. Лучший способ представить себе приглушённый свет - это посмотреть на освещение тени. Если здание отбрасывает тень от солнечного света на землю, тень не будет абсолютно чёрной. А всё из-за приглушённого света, который (в реальной жизни) является просто светом, отражённым от множества поверхностей и более не имеет определённого источника.
  • Бликовый (Specular) свет. Представляет собой блик (блеск) от отражения и применяется для придания определённой подсветки объекту. Блики используются для придания объекту сияния, как например у хромированного бампера автомобиля. Свойство specular позволяет указать цвет бликов на материале. Чаще всего это белый цвет, либо с оттенками серого. Дополнительно указываем в скрипте материала значение свойства Power, контролирующее резкость бликов. Чем выше значение, тем более резкими будут блики, в то время как меньшее значение увеличивает площадь наложения блика и снижает степень выраженности данного эффекта (что прекрасно подходит для создания больших, рассеянных участков бликовой подсветки).
  • Излучаемый (Emissive) свет. Данное свойство не используется для отражённого света. Вместо этого предмет сам излучает свет подобно лампочке. Данное свойство заставляет материал излучать свет, причём строго назначенного в скрипте цвета. Данный тип света идеально подходит для создания материалов, которые должны светиться в темноте. При использовании данного типа света важно помнить, что светимость объекта с данным материалом во многом неполноценна, т.к. он (материал) освещает только те грани, к которым применён данный материал. При этом на все остальные объекты сцены свет данного материала не распространяется.

Система материалов

Материал представляет собой обычный ресурс, который мы можем загружать через нашу систему управления ресурсами (resource management system), разработанную ранее. На деле материал представляет собой сложный (составной) ресурс, состоящий из скрипта и текстуры (которая сама по себе также является структурой). К счастью в нашем случае мы просто "обернём" его в один класс Material и будем обращаться с ним как с обычным ресурсом.

Создаём Material.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Material.h".
  • Жмём "Добавить".

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

  • В только что созданном и открытом файле Material.h набираем следующий код:

Material.h (Проект Engine)
//-----------------------------------------------------------------------------
// Материал, состоящий из текстуры и описания свойств освещения.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef MATERIAL_H
#define MATERIAL_H

//-----------------------------------------------------------------------------
// Material Class
//-----------------------------------------------------------------------------
class Material : public Resource< Material >
{
public:
	Material( char *name, char *path = "./" );
	virtual ~Material();

	IDirect3DTexture9 *GetTexture();
	D3DMATERIAL9 *GetLighting();
	unsigned long GetWidth();
	unsigned long GetHeight();
	bool GetIgnoreFace();
	bool GetIgnoreFog();
	bool GetIgnoreRay();

private:
	IDirect3DTexture9 *m_texture; // Direct3D texture.
	D3DMATERIAL9 m_lighting; // Свойства освещения.
	unsigned long m_width; // Ширина текстуры.
	unsigned long m_height; // Высота текстуры.
	bool m_ignoreFace; // Указывает, должны ли игнорироваться грани с данным материалом.
	bool m_ignoreFog; // Указывает, должны ли грани с данным материалом игнорировать туман (fog).
	bool m_ignoreRay; // Указывает, должны ли грани с данным материалом игнорировать пересечение световых лучей (ray intersection).
};

#endif

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

Исследуем код Material.h

Исходный код Material.h содержит определение всего 1-го класса - Material, созданного на основе шаблонного класса Resource (см. Главу 1.4) . Остальное понятно из комментариев.

Создаём Material.cpp (Проект Engine)

В файле исходного кода Network.cpp будут размещаться реализации функций, объявленных в Material.h.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле Material.cpp набираем следующий код:

Material.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация класса Material, объявленного в Material.h
// Refer to the Material.h interface for more details.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The material class constructor.
//-----------------------------------------------------------------------------
Material::Material( char *name, char *path ) : Resource< Material >( name, path )
{
	D3DXIMAGE_INFO info;

	// Загружаем скрипт для данного материала.
	Script *script = new Script( name, path );

	// Проверяем, имеет ли текстура материала участки прозрачности.
	if( script->GetColourData( "transparency" )->a == 0.0f )
	{
		// Загружаем текстуру без прозрачности.
		D3DXCreateTextureFromFileEx( g_engine->GetDevice(), script->GetStringData( "texture" ), D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_TRIANGLE, D3DX_FILTER_TRIANGLE, 0, &info, NULL, &m_texture );
	}
	else
	{
		// Загружаем текстуру с выбранным цветом участков прозрачности (key color).
		D3DCOLORVALUE *colour = script->GetColourData( "transparency" );
		D3DCOLOR transparency = D3DCOLOR_COLORVALUE( colour->r, colour->g, colour->b, colour->a );
		D3DXCreateTextureFromFileEx( g_engine->GetDevice(), script->GetStringData( "texture" ), D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_TRIANGLE, D3DX_FILTER_TRIANGLE, transparency, &info, NULL, &m_texture );
	}

	// Сохраняем ширину и высоту текстуры.
	m_width = info.Width;
	m_height = info.Height;

	// Устанавливаем свойства освещения материала.
	m_lighting.Diffuse = *script->GetColourData( "diffuse" );
	m_lighting.Ambient = *script->GetColourData( "ambient" );
	m_lighting.Specular = *script->GetColourData( "specular" );
	m_lighting.Emissive = *script->GetColourData( "emissive" );
	m_lighting.Power = *script->GetFloatData( "power" );

	// Устанавливаем флаг игнорирования грани с данным материалом.
	m_ignoreFace = *script->GetBoolData( "ignore_face" );

	// Устанавливаем флаг игнорирования тумана гранью с данным материалом.
	m_ignoreFog = *script->GetBoolData( "ignore_fog" );

	// Устанавливаем флаг игнорирования пересечения световых лучей гранью с данным материалом.
	m_ignoreRay = *script->GetBoolData( "ignore_ray" );

	// Уничтожаем скрипт с материалом.
	SAFE_DELETE( script );
}

//-----------------------------------------------------------------------------
// The material class destructor.
//-----------------------------------------------------------------------------
Material::~Material()
{
	SAFE_RELEASE( m_texture );
}

//-----------------------------------------------------------------------------
// Возвращает текстуру материала.
//-----------------------------------------------------------------------------
IDirect3DTexture9 *Material::GetTexture()
{
	return m_texture;
}

//-----------------------------------------------------------------------------
// Возвращает свойства освещения (lighting properties) материала.
//-----------------------------------------------------------------------------
D3DMATERIAL9 *Material::GetLighting()
{
	return &m_lighting;
}

//-----------------------------------------------------------------------------
// Возвращает ширину (width) текстуры материала.
//-----------------------------------------------------------------------------
unsigned long Material::GetWidth()
{
	return m_width;
}

//-----------------------------------------------------------------------------
// Возвращает высоту (height) текстуры материала.
//-----------------------------------------------------------------------------
unsigned long Material::GetHeight()
{
	return m_height;
}

//-----------------------------------------------------------------------------
// Возвращает значения флага игнорирования граней с данным материалом.
//-----------------------------------------------------------------------------
bool Material::GetIgnoreFace()
{
	return m_ignoreFace;
}

//-----------------------------------------------------------------------------
// Возвращает значения флага игнорирования тумана гранями с данным материалом.
//-----------------------------------------------------------------------------
bool Material::GetIgnoreFog()
{
	return m_ignoreFog;
}

//-----------------------------------------------------------------------------
// Возвращает значения флага игнорирования пересечения световых лучей гранями с данным материалом.
//-----------------------------------------------------------------------------
bool Material::GetIgnoreRay()
{
	return m_ignoreRay;
}

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

Исследуем код Material.cpp (Проект Engine)

Как и любой другой ресурс, ресурс Material принимает 2 параметра в своём конструкторе:

  • имя ресурса;
  • путь к ресурсу.
Фрагмент Material.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The material class constructor.
//-----------------------------------------------------------------------------
Material::Material( char *name, char *path ) : Resource< Material >( name, path )
{
	D3DXIMAGE_INFO info;

	// Загружаем скрипт для данного материала.
	Script *script = new Script( name, path );
...

Эти данные затем передаются в класс Resource, на основе которого и создан класс Material.
Структура D3DXIMAGE_INFO понадобится позднее для извлечения (retrieving) свойств текстуры, используемой материалом.
Следующий шаг - загрузка скрипта материала. Данная задача решается всего одной строкой кода, где создаётся новый скрипт, с использованием имени (name) и пути (path), переданными в конструкторе класса Material.
Как только скрипт материала окажется загружен, первое, что нужно сделать - это проверить, использует ли материал участки прозрачности (transparency). Другими словами, здесь проверяется, есть ли у материала ключевой цвет (color key):

Фрагмент Material.cpp (Проект Engine)
...
	// Проверяем, имеет ли текстура материала участки прозрачности.
	if( script->GetColourData( "transparency" )->a == 0.0f )
	{
		// Загружаем текстуру без прозрачности.
		D3DXCreateTextureFromFileEx( g_engine->GetDevice(), script->GetStringData( "texture" ), D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_TRIANGLE, D3DX_FILTER_TRIANGLE, 0, &info, NULL, &m_texture );
	}
	else
	{
		// Загружаем текстуру с выбранным цветом участков прозрачности (key color).
		D3DCOLORVALUE *colour = script->GetColourData( "transparency" );
		D3DCOLOR transparency = D3DCOLOR_COLORVALUE( colour->r, colour->g, colour->b, colour->a );
		D3DXCreateTextureFromFileEx( g_engine->GetDevice(), script->GetStringData( "texture" ), D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_TRIANGLE, D3DX_FILTER_TRIANGLE, transparency, &info, NULL, &m_texture );
	}
...

Нам необходима эта проверка, так как это влияет на способ загрузки текстуры для данного материала. Самые внимательные заметят, что в случае материала без прозрачности 11-й параметр функции D3DXCreateTextureFromFileEx, стандартной DirectX-функции для загрузки текстуры из файла с изображением, установлен в 0. Кстати, её прототип выглядит так:

Прототип функции D3DXCreateTextureFromFileEx
HRESULT WINAPI D3DXCreateTextureFromFileEx
(
  // Указатель на интерфейс объекта устройства Direct3D
  LPDIRECT3DDEVICE9 pDevice,

  // Полное имя файла (имя + путь до него) загружаемой текстуры
  LPCTSTR pSrcFile,

  // Ширина (width) и высота (height) текстуры в пикселах
  UINT Width,
  UINT Height,

  // Число mip-уровней. Значение D3DX_DEFAULT создаёт полный набор
  UINT MipLevels,

  // Флаги использования ресурса
  DWORD Usage,

  // (Цветовой) формат пикселей текстуры
  D3DFORMAT Format,

  // Класс памяти, в котором текстура должна быть сохранена
  D3DPOOL Pool,

  // Флаги фильтрации текстуры
  DWORD Filter,

  // Флаги MipMap-фильтрации текстуры
  DWORD MipFilter,

  // Ключевой цвет прозрачности текстуры. Значение 0 отключает использование прозрачности
  D3DCOLOR ColorKey,

  // Описание данных, содержащихся в исходной (source) текстуре
  D3DXIMAGE_INFO *pSrcInfo,

  // 256-цветная палитра
  PALETTEENTRY *pPalette

  // Адрес указателя на участок памяти, куда будет сохранена новая текстура
  LPDIRECT3DTEXTURE9 *ppTexture
);

Функция D3DXCreateTextureFromFileEx безумно большая. Но хорошая новость заключается в том, что в большинстве её параметров можно выставить значения по умолчанию, и позволить её работать преимущественно без нашего участия.
В первом параметре нам необходимо передать указатель на интерфейс объекта устройства Direct3D, который в нашем случае мы запрашиваем у движка с помощью ранее созданной служебной функции g_engine->GetDevice().
Во втором параметре мы указываем полное имя файла загружаемой текстуры. В нашем случае мы просто запросим значения, переданные в класс Resource конструктором класса Material, применив специальную функцию script->GetStringData( "texture" ).
Ширину и высоту загружаемой текстуры мы устанавливаем в D3DX_DEFAULT, также как и параметр MipLevels, что позволяет получить данные значения из самого файла текстуры автоматически, создав полную цепочку Mip-уровней.
Параметр Usage устанавливаем в 0, так как нам не нужны какие-либо специальные фичи при загрузке текстуры.
Указав в параметре Format значение D3DFMT_UNKNOWN, мы указываем DirectX самостоятельно извлечь цветовой формат пикселей из файла-источника текстуры.
Параметр Pool позволяет указывать способ хранения ресурса в памяти. Вот его возможные значения:

Значение Описание
D3DPOOL_DEFAULT Ресурс размещается в наиболее подходящей памяти, в зависимости от его назначения. Обычно это видеопамять. Данный вид ресурсов необходимо пересоздавать заново всякий раз когда теряется (разрушается) объект устройства Direct3D.
D3DPOOL_MANAGED Direct3D динамически ("на лету") распределяет, где сохранить ресурс. При использовании ресурса средствами Direct3D используется видеопамять (device-accessible memory). В противном случае ресурс хранится в системной памяти (system memory). Весь процесс происходит автоматически и их (ресурсы) нет необходимости пересоздавать в случае утери объекта устройства Direct3D, т.к. при выборе данной опции Direct3D хранит копию всех ресурсов в системной памяти.
D3DPOOL_SYSTEMMEM Ресурс хранится в системной памяти и поэтому его не нужно пересоздавать заново при утере объекта устройства Direct3D. Обычно системная память недоступна для аппаратного ускорителя 3D-графики (3D-hardware).

В нашем случае мы установили значение D3DPOOL_MANAGED, т.к. использование этого режима требует наименьших усилий с нашей стороны. Нам не нужно думать о перемещении ресурсов между видеопамятью и системной памятью и нам не нужно заморачиваться над пересозданием ресурса в случае утери объекта устройства Direct3D.
Следующие два параметра Filter и MipFilter позволяют задавать режим фильтрации текстуры. Наиболее применяемые значения здесь:

  • D3DX_FILTER_POINT (самая простая фильтрация, наименее требовательна к ресурсам);
  • D3DX_FILTER_LINEAR;
  • D3DX_FILTER_TRIANGLE (наиболее эффективная и требовательная к ресурсам фильтрация, наш выбор).

Параметр ColorKey устанавливаем в 0, так как нам не требуется ключевой цвет для задания цвета прозрачности.
В параметре pSrcInfo мы указываем созданную нами D3DXIMAGE_INFO структуру с именем info. Когда функция D3DXCreateTextureFromFileEx завершит свою работу и возвратит результат, данная структура будет заполнена различными сведениями о текстуре, как например размеры, глубина цветности (color depth) и формат пикселей (pixel format). Некоторые из них нам понадобятся в самое ближайшее время.
Параметр pPalette используется только при создании палитровой текстуры (paletized texture). В общем случае это обычная 256-цветная текстура, которая использует программную палитру для хранения индекса всех своих цветов. В нашем случае устанавливаем данный параметр в NULL, так как мы не будем использовать палитровые текстуры.
В последнем параметре ppTexture указываем значение m_texture, что является адресом указателя с которым наша новая текстура будет сохранена в памяти.
Вот и весь процесс загрузки текстуры из памяти.
Также не забываем о втором случае, когда наша текстура использует ключевой цвет для задания участков прозрачности (см. исходный код выше). Здесь можно видеть, что мы проверяем наличие/отсутствие в скрипте материала ключевого цвета прозрачности (transparency). И если он установлен, то выполняем вариант функции D3DXCreateTextureFromFileEx с учётом цветового ключа (в 11-ом параметре указывается переменная transparency, инициализированная стройкой выше).

Фрагмент Material.cpp (Проект Engine)
...
		// Загружаем текстуру с выбранным цветом участков прозрачности (key color).
		D3DCOLORVALUE *colour = script->GetColourData( "transparency" );
		D3DCOLOR transparency = D3DCOLOR_COLORVALUE( colour->r, colour->g, colour->b, colour->a );
...

Именно в этих двух строках кода мы считываем значение ключевого цвета прозрачности из скрипта материала и сохраняем его сначала в переменную transparency, а затем в структуру D3DCOLOR_COLORVALUE. После этого функция D3DXCreateTextureFromFileEx принимает значение D3DCOLOR_COLORVALUE в качестве ключевого цвета (хотя по сути это переменная типа DWORD). Поэтому в данной функции мы используем макрос D3DCOLOR_COLORVALUE для конвертирования нашей структуры D3DCOLORVALUE в значение вида DWORD D3DCOLOR, которое мы назвали transparency. Затем мы указываем это имя в качестве 11-го параметра функции D3DXCreateTextureFromFileEx.
Что же, самая трудная часть уже позади. Мы написали код загрузки текстуры и даже оснастили его проверкой наличия/отсутствия в скрипте ключевого цвета прозрачности. Следующий шаг - настроить загруженный материал в соответствии с настройками его скрипта. Весь принцип довольно прост и понятен из дальнейшего исходного кода:

Фрагмент Material.cpp (Проект Engine)
...
	// Сохраняем ширину и высоту текстуры.
	m_width = info.Width;
	m_height = info.Height;

	// Устанавливаем свойства освещения материала.
	m_lighting.Diffuse = *script->GetColourData( "diffuse" );
	m_lighting.Ambient = *script->GetColourData( "ambient" );
	m_lighting.Specular = *script->GetColourData( "specular" );
	m_lighting.Emissive = *script->GetColourData( "emissive" );
	m_lighting.Power = *script->GetFloatData( "power" );

	// Устанавливаем флаг игнорирования грани с данным материалом.
	m_ignoreFace = *script->GetBoolData( "ignore_face" );

	// Устанавливаем флаг игнорирования тумана гранью с данным материалом.
	m_ignoreFog = *script->GetBoolData( "ignore_fog" );

	// Устанавливаем флаг игнорирования пересечения световых лучей гранью с данным материалом.
	m_ignoreRay = *script->GetBoolData( "ignore_ray" );

	// Уничтожаем скрипт с материалом.
	SAFE_DELETE( script );
}
...

В первых строках видим как в дело вступает заполненная ранее структура D3DXIMAGE_INFO (по имени info) для сохранения в локальных переменных значений ширины (width) и высоты (height) текстуры.
Следующий шаг - заполнить элементы структуры D3DMATERIAL9 (с локальным именем m_lighting) соответствующими значениями свойств освещения/освещённности из скрипта материала. Мы считываем все их напрямую из скрипта. Затем мы в один проход устанавливаем флаги материала, также взятые из его скрипта. В конце мы уничтожаем, загруженный в память, экземпляр скрипта настроек, т.к. к этому моменту мы закончили настройку материала и он (экземпляр скрипта в памяти) больше не нужен.
Деструктор класса Material совсем простой.

Фрагмент Material.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The material class destructor.
//-----------------------------------------------------------------------------
Material::~Material()
{
	SAFE_RELEASE( m_texture );
}
...

В нём мы освобождаем (release) наш интерфейс объекта texture (который был создан в конструкторе) с использованием макроса безопасного удаления SAFE_RELEASE.
Остальная часть листинга Material.cpp отведена под различные служебные функции, необходимые для доступа к данным, которые позволяют получить всю необходимую информацию о материале.
На этом всё, переходим к интегрированию системы (поддержки) материалов в движок.

Интегрируем систему материалов в движок

Принцип тот же, что и при интегрировании других систем.

Изменения в Material.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла Material.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь строку #include "Material.h"

сразу после строки #include "SoundSystem.h":

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Font.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "Network.h"
#include "SoundSystem.h"
#include "Material.h"
#include "State.h"
...

  • Добавь строку void (*CreateMaterialResource)( Material **resource, char *name, char *path );

в структуру EngineSetup, сразу после строки void (*StateSetup)(); .

Что это за функция CreateMaterialResource? В Главе 1.4 мы оснастили наш класс ResourceManager функцией обратного вызова CreateResource, которая используется им (классом ResourceManager) для указания пользовательского метода, специфичного для приложения (custom application-specific method), создающего ресурсы, специфичные для данной игры. Другими словами, всякий раз, когда создаётся один из подобных ресурсов, из приложения вызывается специально назначенная пользовательская функция, управляющая способом загрузки данного (уникального для конкретной игры) ресурса. В нашем случае это функция CreateMaterialResource. Если при загрузке ресурса никакая функция не указана (как это было в нашем случае при создании менеджера скриптов), то в этом случае ресурс загружается одним из загрузчиков по умолчанию движка.
Для наших материалов мы хотим разрешить приложению выполнять их загрузку различными способами (при необходимости). А всё из-за того, что мы изначально решили иметь возможность добавлять в материалы свойства, специфичные для приложения (application-specific properties). Так как наш базовый (generic, размещённый в исходном коде движка) класс Material не имеет возможности обрабатывать кастомные (пользовательские) свойства материалов, нашему движку необходимы другие способы для их загрузки. Для реализации такого способа мы позволим игрокодеру передавать свою функцию обратного вызова, специфичную для приложения (application-specific call-back function), с именем CreateMaterialResource. При этом, как мы видим, она будет вызываться из структуры EngineSetup.
Присвоим начальное значение NULL функции CreateMaterialResource:

  • Добавь строку CreateMaterialResource = NULL;

в самый конец конструктора структуры EngineSetup:

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	GUID guid; // Application GUID.
	char *name; // Название приложения.
	float scale; // Масштаб (scale) в метрах/футах.
	unsigned char totalBackBuffers; // Число используемых бэкбуферов.
	void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Обработчик сетевых сообщений.
	void (*StateSetup)(); // Функция подготовки стейта.
	void (*CreateMaterialResource)( Material **resource, char *name, char *path ); // Функция создания материала.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		GUID defaultGUID = { 0x24215591, 0x24c0, 0x4316, { 0xb5, 0xb2, 0x67, 0x98, 0x2c, 0xb3, 0x82, 0x54 } };

		instance = NULL;
		name = "Application"; // Название игрового приложения
		scale = 1.0f;
		totalBackBuffers = 1;
		HandleNetworkMessage = NULL;
		StateSetup = NULL;
		CreateMaterialResource = NULL;
	}
};
...

  • Добавь строку ResourceManager< Material > *m_materialManager;

в секцию private объявления класса Engine, сразу после строки ResourceManager< Script > *m_scriptManager;:

Фрагмент Engine.h (Проект Engine)
...
	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.

	ResourceManager< Script > *m_scriptManager; // Менеджер скриптов
	ResourceManager< Material > *m_materialManager; // Менеджер материалов.
...

  • Добавь строку ResourceManager< Material > *GetMaterialManager();

в секцию public объявления класса Engine, сразу после строки ResourceManager< Script > *GetScriptManager();:

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

	HWND GetWindow();
	void SetDeactiveFlag( bool deactive );

	float GetScale(); 
	IDirect3DDevice9 *GetDevice(); 
	D3DDISPLAYMODE *GetDisplayMode(); 
	ID3DXSprite *GetSprite();

	void AddState( State *state, bool change = true );
	void RemoveState( State *state );
	void ChangeState( unsigned long id );
	State *GetCurrentState();

	ResourceManager< Script > *GetScriptManager();
	ResourceManager< Material > *GetMaterialManager();

	Input *GetInput();
	Network *GetNetwork();
	SoundSystem *GetSoundSystem();
...

Функция GetMaterialManager() позволяет получить доступ к текущему менеджеру материалов из любой точки движка для последующей загрузки материалов через него.

Изменения в Engine.cpp (Проект Engine)

  • Добавь строку m_materialManager = new ResourceManager< Material >( m_setup->CreateMaterialResource );

в конструктор класса Engine, сразу после строки m_scriptManager = new ResourceManager< Script >;:

Фрагмент Engine.cpp (Проект Engine)
...
	// Создаём интерфейс спрайта.
	D3DXCreateSprite( m_device, &m_sprite );

	// Создаём связный список стейтов.
	m_states = new LinkedList< State >;
	m_currentState = NULL;

	// Создаём менеджеры ресурсов.
	m_scriptManager = new ResourceManager< Script >;
	m_materialManager = new ResourceManager< Material >( m_setup->CreateMaterialResource );
...

  • Добавь строку SAFE_DELETE( m_materialManager );

в деструктор класса Engine, сразу после строки SAFE_DELETE( m_input );:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).

		// Уничтожаем связные списки со стейтами.
		if( m_currentState != NULL )
			m_currentState->Close();
		SAFE_DELETE( m_states );

		// Уничтожаем ранее созданные объекты.
		SAFE_DELETE( m_soundSystem );
		SAFE_DELETE( m_network );
		SAFE_DELETE( m_input );
		SAFE_DELETE( m_materialManager );
		SAFE_DELETE( m_scriptManager );
...

  • Добавь реализацию функции GetMaterialManager:

ResourceManager< Material > *Engine::GetMaterialManager()
{
return m_materialManager;
}
сразу после реализации функции ResourceManager< Script > *Engine::GetScriptManager(), в самом конце Engine.cpp:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает указатель на текущий менеджер скриптов.
//-----------------------------------------------------------------------------
ResourceManager< Script > *Engine::GetScriptManager()
{
	return m_scriptManager;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущий менеджер материалов.
//-----------------------------------------------------------------------------
ResourceManager< Material > *Engine::GetMaterialManager()
{
	return m_materialManager;
}
...

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


Ты увидишь систему материалов в действии уже в конце этой главы. Что касается создания других материалов, то эту тему мы рассмотрим во второй части данного курса при разработке игры. В общих чертах мы будем использовать возможность загрузки кастомного (пользовательского) материала путём добавления новых свойств в скрипт материала, которые позволят "на лету" менять звуки шагов игрока при его хождении по разным поверхностям (= поверхностям с разными материалами) 3D-сцены.

Рис. 3 Наиболее распространённые формы ограничивающих объёмов: прямоугольник (куб), сфера и эллипсоид
Рис. 3 Наиболее распространённые формы ограничивающих объёмов: прямоугольник (куб), сфера и эллипсоид

Ограничивающие объёмы (Bounding volumes)

Ограничивающий объём представляет собой 3D-примитив (basic 3D-shape), который заключён 3D-объект, обычно имеющий более сложную форму. Обычно в качестве ограничивающих объёмов применяют простые геометрические фигуры. В основном это прямоугольник, куб или сфера, т.к. ими проще управлять (manage) и они наименее ресурсоёмкие (computationally inexpensive). На эту тему читаем хорошую статью на Хабре: https://habrahabr.ru/post/257339/(external link). Вообще в качестве ограничивающего объёма можно использовать любую фигуру; всё зависит от области его применения. На деле существует третья общепринятая форма ограничивающего объёма для 3D-объекта с формой человеческой фигуры - эллипсоид (ellipsoid).
Как ты знаешь, прямоугольник имеет длину (lenght) ширину (width) и высоту (height), а сфера имеет радиус (radius). Эллипсоид сочетает в себе свойства прямоугольника и сферы, т.к. он имеет целых 3 радиуса: 1 - для своей длины, 1 - для ширины и 1 - для высоты (см. Рис.3).
Наиболее часто ограничивающие объёмы применяются в программировании компьютерных игр для заключения внутри них более сложных объектов с целью упрощения определения столкновения (collision detection). К примеру, если персонаж игрока представлен 3D-моделью человека, состоящей из нескольких сотен граней, реализация корректного (и достаточно быстрого) определения её столкновения с другими объектами сцены может быть сильно затруднено. Вместо этого куда проще разместить 3D-модель персонажа игрока в одной из вышеуказанных фигур ограничивающих объёмов, "прикрепив" фигуру к нему. В этом случае определение столкновений 3D-персонажа сводится к определению столкновений его ограничивающего объёма, что с точки зрения компьютера в разы проще, нежели проверять пересечение плоскостей многогранной модели. Существует также множество других способов применения ограничивающих объёмов, о которых будет говориться во второй части данного курса при разработке игры.
Для внедрения техники ограничивающих объёмов мы разработаем и добавим в движок специальный служебный (utility) класс BoundingVolume. Для этого:

Создаём BoundingVolume.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "BoundingVolume".
  • Жмём "Добавить".

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

  • В только что созданном и открытом файле BoundingVolume набираем следующий код:

BoundingVolume.h (Проект Engine)
//-----------------------------------------------------------------------------
// Используется для хранения ограничивающих объёмов (куб, сфера)
// Дополнительно ограничивающая сфера (bounding sphere) может выступать в качестве 3D-эллипсоида.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef BOUNDING_VOLUME_H
#define BOUNDING_VOLUME_H

//-----------------------------------------------------------------------------
// Bounding Box Structure
//-----------------------------------------------------------------------------
struct BoundingBox
{
	D3DXVECTOR3 min; // минимальный выступ (extent) ограничивающего куба.
	D3DXVECTOR3 max; // максимальный выступ (extent) ограничивающего куба.
	float halfSize; // Расстояние от центра обёма до наиболее удалённой (further) точки по всем осям.
};

//-----------------------------------------------------------------------------
// Bounding Sphere Structure
//-----------------------------------------------------------------------------
struct BoundingSphere
{
	D3DXVECTOR3 centre; // Центральная точка ограничивающей сферы.
	float radius; // Радиус ограничивающей сферы.
};

//-----------------------------------------------------------------------------
// Bounding Volume Class
//-----------------------------------------------------------------------------
class BoundingVolume
{
public:
	BoundingVolume();
	virtual ~BoundingVolume();

	void BoundingVolumeFromMesh( ID3DXMesh *mesh, D3DXVECTOR3 ellipsoidRadius = D3DXVECTOR3( 1.0f, 1.0f, 1.0f ) );
	void BoundingVolumeFromVertices( D3DXVECTOR3 *vertices, unsigned long totalVertices, unsigned long vertexStride, D3DXVECTOR3 ellipsoidRadius = D3DXVECTOR3( 1.0f, 1.0f, 1.0f ) );
	void CloneBoundingVolume( BoundingBox *box, BoundingSphere *sphere, D3DXVECTOR3 ellipsoidRadius = D3DXVECTOR3( 1.0f, 1.0f, 1.0f ) );
	void RepositionBoundingVolume( D3DXMATRIX *location );

	void SetBoundingBox( D3DXVECTOR3 min, D3DXVECTOR3 max );
	BoundingBox *GetBoundingBox();

	void SetBoundingSphere( D3DXVECTOR3 centre, float radius, D3DXVECTOR3 ellipsoidRadius = D3DXVECTOR3( 1.0f, 1.0f, 1.0f ) );
	BoundingSphere *GetBoundingSphere();

	void SetEllipsoidRadius( D3DXVECTOR3 ellipsoidRadius );
	D3DXVECTOR3 GetEllipsoidRadius();

private:
	BoundingBox *m_box; // Репрезентация ограничивающего объёма в виде куба.
	BoundingSphere *m_sphere; // Репрезентация ограничивающего объёма в виде сферы.

	D3DXVECTOR3 m_originalMin; // Исходный (original) минимальный выступ ограничивающего прямоугольника (куба).
	D3DXVECTOR3 m_originalMax; // Исходный (original) минимальный выступ ограничивающего прямоугольника (куба).
	D3DXVECTOR3 m_originalCentre; // Исходная центральная точка ограничивающей сферы.

	D3DXVECTOR3 m_ellipsoidRadius; // Радиус эллипсоида (т.е. радиус по всем осям).
};

#endif

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

Исследуем код BoundingVolume.h

Как видим, в коде заголовочного файла BoundingVolume.h ничего лишнего.
Класс BoundingVolume представляет собой законченное решение, поддерживающее обработку всех трёх ограничивающих объёмов, упоминавшихся ранее. Он использует две небольшие структуры BoundingBox и BoundingSphere для хранения информации об ограничивающих прямоугольнике и сфере соответственно. Ограничивающий эллипсоид представляет собой модифицированную версию ограничивающей сферы. Переменная m_ellipsoidRadius хранит все три радиуса эллипсоида, которые вычисляются в долях (процентах) от радиуса ограничивающей сферы.
К примеру у нас есть ограничивающая сфера, имеющая радиус 10.0 единиц. При установке значений радиусов эллипсоида (с помощью функции SetEllipsoidRadius) как 0.53, 1.0, 0.4 мы указываем, что хотим, чтобы эллипсоид имел радиус по оси X равный 53% от радиуса ограничивающей сферы, радиус по оси Y равный 100% от радиуса ограничивающей сферы и радиус по оси Z равный 40% от радиуса ограничивающей сферы. Другими словами в результате получим эллипсоид с радиусами 0.53х10.0, 1.0х10.0, 0.4х10.0, что после перемножения даёт результат 5.3, 10.0, 4.0 .
Класс BoundingVolume предоставляет целый набор функций, которые позволяют создавать и манипулировать тремя различными формами ограничивающих объёмов.
Вызов функции BoundingVolumeFromMesh позволяет строить ограничивающий объём вокруг данной полигональной сетки (3D-меша; с использованием класса Mesh, который мы рассмотрим уже очень скоро).
Функция BoundingVolumeFromVertices позволяет строить ограничивающий объём на основе списка вершин. Таким образом обе вышеуказанные функции создают вокруг данной группы вершин минимально возможный (по размерам) ограничивающий объём (будь то прямоугольник или сфера), который заключает данную геометрическую фигуру (состоящую из данных вершин). Причём мы можем создавать ограничивающий объём как вокруг списка вершин, так и вокруг полигональной сетки (3D-меша). При создании ограничивающего эллипсоида ты также можешь указывать процентные доли каждого из радиусов.
Функция RepositionBoundingVolume позволяет перемещать ограничивающий объём в 3D-пространстве. Иметь ограничивающий объём вокруг 3D-объекта это, конечно, здорово. Но когда объект двигается в 3D-пространстве его ограничивающий объём также должен повсеместно следовать за ним, что, собственно, и обеспечивает данная функция. Ты не раз увидишь её в действии, когда мы начнём использовать объекты в нашей 3D-сцене.
Также класс BoundingVolume предоставляет доступ к ещё нескольким функциям, которые позволяют вручную (set) устанавливать и получать (retrieve) размеры каждой из трёх форм ограничивающих объёмов.

Создаём BoundingVolume.cpp (Проект Engine)

В файле исходного кода BoundingVolume.cpp будут размещаться реализации функций, объявленных в BoundingVolume.h.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле BoundingVolume.cpp набираем следующий код:

BoundingVolume.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация функций, объявленных в BoundingVolume.h .
// Refer to the BoundingVolume.h interface for more details.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The bounding volume class constructor.
//-----------------------------------------------------------------------------
BoundingVolume::BoundingVolume()
{
	m_box = new BoundingBox;
	m_sphere = new BoundingSphere;

	m_ellipsoidRadius = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
}

//-----------------------------------------------------------------------------
// The bounding volume class destructor.
//-----------------------------------------------------------------------------
BoundingVolume::~BoundingVolume()
{
	SAFE_DELETE( m_box );
	SAFE_DELETE( m_sphere );
}

//-----------------------------------------------------------------------------
// Строит ограничивающий объём, заключающий внутри себя данный 3D-меш.
//-----------------------------------------------------------------------------
void BoundingVolume::BoundingVolumeFromMesh( ID3DXMesh *mesh, D3DXVECTOR3 ellipsoidRadius )
{
	D3DXVECTOR3 *vertices;
	if( SUCCEEDED( mesh->LockVertexBuffer( D3DLOCK_READONLY, (void**)&vertices ) ) )
	{
		D3DXComputeBoundingBox( vertices, mesh->GetNumVertices(), D3DXGetFVFVertexSize( mesh->GetFVF() ), &m_box->min, &m_box->max );
		D3DXComputeBoundingSphere( vertices, mesh->GetNumVertices(), D3DXGetFVFVertexSize( mesh->GetFVF() ), &m_sphere->centre, &m_sphere->radius );
		mesh->UnlockVertexBuffer();
	}

	m_sphere->centre.x = m_box->min.x + ( ( m_box->max.x - m_box->min.x ) / 2.0f );
	m_sphere->centre.y = m_box->min.y + ( ( m_box->max.y - m_box->min.y ) / 2.0f );
	m_sphere->centre.z = m_box->min.z + ( ( m_box->max.z - m_box->min.z ) / 2.0f );

	m_box->halfSize = (float)max( fabs( m_box->max.x ), max( fabs( m_box->max.y ), fabs( m_box->max.z ) ) );
	m_box->halfSize = (float)max( m_box->halfSize, max( fabs( m_box->min.x ), max( fabs( m_box->min.y ), fabs( m_box->min.z ) ) ) );

	m_originalMin = m_box->min;
	m_originalMax = m_box->max;
	m_originalCentre = m_sphere->centre;

	SetEllipsoidRadius( ellipsoidRadius );
}

//-----------------------------------------------------------------------------
// Строит ограничивающий объём, заключающий внутри себя данный список вершин.
//-----------------------------------------------------------------------------
void BoundingVolume::BoundingVolumeFromVertices( D3DXVECTOR3 *vertices, unsigned long totalVertices, unsigned long vertexStride, D3DXVECTOR3 ellipsoidRadius )
{
	D3DXComputeBoundingBox( vertices, totalVertices, vertexStride, &m_box->min, &m_box->max );
	D3DXComputeBoundingSphere( vertices, totalVertices, vertexStride, &m_sphere->centre, &m_sphere->radius );

	m_sphere->centre.x = m_box->min.x + ( ( m_box->max.x - m_box->min.x ) / 2.0f );
	m_sphere->centre.y = m_box->min.y + ( ( m_box->max.y - m_box->min.y ) / 2.0f );
	m_sphere->centre.z = m_box->min.z + ( ( m_box->max.z - m_box->min.z ) / 2.0f );

	m_box->halfSize = (float)max( fabs( m_box->max.x ), max( fabs( m_box->max.y ), fabs( m_box->max.z ) ) );
	m_box->halfSize = (float)max( m_box->halfSize, max( fabs( m_box->min.x ), max( fabs( m_box->min.y ), fabs( m_box->min.z ) ) ) );

	m_originalMin = m_box->min;
	m_originalMax = m_box->max;
	m_originalCentre = m_sphere->centre;

	SetEllipsoidRadius( ellipsoidRadius );
}

//-----------------------------------------------------------------------------
// Строит ограничивающий объём на основе уже имющегося.
//-----------------------------------------------------------------------------
void BoundingVolume::CloneBoundingVolume( BoundingBox *box, BoundingSphere *sphere, D3DXVECTOR3 ellipsoidRadius )
{
	m_box->min = box->min;
	m_box->max = box->max;
	m_sphere->centre = sphere->centre;
	m_sphere->radius = sphere->radius;

	m_box->halfSize = (float)max( fabs( m_box->max.x ), max( fabs( m_box->max.y ), fabs( m_box->max.z ) ) );
	m_box->halfSize = (float)max( m_box->halfSize, max( fabs( m_box->min.x ), max( fabs( m_box->min.y ), fabs( m_box->min.z ) ) ) );

	m_originalMin = m_box->min;
	m_originalMax = m_box->max;
	m_originalCentre = m_sphere->centre;

	SetEllipsoidRadius( ellipsoidRadius );
}

//-----------------------------------------------------------------------------
// Репозиционирует ограничивающий объём (изменяет его текущее положение в 3D-пространстве) на основе данной матрицы.
//-----------------------------------------------------------------------------
void BoundingVolume::RepositionBoundingVolume( D3DXMATRIX *location )
{
	D3DXVec3TransformCoord( &m_box->min, &m_originalMin, location );
	D3DXVec3TransformCoord( &m_box->max, &m_originalMax, location );
	D3DXVec3TransformCoord( &m_sphere->centre, &m_originalCentre, location );
}

//-----------------------------------------------------------------------------
// Устанавливает свойства ограничивающего прямоугольника.
//-----------------------------------------------------------------------------
void BoundingVolume::SetBoundingBox( D3DXVECTOR3 min, D3DXVECTOR3 max )
{
	m_originalMin = m_box->min = min;
	m_originalMax = m_box->max = max;

	m_box->halfSize = (float)max( fabs( m_box->max.x ), max( fabs( m_box->max.y ), fabs( m_box->max.z ) ) );
	m_box->halfSize = (float)max( m_box->halfSize, max( fabs( m_box->min.x ), max( fabs( m_box->min.y ), fabs( m_box->min.z ) ) ) );
}

//-----------------------------------------------------------------------------
// Возвращает текущий ограничивающий прямоугольник объекта.
//-----------------------------------------------------------------------------
BoundingBox *BoundingVolume::GetBoundingBox()
{
	return m_box;
}

//-----------------------------------------------------------------------------
// Устанавливает свойства ограничивающей сферы.
//-----------------------------------------------------------------------------
void BoundingVolume::SetBoundingSphere( D3DXVECTOR3 centre, float radius, D3DXVECTOR3 ellipsoidRadius )
{
	m_originalCentre = m_sphere->centre = centre;
	m_sphere->radius = radius;

	SetEllipsoidRadius( ellipsoidRadius );
}

//-----------------------------------------------------------------------------
// Возвращает текущую ограничивающую сферу объекта.
//-----------------------------------------------------------------------------
BoundingSphere *BoundingVolume::GetBoundingSphere()
{
	return m_sphere;
}

//-----------------------------------------------------------------------------
// Устанавливает радиусы эллипсоида в процентном соотношениии к радиусу сферы.
//-----------------------------------------------------------------------------
void BoundingVolume::SetEllipsoidRadius( D3DXVECTOR3 ellipsoidRadius )
{
	m_ellipsoidRadius = D3DXVECTOR3( m_sphere->radius * ellipsoidRadius.x, m_sphere->radius * ellipsoidRadius.y, m_sphere->radius * ellipsoidRadius.z );
}

//-----------------------------------------------------------------------------
// Возвращает радиус данного эллипсоида.
//-----------------------------------------------------------------------------
D3DXVECTOR3 BoundingVolume::GetEllipsoidRadius()
{
	return m_ellipsoidRadius;
}

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

Исследуем код BoundingVolume.cpp

Интегрируем класс BoundingVolume в движок

Принцип тот же, что и при интегрировании других систем.

Изменения в BoundingVolume.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла BoundingVolume.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь строку #include "BoundingVolume.h"

сразу после строки #include "SoundSystem.h":

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Font.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "Network.h"
#include "SoundSystem.h"
#include "BoundingVolume.h"
#include "Material.h"
#include "Mesh.h"
#include "RenderCache.h"
#include "State.h"
...

Файловый формат DirectX (*.x)

Прежде чем рассмотреть класс Mesh, который будет обрабатывать все полигональные сетки (меши) в игре, необходимо рассмотреть некоторые базовые моменты. В терминах Direct3D меш представляет собой организованную коллекцию граней, которая рассматривается как единый объект. DirectX работает с мешами посредством собственного очень гибкого формата файлов, имеющего расширение .x . Он позволяет создавать 3D-меши, сохранять их в отдельные файлы и загружать из них. Кроме того x-формат поддерживает шаблоны, что придаёт ему особую гибкость и отличную расширяемость (extendability). В общих чертах это означает, что всё содержимое x-файла определено специальным шаблоном. С точки зрения Direct3D шаблон во многом схож с нашими файлами скриптов: он определяет какое-то число внутренних переменных, которые хранят данные для данного шаблона. Direct3D содержит множество стандартных шаблонов, которые позволяют выполнять большинство наиболее часто встречающихся задач, как например сохранение меша, его анимации и данных об используемых текстурах. Кроме того никто не мешает создавать свои собственные шаблоны, хранящие любую возможную информацию. Вообще совсем необязательно использовать файлы x-формата для хранения одних лишь мешей. Теоретически в них может храниться любая информация. Создание собственных шаблонов для последующего сохранения в файлах формата X не входит в данный курс и в нашем случае вовсе необязательно.

Применение 3D-редакторов для создания 3D-моделей и их экспортирования в формат .x

Создание файла формата .x для корректного сохранения меша может оказаться чересчур сложной задачей для новичков. С технической точки зрения файл формата .x представляет собой обычный текстовый файл (x-файл также может быть сохранён в двоичном формате). Его можно без труда создать в любом текстовом редакторе (например в Блокноте Windows), вручную прописать необходимые свойства меша а затем сохранить как файл .x. Но данный способ крайне трудоёмок и чреват многочисленными ошибками. Да и вряд ли таким способом удастся создать что-либо сложнее куба. На практике для создания 3D-моделей и их экспортирования в формат .x применяют программные пакеты для 3D-моделирования. Самые известные из них - 3DSMax(external link) (ранее 3D Studio Max) и Maya(external link). Оба пакета стоят в районе 3000-3500 долларов США, что для наших целей не годится.
Из других вариантов:

  • Загрузить триальные (trial, временно ограниченные) пробные версии данных программных пакетов с официального сайта www.autodesk.ru. Идея неплохая. Тем более что в них часто сохранён полный функционал. Но через 30 дней они перестанут работать. И даже переустановка (снос + установка заново) помогают далеко не всегда (если, конечно, ты не станешь "подчищать" следы из реестра вручную). Кроме того, данный софт весьма солидный объём (Дистрибутив с пробной версией 3DS Max 2018 "весит" аж 5 гигабайт!), а его установка занимает довольно много времени.
  • Поискать данные программные пакеты на торрент-трекерах или на дисках с софтом (да, сам видел как их всё ещё продают по соседству с шавермой в Петербурге осенью 2016 г.).

Для низкополигонального 3D-моделирования (low-poly modeling; именно он и применяется в игрокодинге) подойдёт почти любая версия вышеуказанных программ. Так например модели и большая часть сцен легендарной Half-Life создавались в 3D Studio Max 1.0. Лишь позднее Valve выпустила собственный 3D-редактор Hammer Editor. 3DS Max и Maya не умеют экспортировать модели в формат .X. X-экспортеры для разных версий 3DS Max и Maya разные (т.е. для версии 3,5 - создавали свой экспортер, для 4.0 - тоже свой и т.д.). На практике сначала находят .X-экспортер, а уже к нему скачивают триальную версию 3D-редактора.
В Интернете существует множество различных плагинов для экспорта .X-моделей. Большинство из них бесплатные. Рекомендуем отличную подборку т.н. Panda-плагинов от Энди Тотера (Andy Tather), размещённую на его сайте http://www.andytather.co.uk/Panda/panda_menu.aspx(external link). Для установки в большинстве случаев достаточно поместить файл плагина в папку plugins в каталоге с установленным 3DS Max или Maya. При следующем запуске 3D-редактора установленный плагин автоматически загружается и в диалоговом окне экспорта (Export) во всплываюшем списке расширений появляется новый формат .X.

  • Скачать и установить Gmax(external link). В 2004 году компания Discreet выпустила сильно урезанный, но зато совершенно бесплатный вариант своего пакета 3D Studio Max 4.0, назвав его Gmax(external link). В Gmax отсутствуют модули рендеринга, продвинутой анимации, масштабирования и много чего ещё. Но создавать 3D-объекты из базовых примитивов и редактировать их в нём вполне возможно. В 2006 году поддержка Gmax была прекращена, поэтому с тех пор он ничуть не изменился. Подробнее о том, где взять Gmax и как его настроить на сохранение .X-файлов читай в статье Gmax установка и настройка экспорта .X-файлов, как всегда, эксклюзивно представленной на Igrocoder.ru .

В нашем учебном курсе будем ориентироваться на Gmax, с настроенным .X-экспортером.

MeshView в действии
MeshView в действии

Просмотр .X-файлов

Полученный файл с расширением .x можно просмотреть с помощью специальной программы-просмотрщика MeshView (позднее на её основе был создан DXViewer). До 2009 г. такой просмотрщик входил в стандартный набор утилит DirectX SDK. В версии DirectX SDK за август 2009 г. его убрали. Видимо за ненадобностью...
В Интернете его тоже не найти из-за запрета Microsoft распространения DXViewer отдельно от SDK.
Тем не менее, слегка усовершенствованную версию DX Viewer-а можно скачать здесь: http://www.cgdev.net/axe/download.php(external link) (объём прибл. 1 Мб). В некоторых версиях DirectX SDK DXViewer шёл в виде исходного кода, чем и воспользовались программеры с www.cgdev.net(external link), слегка видоизменив стандартный просмотрщик 3D-мешей от Microsoft. Данная версия DXViewer-а не поддерживает показ древовидной структуры встроенной иерархии объектов 3D-меша, что очень плохо, т.к. данная функция очень важна при создании 3D-объектов для игр. Поэтому мы найдём "родной" просмотрщик, идущий в наборе с одной из старых версий DirectXSDK и обладающий полным функционалом. Для этого:

  • Найди в Интернете одну из старых версий DirectX SDK...

В нашем случае это оказался DirectX SDK 8.1 аж 2001 года выпуска. В те времена DXViewer назывался MeshView. По мнению форумчан с www.gamedev.ru(external link), MeshView более удобен и информативен. Так что на нём и остановимся.

Ссылок на старые версии DirectXSDK с каждым годом становится всё меньше. А с учётом маниакального стремления Майкрософт что-либо вырезать в будущих редакциях, настоятельно рекомендуем сохранить скачанный архив в надёжном месте. В будущем (может даже через годы) он обязательно пригодится тебе в качестве источника дополнительной информации по т.н. "неуправляемому" (unmanaged; т.е. на чистом C++, без использования .NET Framework) игрокодингу. Да и примеры там интересные. Каждый DirectX игрокодер обязательно хранит у себя на компьютере несколько старых версий DirectX SDK.
Скачанный ZIP-архив содержит каталог DirectXSDK с установочными файлами DirectX SDK 8.1.

  • Извлеки (распакуй) папку DirectXSDK в любое место на жёстком диске (например, на Рабочий стол).

Устанавливать DirectX SDK в нашем случае не нужно. Искомая программа MeshView расположена по пути ..\DirectXSDK\DXSDK\bin\DXUtils\mview.exe .

  • Скопируй файл mview.exe в любое доступное место на жёстком диске (например, скопируй его на тот же Рабочий стол).

Остальные файлы распакованного дистрибутива можно удалить.
Более подробную информацию по использованию MeshView можно найти в статье Gmax Наложение текстур и материалов.

Иерархия объектов в .X-файлах

3D-меш сохраняется внутри .x-файла с использованием иерархии. При просмотре .x-файла в программе MeshView можно увидеть встроенную иерархию объектов, содержащихся в нём. Для этого в верхней строке меню выбираем View->Hierarchy. При этом появляется новое окно, демонстрирующее верхний уровень (top level) иерархии. Щёлкнув мышью по узлу (nod) в виде символа +, открывается соответствующая ветвь древовидной структуры меша, которая в свою очередь может содержать другие вложенные ветви, а те другие ветви и т.д. Если подобъект не имеет значка +, значит был достигнут самый нижний уровень иерархии.

  • Найди в Интернете любой .X- файл, содержащий 3D-объект, либо возьми всё из того же DirectX SDK 8.1 и просмотри его иерархию с помощью MeshView.

Каждая запись (ветвь) в иерархии хранится в виде т.н. фрэйма (от англ. "frame" - кадр, рамка). Для работы с фреймами 3D-мешей в DirectX есть специальная структура D3DXFRAME. Вот её определение:

Структура D3DXFRAME
typedef struct D3DXFRAME {
// Имя кадра
  LPSTR               Name;

// Матрица, определяющая трансформацию фрейма
  D3DXMATRIX          TransformationMatrix;

// Указатель на контейнер с 3D-мешем
  LPD3DXMESHCONTAINER pMeshContainer;

// Указатели на одноуровневый (sibling) и дочерний (child) фреймы
  D3DXFRAME           *pFrameSibling;
  D3DXFRAME           *pFrameFirstChild;
} D3DXFRAME, *LPD3DXFRAME;

Структура прекрасно подходит для хранения основных данных о фрейме, как например его имя и матрица трансформации (transformation matrix; которую использует DirectX для определения сводной трансформации объекта, включающую позицию, вращение и изменение масштаба), а также собственно инстанс меша, к которому данный фрейм относится. Последние 2 указателя используются для включения данного фрейма в иерархию, точно так же как новый член связного списка "сцепляется" с другими. Разница лишь в том, что связный список линеен и имеет всего одну ветвь, в то время как иерархическая (древовидная) структура может иметь несколько ветвей.

Фактические данные о 3D-меше хранятся в структуре D3DXMESHCONTAINER, которая также представлена библиотекой D3DX. Сами меши хранятся внутри фреймов, как это представлено в структуре D3DXFRAME. Каждый фрейм может содержать один или несколько мешей т.к. структура D3DXMESHCONTAINER может соединять (link) саму себя с другими мешами точно также, как это делается в связном списке (т.е. с указанием следующего за ним члена). Вот определение структуры D3DXMESHCONTAINER:

Структура D3DXFRAME
typedef struct D3DXMESHCONTAINER {
// Имя меша
  LPSTR                Name;

// Определяет тип меша и хранит его данные
  D3DXMESHDATA         MeshData;

// Массив материалов, используемых мешем (данные освещения)
  LPD3DXMATERIAL       pMaterials;

// Указатель на эффекты. Пока не будем его использовать
  LPD3DXEFFECTINSTANCE pEffects;

// Общее число материалов в массиве pMaterials
  DWORD                NumMaterials;

// Информация о регулировке (тонкой настройке) граней меша
  DWORD                *pAdjacency;

// Указатель на информацию о скине (skin). Используется для мешей со скинами (skinned meshes)
  LPD3DXSKININFO       pSkinInfo;

// Указатель на следующий меш в списке.
  D3DXMESHCONTAINER    *pNextMeshContainer;
} D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;
Рис.4 Окно настроек Panda Exporter для 3D Studio Max 7
Рис.4 Окно настроек Panda Exporter для 3D Studio Max 7

Как мы до этого говорили, при создании меша и сохранения его в .X-файл он сохраняется внутри фрейма, который включён в общую иерархию фреймов. Если ты создашь в одной сцена два куба, не соприкасающихся друг с другом, и экспортируешь сцену в .X-формат, то при просмотре иерархии полученного .X-файла в MeshView будут видны две ветви, указывающие на два отдельных фрейма.

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

Впрочем, Panda-exporter позволяет даже сцену с двумя раздельными кубами сохранить в один фрейм. Для этого на экране настроек экспорта на вкладке "X File Settings" поставь галку напротив пункта "Top Frame Only". См. Рис.4.
О том, как установить 3D Studio Max 7 и как настроить Panda DirectX ExporterX читай статью 3D Studio Max 7 Установка и настройка экспорта .X-файлов

Настоящая сила фреймов остаётся не видна до тех пор, пока не начнёшь делать анимацию. При анимации внутри 3D-меша необходимо разместить т.н. кости (bones), которые связаны (linked) с его вершинами. Кости представляют собой служебные вспомогательные объекты, которые (по умолчанию) не видны во время рендеринга и которые работают точно также как и кости внутри твоего тела (от этого и их название), создавая внутри 3D-меша своеобразный скелет. Когда кость двигается, все вершины меша, связанные с данной костью, также перемещаются вслед за ней.
Работу с костями поддерживают все нормальные 3D-редакторы, в том числе и 3D Studio Max 7 в состав которой входит модуль анимирования персонажей Character Studio. Анимация на основе костей является одним из первых видов анимаций, появившихся в самых ранних версиях 3D-редакторов.
Создание костей происходит с использованием иерархии наподобие той, что лежит в основе твоего собственного скелета (пальцы крепятся к ладони, ладонь к руке, рука к плечу и т.д.). Вот почему иерархическая структура .x-файлов столь важна и необходима.
Прежде чем двигаться дальше, рассмотрим ещё две структуры, одна из которых положит начало дальнейшей дискуссии.
Как бы ни были великолепны структуры D3DXFRAME и D3DXMESHCONTAINER, они не могут учесть всего. "За бортом" тем не менее остаются некоторые свойства фрейма или меша, которые нам необходимо сохранить, но которые данные структуры попросту не поддерживают. Для решения этой проблемы мы создадим две новые структуры (назовём их Frame и MeshContainer), которые будут ветвиться от уже знакомых нам структур D3DXFRAME и D3DXMESHCONTAINER.

Создаём Mesh.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Mesh.h".
  • Жмём "Добавить".

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

  • В только что созданном и открытом файле Mesh.h набираем следующий код:

Mesh.h (Проект Engine)
//-----------------------------------------------------------------------------
// Применяется для загрузки и управления статичных (static) и
// анимированных (animated) 3D-мешей.
//
// Original Sourcecode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef MESH_H
#define MESH_H

//-----------------------------------------------------------------------------
// Frame Structure
//-----------------------------------------------------------------------------
struct Frame : public D3DXFRAME
{
	D3DXMATRIX finalTransformationMatrix; // Финальная трансформация фрейма перед тем, как он будет скомбинирован со своим родителем.

	//-------------------------------------------------------------------------
	// Возвращает трансляцию (translation) фрейма.
	//-------------------------------------------------------------------------
	D3DXVECTOR3 GetTranslation()
	{
		return D3DXVECTOR3( finalTransformationMatrix._41, finalTransformationMatrix._42, finalTransformationMatrix._43 );
	}
};

//-----------------------------------------------------------------------------
// Mesh Container Structure
//-----------------------------------------------------------------------------
struct MeshContainer : public D3DXMESHCONTAINER
{
	char **materialNames; // Временный массив имён материала (текстуры).
	Material **materials; // Массив материалов, используемых меш-контейнером (mesh container).
	ID3DXMesh *originalMesh; // Исходный (original) меш.
	D3DXATTRIBUTERANGE *attributeTable; // Таблица атрибутов меша.
	unsigned long totalAttributeGroups; // Общее количество групп атрибутов.
	D3DXMATRIX **boneMatrixPointers; // Массив указателей на матрицы трансформации костей (bone transformation matrices).
};

//-----------------------------------------------------------------------------
// Allocate Hierarchy Class
//-----------------------------------------------------------------------------
class AllocateHierarchy : public ID3DXAllocateHierarchy
{
	STDMETHOD( CreateFrame )( THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame );
	STDMETHOD( CreateMeshContainer )( THIS_ LPCSTR Name, CONST D3DXMESHDATA *pMeshData, CONST D3DXMATERIAL *pMaterials, CONST D3DXEFFECTINSTANCE *pEffectInstances, DWORD NumMaterials, CONST DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER *ppNewMeshContainer );
	STDMETHOD( DestroyFrame )( THIS_ LPD3DXFRAME pFrameToFree );
	STDMETHOD( DestroyMeshContainer )( THIS_ LPD3DXMESHCONTAINER pMeshContainerToFree );
};

//-----------------------------------------------------------------------------
// Mesh Class
//-----------------------------------------------------------------------------
class Mesh : public BoundingVolume, public Resource< Mesh >
{
public:
	Mesh( char *name, char *path = "./" );
	virtual ~Mesh();

	void Update();
	void Render();

	void CloneAnimationController( ID3DXAnimationController **animationController );

	MeshContainer *GetStaticMesh();
	Vertex *GetVertices();
	unsigned short *GetIndices();

	LinkedList< Frame > *GetFrameList();
	Frame *GetFrame( char *name );
	Frame *GetReferencePoint( char *name );

private:
	void PrepareFrame( Frame *frame );
	void UpdateFrame( Frame *frame, D3DXMATRIX *parentTransformationMatrix = NULL );
	void RenderFrame( Frame *frame );

private:
	Frame *m_firstFrame; // Первый (топовый) фрейм в иерархии фреймов меша.
	ID3DXAnimationController *m_animationController; // Контроллер анимации.

	D3DXMATRIX *m_boneMatrices; // Массив матриц трансформации костей (bone transformation matrices).
	unsigned long m_totalBoneMatrices; // Число костей в массиве.

	MeshContainer *m_staticMesh; // Статичная (неанимированная) версия меша.
	Vertex *m_vertices; // Массив вершин из статичного меша.
	unsigned short *m_indices; // Массив индексов в массиве вершин.

	LinkedList< Frame > *m_frames; // Связный список (linked list) указателей на все фреймы меша.
	LinkedList< Frame > *m_refPoints; // Связный список (linked list) указателей на на все референсные точки (reference points) меша.
};

#endif

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

Исследуем код Mesh.h

Структура Frame

В начале исходного кода Mesh.h видим объявление структуры Frame:

Фрагмент Mesh.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Frame Structure
//-----------------------------------------------------------------------------
struct Frame : public D3DXFRAME
{
	D3DXMATRIX finalTransformationMatrix; // Финальная трансформация фрейма перед тем, как он будет скомбинирован со своим родителем.

	//-------------------------------------------------------------------------
	// Возвращает трансляцию (translation) фрейма.
	//-------------------------------------------------------------------------
	D3DXVECTOR3 GetTranslation()
	{
		return D3DXVECTOR3( finalTransformationMatrix._41, finalTransformationMatrix._42, finalTransformationMatrix._43 );
	}
};
...

Так как фреймы соединены друг с другом по принципу иерархии, из этого следует, что какие-то фреймы будут иметь родителей (parent frame), а какие-то - дочерние фреймы (children frame). Чаще всего фреймы будут иметь и родительские и дочерние фреймы одновременно. Это означает, что у каждого фрейма теоретически могут быть фреймы, стоящие выше в иерархической структуре, а также могут быть стоящие ниже, в пределах одной ветви древовидной структуры. Ты также знаешь, что у каждого фрейма есть своя матрица трансформации (она так и называется - TransformationMatrix), которая определяет положение фрейма в 3D-пространстве. Для тех, кто подзабыл что такое математические матрицы и с чем их едят, через пару абзацев будет теоретическая вставка на эту тему.
А что же произойдёт, когда родительский фрейм начнёт двигаться? Что произойдет со всеми его дочерними фреймами (children frames)? На данном этапе это нас мало волнует, но данные вопросы не раз будут подниматься как только мы начнём анимировать фреймы. Так что уделим этому внимание прямо сейчас. Как мы раннее упоминали, для анимации 3D-меша применяются кости (чаще всего; в игрокодинге). Кости, в свою очередь, представлены в виде фреймов, включённых в общую иерархию. Для анимирования 3D-меша необходимо перемещать его кости. При это происходит трансформация каждого его фрейма в 3D-пространстве. Когда родительский фрейм начинает двигаться, он "увлекает" за собой все свои дочерние фреймы, также приводя их в движение.
Лучший способ наглядно представить этот процесс это посмотреть на своё собственное тело. Представь, что кости твоей руки - это фреймы, а остальная часть руки (из кожи и плоти) - 3D-меш. В этом случае каждая кость представлена фреймом, соединена к "родительской" кости (если смотреть выше, к предплечью), а также имеет несколько дочерних костей (кисть, пальцы). Когда ты перемещаешь одну из костей своей руки, все её дочерние кости также перемещаются вместе с ней. А всё из-за того, что каждая кость при движении вниз по иерархии автоматически приходит в движение при перемещении родительской кости, комбинируя собственное движение с движением родителя.
Для того, чтобы просчитать и сохранить трансформацию фрейма, основанную на перемещениях его фрейма-родителя с наложением его собственного движения, нам понадобится ещё одна переменная, в которой можно будет сохранять новые данные о текущей позиции фрейма. Поэтому на основе структуры D3DXFRAME мы создаём свою собственную структуру-потомок (derived structure) с названием Frame. Внутри неё мы определяем новые свойства. Забегая вперёд, скажем, что в реализации класса Mesh мы будем использовать именно новосозданную структуру Frame, а не D3DXFRAME.
Дополнительно внутри структуры Frame создана функция GetTranslation, которая извлекает координаты положения (positional coordinates) x,y,z из матрицы finalTransformationMatrix и возвращает их. Данная функция применяется для определения текущего положения 3D-фрейма в 3D-пространстве.

Структура MeshContainer

Фрагмент Mesh.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Mesh Container Structure
//-----------------------------------------------------------------------------
struct MeshContainer : public D3DXMESHCONTAINER
{
	char **materialNames; // Временный массив имён материала (текстуры).
	Material **materials; // Массив материалов, используемых меш-контейнером (mesh container).
	ID3DXMesh *originalMesh; // Исходный (original) меш.
	D3DXATTRIBUTERANGE *attributeTable; // Таблица атрибутов меша.
	unsigned long totalAttributeGroups; // Общее количество групп атрибутов.
	D3DXMATRIX **boneMatrixPointers; // Массив указателей на матрицы трансформации костей (bone transformation matrices).
};
...

Именно её мы будем использовать вместо D3DXMESHCONTAINER. Структура MeshContainer ветвится от структуры D3DXMESHCONTAINER, при этом она дополнена несколькими новыми свойствами и сохраняет полный функционал своего DirectX-родителя.
Из добавленного можно выделить следующие элементы:

  • Массив имён материалов (array of material names);
  • Массив используемых материалов (array of actual materials; т.е. материалов, применённых к мешу в данный момент).

По своей структуре они являются обычными материалами, структуру которого мы создали чуть ранее.

  • Указатель originalMesh позволяет сохранять копию исходного немодифицированного меша для использования её в качестве "точки отсчёта" (reference point).
  • Члены attributeTable и totalAttributeGroups используются для рендеринга меша.
  • Член boneMatrixPointers используется для анимации меша.

Очень скоро ты увидишь все эти новые члены в действии, при рассмотрении реализации класса Mesh.

Рис.5 Матрица размером 4х4
Рис.5 Матрица размером 4х4

Математические матрицы (теоретическая вставка)

Ты наверняка не раз встречал понятие матрицы (matrix) при изучении исходного кода движка. Интенсивность их применения с каждой новой Главой Учебного курса будет лишь возрастать. Поэтому без теории - никуда, к тому же здесь мы постараемся изложить теорию матриц как можно доступнее, не вдаваясь в математические дебри.
Основной причиной использования матриц при программировании под DirectX является то обстоятельство, что библиотека D3DX изначально оснащена рядом готовых функций и структур, специально созданных для манипулирования матрицами. Это означает, что тебе не придётся вдаваться во внутренние механизмы матричных преобразований. По крайней мере на первых порах. Но прежде чем начать изучение возможностей D3DX, рассмотрим, что же собой представляют матрицы в математике.
Матрицы применяются в Direct3D для выполнения т.н. трансформаций (от англ. "transformation" - преобразование). Трансформации в этом случае происходят в 3D-пространстве, что означает возможность применения трансформаций к текущему положению объекта в 3D-пространстве (3D position) или, например, к вектору. 3D-трансформации применяются для выполнения ряда задач, как например выполнения (expressing):

  • позиции относительно какой-либо точки в 3D-пространстве,
  • вращения (rotating),
  • изменения размера (scaling),
  • для изменения позиции взгляда игрока (player's view) относительно 3D-сцены. (Другими словами, для изменения положения, направления и перспективы виртуальной камеры в 3D-пространстве.)

Обычная матрица определяется некоторым числом столбцов (columns) и строк (rows). Наиболее часто встречается в литературе (и с которой мы в основном и будем работать в дальнейшем) матрица размером 4х4 (4 столбца и 4 строки; см. Рис.5; Источник: http://www.cg.info.hiroshima-cu.ac.jp/~miyazaki/knowledge/teche23.html(external link)).
Как мы ранее упоминали, библиотека D3DX содержит специальную структуру D3DXMATRIX, созданную как раз для хранения матриц размером 4х4. Вот её определение (в самом простом варианте):

Структура D3DXMATRIX
typedef struct D3DXMATRIX {
  FLOAT _11, FLOAT _12, FLOAT _13, FLOAT _14,
  FLOAT _21, FLOAT _22, FLOAT _23, FLOAT _24,
  FLOAT _31, FLOAT _32, FLOAT _33, FLOAT _34,
  FLOAT _41, FLOAT _42, FLOAT _43, FLOAT _44;
}D3DXMATRIX;

К примеру, если необходимо получить доступ к элементу, расположенном во второй строке и третьем ряду (колонке), используем идентификатор _23.
В 3D-математике матрицы чаще всего используются для трансформации объекта из одного положения в 3D-пространства в другое. Далеко не всегда это простое перемещение объекта. Здесь как правило применяются:

Матрица трансляции (перемещения; translation matrix) Просто перемещает объект из одной точки 3D-пространства в другую.
Матрица вращения (rotation matrix) Вращает объект вкруг определённой точки (обычно это центр объекта).
Матрица масштабирования (scaling matrix) Изменяет масштаб объекта. Другими словами увеличивает либо уменьшает размер объекта.

Таким образом для перемещения объекта необходимо построить матрицу трансляции, а затем применить её к текущей позиции 3D-объекта. Для вращения объекта вокруг точки достаточно построить матрицу вращения и применить её к 3D-объекту (вернее к его вращению).
Главное преимущество использования матриц заключается в том, что эффект двух или более матриц можно комбинировать для получения единого результата путём их простого перемножения. Эту операцию также называют конкатенацией (объединением) матриц.

Рис.6 Пример формулы конкатенации (комбинирования) трёх матриц
Рис.6 Пример формулы конкатенации (комбинирования) трёх матриц

Например в нашем предыдущем примере мы одновременно транслируем (перемещаем) и вращаем воображаемый объект в 3D-пространстве путём применения к нему двух матриц: матрицы трансляции и вращения. Тот же самый результат можно получить путём комбинирования (объединения) матрицы трансляции и вращения в одну новую результирующую матрицу. Применив эту новую матрицу к объекту мы получим тот же самый результат, что и при применении каждой из исходных матриц по отдельности.

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

При комбинировании (перемножении) матриц важно помнить, что данная операция не коммутативна (то есть на результат влияет порядок, в котором перемножаются матрицы). Лучший способ запомнить это - использовать правило перемножения в порядке "слева - направо". Взгляни на следующую формулу, в которой конкатенируются вместе 3 матрицы: вращения, трансляции и масштаба (см. Рис.6).

Данная конкатенация имеет следующий эффект. Сначала объект вращается, затем изменяется его положение в 3D-пространстве и уже в последнюю очередь он приобретает новый размер (масштаб). При изменении в формуле порядка конкатенации матриц также изменится и то, в каком порядке они будут применены к объекту. Результат в каждом случае будет разный.

Это было очень сжатое введение в теорию матриц. К счастью для использования матриц на начальном этапе игрокодеру нет необходимости знать всю подноготную по данной теме. Более подробно на эту тему смотри документацию к DirectX SDK, секцию DirectX Graphics Programming Guide. Суть матриц станет более понятной, как только мы начнём их применять в нашем движке в последующих главах.

Загрузка иерархии 3D-меша (Mesh Hierarchy)

Тема мешей и их поддержки в DirectX необычайно обширна. В данной главе мы рассмотрим лишь базовые принципы работы мешей и быстро пробежимся по реализациям соответствующих классов.
Теперь, после небольшого краш-курса на тему матриц, мы можем смело приступать к рассмотрению 3D-мешей. И начнём мы с рассмотрения загрузки иерархии меша из .x-файла.
Как только ты создал меш, и экспортировал его в .x-файл, первым делом его необходимо загрузить средствами движка, считать его иерархию и сохранить её для дальнейшего использования в игре. Как мы уже говорили, при загрузке .x-файла считывается его иерархия, состоящая из отдельных фреймов, каждый из которых в свою очередь будет сохранён в нашей игре посредством структуры Frame, рассмотренной чуть ранее. Собственно данные полигональной сетки меша могут содержаться в одном или нескольких фреймах и будут сохранены в заранее подготовленной структуре MeshContainer.
Для загрузки иерархии меша библиотека D3DX предоставляет специальный интерфейс ID3DXAllocateHierarchy, который предназначен для обнаружения и загрузки иерархии меша в .x-файле. Данный интерфейс экспонирует 4 функции, каждая из которых работает по принципу во многом схожему с работой функций обратного вызова. На основе интерфейса ID3DXAllocateHierarchy мы создадим новый ветвящийся (derived) класс AllocateHierarchy и переопределим (override) каждую из 4-х его функций своими собственными, специфичными для нашего приложения. Во время загрузки в игру .x-файла будут вызваны эти самые переопределённые функции для загрузки мешей и фреймов, в то время как D3DX сделает всю черновую работу по воссозданию его текущей иерархии.

Исследуем код Mesh.h (продолжение)

Объявление класса AllocateHierarchy

Фрагмент Mesh.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Allocate Hierarchy Class
//-----------------------------------------------------------------------------
class AllocateHierarchy : public ID3DXAllocateHierarchy
{
	STDMETHOD( CreateFrame )( THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame );
	STDMETHOD( CreateMeshContainer )( THIS_ LPCSTR Name, CONST D3DXMESHDATA *pMeshData, CONST D3DXMATERIAL *pMaterials, CONST D3DXEFFECTINSTANCE *pEffectInstances, DWORD NumMaterials, CONST DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER *ppNewMeshContainer );
	STDMETHOD( DestroyFrame )( THIS_ LPD3DXFRAME pFrameToFree );
	STDMETHOD( DestroyMeshContainer )( THIS_ LPD3DXMESHCONTAINER pMeshContainerToFree );
};
...

Здесь мы переопределили 4 функции интерфейса ID3DXAllocateHierarchy, дав им новые названия:

  • CreateFrame
  • CreateMeshContainer
  • DestroyFrame
  • DestroyMeshContainer

Первые две функции создают фрейм и меш-контейнер соответственно, а другие две - их уничтожают.
У данных функций есть несколько параметров. Особенно их много у функции CreateMeshContainer. Сейчас на нет необходимости указывать корректные данные, которые в них должны передаваться, т.к. почти все они заполняются библиотекой D3DX во время загрузки .x-файла. Вышеперечисленные функции вызываются D3DX чтобы дать нам возможность загрузить наши фреймы и меш-контейнеры как только будет создана иерархия загруженного .x-файла. При этом вся информация, необходимая для загрузки фрейма или меш-контейнера будет также предоставлена библиотекой D3DX. Так что нам даже не придётся заботиться об этом. Более подробно о работе данных функций читай ниже, в пункте "Исследуем исходный код Mesh.cpp (Проект Engine)".

Система управления 3D-мешами (Mesh System)

Ты, должно быть, слегка запутался во всех тонкостях загрузки иерархии меша из .x-файла. Не запаривайся, т.к. сейчас важно понимание этого процесса лишь в общих чертах. К концу этой нереально большой главы у тебя сложится целостная картина всего происходящего. Но вообще, полностью ты поймёшь данную тему, как только увидишь готовую систему загрузки мешей в действии, при разработке игры. Вообще данная система управления 3D-мешами (Mesh System) является одной из самых сложных в данном курсе и на данную тему написаны весьма внушительные книги. Поэтому мы кратенько охватим лишь основные моменты для того чтобы ты получил достаточно информации для последующего применения данного исходного кода в реальных проектах. Со временем многие программисты возвращаются к данному исходному коду с целью модифицирования поведения системы и добавления в неё новых фич.

Объявление класса Mesh

Фрагмент Mesh.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Mesh Class
//-----------------------------------------------------------------------------
class Mesh : public BoundingVolume, public Resource< Mesh >
{
public:
	Mesh( char *name, char *path = "./" );
	virtual ~Mesh();

	void Update();
	void Render();

	void CloneAnimationController( ID3DXAnimationController **animationController );

	MeshContainer *GetStaticMesh();
	Vertex *GetVertices();
	unsigned short *GetIndices();

	LinkedList< Frame > *GetFrameList();
	Frame *GetFrame( char *name );
	Frame *GetReferencePoint( char *name );

private:
	void PrepareFrame( Frame *frame );
	void UpdateFrame( Frame *frame, D3DXMATRIX *parentTransformationMatrix = NULL );
	void RenderFrame( Frame *frame );

private:
	Frame *m_firstFrame; // Первый (топовый) фрейм в иерархии фреймов меша.
	ID3DXAnimationController *m_animationController; // Контроллер анимации.

	D3DXMATRIX *m_boneMatrices; // Массив матриц трансформации костей (bone transformation matrices).
	unsigned long m_totalBoneMatrices; // Число костей в массиве.

	MeshContainer *m_staticMesh; // Статичная (неанимированная) версия меша.
	Vertex *m_vertices; // Массив вершин из статичного меша.
	unsigned short *m_indices; // Массив индексов в массиве вершин.

	LinkedList< Frame > *m_frames; // Связный список (linked list) указателей на все фреймы меша.
	LinkedList< Frame > *m_refPoints; // Связный список (linked list) указателей на на все референсные точки (reference points) меша.
};
...

Этот здоровенный класс включает в себя полный набор функций для манипуляций с 3D-мешами. Выделим основные моменты в нём.
Первое, что бросается в глаза, это то, что класс Mesh ветвится сразу от двух других классов - BoundingVolume и Resource. Ветвление от класса Resource здесь вполне очевидно, ведь .x-файл с 3D-мешем имеет имя и полный путь до него. Поэтому любой меш имеет все свойства ресурса и будет рассматриваться как оный. Кроме того, мы сможем легко управлять им, применив систему менеджмента ресурсов (Resource Management System), созданную ранее.
Класс BoundingVolume создан нами совсем недавно и мы ещё не видели его в деле. Напомним, что он необходим для создания вокруг мешей т.н. ограничивающих объёмов (прямоугольника, сферы или эллипсоида), необходимых для упрощённого просчёта столкновений. Вот и в нашем случае вокруг каждого меша мы будем создавать набор ограничивающих объёмов. Создание набора ограничивающих объёмов вокруг меша прямо во время его загрузки из .x-файла заметно упростит дальнейший доступ к информации о том, какой именно объём используется в настоящий момент без необходимости производить дополнительные операции по его настройке.
Класс Mesh поддерживает все стандартные функции, включая конструктор и деструктор (которые применяются для создания и уничтожения 3D-меша соответственно). Также он содержит функции Update и Render, которые вызываются всякий раз, когда меш необходимо обновить или отрендерить соответственно. Класс также содержит несколько публичных (public) служебных (utility) функций, которые применяются для получения различных сведений о меше, например о его фреймах.
Также ты заметил несколько приватных (с ограниченным доступом, private) функций, которые используются только внутри данного класса для подготовки (preparing), обновления (updating) и рендеринга (rendering) меша. Данные функции работают по принципу рекурсии (recursive) и мы обязательно обсудим их чуть позднее.
Также в классе Mesh содержится много переменных членов (variable members). Большинство из них нам пока незнакомы, но это ненадолго. Мы обсудим их вскоре при рассмотрении различных вариантов реализации (implemebtation snippets) класса Mesh, начиная с его конструктора, в котором 3D-меш загружается.

Создаём Mesh.cpp (Проект Engine)

В файле исходного кода Mesh.cpp будут размещаться реализации функций, объявленных в Mesh.h.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле Mesh.cpp набираем следующий код:

Mesh.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация функций, объявленных в Mesh.h.
//
// Original Sourcecode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// Создаёт новый фрейм.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::CreateFrame( THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame )
{
	// Создаём новый фрейм и обнуляем его память.
	Frame *frame = new Frame;
	ZeroMemory( frame, sizeof( Frame ) );

	// Копируем имя фрейма.
	if( Name == NULL )
	{
		// Имя отсутствует, поэтому создаём уникальное имя.
		static unsigned long nameCount = 0;
		char newName[32];
		sprintf( newName, "unknown_frame_%d", nameCount );
		nameCount++;

		frame->Name = new char[strlen( newName ) + 1];
		strcpy( frame->Name, newName );
	}
	else
	{
		frame->Name = new char[strlen( Name ) + 1];
		strcpy( frame->Name, Name );
	}

	*ppNewFrame = frame;

	return S_OK;
}

//-----------------------------------------------------------------------------
// Создаёт новый меш-контейнер.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::CreateMeshContainer( THIS_ LPCSTR Name, CONST D3DXMESHDATA *pMeshData, CONST D3DXMATERIAL *pMaterials, CONST D3DXEFFECTINSTANCE *pEffectInstances, DWORD NumMaterials, CONST DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER *ppNewMeshContainer )
{
	// Создаём новый меш-контейнер и обнуляем его память.
	MeshContainer *meshContainer = new MeshContainer;
	ZeroMemory( meshContainer, sizeof( MeshContainer ) );

	// Копируем имя меша.
	if( Name == NULL )
	{
		// Имя отсутствует, поэтому создаём уникальное имя.
		static unsigned long nameCount = 0;
		char newName[32];
		sprintf( newName, "unknown_mesh_%d", nameCount );
		nameCount++;

		meshContainer->Name = new char[strlen( newName ) + 1];
		strcpy( meshContainer->Name, newName );
	}
	else
	{
		meshContainer->Name = new char[strlen( Name ) + 1];
		strcpy( meshContainer->Name, Name );
	}

	// Проверяем, есть ли у меша какие-либо материалы.
	if( ( meshContainer->NumMaterials = NumMaterials ) > 0 )
	{
		// Выделяем память для материалов меша, а также их имён (т.е. имён текстур).
		meshContainer->materials = new Material*[meshContainer->NumMaterials];
		meshContainer->materialNames = new char*[meshContainer->NumMaterials];

		// Сохраняем все имена материалов (текстур).
		for( unsigned long m = 0; m < NumMaterials; m++ )
		{
			if( pMaterials[m].pTextureFilename )
			{
				meshContainer->materialNames[m] = new char[strlen( pMaterials[m].pTextureFilename ) + 1];
				memcpy( meshContainer->materialNames[m], pMaterials[m].pTextureFilename, ( strlen( pMaterials[m].pTextureFilename ) + 1 ) * sizeof( char ) );
			}
			else
				meshContainer->materialNames[m] = NULL;

			meshContainer->materials[m] = NULL;
		}
	}

	// Сохраняем информацию о текущих настройках меша (mesh's adjacency information).
	meshContainer->pAdjacency = new DWORD[pMeshData->pMesh->GetNumFaces() * 3];
	memcpy( meshContainer->pAdjacency, pAdjacency, sizeof( DWORD ) * pMeshData->pMesh->GetNumFaces() * 3 );

	// Сохраняем данные меша.
	meshContainer->MeshData.pMesh = meshContainer->originalMesh = pMeshData->pMesh;
	meshContainer->MeshData.Type = D3DXMESHTYPE_MESH;
	pMeshData->pMesh->AddRef();
	pMeshData->pMesh->AddRef();

	// Проверяем, является ли меш скинированным (skinned mesh).
	if( pSkinInfo != NULL )
	{
		// Сохраняем информацию о скине и о меше.
		meshContainer->pSkinInfo = pSkinInfo;
		pSkinInfo->AddRef();

		// Клонируем исходный меш для создания скинированного (skinned) меша.
		meshContainer->originalMesh->CloneMeshFVF( D3DXMESH_MANAGED, meshContainer->originalMesh->GetFVF(), g_engine->GetDevice(), &meshContainer->MeshData.pMesh );

		// Сохраняем таблицу атрибутов.
		meshContainer->MeshData.pMesh->GetAttributeTable( NULL, &meshContainer->totalAttributeGroups );
		meshContainer->attributeTable = new D3DXATTRIBUTERANGE[meshContainer->totalAttributeGroups];
		meshContainer->MeshData.pMesh->GetAttributeTable( meshContainer->attributeTable, NULL );
	}

	*ppNewMeshContainer = meshContainer;

	return S_OK;
}

//-----------------------------------------------------------------------------
// Уничтожает данный фрейм.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::DestroyFrame( THIS_ LPD3DXFRAME pFrameToFree )
{
	SAFE_DELETE_ARRAY( pFrameToFree->Name );
	SAFE_DELETE( pFrameToFree );

	return S_OK;
}

//-----------------------------------------------------------------------------
// Уничтожает данный фрейм-контейнер.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::DestroyMeshContainer( THIS_ LPD3DXMESHCONTAINER pMeshContainerToFree )
{
	MeshContainer *meshContainer = (MeshContainer*)pMeshContainerToFree;

	// Удаляет все материалы меша из менеджера материалов.
	for( unsigned long m = 0; m < meshContainer->NumMaterials; m++ )
		if( meshContainer->materials )
			g_engine->GetMaterialManager()->Remove( &meshContainer->materials[m] );

	// Уничтожает меш-контейнер.
	SAFE_DELETE_ARRAY( meshContainer->Name );
	SAFE_DELETE_ARRAY( meshContainer->pAdjacency );
	SAFE_DELETE_ARRAY( meshContainer->pMaterials );
	SAFE_DELETE_ARRAY( meshContainer->materialNames );
	SAFE_DELETE_ARRAY( meshContainer->materials );
	SAFE_DELETE_ARRAY( meshContainer->boneMatrixPointers );
	SAFE_DELETE_ARRAY( meshContainer->attributeTable );
	SAFE_RELEASE( meshContainer->MeshData.pMesh );
	SAFE_RELEASE( meshContainer->pSkinInfo );
	SAFE_RELEASE( meshContainer->originalMesh );
	SAFE_DELETE( meshContainer );

	return S_OK;
}

//-----------------------------------------------------------------------------
// The mesh class constructor.
//-----------------------------------------------------------------------------
Mesh::Mesh( char *name, char *path ) : Resource< Mesh >( name, path )
{
	// Создаём список опорных точек (reference points).
	m_frames = new LinkedList< Frame >;
	m_refPoints = new LinkedList< Frame >;

	// Загружаем иерархию меша.
	AllocateHierarchy ah;
	D3DXLoadMeshHierarchyFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &ah, NULL, (D3DXFRAME**)&m_firstFrame, &m_animationController );

	// Изначально отключаем все треки анимации (animation tracks).
	if( m_animationController != NULL )
		for( unsigned long t = 0; t < m_animationController->GetMaxNumTracks(); ++t )
			m_animationController->SetTrackEnable( t, false );

	// Обнуляем (Invalidate) массив матрицы трансформации костей (bone transformation matrices array).
	m_boneMatrices = NULL;
	m_totalBoneMatrices = 0;

	// Подготавливаем иерархию фреймов.
	PrepareFrame( m_firstFrame );

	// Выделяем память для матриц трансформации костей.
	m_boneMatrices = new D3DXMATRIX[m_totalBoneMatrices];

	// Создаём статичную (неанимированную) версию меша.
	m_staticMesh = new MeshContainer;
	ZeroMemory( m_staticMesh, sizeof( MeshContainer ) );

	// Загружаем меш.
	ID3DXBuffer *materialBuffer, *adjacencyBuffer;
	D3DXLoadMeshFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &adjacencyBuffer, &materialBuffer, NULL, &m_staticMesh->NumMaterials, &m_staticMesh->originalMesh );

	// Оптимизируем меш для лучшей производительности рендеринга.
	m_staticMesh->originalMesh->OptimizeInplace( D3DXMESHOPT_COMPACT | D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_VERTEXCACHE, (DWORD*)adjacencyBuffer->GetBufferPointer(), NULL, NULL, NULL );

	// Закончили работать с буфером подстройки (adjacency buffer), поэтому уничтожаем его.
	SAFE_RELEASE( adjacencyBuffer );

	// Проверяем, есть ли у меша материалы.
	if( m_staticMesh->NumMaterials > 0 )
	{
		// Создаём массив материалов.
		m_staticMesh->materials = new Material*[m_staticMesh->NumMaterials];

		// Получаем список материалов из буфера материалов.
		D3DXMATERIAL *materials = (D3DXMATERIAL*)materialBuffer->GetBufferPointer();

		// Загружаем каждый материал в массив через менеджер материалов.
		for( unsigned long m = 0; m < m_staticMesh->NumMaterials; m++ )
		{
			// Убеждаемся, что материал имеет текстуру.
			if( materials[m].pTextureFilename )
			{
				// Получаем имя скрипта материала и загружаем его.
				char *name = new char[strlen( materials[m].pTextureFilename ) + 5];
				sprintf( name, "%s.txt", materials[m].pTextureFilename );
				m_staticMesh->materials[m] = g_engine->GetMaterialManager()->Add( name, GetPath() );
				SAFE_DELETE_ARRAY( name );
			}
			else
				m_staticMesh->materials[m] = NULL;
		}
	}

	// Создаём ограничивающий объём вокруг меша.
	BoundingVolumeFromMesh( m_staticMesh->originalMesh );

	// Уничтожаем буфер материалов.
	SAFE_RELEASE( materialBuffer );

	// Создаём массив вершин (vertex array) и массив индексов (array of indices) в нём.
	m_vertices = new Vertex[m_staticMesh->originalMesh->GetNumVertices()];
	m_indices = new unsigned short[m_staticMesh->originalMesh->GetNumFaces() * 3];

	// Используем массивы для сохранения локальной копии массива вершин статичного меша и
	// индексов чтобы затем их можно было использовать в менеджере сцены что называется "на лету".
	Vertex* verticesPtr;
	m_staticMesh->originalMesh->LockVertexBuffer( 0, (void**)&verticesPtr );
	unsigned short *indicesPtr;
	m_staticMesh->originalMesh->LockIndexBuffer( 0, (void**)&indicesPtr );

	memcpy( m_vertices, verticesPtr, VERTEX_FVF_SIZE * m_staticMesh->originalMesh->GetNumVertices() );
	memcpy( m_indices, indicesPtr, sizeof( unsigned short ) * m_staticMesh->originalMesh->GetNumFaces() * 3 );

	m_staticMesh->originalMesh->UnlockVertexBuffer();
	m_staticMesh->originalMesh->UnlockIndexBuffer();
}

//-----------------------------------------------------------------------------
// The mesh class destructor.
//-----------------------------------------------------------------------------
Mesh::~Mesh()
{
	// Уничтожаем иерархию фреймов.
	AllocateHierarchy ah;
	D3DXFrameDestroy( m_firstFrame, &ah );

	// Уничтожаем список фреймов и список опорных точек (reference points list).
	m_frames->ClearPointers();
	SAFE_DELETE( m_frames );
	m_refPoints->ClearPointers();
	SAFE_DELETE( m_refPoints );

	// Освобождаем контроллер анимации.
	SAFE_RELEASE( m_animationController );

	// Уничтожаем матрицы (преобразования, деформации) костей.
	SAFE_DELETE_ARRAY( m_boneMatrices );

	// Уничтожаем статичный меш.
	if( m_staticMesh )
	{
		// Удаляем все текстуры статичного меша.
		for( unsigned long m = 0; m < m_staticMesh->NumMaterials; m++ )
			if( m_staticMesh->materials )
				g_engine->GetMaterialManager()->Remove( &m_staticMesh->materials[m] );

		// Подчищаем остальное.
		SAFE_DELETE_ARRAY( m_staticMesh->materials );
		SAFE_RELEASE( m_staticMesh->originalMesh );
		SAFE_DELETE( m_staticMesh );
	}

	// Уничтожаем массивы вершин и индексов вершин.
	SAFE_DELETE_ARRAY( m_vertices );
	SAFE_DELETE_ARRAY( m_indices );
}

//-----------------------------------------------------------------------------
// Обновляет меш.
//-----------------------------------------------------------------------------
void Mesh::Update()
{
	UpdateFrame( m_firstFrame );
}

//-----------------------------------------------------------------------------
// Рендерит меш.
//-----------------------------------------------------------------------------
void Mesh::Render()
{
	RenderFrame( m_firstFrame );
}

//-----------------------------------------------------------------------------
// Создаёт клон контроллера анимации меша (mesh's animation controller).
//-----------------------------------------------------------------------------
void Mesh::CloneAnimationController( ID3DXAnimationController **animationController )
{
	if( m_animationController )
		m_animationController->CloneAnimationController( m_animationController->GetMaxNumAnimationOutputs(), m_animationController->GetMaxNumAnimationSets(), m_animationController->GetMaxNumTracks(), m_animationController->GetMaxNumEvents(), &*animationController );
	else
		*animationController = NULL;
}

//-----------------------------------------------------------------------------
// Возвращает статичную (не анимированную) версию меша.
//-----------------------------------------------------------------------------
MeshContainer *Mesh::GetStaticMesh()
{
	return m_staticMesh;
}

//-----------------------------------------------------------------------------
// Возвращает статичные (не анимированные) вершины меша.
//-----------------------------------------------------------------------------
Vertex *Mesh::GetVertices()
{
	return m_vertices;
}

//-----------------------------------------------------------------------------
// Возвращает индексы вершин меша.
//-----------------------------------------------------------------------------
unsigned short *Mesh::GetIndices()
{
	return m_indices;
}

//-----------------------------------------------------------------------------
// Возвращает список фреймов в меше.
//-----------------------------------------------------------------------------
LinkedList< Frame > *Mesh::GetFrameList()
{
	return m_frames;
}

//-----------------------------------------------------------------------------
// Возвращает фрейм с данным именем.
//-----------------------------------------------------------------------------
Frame *Mesh::GetFrame( char *name )
{
	m_frames->Iterate( true );
	while( m_frames->Iterate() )
		if( strcmp( m_frames->GetCurrent()->Name, name ) == 0 )
			return m_frames->GetCurrent();

	return NULL;
}

//-----------------------------------------------------------------------------
// Возвращает опорную точку (reference point) с данным именем.
//-----------------------------------------------------------------------------
Frame *Mesh::GetReferencePoint( char *name )
{
	m_refPoints->Iterate( true );
	while( m_refPoints->Iterate() )
		if( strcmp( m_refPoints->GetCurrent()->Name, name ) == 0 )
			return m_refPoints->GetCurrent();

	return NULL;
}

//-----------------------------------------------------------------------------
// Подготавливает данный фрейм.
//-----------------------------------------------------------------------------
void Mesh::PrepareFrame( Frame *frame )
{
	m_frames->Add( frame );

	// Проверяем, является ли данный фрейм опорной точкой.
	if( strncmp( "rp_", frame->Name, 3 ) == 0 )
		m_refPoints->Add( frame );

	// Устанавливаем исходную финальную трансформацию.
	frame->finalTransformationMatrix = frame->TransformationMatrix;

	// Подготавливаем меш-контейнер фрейма, если таковой имеется.
	if( frame->pMeshContainer != NULL )
	{
		MeshContainer *meshContainer = (MeshContainer*)frame->pMeshContainer;

		// Проверяем, является ли данный меш скинированным (skinned).
		if( meshContainer->pSkinInfo != NULL )
		{
			// Создаём массив указателей матрицы (преобразования) костей.
			meshContainer->boneMatrixPointers = new D3DXMATRIX*[meshContainer->pSkinInfo->GetNumBones()];

			// Устанавливаем указатели на матрицы трансформации костей меша.
			for( unsigned long b = 0; b < meshContainer->pSkinInfo->GetNumBones(); b++ )
			{
				Frame *bone = (Frame*)D3DXFrameFind( m_firstFrame, meshContainer->pSkinInfo->GetBoneName( b ) );
				if( bone == NULL )
					continue;

				meshContainer->boneMatrixPointers[b] = &bone->finalTransformationMatrix;
			}

			// Отслеживаем, сколько костей содержится во всех меш-контейнерах.
			if( m_totalBoneMatrices < meshContainer->pSkinInfo->GetNumBones() )
				m_totalBoneMatrices = meshContainer->pSkinInfo->GetNumBones();
		}

		// Проверяем, есть ли у меша материал.
		if( meshContainer->NumMaterials > 0 )
		{
			// Загружаем все материалы через менеджер материалов.
			for( unsigned long m = 0; m < meshContainer->NumMaterials; m++ )
			{
				// Убеждаемся, что материал содержит текстуру.
				if( meshContainer->materialNames[m] != NULL )
				{
					// Получаем имя скрипта материала и загружаем его.
					char *name = new char[strlen( meshContainer->materialNames[m] ) + 5];
					sprintf( name, "%s.txt", meshContainer->materialNames[m] );
					meshContainer->materials[m] = g_engine->GetMaterialManager()->Add( name, GetPath() );
					SAFE_DELETE_ARRAY( name );
				}
			}
		}
	}

	// Подготавливаем одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		PrepareFrame( (Frame*)frame->pFrameSibling );

	// Подготавливаем дочерние (нижестоящие) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		PrepareFrame( (Frame*)frame->pFrameFirstChild );
}

//-----------------------------------------------------------------------------
// Обновляет матрицы преобразований данного фрейма.
//-----------------------------------------------------------------------------
void Mesh::UpdateFrame( Frame *frame, D3DXMATRIX *parentTransformationMatrix )
{
	if( parentTransformationMatrix != NULL )
		D3DXMatrixMultiply( &frame->finalTransformationMatrix, &frame->TransformationMatrix, parentTransformationMatrix );
	else
		frame->finalTransformationMatrix = frame->TransformationMatrix;

	// Обновляем одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		UpdateFrame( (Frame*)frame->pFrameSibling, parentTransformationMatrix );

	// Обновляем дочерние (нижестоящие) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		UpdateFrame( (Frame*)frame->pFrameFirstChild, &frame->finalTransformationMatrix );
}

//-----------------------------------------------------------------------------
// Рендерит меш-контейнеры данного фрейма, если таковые имеются.
//-----------------------------------------------------------------------------
void Mesh::RenderFrame( Frame *frame )
{
	MeshContainer *meshContainer = (MeshContainer*)frame->pMeshContainer;

	// Рендерим меш данного фрейма, если таковой имеется.
	if( frame->pMeshContainer != NULL )
	{
		// Проверяем, являетося ли данный меш скинированным (skined).
		if( meshContainer->pSkinInfo != NULL )
		{
			// Создаём трансформации костей с использованием матриц преобразования меша.
			for( unsigned long b = 0; b < meshContainer->pSkinInfo->GetNumBones(); ++b )
				D3DXMatrixMultiply( &m_boneMatrices[b], meshContainer->pSkinInfo->GetBoneOffsetMatrix( b ), meshContainer->boneMatrixPointers[b] );

			// Обновляем вершины меша с учётом матриц преобразований меша.
			PBYTE sourceVertices, destinationVertices;
			meshContainer->originalMesh->LockVertexBuffer( D3DLOCK_READONLY, (void**)&sourceVertices );
			meshContainer->MeshData.pMesh->LockVertexBuffer( 0, (void**)&destinationVertices );
			meshContainer->pSkinInfo->UpdateSkinnedMesh( m_boneMatrices, NULL, sourceVertices, destinationVertices );
			meshContainer->originalMesh->UnlockVertexBuffer();
			meshContainer->MeshData.pMesh->UnlockVertexBuffer();

			// Рендерим меш по группам атрибутов.
			for( unsigned long a = 0; a < meshContainer->totalAttributeGroups; a++ )
			{
				g_engine->GetDevice()->SetMaterial( meshContainer->materials[meshContainer->attributeTable[a].AttribId]->GetLighting() );
				g_engine->GetDevice()->SetTexture( 0, meshContainer->materials[meshContainer->attributeTable[a].AttribId]->GetTexture() );
				meshContainer->MeshData.pMesh->DrawSubset( meshContainer->attributeTable[a].AttribId );
			}
		}
		else
		{
			// Это не скинированный меш, поэтому его будем рендерить как статичный (static).
			for( unsigned long m = 0; m < meshContainer->NumMaterials; m++)
			{
				if( meshContainer->materials[m] )
				{
					g_engine->GetDevice()->SetMaterial( meshContainer->materials[m]->GetLighting() );
					g_engine->GetDevice()->SetTexture( 0, meshContainer->materials[m]->GetTexture() );
				}
				else
					g_engine->GetDevice()->SetTexture( 0, NULL );

				meshContainer->MeshData.pMesh->DrawSubset( m );
			}
		}
	}

	// Рендерим одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		RenderFrame( (Frame*)frame->pFrameSibling );

	// Рендерим дочерние (нижестоящие в иерархии) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		RenderFrame( (Frame*)frame->pFrameFirstChild );
}

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


Исследуем код Mesh.cpp

Реализация функций класса AllocateHierarchy

Обсудим принцип работы каждой из четырёх функций, объявленных в классе AllocateHierarchy (см. исходный код Mesh.h):

  • CreateFrame
  • CreateMeshContainer
  • DestroyFrame
  • DestroyMeshContainer
Закрыть
noteОбрати внимание

Напомним, что данный исходный код является по большей части фундаментально-служебным (underlying utility sourcecode), и предназнeачен для поддержки класса более высокого уровня (в данном случае - класса Mesh). При разработке игры ты увидишь, что на самом деле тебе никогда не придётся иметь дело с данным кодом. Будучи однажды написан, он не требует дальнейшего вмешательства до тех пор, пока ты не решишь модифицировать способ загрузки 3D-меша, либо добавить в данный процесс новую фичу.

Сначала рассмотрим как класс AllocateHierarchy выполняет создание и удаление одного фрейма с использованием структуры Frame.
Как только .x-файл загружен, всякий раз, когда во время процесса загрузки встречается новый фрейм иерархии, D3DX вызывает функцию CreateFrame:

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Создаёт новый фрейм.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::CreateFrame( THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame )
{
	// Создаём новый фрейм и обнуляем его память.
	Frame *frame = new Frame;
	ZeroMemory( frame, sizeof( Frame ) );

	// Копируем имя фрейма.
	if( Name == NULL )
	{
		// Имя отсутствует, поэтому создаём уникальное имя.
		static unsigned long nameCount = 0;
		char newName[32];
		sprintf( newName, "unknown_frame_%d", nameCount );
		nameCount++;

		frame->Name = new char[strlen( newName ) + 1];
		strcpy( frame->Name, newName );
	}
	else
	{
		frame->Name = new char[strlen( Name ) + 1];
		strcpy( frame->Name, Name );
	}

	*ppNewFrame = frame;

	return S_OK;
}
...

При этом структура Frame автоматически создаётся, заполняется и возвращается с уже сохранёнными в ней данными о новом фрейме. Всякий раз при вызове CreateFrame библиотека D3DX передаёт имя нового фрейма (которое она считывает из .x-файла) и адрес указателя на структуру D3DXFRAME, с которой она в принципе всегда работает. Так как наша структура Frame ветвится от структуры D3DXFRAME, то с её помощью мы можем без труда создать новый экземпляр структуры Frame, присвоить ему имя и затем вернуть указатель обратно, в функцию CreateFrame. Обо остальном позаботиться библиотека D3DX. Она же корректно встроит новый фрейм в текущую иерархию.
Когда придёт время уничтожать фрейм (например при завершении работы приложения), библиотека D3DX вызовет функцию DestroyFrame, передав в неё указатель уничтожаемого фрейма:

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Уничтожает данный фрейм.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::DestroyFrame( THIS_ LPD3DXFRAME pFrameToFree )
{
	SAFE_DELETE_ARRAY( pFrameToFree->Name );
	SAFE_DELETE( pFrameToFree );

	return S_OK;
}
...

Как видим, уничтожить фрейм даже проще, чем создать его. При этом происходит освобождение участка памяти, выделенного при выполнении функции CreateFrame. Здесь мы также применяем макрос SAFE_DELETE, передавая указатель уничтожаемого фрейма. Функция DestroyMeshContainer работает схожим образом:

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Уничтожает данный фрейм-контейнер.
//-----------------------------------------------------------------------------
HRESULT AllocateHierarchy::DestroyMeshContainer( THIS_ LPD3DXMESHCONTAINER pMeshContainerToFree )
{
	MeshContainer *meshContainer = (MeshContainer*)pMeshContainerToFree;

	// Удаляет все материалы меша из менеджера материалов.
	for( unsigned long m = 0; m < meshContainer->NumMaterials; m++ )
		if( meshContainer->materials )
			g_engine->GetMaterialManager()->Remove( &meshContainer->materials[m] );

	// Уничтожает меш-контейнер.
	SAFE_DELETE_ARRAY( meshContainer->Name );
	SAFE_DELETE_ARRAY( meshContainer->pAdjacency );
	SAFE_DELETE_ARRAY( meshContainer->pMaterials );
	SAFE_DELETE_ARRAY( meshContainer->materialNames );
	SAFE_DELETE_ARRAY( meshContainer->materials );
	SAFE_DELETE_ARRAY( meshContainer->boneMatrixPointers );
	SAFE_DELETE_ARRAY( meshContainer->attributeTable );
	SAFE_RELEASE( meshContainer->MeshData.pMesh );
	SAFE_RELEASE( meshContainer->pSkinInfo );
	SAFE_RELEASE( meshContainer->originalMesh );
	SAFE_DELETE( meshContainer );

	return S_OK;
}
...

При её вызове передаётся указатель уничтожаемого меш-контейнера. Всё, что остаётся сделать программеру - это выполнить в обратном порядке действия, выполняемые при вызове функции CreateMeshContainer, освободив выделенную ранее память.
Последняя функция, которую необходимо рассмотреть, это CreateMeshContainer. Данная функция самая сложная из всех четырёх, в основном из-за того, что в ней содержится необычайно много различных параметров, на которые стоит обратить внимание. Основная задача данной функции - создать новую структуру MeshContainer, и вернуть указатель на него. (Точно также, как и функция CreateFrame создаёт структуру нового фрейма и возвращает указатель на него.) Помимо этого на неё также возложено множество других задач, как например сохранение актуальной информации о данном меше и об используемых им материалах.

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

Если внимательно рассмотреть исходный код, в котором мы сохраняем детальную информацию об используемых материалах, то можно увидеть, что мы просто сохраняем имена текстур, которые на самом деле являются именами скриптов материалов. В обычном случае здесь загружают и сохраняют именно имена текстур. В любом случае мы понятия не имеем где расположены скрипты материалов. Другими словами, у нас есть имя материала, но мы не знаем путь (path) по которому он находится. Поэтому мы просто сохраняем имя, а затем позднее будем загружать каждый скрипт материала с использованием того же пути, по которому расположен .x-файл с мешем.

Загрузка 3D-меша

  • Происходит именно в конструкторе класса Mesh:
Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The mesh class constructor.
//-----------------------------------------------------------------------------
Mesh::Mesh( char *name, char *path ) : Resource< Mesh >( name, path )
{
	// Создаём список опорных точек (reference points).
	m_frames = new LinkedList< Frame >;
	m_refPoints = new LinkedList< Frame >;

	// Загружаем иерархию меша.
	AllocateHierarchy ah;
	D3DXLoadMeshHierarchyFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &ah, NULL, (D3DXFRAME**)&m_firstFrame, &m_animationController );

	// Изначально отключаем все треки анимации (animation tracks).
	if( m_animationController != NULL )
		for( unsigned long t = 0; t < m_animationController->GetMaxNumTracks(); ++t )
			m_animationController->SetTrackEnable( t, false );

	// Обнуляем (Invalidate) массив матрицы трансформации костей (bone transformation matrices array).
	m_boneMatrices = NULL;
	m_totalBoneMatrices = 0;

	// Подготавливаем иерархию фреймов.
	PrepareFrame( m_firstFrame );

	// Выделяем память для матриц трансформации костей.
	m_boneMatrices = new D3DXMATRIX[m_totalBoneMatrices];

	// Создаём статичную (неанимированную) версию меша.
	m_staticMesh = new MeshContainer;
	ZeroMemory( m_staticMesh, sizeof( MeshContainer ) );

	// Загружаем меш.
	ID3DXBuffer *materialBuffer, *adjacencyBuffer;
	D3DXLoadMeshFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &adjacencyBuffer, &materialBuffer, NULL, &m_staticMesh->NumMaterials, &m_staticMesh->originalMesh );

	// Оптимизируем меш для лучшей производительности рендеринга.
	m_staticMesh->originalMesh->OptimizeInplace( D3DXMESHOPT_COMPACT | D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_VERTEXCACHE, (DWORD*)adjacencyBuffer->GetBufferPointer(), NULL, NULL, NULL );

	// Закончили работать с буфером подстройки (adjacency buffer), поэтому уничтожаем его.
	SAFE_RELEASE( adjacencyBuffer );

	// Проверяем, есть ли у меша материалы.
	if( m_staticMesh->NumMaterials > 0 )
	{
		// Создаём массив материалов.
		m_staticMesh->materials = new Material*[m_staticMesh->NumMaterials];

		// Получаем список материалов из буфера материалов.
		D3DXMATERIAL *materials = (D3DXMATERIAL*)materialBuffer->GetBufferPointer();

		// Загружаем каждый материал в массив через менеджер материалов.
		for( unsigned long m = 0; m < m_staticMesh->NumMaterials; m++ )
		{
			// Убеждаемся, что материал имеет текстуру.
			if( materials[m].pTextureFilename )
			{
				// Получаем имя скрипта материала и загружаем его.
				char *name = new char[strlen( materials[m].pTextureFilename ) + 5];
				sprintf( name, "%s.txt", materials[m].pTextureFilename );
				m_staticMesh->materials[m] = g_engine->GetMaterialManager()->Add( name, GetPath() );
				SAFE_DELETE_ARRAY( name );
			}
			else
				m_staticMesh->materials[m] = NULL;
		}
	}

	// Создаём ограничивающий объём вокруг меша.
	BoundingVolumeFromMesh( m_staticMesh->originalMesh );

	// Уничтожаем буфер материалов.
	SAFE_RELEASE( materialBuffer );

	// Создаём массив вершин (vertex array) и массив индексов (array of indices) в нём.
	m_vertices = new Vertex[m_staticMesh->originalMesh->GetNumVertices()];
	m_indices = new unsigned short[m_staticMesh->originalMesh->GetNumFaces() * 3];

	// Используем массивы для сохранения локальной копии массива вершин статичного меша и
	// индексов чтобы затем их можно было использовать в менеджере сцены что называется "на лету".
	Vertex* verticesPtr;
	m_staticMesh->originalMesh->LockVertexBuffer( 0, (void**)&verticesPtr );
	unsigned short *indicesPtr;
	m_staticMesh->originalMesh->LockIndexBuffer( 0, (void**)&indicesPtr );

	memcpy( m_vertices, verticesPtr, VERTEX_FVF_SIZE * m_staticMesh->originalMesh->GetNumVertices() );
	memcpy( m_indices, indicesPtr, sizeof( unsigned short ) * m_staticMesh->originalMesh->GetNumFaces() * 3 );

	m_staticMesh->originalMesh->UnlockVertexBuffer();
	m_staticMesh->originalMesh->UnlockIndexBuffer();
}
...

Исходный код неплохо комментирован. Поэтому мы остановимся лишь на самых значимых моментах.
Мы начинаем с извлечения из .x-файла сведений о иерархии меша (mesh hierarchy), за что отвечают следующие пара строк кода:

Фрагмент Mesh.cpp (Проект Engine)
...
	// Загружаем иерархию меша.
	AllocateHierarchy ah;
	D3DXLoadMeshHierarchyFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &ah, NULL, (D3DXFRAME**)&m_firstFrame, &m_animationController );
...

Сперва мы создаём инстанс класса AllocateHierarchy. Напомним, что он применяется для загрузки и уничтожения фреймов и меш-контейнеров в пределах иерархии данного .x-файла. Далее вызываем функцию D3DXLoadMeshHierarchyFromX, которая предоставлена библиотекой D3DX. Вот её прототип:

Прототип (шаблон) функции D3DXLoadMeshHierarchyFromX
HRESULT WINAPI D3DXLoadMeshHierarchyFromX
{
  LPCSTR FileName,  // Полное имя файла (имя + путь до него) загружаемого .x-файла.
  DWORD MeshOptions,  // Опциональные флаги создания меша.
  LPDIRECT3DDEVICE9 pDevice,  // Указатель на экземпляр устройства Direct3D будет ассоциирован создаваемый меш.
  LPD3DXALLOCATEHIERARCHY pAlloc,  // Указатель на интерфейс ID3DXAllocateHierarchy.
  LPD3DXLOADUSERDATA pUserDataLoader,  // Указатель на интерфейс для загрузки данных, специфичных для приложения.
  LPD3DXFRAME* ppFrameHierarchy,  // Возвращённый указатель на первый кадр в загруженной иерархии.
  LPD3DXANIMATIONCONTROLLER* ppAnimController  // Возвращённый указатель на контроллер анимации для анимаций меша.
};

В первом параметре передаётся полное имя (имя + путь) .x-файла, которое сохранено классом Mesh внутри класса Resource.
Во втором параметре можно указать множество опций. Все они представлены в энумерации D3DXMESH:

Энумерация D3DXMESH
typedef enum D3DXMESH { 
  D3DXMESH_32BIT                  = 0x001,
  D3DXMESH_DONOTCLIP              = 0x002,
  D3DXMESH_POINTS                 = 0x004,
  D3DXMESH_RTPATCHES              = 0x008,
  D3DXMESH_NPATCHES               = 0x4000,
  D3DXMESH_VB_SYSTEMMEM           = 0x010,
  D3DXMESH_VB_MANAGED             = 0x020,
  D3DXMESH_VB_WRITEONLY           = 0x040,
  D3DXMESH_VB_DYNAMIC             = 0x080,
  D3DXMESH_VB_SOFTWAREPROCESSING  = 0x8000,
  D3DXMESH_IB_SYSTEMMEM           = 0x100,
  D3DXMESH_IB_MANAGED             = 0x200,
  D3DXMESH_IB_WRITEONLY           = 0x400,
  D3DXMESH_IB_DYNAMIC             = 0x800,
  D3DXMESH_IB_SOFTWAREPROCESSING  = 0x10000,
  D3DXMESH_VB_SHARE               = 0x1000,
  D3DXMESH_USEHWONLY              = 0x2000,
  D3DXMESH_SYSTEMMEM              = 0x110,
  D3DXMESH_MANAGED                = 0x220,
  D3DXMESH_WRITEONLY              = 0x440,
  D3DXMESH_DYNAMIC                = 0x880,
  D3DXMESH_SOFTWAREPROCESSING     = 0x18000
} D3DXMESH, *LPD3DXMESH;

Вот описание всех этих флагов:

Флаг Описание
D3DXMESH_32BIT Создаваемый меш имеет 32-битные индексы вместо 16-битных (принятых по умолчанию).
D3DXMESH_DONOTCLIP Указывается в случае, когда содержимое вершинного буфера (vertex buffer) никогда не потребует обрезания (clipping). Нельзя использовать совместно с рендер-стейтом D3DRS_CLIPPING.
D3DXMESH_POINTS Указывается в случае, когда содержимое вершинного буфера (или его индекса) будет использовано для прорисовки точечных спрайтов (point sprites). В случае выбора программной обработки вершин (software buffer processing), необходимой для эмуляции точечных спрайтов, в этом случае буфер будет загружен в системную память.
D3DXMESH_RTPATCHES Указывается в случае, когда вершинный буфер будет использоваться для создания примитивов высокого порядка (high-order primitives).
D3DXMESH_NPATCHES Указание данного флага ведёт к созданию вершинного и индексного буфера с использованием флага D3DUSAGE_NPATCHES. Это необходимо когда меш создаётся с использованием улучшений N-патча (N-patch enhancement), поддерживаемых Direct3D.
D3DXMESH_VB_SYSTEMMEM Ресурсы вершинного буфера расположены в системной памяти, что нетипично для организации доступа к ним объекта устройства Direct3D. Такое расположение занимает системную память (system RAM), но не снижает объёма, используемой страничной (pageable) памяти. Данные ресурсы не требуется пересоздавать заново в случае утери объекта устройства Direct3D.
D3DXMESH_VB_MANAGED Ресурсы вершинного буфера автоматически копируются в аппаратную (device-accessible) память при необходимости. Управляемые (managed) ресурсы изначально хранятся в системной памяти и их не требуется пересоздавать в случае утери объекта устройства Direct3D. Управляемые устройства могут быть заблокированы (locked). Только копия ресурсов в системной памяти может быть модифицирована напрямую. Direct3D затем копирует внесённые изменения в аппаратную память при необходимости.
D3DXMESH_VB_WRITEONLY Информирует систему о том, что вершинный буфер доступен только для записи. При использовании данного флага драйвер устройства автоматически выбирает наилучшее расположение памяти для эффективной записи и быстрого рендеринга. Любая попытка считать содержимое буфера будет провалена. Немного повышает быстродействие.
D3DXMESH_VB_DYNAMIC Информирует систему о том, что вершинный буфер требует динамического выделения памяти. Обычно данный флаг полезен при разработке драйверов устройств, т.к. позволяет им самостоятельно решать, в какой памяти разместить буфер. Обычно статичные буферы размещаются в видеопамяти, а динамические - в AGP-памяти.
D3DXMESH_VB_SOFTWAREPROCESSING Обработка вершин происходит программно (software; т.е. с преимущественным участием центрального процессора). При отсутствии данного флага обработка вершин происходит аппаратно (hardware; т.е. с использованием ресурсов видеокарты).
D3DXMESH_IB_SYSTEMMEM Ресурсы буфера индексов расположены в системной памяти, что нетипично для организации доступа к ним объекта устройства Direct3D. Такое расположение занимает системную память (system RAM), но не снижает объёма, используемой страничной (pageable) памяти. Данные ресурсы не требуется пересоздавать заново в случае утери объекта устройства Direct3D.
D3DXMESH_IB_MANAGED Ресурсы буфера индексов автоматически копируются в аппаратную (device-accessible) память при необходимости. Управляемые (managed) ресурсы изначально хранятся в системной памяти и их не требуется пересоздавать в случае утери объекта устройства Direct3D. Управляемые устройства могут быть заблокированы (locked). Только копия ресурсов в системной памяти может быть модифицирована напрямую. Direct3D затем копирует внесённые изменения в аппаратную память при необходимости.
D3DXMESH_IB_WRITEONLY Информирует систему о том, что буфер индексов доступен только для записи. При использовании данного флага драйвер устройства автоматически выбирает наилучшее расположение памяти для эффективной записи и быстрого рендеринга. Любая попытка считать содержимое буфера будет провалена. Немного повышает быстродействие.
D3DXMESH_IB_DYNAMIC Информирует систему о том, что буфер индексов требует динамического выделения памяти. Обычно данный флаг полезен при разработке драйверов устройств, т.к. позволяет им самостоятельно решать, в какой памяти разместить буфер. Обычно статичные буферы размещаются в видеопамяти, а динамические - в AGP-памяти.
D3DXMESH_IB_SOFTWAREPROCESSING Обработка индексов вершин происходит программно (software; т.е. с преимущественным участием центрального процессора). При отсутствии данного флага обработка индексов вершин происходит аппаратно (hardware; т.е. с использованием ресурсов видеокарты).
D3DXMESH_VB_SHARE Заставляет клонированные меши совместно использовать одни и те же вершинные буферы.
D3DXMESH_USEHWONLY Даёт команду использовать только аппаратную обработку (hardware processing).
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.

В нашем случае мы используем всего один флаг - D3DXMESH_MANAGED, который говорит функции D3DXLoadMeshHierarchyFromX управлять загруженным мешем аппаратно. Если помнишь, это позволяет ресурсу находиться в системной памяти, но при необходимости он будет скопирован в аппаратную память для более быстрого рендеринга. Использование данного флага также означает, что ресурс не требуется пересоздавать заново в случае утери объекта устройства Direct3D, т.к. его копия всегда хранится в системной памяти.
В третьем параметре мы передаём указатель на наш объект устройства Direct3D. В этот момент меш ассоциируется именно с этим устройством, что означает, в случае, когда объект устройства утрачен или уничтожен, можно больше не пытаться рендерить на нём загруженный меш.
В четвёртом параметре передаём указатель на экземпляр класса AllocateHierarchy. Как видно, фактически он запрашивает указатель на интерфейс ID3DXAllocateHierarchy. Но раз уж наш класс AllocateHierarchy ветвится от интерфейса ID3DXAllocateHierarchy, мы можем подставить здесь нашу версию. Это в свою очередь означает, что в классе AllocateHierarchy для создания фреймов и меш-контейнеров (после построения иерархии меша) будут вызваны как раз переопределённые функции.
Пятый параметр можно проигнорировать, так как он обычно применяется для загрузки дополнительных пользовательских данных, сохранённых в .X-файле. Мы уже кратко говорили об этом в начале данной Главы, и ещё раз напомним, в нашем курсе мы не будем использовать данную опцию.
Шестой и седьмой параметры представляют собой указатели, возвращаемые данной функцией.
В шестом параметре мы указываем m_firstFrame, что является указателем на структуру Frame. Так, при завершении выполнения функции данный параметр указывает на первый фрейм в загруженной иерархии фреймов меша. Чуть позднее ты увидишь как использовать данный указатель в работе рекурсивных функций для просмотра иерархии и выполнения операций над каждым фреймом (например их обновление (update) и рендеринг).
В седьмом параметре мы передаём переменный член m_animationController, который представляет собой указатель на интерфейс ID3DXAnimationController. Данный интерфейс используется для контроля любых анимаций, встроенных в меш и сохранённых в .X-файле. Мы обязательно подробно рассмотрим данный процесс в последующих Главах данного курса, при изучении анимированных объектов.

Предположим, что функция D3DXLoadMeshHierarchyFromX успешно выполнена, .X-файл прочитан и иерархия фреймов меша успешно загружена. Тем не менее перед началом использования меша нам остаётся выполнить ещё два важных шага:

  • подготовить иерархию фреймов (frame hierarchy);
  • создать статичную (static = неподвижную) версию меша.

Готовим иерархию фреймов (функция PrepareFrame)

Для этого мы вызываем функцию PrepareFrame, объявленную в секции private класса Mesh.

Фрагмент Mesh.cpp (Проект Engine)
...
	// Подготавливаем иерархию фреймов.
	PrepareFrame( m_firstFrame );
...

Она принимает всего один параметр - указатель на инстанс структуры Frame - m_firstFrame.

Реализация функции PrepareFrame содержится в Mesh.cpp и выглядит так:

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Подготавливает данный фрейм.
//-----------------------------------------------------------------------------
void Mesh::PrepareFrame( Frame *frame )
{
	m_frames->Add( frame );

	// Проверяем, является ли данный фрейм опорной точкой.
	if( strncmp( "rp_", frame->Name, 3 ) == 0 )
		m_refPoints->Add( frame );

	// Устанавливаем исходную финальную трансформацию.
	frame->finalTransformationMatrix = frame->TransformationMatrix;

	// Подготавливаем меш-контейнер фрейма, если таковой имеется.
	if( frame->pMeshContainer != NULL )
	{
		MeshContainer *meshContainer = (MeshContainer*)frame->pMeshContainer;

		// Проверяем, является ли данный меш скинированным (skinned).
		if( meshContainer->pSkinInfo != NULL )
		{
			// Создаём массив указателей матрицы (преобразования) костей.
			meshContainer->boneMatrixPointers = new D3DXMATRIX*[meshContainer->pSkinInfo->GetNumBones()];

			// Устанавливаем указатели на матрицы трансформации костей меша.
			for( unsigned long b = 0; b < meshContainer->pSkinInfo->GetNumBones(); b++ )
			{
				Frame *bone = (Frame*)D3DXFrameFind( m_firstFrame, meshContainer->pSkinInfo->GetBoneName( b ) );
				if( bone == NULL )
					continue;

				meshContainer->boneMatrixPointers[b] = &bone->finalTransformationMatrix;
			}

			// Отслеживаем, сколько костей содержится во всех меш-контейнерах.
			if( m_totalBoneMatrices < meshContainer->pSkinInfo->GetNumBones() )
				m_totalBoneMatrices = meshContainer->pSkinInfo->GetNumBones();
		}

		// Проверяем, есть ли у меша материал.
		if( meshContainer->NumMaterials > 0 )
		{
			// Загружаем все материалы через менеджер материалов.
			for( unsigned long m = 0; m < meshContainer->NumMaterials; m++ )
			{
				// Убеждаемся, что материал содержит текстуру.
				if( meshContainer->materialNames[m] != NULL )
				{
					// Получаем имя скрипта материала и загружаем его.
					char *name = new char[strlen( meshContainer->materialNames[m] ) + 5];
					sprintf( name, "%s.txt", meshContainer->materialNames[m] );
					meshContainer->materials[m] = g_engine->GetMaterialManager()->Add( name, GetPath() );
					SAFE_DELETE_ARRAY( name );
				}
			}
		}
	}

	// Подготавливаем одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		PrepareFrame( (Frame*)frame->pFrameSibling );

	// Подготавливаем дочерние (нижестоящие) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		PrepareFrame( (Frame*)frame->pFrameFirstChild );
}
...


Рекурсия функции PrepareFrame
Ты уже не раз встречал термин рекурсивная функция, но мы толком не обсудили его значения. Функция PrepareFrame также является рекурсивной (т.е. вызывающей саму себя). Функция выполняет некоторые действия, а затем ещё раз вызывает саму себя для повторения этих действий. Она продолжает вызывать саму себя до тех пор, пока не выполнит текущие задачи определённое количество раз (число выполнений предустанавливает программист). Точно также как это делает цикл (loop). Но что делает рекурсивную функцию по-настоящему удивительной и полезной, так это то, что ты можешь передавать в качестве её вводных параметров результаты вычислений её предыдущих рекурсий.
Рассмотрим сильно упрощённый пример рекурсивной функции:

Пример рекурсивной функции
...
void Count( int number, int CountTo)
{
 number++;

 printf( "%d\n", number);
 
 if(number < CountTo)
  Count( number, CountTo );
}
...

В данном примере функция выполняет счёт (перечисление) от начального значения number до конечного значения CountTo, выводя результат в командной строке. Ты можешь легко проверить данный код в работе, скопировав его в заготовку консольного приложения, созданной мастером приложений MSVC++. При запуске скомпилированного приложения через командную строку (Count(0, 10)) в консоль будут выведены числа от 0 до 10, сигнализируя о том, что данная функция вызвала саму себя 10 раз подряд. Данная рекурсивная функция инкрементирует (увеличивает на 1) начальное значение number и затем проверяет, не равно ли полученное значение конечному значению CountTo, указанному во втором параметре. Если нет, то функция вновь вызывает саму себя, передавая в качестве параметров инкрементированное до этого значение переменной number и конечное значение CountTo. Следующая рекурсия вновь инкрементирует значение переменной number и проверяет, не равно ли оно переменной CountTo. И так продолжается до достижения конечного значения, равного значению параметра CountTo. Как только это значение достигнуто, функция перестаёт вызывать саму себя и возвращает управление главной функции программы, затем все предыдущие рекурсии делаю то же самое до тех пор, пока все вызовы функции Call (её рекурсии) не завершат свою работу.

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

Ты всегда должен быть уверен в том, что рекурсивная функция гарантированно выдаст результат. Другими словами в коде должна обязательно присутствовать проверка наличия результата выполнения рекурсивной функции, которая позволит грамотно завершить её работу в случае отсутствия оного. В предыдущем примере мы просто проверяли, не достигло ли текущее инкрементированное значение конечного значения. Если таких проверок не делать, велик риск того, что функция будет рекурсироваться неограниченно. При этом программа как правило перестаёт отвечать на запросы, начинается повышенный расход ресурсов компьютера и, как результат, зависание ОС.


В Mesh.cpp представлена реализация функции PrepareFrame, где в самом её конце наглядно видно как она вызывает саму себя, т.е. выполняет рекурсию:

Фрагмент Mesh.cpp (Проект Engine)
...
        // Подготавливаем одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		PrepareFrame( (Frame*)frame->pFrameSibling );

	// Подготавливаем дочерние (нижестоящие) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		PrepareFrame( (Frame*)frame->pFrameFirstChild );
...

Рис.7 Трассировка иерархии фреймов с применением рекурсивной функции PrepareFrame
Рис.7 Трассировка иерархии фреймов с применением рекурсивной функции PrepareFrame


Сама структура Frame имеет два члена:

  • pFrameSibling
  • pFrameFirstChild

Оба они наследуются от интерфейса D3DXFRAME и используются для включения фрейма в иерархию.
pFrameSibling соединяет фрейм с другими фреймами на том же уровне иерархии (hierarchy level). Другими словами у них всех общий родитель.
pFrameFirstChild служит для указания первого дочернего фрейма (child frame), который на самом деле является первым фреймом на следующем нижестоящим уровне иерархии, прямо под своим родителем (parent frame).
Для подготовки текущей иерархии фреймов необходимо вызвать функцию PrepareFrame для каждого фрейма данной иерархии. При каждом её вызове в неё передаётся указатель очередного фрейма. Рекурсия начинается с подготовки фреймов, расположенных на одном уровне с текущим фреймом (siblings), если таковые имеются. Когда все одноуровневые фреймы (siblings) будут обработаны, рекурсия затем переходит к первому дочернему фрейму (child frame, потомок) последнего одноуровневого фрейма. Затем обрабатываются все фреймы, расположенные на одном уровне с данным дочерним фреймом (т.е. являющихся по отношению к нему одноуровневыми фреймами, siblings). Затем таким же образом обрабатываются дочерние фреймы последующих низлежащих уровней (см. Рис.7).
Рекурсия достигает конца ветви древовидной структуры иерархии фреймов когда оба члена структуры Frame (pFrameSibling и pFrameFirstChild) равны нулю (NULL). Это означает, что у данного родительского фрейма больше нет необработанных дочерних фреймов (одного и того же уровня, т.е. siblings), а текущий (последний в своей ветви) фрейм не имеет потомков. В этот момент рекурсивная функция завершает свою работу, что позволяет также завершить работу всем предыдущим рекурсиям. При этом происходит возврат к самому верхнему уровню иерархии до тех пор, пока не будет обнаружены фреймы с необработанными дочерними фреймами (потомками). Здесь мы входим в новую рекурсию, начав обработку фреймов новой ветви, пока не будет достигнут её конец. То же самое происходит с остальными ветвями иерархии. При этом каждая запущенная рекурсия возвращается к вершине иерархии, к самому первому фрейму, с которого всё начиналось. Последний незавершённый вызов рекурсивной функции (который на самом деле является первым, с которого и начался процесс обработки фреймов) возвращает управление приложению и на этом процесс трассировки фреймов заканчивается.
Теперь у тебя должно более или менее чёткое представление о том, как рекурсивная функция PrepareFrame проходит через все фреймы иерархии и обрабатывает их. Если внимательно рассмотреть весь исходный код реализации функции PrepareFrame (Mesh.cpp), то можно увидеть, как именно происходит обработка каждого фрейма. Наиболее важным моментом здесь является то, что функция всякий раз проверяет, есть ли у текущего фрейма меш-контейер. Если да, то он немедленно готовится к рендерингу. Подготовка происходит в два этапа:

  1. сохраняются указатели матриц преобразований костей (bone matrices);
  2. загружаются материалы меша.

Рассмотрим каждый из них подробно.

1. Сохранение указателей матриц преобразования костей (bone matrices)
Чуть выше мы обсуждали каким образом меш использует кости для собственной анимации, подобно тому как работают кости в нашем теле. Предположим у нас есть фрейм, который заключает в себе твою левую ногу и содержит в себе меш-контейнер, который, в свою очередь, вмещает в себя (houses) данные о 3D-меше (mesh data) твоей левой ноги. Для того, чтобы переместить эту ногу необходимо переместить кости, которые содержатся внутри неё (на самом деле в реальном организме кости приводят в действие мышцы, но в нашем случае это неважно). Когда ты хочешь переместить только левую ногу, ты перемещаешь только кости левой ноги. Другими словами, если начинают двигаться кости твоей правой ноги или, например, рук, то в данный момент (допустим) ты не хочешь, чтобы твоя левая нога двигалась. Для этого мы организовываем список указателей (list of pointers) на матрицы преобразований для каждой из костей левой ноги. Напомним, что каждая кость представляет собой всего лишь фрейм, имеющий собственную матрицу преобразований (transformation matrix), которая в свою очередь является суммой трёх матриц (положения, вращения и масштабирования) и которая отражает процесс перемещения в 3D-пространстве, вращение либо изменение размера (масштабирование, scaling) фрейма. Когда эти фреймы анимируются, изменяются их соответствующие матрицы положения, вращения и масштабирования. И когда эти фреймы анимируются, матрицы изменяются. А когда эти матрицы применяются к вершинам меша, они двигаются вслед за костями, создавая анимацию.
В реализации функции PrepareFrame данный шаг представлен так:

Фрагмент Mesh.cpp (Проект Engine)
...
	// Подготавливаем меш-контейнер фрейма, если таковой имеется.
	if( frame->pMeshContainer != NULL )
	{
		MeshContainer *meshContainer = (MeshContainer*)frame->pMeshContainer;

		// Проверяем, является ли данный меш скинированным (skinned).
		if( meshContainer->pSkinInfo != NULL )
		{
			// Создаём массив указателей матрицы (преобразования) костей.
			meshContainer->boneMatrixPointers = new D3DXMATRIX*[meshContainer->pSkinInfo->GetNumBones()];

			// Устанавливаем указатели на матрицы трансформации костей меша.
			for( unsigned long b = 0; b < meshContainer->pSkinInfo->GetNumBones(); b++ )
			{
				Frame *bone = (Frame*)D3DXFrameFind( m_firstFrame, meshContainer->pSkinInfo->GetBoneName( b ) );
				if( bone == NULL )
					continue;

				meshContainer->boneMatrixPointers[b] = &bone->finalTransformationMatrix;
			}

			// Отслеживаем, сколько костей содержится во всех меш-контейнерах.
			if( m_totalBoneMatrices < meshContainer->pSkinInfo->GetNumBones() )
				m_totalBoneMatrices = meshContainer->pSkinInfo->GetNumBones();
		}
...

Здесь нам необходимо найти все кости, которые влияют на меш в текущем фрейме. Затем мы сохраняем указатель на матрицу преобразования фрейма в каждой из костей. Чуть позднее ты увидишь, как работают и где применяются эти матрицы преобразования костей, когда мы будем рендерить 3D-меш.

2. Загрузка материалов 3D-меша
Если ты чётко усвоил принцип совместной работы системы поддержки материалов и менеджера ресурсов, то сам процесс загрузки материалов 3D-меша не вызовет у тебя затруднений.
В реализации функции PrepareFrame данный шаг представлен так:

Фрагмент Mesh.cpp (Проект Engine)
...
		// Проверяем, есть ли у меша материал.
		if( meshContainer->NumMaterials > 0 )
		{
			// Загружаем все материалы через менеджер материалов.
			for( unsigned long m = 0; m < meshContainer->NumMaterials; m++ )
			{
				// Убеждаемся, что материал содержит текстуру.
				if( meshContainer->materialNames[m] != NULL )
				{
					// Получаем имя скрипта материала и загружаем его.
					char *name = new char[strlen( meshContainer->materialNames[m] ) + 5];
					sprintf( name, "%s.txt", meshContainer->materialNames[m] );
					meshContainer->materials[m] = g_engine->GetMaterialManager()->Add( name, GetPath() );
					SAFE_DELETE_ARRAY( name );
				}
			}
...

Всё, что нам нужно сделать, это пройтись через список имён материалов, который мы создали при создании меш-контейнера. Для каждого валидного имени материала мы вызываем (invoke) менеджер материалов движка для того, чтобы он мог загрузить скрипт материала. Менеджер материалов, в свою очередь, постоянно контролирует, чтобы в памяти в данный момент было загружено не более 1-й копии данного материала, а также заполняет все необходимые данные загружаемого материала (например, о его текстуре) в момент его создания (инициализации) и первой загрузки в память. Мы сохраняем указатель на каждый загруженный материал в массив materials, который является членом структуры MeshContainer.

Вот и всё, что необходимо знать о подготовке меш-контейнера. На этом мы также завершаем наш рассказ о функции PrepareFrame.

Создание статичного (static) меша

Последнюю тему, которую нам осталось обсудить, это создание статичного (static) меша. Данным шагом завершается процесс загрузки меша. Его исходный код расположен в конструкторе класса Mesh и выглядит так:

Фрагмент Mesh.cpp (Проект Engine)
...
	// Создаём статичную (неанимированную) версию меша.
	m_staticMesh = new MeshContainer;
	ZeroMemory( m_staticMesh, sizeof( MeshContainer ) );

	// Загружаем меш.
	ID3DXBuffer *materialBuffer, *adjacencyBuffer;
	D3DXLoadMeshFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &adjacencyBuffer, &materialBuffer, NULL, &m_staticMesh->NumMaterials, &m_staticMesh->originalMesh );

	// Оптимизируем меш для лучшей производительности рендеринга.
	m_staticMesh->originalMesh->OptimizeInplace( D3DXMESHOPT_COMPACT | D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_VERTEXCACHE, (DWORD*)adjacencyBuffer->GetBufferPointer(), NULL, NULL, NULL );
...

Как ты уже знаешь, статичный меш представляет собой меш, у которого отсутствует анимация. Это также означает, что ему не нужны кости. Последнее обстоятельство сильно упрощает нам жизнь ввиду того, что позволяет нам полностью убрать из статичного фрейма иерархию фреймов. Поэтому вместо использования иерархии фреймов мы просто размещаем весь меш в одном меш-контейнере, расположенном внутри одного единственного фрейма. По понятным причинам это также значительно повышает производительность его рендеринга. Ведь во-первых, в этом случае у нас нет необходимости трассировать иерархию фреймов всякий раз, когда мы отправляем меш на рендеринг. А во-вторых, мы можем отрендерить меш всего за один вызов функции PrepareFrame, если, конечно, он использует не больше одного материала. Другими словами мы можем за один раз отправить меш со всеми его данными в видеопамять, вместо того, чтобы по одиночке отправлять каждый меш-контейнер иерархии фреймов. Данный способ прекрасно подходит для мешей, которые не требуется анимировать, как например сама игровая сцена (scenery) и статичные объекты, размещённые на ней.
Любой меш можно легко конвертировать в статичный, даже если у него есть кости и иерархия фреймов. Для этого применяем специальную функцию D3DXLoadMeshFromX, предоставляемую библиотекой D3DX и предельно простую в использовании. Точно также как и её ближайшая родственница функция D3DXLoadMeshHierarchyFromX, D3DXLoadMeshFromX загружает меш в память из .X-файла. Единственное отличие состоит в том, что последняя не использует класс AllocateHierarchy. Таким образом она просто игнорирует иерархию фреймов меша (если даже таковая сохранена в .X-файле) и просто "сворачивает" (collapses) всё содержимое .X-файла в один фрейм. В результате в память загружается меш, имеющий 1 фрейм и 1 меш-контейнер.
В конструкторе класса Mesh наглядно видно использование функции D3DXLoadMeshFromX:

Фрагмент Mesh.cpp (Проект Engine)
...
	D3DXLoadMeshFromX( GetFilename(), D3DXMESH_MANAGED, g_engine->GetDevice(), &adjacencyBuffer, &materialBuffer, NULL, &m_staticMesh->NumMaterials, &m_staticMesh->originalMesh );
...

В последнем параметре сохраняется указатель на только что созданный в памяти меш, возвращаемый при передаче управления главной функции программы.
Как только статичный меш загружен, нам остаётся проделать ещё несколько шагов, которые описаны в исходном коде конструктора класса Mesh (Mesh.cpp). Самый важный из них - загрузка материалов меша. К счастью сделать это не сложнее, чем подготовить иерархию фреймов. Принцип тот же. Материалы меша хранятся в экземпляре (инстансе) структуры MeshContainer, которая также хранит в себе наш статичный меш.
Так как до этого мы уже загрузили меш с применением фреймов, то все его сопутствующие материалы меша также были загружены в память ещё на стадии подготовки иерархии фреймов. Из этого следует, что если даже мы попытаемся повторно загрузить материалы того же меша, то в этом случае ничего не произойдёт и в памяти по-прежнему останется только 1 экземпляр данных материалов. В этой ситуации наш менеджер материалов определит, что материалы для данного меша уже загружены и просто вернёт указатели на них.

Завершающий шаг - заполнение данных ограничивающего объёма (bouning volume) загружаемого меша. Если помнишь, мы ответвили класс Mesh от класса BoundingVolume. Поэтому мы можем легко создать набор ограничивающих объёмов вокруг данного меша и заем ссылаться на него при необходимости. Теперь настал момент создать набор ограничивающих объёмов вокруг создаваемого статичного меша. Для этого мы просто вызываем функцию BoundingVolumeFromMesh, которая является членом класса BoundingVolume. Всё происходит всё в том же конструкторе класса Mesh:

Фрагмент Mesh.cpp (Проект Engine)
...
	// Создаём ограничивающий объём вокруг меша.
	BoundingVolumeFromMesh( m_staticMesh->originalMesh );
...

Здесь в единственном параметре мы передаём указатель на наш экземпляр объекта ID3DXMesh (в нашем случае - m_staticMesh->originalMesh), который является указателем на наш статичный меш. Функция автоматически назначит мешу и сохранит ограничивающие куб, сферу и эллипсоид (в нашем случае это тоже будет сфера, т.к. мы не указали радиус эллипсоида во втором параметре), которые будут окружать его со всех сторон.

Последние несколько строк исходного кода конструктора класса Mesh используются для создания буфера вершин и буфера индексов, содержащие все вершины и их индексы меша соответственно. Данные буферы потребуются позднее менеджеру сцены (scene manager) для доступа к вершинам геометрических объектов сцены. О вершинных буферах и буферах индексов мы расскажем в последующих главах данного курса, как только начнём применять их в деле.

Обновление (updating) и рендеринг меша

В классе Mesh содержатся две функции Update и Render, реализация которых представлена в Mesh.cpp и выглядит так:

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляет меш.
//-----------------------------------------------------------------------------
void Mesh::Update()
{
	UpdateFrame( m_firstFrame );
}

//-----------------------------------------------------------------------------
// Рендерит меш.
//-----------------------------------------------------------------------------
void Mesh::Render()
{
	RenderFrame( m_firstFrame );
}
...

Они применяются для обновления и рендеринга иерархии фреймов соответственно. Каждая из них вызывает в свою очередь внутренние функции класса Mesh (UpdateFrame и RenderFrame), которые стартуют рекурсивную трассировку иерархии фреймов. То есть, функции UpdateFrame и RenderFrame также как и PrepareFrame являются рекурсивными. Каждая из них принимает в качестве вводного параметра указатель на экземпляр структуры Frame, который затем используется для начала обработки текущей иерархии фреймов, начиная с самого первого (m_firstFrame). Обе эти функции трассируют иерархию фреймов точно таким же образом, как это делает PrepareFrame.
Рассмотрим каждую из внутренних функций по отдельности.

Функция UpdateFrame

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляет матрицы преобразований данного фрейма.
//-----------------------------------------------------------------------------
void Mesh::UpdateFrame( Frame *frame, D3DXMATRIX *parentTransformationMatrix )
{
	if( parentTransformationMatrix != NULL )
		D3DXMatrixMultiply( &frame->finalTransformationMatrix, &frame->TransformationMatrix, parentTransformationMatrix );
	else
		frame->finalTransformationMatrix = frame->TransformationMatrix;

	// Обновляем одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		UpdateFrame( (Frame*)frame->pFrameSibling, parentTransformationMatrix );

	// Обновляем дочерние (нижестоящие) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		UpdateFrame( (Frame*)frame->pFrameFirstChild, &frame->finalTransformationMatrix );
}
...

Рис.8 Матрицы преобразований фреймов, скомбинированные в иерархическом порядке.
Рис.8 Матрицы преобразований фреймов, скомбинированные в иерархическом порядке.


Как можно видеть в последних нескольких строках, функция UpdateFrame трассирует иерархию фреймов с использованием рекурсивного подхода. Но самое интересное как раз происходит в первых строках её реализации, где происходит построение финальной (результирующей) матрицы преобразований фрейма (frames' transformation matrix). Каждый фрейм хранит в себе свою собственную матрицу преобразований в объекте TransformationMatrix, который отображает перемещение, поворот или масштабирование данного фрейма. Как ты помнишь, во время анимации, когда родительский фрейм двигается, то и все его потомки (children) двигаются вместе с ним. По этой причине нам необходимо внедрить для каждого фрейма ещё по одной матрице, которая будет представлять собой произведение (комбинацию) собственной матрицы фрейма с матрицей её родительского фрейма. В нашем случае данной результирующей матрицей является finalTransformationMatrix.
В представленном выше исходном коде видно, что когда у фрейма есть родительский фрейм (parent), то функция запрашивает его (родителя) матрицу трансформаций и затем комбинирует (перемножает) её с собственной матрицей преобразований фрейма путём вызова функции D3DXMatrixMultiply. Затем при трассировке иерархии фреймов мы передаём получившуюся матрицу преобразований родительского фрейма (parents' transformation matrix). Когда мы переходим к обработке одноуровневого фрейма (sibling; т.е. который имеет общего родителя с предыдущим фреймом), мы передаём матрицу преобразований того же родителя. При обработке фрейма-потомка (child), мы передаём в него результирующую матрицу преобразований (final transformation matrix) текущего фрейма, которая уже включает в себя трансформации его родителя и других фреймов-предков, стоящих на более высоких ступенях той же ветви иерархии.

Функция RenderFrame

Фрагмент Mesh.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Рендерит меш-контейнеры данного фрейма, если таковые имеются.
//-----------------------------------------------------------------------------
void Mesh::RenderFrame( Frame *frame )
{
	MeshContainer *meshContainer = (MeshContainer*)frame->pMeshContainer;

	// Рендерим меш данного фрейма, если таковой имеется.
	if( frame->pMeshContainer != NULL )
	{
		// Проверяем, являетося ли данный меш скинированным (skined).
		if( meshContainer->pSkinInfo != NULL )
		{
			// Создаём трансформации костей с использованием матриц преобразования меша.
			for( unsigned long b = 0; b < meshContainer->pSkinInfo->GetNumBones(); ++b )
				D3DXMatrixMultiply( &m_boneMatrices[b], meshContainer->pSkinInfo->GetBoneOffsetMatrix( b ), meshContainer->boneMatrixPointers[b] );

			// Обновляем вершины меша с учётом матриц преобразований меша.
			PBYTE sourceVertices, destinationVertices;
			meshContainer->originalMesh->LockVertexBuffer( D3DLOCK_READONLY, (void**)&sourceVertices );
			meshContainer->MeshData.pMesh->LockVertexBuffer( 0, (void**)&destinationVertices );
			meshContainer->pSkinInfo->UpdateSkinnedMesh( m_boneMatrices, NULL, sourceVertices, destinationVertices );
			meshContainer->originalMesh->UnlockVertexBuffer();
			meshContainer->MeshData.pMesh->UnlockVertexBuffer();

			// Рендерим меш по группам атрибутов.
			for( unsigned long a = 0; a < meshContainer->totalAttributeGroups; a++ )
			{
				g_engine->GetDevice()->SetMaterial( meshContainer->materials[meshContainer->attributeTable[a].AttribId]->GetLighting() );
				g_engine->GetDevice()->SetTexture( 0, meshContainer->materials[meshContainer->attributeTable[a].AttribId]->GetTexture() );
				meshContainer->MeshData.pMesh->DrawSubset( meshContainer->attributeTable[a].AttribId );
			}
		}
		else
		{
			// Это не скинированный меш, поэтому его будем рендерить как статичный (static).
			for( unsigned long m = 0; m < meshContainer->NumMaterials; m++)
			{
				if( meshContainer->materials[m] )
				{
					g_engine->GetDevice()->SetMaterial( meshContainer->materials[m]->GetLighting() );
					g_engine->GetDevice()->SetTexture( 0, meshContainer->materials[m]->GetTexture() );
				}
				else
					g_engine->GetDevice()->SetTexture( 0, NULL );

				meshContainer->MeshData.pMesh->DrawSubset( m );
			}
		}
	}

	// Рендерим одноуровневые фреймы (siblings) данного фрейма.
	if( frame->pFrameSibling != NULL )
		RenderFrame( (Frame*)frame->pFrameSibling );

	// Рендерим дочерние (нижестоящие в иерархии) фреймы (childrens) данного фрейма.
	if( frame->pFrameFirstChild != NULL )
		RenderFrame( (Frame*)frame->pFrameFirstChild );
}
...

Мда, рендеринг - штука отнюдь не простая. Функция RenderFrame начинает рендеринг исходя из того, что у фрейма, отправленного в неё, есть предварительно заполненный инстанс структуры MeshContainer, а также собственно 3D-меш, размещённый внутри меш-контейнера.
Первым делом проверяем, содержит ли меш-контейнер какую-либо информацию о скине, что в свою очередь укажет на то, скинирован ли данный меш или нет (то есть, имеются ли у него кости, которые могут быть анимированы).
В случае когда загружаемый меш имеет кости, он загружается через функцию D3DXLoadMeshHierarchyFromX. Также в этом случае для меша создаётся инстанс интерфейса ID3DXSkinInfo, используемый для управления и манипулирования матрицами преобразования костей (bone transformation matrices), о которых говорилось чуть выше. Очень скоро ты также увидишь его в деле.
При рендеринге скинированного меша первым делом необходимо создать трансформации костей с использованием матриц трансформации костей, сохранённых в меш-контейнере. Напомним, что при создании меш-контейнера мы сохраняем указатели на кости (читай - фреймы), которые влияют на каждый меш-контейнер. Как только у нас есть матрица трансформации для каждой кости, которая влияет на меш-контейнер, мы можем применить их к вершинам меша для их трансформирования. Это происходит путём блокирования внутреннего буфера вершин меша (meshs' internal vertex buffer) и последующего вызова функции UpdateSkinnedMesh (экспонированную интерфейсом ID3DXSkinInfo). В неё передаются матрицы преобразования и вершины меша.
Не волнуйся о подробностях сего действа (особенно в том месте, где описываются буферы вершин, т.к. мы поговорим о них позднее). Просто постарайся представить себе общую картину происходящего. Поначалу вышеописанные операции кажутся безумно сложными. Но это тот самый механизм, который уже был придуман для тебя и тебе не потребуется его трогать в дальнейшем, если только не захочешь в нём что-либо поменять. Последующие практические Главы многое прояснят в этой теме.
Финальный шаг - рендеринг меша, с учётом его групп атрибутов (attribute groups). Единый меш разделяется на две или более групп атрибутов. Причём каждая грань меша обязательно принадлежит к одной из них. Группа атрибутов определяет подробные установки (details) того, как именно будут рендериться грани, входящие в неё. К примеру, допустим у нас есть меш, к которому применены 2 материала. Все грани данного меша, на которые нанесён первый материал, образуют одну группу атрибутов, в то время как все другие грани (со вторым материалом, применённым к ним) образуют вторую группу атрибутов. Когда мы рендерим меш, процедуры исходного кода просматривают поочерёдно каждую группу атрибутов и настраивают соответствующим образом объект устройства Direct3D, чтобы он был готов рендерить грани меша, принадлежащие выбранной группе атрибутов. Как только объект устройства отрендерил все грани данной группы атрибутов, он переходит к следующей группе, в очередной раз готовя объект устройства к рендерингу граней этой группы атрибутов и, собственно, в финале снова производя их рендеринг. Этот процесс продолжается до тех пор, пока не будут просмотрены все группы атрибутов и, как следствие, отрендерены все грани данного меша.
Как видно из исходного кода функции RenderFrame, мы устанавливаем текстуру и материал Direct3D (в частности опции освещённости) для каждой группы атрибутов ДО вызова в данном меше функции DrawSubset. При вызове функции DrawSubset передаём в неё ID текущей группы атрибутов, которую собираемся рендерить. Это инструктирует её рендерить только грани, принадлежащие к данной группе атрибутов.
Как только функция RenderFrame пройдёт через все группы атрибутов, наш меш (расположенный в текущем меш-контейнере) будет полностью отрендерен с подходящим материалом, применённым на каждую из граней.
Из исходного кода функции RenderFrame также видно, что происходит в том случае, когда мы рендерим меш, не имеющий информации о скине (т.е. нескинированный меш). Другими словами данный меш во многом схож с обычным статичным (static) мешем, в котором нет никаких преобразований костей (как и самих костей), которые были бы применены к его вершинам. Данный меш мы рендерим практическим тем же способом, что и скинированный. Единственная разница состоит в том, что нам не требуется вычислять матрицы преобразований костей (bone transformation matrices) и применять их к вершинам меша. Всё, что нужно для этого сделать, это пройти через каждый материал меша (т.е. через каждую группу атрибутов) и отрендерить грани, которые используют каждый материал по-отдельности, применив функцию DrawSubset. Точно также, как мы это делали до этого.

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

Интегрируем систему поддержки мешей (Mesh system) в движок

Принцип тот же, что и при интегрировании других систем.

Изменения в Engine.h (Проект Engine)

  • Добавь строку #include "Mesh.h"

сразу после строки #include "Material.h":

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Font.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "Network.h"
#include "SoundSystem.h"
#include "BoundingVolume.h"
#include "Material.h"
#include "Mesh.h"
#include "RenderCache.h"
#include "State.h"
...

  • Добавь строку ResourceManager< Mesh > *m_meshManager;

в секцию private объявления класса Engine, сразу после строки ResourceManager< Material > *m_materialManager;:

Фрагмент Engine.h (Проект Engine)
...
	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.

	ResourceManager< Script > *m_scriptManager; // Менеджер скриптов
	ResourceManager< Material > *m_materialManager; // Менеджер материалов.
	ResourceManager< Mesh > *m_meshManager; // Mesh manager.
...

  • Добавь строку ResourceManager< Mesh > *GetMeshManager();

в секцию public объявления класса Engine, сразу после строки ResourceManager< Material > *GetMaterialManager();:

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

	HWND GetWindow();
	void SetDeactiveFlag( bool deactive );

	float GetScale(); 
	IDirect3DDevice9 *GetDevice(); 
	D3DDISPLAYMODE *GetDisplayMode(); 
	ID3DXSprite *GetSprite();

	void AddState( State *state, bool change = true );
	void RemoveState( State *state );
	void ChangeState( unsigned long id );
	State *GetCurrentState();

	ResourceManager< Script > *GetScriptManager();
	ResourceManager< Material > *GetMaterialManager();
	ResourceManager< Mesh > *GetMeshManager();

	Input *GetInput();
	Network *GetNetwork();
	SoundSystem *GetSoundSystem();
...

В секции private класса Engine мы объявили новый менеджер ресурсов m_meshManager, а в секции public добавили объявление функции GetMeshManager() для получения внешнего доступа к нему за пределами движка.

Изменения в Engine.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла Mesh.cpp (проверь её наличие).


Точно также как и все другие менеджеры ресурсов, мы создаём m_meshManager в конструкторе класса Engine, и уничтожаем в его деструкторе:

  • Добавь строку m_meshManager = new ResourceManager< Mesh >;

в конструктор класса Engine, сразу после строки m_materialManager = new ResourceManager< Material >( m_setup->CreateMaterialResource );:

Фрагмент Engine.cpp (Проект Engine)
...
	// Создаём интерфейс спрайта.
	D3DXCreateSprite( m_device, &m_sprite );

	// Создаём связный список стейтов.
	m_states = new LinkedList< State >;
	m_currentState = NULL;

	// Создаём менеджеры ресурсов.
	m_scriptManager = new ResourceManager< Script >;
	m_materialManager = new ResourceManager< Material >( m_setup->CreateMaterialResource );
	m_meshManager = new ResourceManager< Mesh >;
...

  • Добавь строку SAFE_DELETE( m_meshManager );

в деструктор класса Engine, сразу после строки SAFE_DELETE( m_input );:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).

		// Уничтожаем связные списки со стейтами.
		if( m_currentState != NULL )
			m_currentState->Close();
		SAFE_DELETE( m_states );

		// Уничтожаем ранее созданные объекты.
		SAFE_DELETE( m_soundSystem );
		SAFE_DELETE( m_network );
		SAFE_DELETE( m_input );
		SAFE_DELETE( m_meshManager );
		SAFE_DELETE( m_materialManager );
		SAFE_DELETE( m_scriptManager );
...

  • Добавь реализацию функции GetMeshManager:

ResourceManager< Mesh > *Engine::GetMeshManager()
{
return m_meshManager;
}
сразу после реализации функции ResourceManager< Material > *Engine::GetMaterialManager(), в самом конце Engine.cpp:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает указатель на текущий менеджер материалов.
//-----------------------------------------------------------------------------
ResourceManager< Material > *Engine::GetMaterialManager()
{
	return m_materialManager;
}

//-----------------------------------------------------------------------------
// Returns a pointer to the mesh manager.
//-----------------------------------------------------------------------------
ResourceManager< Mesh > *Engine::GetMeshManager()
{
	return m_meshManager;
}
...

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

Это все изменения, которые необходимо внести в исходный код движка для начала применения 3D-мешей и управления ими посредством такой удобной штуки, как менеджер ресурсов.
До того момента как мы начнём ваять тестовое приложение, рассмотрим ещё один служебный класс RenderCache, который в общем-то мало относится к мешам. Однако он важен для 3D-рендеринга. Поэтому без него никуда.

Класс RenderCache

RenderCahe - это совсем небольшой служебный класс, который мы добавим в наш движок. Мы не будем подробно останавливаться на тонкостях его реализации, т.к. он и в самом деле мал и прост по своей структуре. Вместо этого мы поговорим о том, чем он занимается и как работает, не вдаваясь в подробности.
Несмотря на то, что мы пока подробно не изучали вершинные и индексные буферы (vertex & index buffers), класс RenderCache работает именно с ними. Ты уже знаешь, что вершинный буфер содержит вершины (vertices) любой из граней, готовящейся к рендерингу. Индексный буфер дополняет его, но имеет отличия. В индексном буфере хранятся индексы, которые указывают на вершины в вершинном буфере. Главное назначение этих индексов состоит в организации рендеринга вершин в вершинном буфере. Рассмотрим подробнее принцип применения вершинных и индексных буферов в рендеринге.
Предположим, у нас есть 10 граней, которые мы хоти отрендерить в любом из кадров. (Заранее условимся, что каждая грань состоит из трёх вершин, т.к. в некоторых GPU, например в Sega Saturn, грани состоят из 4-х вершин!) Но совсем не обязательно их рендерить все в каждом кадре. Для рендеринга мы создаём вершинный буфер, в котором есть пространство для 30 вершин, чего достаточно для хранения вершин 10 граней. В каждом кадре ты помещаешь в вершинный буфер определённые вершины граней и отправляешь этот буфер на рендеринг (в видеокарту). Всё бы ничего, но тут есть одна проблема, которую не замечаешь при работе всего с 10 гранями. В реальных играх сцена состоит из тысяч граней и при подобном подходе будет заметно значительное снижение производительности графики (т.е. падение FPS).
Проблема здесь заключается в том, что вершинный буфер в каждом кадре заполняется заново (что требует больших вычислительных ресурсов) и уже затем копируется в аппаратную память видеокарты (что также требует больших вычислительных ресурсов). Перемещение в каждом кадре любых данных между видеопамятью и системной (ОЗУ) памятью крайне неэффективно и этого всегда следует избегать. На профессиональном сленге это явление называется thrashing.
Для снижения объёма данных копируемых в и из аппаратной видеопамяти используют индексный буфер. Вот как он работает. Сначала создаём один статичный буфер вершин (static vertex buffer), который хранит все вершины, необходимые для рендеринга сцены. Мы размещаем его в аппаратной (device-accessible) памяти видеокарты (допустим, что объёма памяти для этого достаточно). После этого создаётся индексный буфер, который используется для хранения индексов вершин вершинного буфера (прямо как в массивах). Другими словами, каждый индекс в индексном буфере соответствует одной вершине в вершинном буфере. В каждом кадре ты решаешь, какие из вершин будут рендериться и помещаешь их индексы в индексный буфер. Теперь, вместо того, чтобы отправлять в видеокарту весь вершинный буфер целиком, достаточно отправить куда меньший по объёму индексный буфер, содержащий в себе индексы только тех вершин, которые будут отрендерены в данном кадре.
Для иллюстрации всего этого процесса проведём аналогию с меню ресторана. Представь, что ты идёшь в ресторан, чтобы пообедать. В любом ресторане есть меню, в котором представлены все возможные блюда, доступные для заказа (меню здесь выступает в качестве вершинного буфера). Приходит официант и принимает у тебя заказ, состоящий из выбранных блюд. Очевидно, что тебе не нужны в данный момент абсолютно все блюда, представленные в меню. Поэтому ты выбираешь лишь несколько блюд и официант (который выступает здесь в роли индексного буфера) устанавливает в своём блокноте своеобразные "ссылки" на блюда из меню (обычно он просто указывает номер блюда в меню). После того, как заказ сделан, официант отправляется на кухню с твоим заказом. Это точно также как отправить весь индексный буфер в видеокарту с одними лишь ссылками на вершины, которые должны быть отрендерены. Повар на кухне готовит выбранные блюда основываясь лишь на ссылках на конкретные блюда из меню, указанные в блокноте официанта. Наконец, официант приносит выбранные блюда и аккуратно раскладывает их на столе. По схожему принципу видеокарта обрабатывает только те вершины, на которые есть ссылки в индексном буфере и представляет их на экране путём рендеринга.
Надеемся, теперь у тебя сложилось чёткое представление о роли индексного буфера в рендеринге и мы можем без промедления начинать рассмотрение класса RenderCache. Его суть заключается в следующем. Программер создаёт вершинный буфер, в котором хранятся вершины всех граней которые возможно потребуется отрендерить. Для каждого материала, используемого гранями, представленными вершинами из вершинного буфера, необходимо создать по одному отдельному рендер-кэшу (render cache). Каждый рендер-кэш обслуживает индексный буфер, который программер заполняет индексами вершин, которые необходимо отрендерить в данном кадре. Сразу после этого каждый рендер-кэш по очереди назначает материалы и затем передаёт свои индексы вершин из вершинного буфера для их последующего рендеринга.

Создаём RenderCache.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "RenderCache.h".
  • Жмём "Добавить".

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

  • В только что созданном и открытом файле RenderCache.h набираем следующий код:

RenderCache.h (Проект Engine)
//-----------------------------------------------------------------------------
// File: RenderCache.h
//
// Version: 1.0
//
// Description: Manages the rendering of indexed faces from a set of vertices.
// Управляет рендерингом индексированных граней из набора вершин.
//
// Change History
// ~~~~~~~~~~~~~~
// v1.0 (17/02/04) - Inception.
//
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef RENDER_CACHE_H
#define RENDER_CACHE_H

//-----------------------------------------------------------------------------
// Render Cache Class
//-----------------------------------------------------------------------------
class RenderCache
{
public:
	RenderCache( IDirect3DDevice9 *device, Material *material );
	virtual ~RenderCache();

	void AddFace();
	void Prepare( unsigned long totalVertices );

	void Begin();
	void RenderFace( unsigned short vertex0, unsigned short vertex1, unsigned short vertex2 );
	void End();

	Material *GetMaterial();

private:
	IDirect3DDevice9 *m_device; // Указатель на объект устройства Direct3D.
	Material *m_material; // Указатель на материал, используемый данным рендер-кэшем.

	IDirect3DIndexBuffer9 *m_indexBuffer; // Индексный буфер, указывающий на вершины, подлежащие рендерингу.
	unsigned short *m_indexPointer; // Указатель для доступа к индексному буферу из других функций и классов.
	unsigned long m_totalIndices; // Общее число индексов, которое поддерживает данный индексный буфер.
	unsigned long m_faces; // Общее число граней, готовящихся к рендерингу.

	unsigned long m_totalVertices; // Общее число вершин.
};

#endif

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

Создаём RenderCache.cpp (Проект Engine)

В файле исходного кода Network.cpp будут размещаться реализации функций, объявленных в RenderCache.h.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле RenderCache.cpp набираем следующий код:

RenderCache.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// File: RenderCache.cpp
//
// Description: RenderCache.h implementation.
//              Refer to the RenderCache.h interface for more details.
// Реализация класса RenderCache, объявленного в RenderCache.h
//
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The render cache class constructor.
//-----------------------------------------------------------------------------
RenderCache::RenderCache( IDirect3DDevice9 *device, Material *material )
{
	m_device = device;

	m_material = material;

	m_indexBuffer = NULL;
	m_totalIndices = 0;
}

//-----------------------------------------------------------------------------
// The render cache class destructor.
//-----------------------------------------------------------------------------
RenderCache::~RenderCache()
{
	SAFE_RELEASE( m_indexBuffer );
}

//-----------------------------------------------------------------------------
// Увеличиваем размер рендер-кэша для управления ещё одной гранью.
//-----------------------------------------------------------------------------
void RenderCache::AddFace()
{
	m_totalIndices += 3;
}

//-----------------------------------------------------------------------------
// Подготавливаем рендер-кэш к использованию. Функция вызывается не раньше установки размера рендер-кэша.
//-----------------------------------------------------------------------------
void RenderCache::Prepare( unsigned long totalVertices )
{
	// Устанавливаем общее число вершин, готовящихся к рендерингу.
	m_totalVertices = totalVertices;

	// Создаём индексный буфер рендер-кэша.
	m_device->CreateIndexBuffer( m_totalIndices * sizeof( unsigned short ), D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_indexBuffer, NULL );
}

//-----------------------------------------------------------------------------
// Информируем рендер-кэш о том, что рендеринг уже начался.
//-----------------------------------------------------------------------------
void RenderCache::Begin()
{
	m_indexBuffer->Lock( 0, 0, (void**)&m_indexPointer, 0 );

	m_faces = 0;
}

//-----------------------------------------------------------------------------
// Добавить индексы вершин данной грани, готовящейся к рендерингу.
//-----------------------------------------------------------------------------
void RenderCache::RenderFace( unsigned short vertex0, unsigned short vertex1, unsigned short vertex2 )
{
	*m_indexPointer++ = vertex0;
	*m_indexPointer++ = vertex1;
	*m_indexPointer++ = vertex2;

	m_faces++;
}

//-----------------------------------------------------------------------------
// Информируем рендер-кэш о том, что рендеринг окончен, поэтому пусть рендерит грани.
//-----------------------------------------------------------------------------
void RenderCache::End()
{
	// Разблокируем (unlock) буфер индексов.
	m_indexBuffer->Unlock();

	// Проверяем, есть ли грани, готовящиеся к рендерингу.
	if( m_faces == 0 )
		return;

	// Проверяем, должен ли материал игнорировать туман.
	if( m_material->GetIgnoreFog() == true )
		m_device->SetRenderState( D3DRS_FOGENABLE, false );

	// Назначаем материал и текстуру.
	m_device->SetMaterial( m_material->GetLighting() );
	m_device->SetTexture( 0, m_material->GetTexture() );

	// Назначаем индексы нужных вершин для рендеринга корректных граней.
	m_device->SetIndices( m_indexBuffer );

	// Рендерим все грани.
	m_device->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, m_totalVertices, 0, m_faces );

	// Восстанавливаем настройки тумана, если те были изменены.
	if( m_material->GetIgnoreFog() == true )
		m_device->SetRenderState( D3DRS_FOGENABLE, true );
}

//-----------------------------------------------------------------------------
// Возвращает указатель на материал, используемый данным рендер-кэшем.
//-----------------------------------------------------------------------------
Material *RenderCache::GetMaterial()
{
	return m_material;
}

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

Исследуем код RenderCache.cpp

Мы не будем рассматривать реализацию класса RenderCache, т.к. там всё понятно из комментариев. Более того, мы даже не сможем увидеть его в деле до тех пор, пока не подойдём к подробному изучению вершинных и индексных буферов. Вместо этого, мы лишь кратко рассмотрим принцип его работы.
При создании инстанса класса RenderCache, необходимо указать в параметрах используемый им объект устройства Direct3D и материал. Мы настроили объект устройства таким образом, чтобы его не требовалось впоследствии повторно запрашивать у движка. При создании вершинного буфер, с которым будет работать создаваемый рендер-кэш, необходимо для каждой из граней, готовящейся к рендерингу, поочерёдно вызвать функцию AddFace для их добавления в рендер-кэш. С каждым новым вызовом функции AddFace, размер рендер-кэша автоматически увеличивается на 3 вершины. Как только все нужные грани окажутся размещены в рендер-кэше, вызывается функция Prepare, создающая внутренний индексный буфер.

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

Важно создавать рендер-кэш достаточно большим (путём применения функции AddFace) для хранения в нём всех возможных вершин, которые когда-либо потребуется отрендерить. Причём это необходимо сделать ДО вызова функции Prepare. Как только функция Prepare вызвана, размер рендер-кэша установлен окончательно и может быть изменён только путём разрушения текущего рендер-кэша и созданием нового. Индексный буфер не должен переполняться

Рендер-кэш создан и готов к применению. Следующий шаг - это назначение вершинного буфера, который будет готовиться к рендерингу (этот шаг мы подробно рассмотрим в последующих главах данного курса) а затем просто применить готовый рендер-кэш для рендеринга граней. Весь процесс происходит в несколько этапов.
Вызываем функцию Begin, которая блокирует (lock) индексный буфер для добавления в него индексов. Всякий раз, когда в вершинном буфере обнаруживается грань, которую необходимо отрендерить (т.е. которая использует тот же материал, что и рендер-кэш), вызывается функция RenderCache, в которую передаются индексы 3-х вершин, из которых она состоит. Теперь эти вершины также хранятся в индексном буфере. Сразу после этого, вызываем функцию End, которая разблокирует индексный буфер и рендерит все вершины, которые содержатся в индексном буфере данного рендер-кэша.

Интегрируем класс RenderCache в движок

Принцип тот же, что и при интегрировании других систем.

Изменения в Mesh.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла RenderCache.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь строку #include "RenderCache.h"

сразу после строки #include "Mesh.h":

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Font.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "Network.h"
#include "SoundSystem.h"
#include "BoundingVolume.h"
#include "Material.h"
#include "Mesh.h"
#include "RenderCache.h"
#include "State.h"
...


Теперь класс RenderCache интегрирован в движок и готов к работе. Для его использования необходим лишь готовый вершинный буфер, заполненный вершинами, готовыми к рендерингу. Применение данного класса будет рассмотрено в ближайших главах данного курса.

Тестовая перекомпиляция Engine.lib

Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

Image
По окончании компиляции (обычно, при создании небольших проектов, она занимает менее 1 секунды) в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции.
В нашем случае компиляция прошла успешно.

Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).

Модифицируем тестовое приложение (Проект Test)

Наконец, после изучения столь сложной Главы, наши старания будут вознаграждены наиболее впечатляющим визуальным примером. Впервые с начала данного курса мы воспроизведём на экране нечто более красивое, чем обычный текст.
В Главе 1.6(external link) для проверки работоспособности движка в нашем Решении GameProject01 мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. В последующих главах его функционал изменялся. Применив полученные в текущей Главе знания на практике, снова дополним исходный код тестового приложения, оснастив его новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProject01, создай его, следуя инструкциям Главы 1.6(external link) .
ОК, приступаем.

  • Открой для редактирования файл исходного кода Main.cpp (Проект Test) и замени содержащийся в нём код на следующий:
Main.cpp (Проект Test)
//-----------------------------------------------------------------------------
// System Includes
//-----------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "..\GameProject01\Engine.h"

//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
public:
	//-------------------------------------------------------------------------
	// Allows the test state to preform any pre-processing construction.
	//-------------------------------------------------------------------------
	virtual void Load()
	{
		m_mesh = new Mesh( "Gun.x", "./Assets/" );

		// Устанавливаем подходящую матрицу просмотра для просмотра тестового меша.
		D3DXMATRIX view;
		D3DXMatrixLookAtLH( &view, &D3DXVECTOR3( -50.0f, 50.0f, -150.0f ), &D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
		g_engine->GetDevice()->SetTransform( D3DTS_VIEW, &view );	
	};

	//--------------------------------------
	// Перемещаем камеру по нажатию клавиш на клавиатуре...
	//----------------------------------------
	virtual void Update(float elapsed)
	{
		static D3DXVECTOR3 pos = D3DXVECTOR3( -50.0f, 50.0f, -150.0f);
			if (g_engine->GetInput()->GetKeyPress(DIK_W, true) == true) pos.z += 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_S, true) == true) pos.z -= 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_A, true) == true) pos.x -= 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_D, true) == true) pos.x += 0.05f;

		D3DXMATRIX view;
		D3DXMatrixLookAtLH(&view, &pos, &D3DXVECTOR3(pos.x + 50.f, pos.y - 50.0f, pos.z + 150.0f),
			&D3DXVECTOR3(0.0f, 1.0f, 0.0f));

		g_engine->GetDevice()->SetTransform(D3DTS_VIEW, &view);
	}

	//-------------------------------------------------------------------------
	// Allows the test state to preform any post-processing destruction.
	//-------------------------------------------------------------------------
	virtual void Close()
	{
		SAFE_DELETE( m_mesh );
	};

	//-------------------------------------------------------------------------
	// Возвращает текущие настройки вьюера для данного кадра.
	//-------------------------------------------------------------------------
	virtual void RequestViewer( ViewerSetup *viewer )
	{
		viewer->viewClearFlags = D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER;
	}

	//-------------------------------------------------------------------------
	// Renders the test state.
	//-------------------------------------------------------------------------
	virtual void Render()
	{
		m_mesh->Render();
	};

private:
	Mesh *m_mesh; // Указатель на 3D-меш.
};

//-----------------------------------------------------------------------------
// Установки стейта, специфичные для приложения.
//-----------------------------------------------------------------------------
void StateSetup()
{
	g_engine->AddState( new TestState, true );
}

//-----------------------------------------------------------------------------
// Точка входа в приложение.
//-----------------------------------------------------------------------------
int WINAPI WinMain( HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow )
{
	// Создаём структуру EngineSetup.
	EngineSetup setup;
	setup.instance = instance;
	setup.name = "Mesh/Material Test";
	setup.scale = 0.01f;
	setup.StateSetup = StateSetup;

	// Создаём инстанс движка (используя структуру EngineSetup), затем запускаем его.
	new Engine( &setup );
	g_engine->Run();

	return true;
}


И вновь в нашем Проекте Test всего 1 файл Main.cpp, который содержит весь исходный код тестового приложения. Перед нами готовая система просмотра 3D-мешей (в виде стейта) в действии.

Исследуем код

При рассмотрении исходного кода стейта TestState видно, что загрузить меш через заранее подготовленный класс Mesh очень просто. Помимо этого во всей красе себя показывает система материалов, основанная на скриптах. В данном случае к мешу применяется текстура, распознанная нашей системой поддержки материалов.
В функции Load стейта TestState мы загружаем меш всего одной строкой кода:

Фрагмент файла Main.cpp (Проект Test)
...
m_mesh = new Mesh( "Gun.x", "./Assets/" );
...

Напомним, что имя файла также должно включать в себя полный путь до него. В нашем случае в учебных целях мы сохраняем все ресурсы игрового приложения в одной папке Assets, расположенной в одном каталоге с исполняемым файлом приложения.

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

Каталог Assets должен быть расположен в одной папке с исполняемым файлом Test.exe . В нашем случае это: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\Debug\

В целях упрощения данного тестового приложения мы не используем менеджер мешей (mesh manager). Это означает, что мы загружаем и удаляем меш самостоятельно. При использовании меш-менеджера нам бы не потребовалось вызывать макрос SAFE_DELETE(m_mesh) в функции Close стейта TestState, т.к. меш-менеджер удаляет более неиспользуемые меши автоматически. Также в этом случае нам не требуется отслеживать не был ли один и тот же ресурс случайно загружен более чем один раз. Как видишь, при разработке приложений различные менеджеры ресурсов часто оказываются просто незаменимы.
Также в функции Load стейта TestState появилось несколько новых строк кода:

Фрагмент файла Main.cpp (Проект Test)
...
		// Устанавливаем подходящую матрицу просмотра для просмотра тестового меша.
		D3DXMATRIX view;
		D3DXMatrixLookAtLH( &view, &D3DXVECTOR3( -50.0f, 50.0f, -150.0f ), &D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
		g_engine->GetDevice()->SetTransform( D3DTS_VIEW, &view );
...

Данный код мы подробно рассмотрим в последующих главах данного курса. Если кратко, то здесь мы создаём матрицу просмотра (view matrix), которую использует Direct3D для позиционирования и ориентирования виртуального "глаза" (или камеры) в 3D-пространстве. Так как в перспективе мы разрабатываем шутер от 1-го лица, то положение и ориентация этого самого "глаза" совпадает с оными виртуального персонажа, глазами которого реальный игрок смотрит на 3D-сцену. Другими словами, матрица просмотра определяет, что ты видишь в кадре в данный момент. В нашем случае мы установили виртуальную камеру для просмотра загруженного меша (первый параметр функции D3DXMatrixLookAtLH) следующим образом:


[+]

Второй параметр функции D3DXMatrixLookAtLH определяет вектор того, на что мы смотрим в данный момент. В нашем случае он имеет координаты 0.0f, 0.0f, 0.0f, т.е. мы смотрим в точку начала отсчёта, где и будет расположен наш меш.
Третий параметр определяет, где верх. Установив значение по оси Y в 1.0f мы указали Direct3D, что хотим чтобы положительный луч оси Y указывал вверх. Кроме того напомним, что в движке мы используем картезианскую леворучную систему координат, поэтому в нашем случае положительный луч оси Z будет совпадать с направлением виртуальной камеры. После удачной компиляции примера (об этом чуть ниже) "поиграй" со значениями первого параметра (вектора положения виртуальной камеры). Ты заметишь, что где бы она ни находилась, она всегда будет "смотреть" на точку, указанную во втором параметре. При изменении второго параметра ты можешь менять точку (и объекты на сцене), на которую смотришь. Что касается третьего параметра, при изменении второй координаты на -1.0f ось Y будет указывать вниз и вся картинка окажется перевёрнутой верх ногами. А всё из-за того, что ты указал Direct3D, что отрицательный луч оси Y смотрит вверх, что, в свою очередь, перевернуло всю текущую координатную систему.

Ты должно быть заметил, что в функции RequestViewer мы очищаем флаги D3DCLEAR_TARGET (связан с render target) и D3DCLEAR_ZBUFFER (z-буфер):

Фрагмент файла Main.cpp (Проект Test)
...
	//-------------------------------------------------------------------------
	// Возвращает текущие настройки вьюера для данного кадра.
	//-------------------------------------------------------------------------
	virtual void RequestViewer( ViewerSetup *viewer )
	{
		viewer->viewClearFlags = D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER;
	}
...

Эта типичная процедура делается всякий раз перед 3D-рендерингом.
Наконец, в функции Render стейта TestState мы вызываем внутреннюю функцию Render класса Mesh на нашем меше, которая рендерит его в каждом кадре.
Но и это ещё не всё. Для придания нашему примеру интерактивности мы связали изменение положения виртуальной камеры с кнопками на клавиатуре:

Фрагмент файла Main.cpp (Проект Test)
...
	//--------------------------------------
	// Перемещаем камеру по нажатию клавиш на клавиатуре...
	//----------------------------------------
	virtual void Update(float elapsed)
	{
		static D3DXVECTOR3 pos = D3DXVECTOR3( -50.0f, 50.0f, -150.0f);
			if (g_engine->GetInput()->GetKeyPress(DIK_W, true) == true) pos.z += 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_S, true) == true) pos.z -= 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_A, true) == true) pos.x -= 0.05f;
			if (g_engine->GetInput()->GetKeyPress(DIK_D, true) == true) pos.x += 0.05f;
...

Происходит это в функции Update стейта TestState, т.е. именно там, где обновляется сцена.
Теперь при нажатии кнопки w виртуальная камера приближается к объекту на сцене (т.е. позиция "прирастает" по оси z), при нажатии s - отдаляется (т.е. позиция "убывает" по оси z). При нажатии a - камера сдвигается влево по оси x (левый "стрейф"), при нажатии d - вправо (правый стрейф).

Подготовка Проекта Test к перекомпиляции

Напомним, что для успешной компиляции библиотека Engine должна быть добавлена в Проект Test (см. Главу 1.6).
Компилируем Проект Test:

  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем: Перестроить.

Компиляция завершилась успешно. Итоговый исполняемый двоичный файл (Test.exe) расположен на жёстком диске ПК в той же директории, что и библиотека движка.
В нашем случае, это: С:\Users\<Имя пользователя>\documents\visual studio 2010\Projects\GameProject01\Debug. (В разных ОС путь к файлу может отличаться от представленного).
Полученная программа сразу после запуска будет автоматически искать файлы в папке Assets:

  • файл с мешем Gun.x;
  • файл текстуры Texture.dds;
  • Файл скрипта текстуры Texture.dds.txt.

Если хотя бы одного файла там не окажется, приложение аварийно завершит свою работу, выдав сообщение об ошибке. Это нормально, т.к. проверку на присутствие запрашиваемого файла в указанном каталоге никто не писал.
Поэтому архив со всеми тремя файлами забираем здесь(external link). Напомним, их необходимо распаковать в подкаталог Assets каталога содержащего исполняемый файл скомпилированного приложения Test.exe (в нашем случае это c:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\).
*Найди и запусти получившееся приложение.
Приложение стартует долго, прибл. 15-20 секунд. Всё это время на экране вообще ничего может не меняться. Но затем появляется модель автомата на чёрном фоне. Нажимая клавиши w, a, s, d можно изменять положение виртуальной камеры. Клавиша выхода из приложения в исходном коде отсутствует. поэтому закрываем окно программы стандартной комбинацией клавиш Alt+F4.
Самые опытные читатели легко модифицируют исходный код приложения, добавив, например вращение виртуальной камеры при перемещении компьютерной мыши.
Также можно почитать доп. информацию в документации к DirectX SDK о мировой матрице (world matrix), которая применяется для позиционирования объектов (например 3D-мешей) в пространстве. Там же описывается как привязать нажатие кнопок клавиатуры к вращению 3D-меша по разным осям.

Итоги главы

Уф, это наверное самая большая Глава во всём учебном курсе, буквально нашпигованная всевозможной информацией. В то же время в результате проделанной работы мы своими руками сотворили и вывели на экран настоящий 3D-рендеринг. Мы создали неплохую, основанную на скриптах, систему поддержки материалов, а также производительную систему поддержки мешей, которая поддерживает как статичные так и анимированные меши. Мы также внедрили в движок пару служебных классов (BoundingVolume и RenderCache) для работы с ограничивающими объёмами и кэширования вершин 3D-объектов. В конце мы создали тестовое приложение в котором увидели в действии рендеринг 3D-объекта и работу системы поддержки материалов.
Исходные коды Решения GameProject01 (для MS Visual C++ 2010), с которым работали в данной Главе, забираем здесь(external link).
В следующей Главе мы возьмём меши и инкапсулируем их внутри так называемых объектов (objects). Забегая вперёд отметим, что класс Object представляет собой понятный и простой в использовании интерфейс, позволяющий управлять рендеримыми энтити (entity - англ. буквально "событие") в 3D-пространстве. Ты увидишь, как создаются объекты, которые размещаются и перемещаются в 3D-пространстве. Мы также создадим два других специальных объекта, один из которых будет обладать способностью анимировать любой 3D-меш, ассоциированный с ним.
Итак, если пример данной Главы показался тебе впечатляющим, то ты пока не видел, что мы сотворим к концу следующей!


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.14 Полигональные сетки и материалы

Contributors to this page: slymentat .
Последнее изменение страницы Среда 11 / Октябрь, 2017 11:02:22 MSK автор slymentat.

Последние комментарии

No records to display