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

DirectX 9 Graphics. Создание и рендеринг объектов


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

Объекты (модели) игры можно создавать двумя способами:
  • генерировать программно;
  • загружать из файлов 3D-моделей, созданных в 3D-редакторах.
В данной статье рассмотрим оба способа.
Для компиляции примера в конце статьи понадобится следующее ПО:
  • MS Visual C++ 2010 Express,
  • Microsoft DirectX SDK 9,
  • Windows SDK 7 и выше.
Всё легко гуглится + есть в разделе "Софт" нашего сайта.

Содержание



Intro. Общий алгоритм рендеринга объекта

Лучший способ познакомиться с миром realtime 3D-graphics - это вывести на экран треугольник, состоящих из трёх вершин.1 На данном этапе нас не волнуют такие аспекты, как освещение и текстуры (о них позднее). Здесь мы сосредоточимся на рендеринге вершин. Но даже для выполнения этой задачи сперва необходимо проделать следующие шаги:

Создаём и назначаем гибкий формат вершин (Flexible Vertex Format; FVF)

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

Создаём вершинный буфер

Как только все вершины созданы, загружаем их в вершинный буфер (vertex buffer), представляющий собой массив вершин.

В каждом кадре:


Устанавливаем матрицы вида, проекции и трансформаций

Здесь указываем Direct3D, где именно в 3D-пространстве будет расположена виртуальная камера (viewpoint). Здесь объект при необходимости трансформируется, чтобы предстать в нужном виде.

Устанавливаем источник потока (stream source)

Здесь загружаем содержимое вершинного буфера в источник потока. Назначаем ранее созданный гибкий формат вершин (FVF)

Рендерим объекты

Как только назначили источник потока и скопировали в него вершины, рендерим их. Здесь (в случае рендеринга примитивов) указываем Direct3D, какие именно примитивы выстраивать из вершин (треугольник, прямоугольник, вентилятор и др.).

Освобождаем вершинный буфер

По завершении вывода вершин на экран, освобождаем вершинный буфер стандартными средствами Direct3D.

Создание и рендеринг вершин (теория)

Вершина (vertex) - это точка в 3D-пространстве. С их помощью создают формы (shapes): две вершины создают отрезок, три -треугольник и т.д. . Такие формы называют примитивами. Перед рендерингом примитива средствами Direct3D необходимо сперва создать структуру, хранящую все его вершины + определить FVF-структуру формата вершин (об этом ниже). Direct3D позволяет определить вершину множеством различных способов.2 К примеру, если ты используешь только 2D-графику, можно просто указать координаты 2D экранных (трансформированных) координатах. С другой стороны, если ты используешь локальные (local) и мировые (world) координаты, то можешь указать координаты в 3D-проекции (нетрансформированные координаты). Вдобавок, Direct3D позволяет включать (в том числе) в каждую вершину данные об используемом цвете или текстуре. Как же отслеживать всю эту инфу и быть уверенным, что Direct3D понимает, что ты делаешь в данный момент?
Встречаем гибкий формат вершин(Flexible Vertex Format).

Гибкий формат вершин (Flexible Vertex Format, FVF)

  • Применяется для конструирования кастомных (=пользовательских) структур данных, хранящих инфу о вершинах для дальнейшего использования в (игровых) приложениях.
В Direct3D вершины представлены в виде структур данных (data structures), каждая из которых хранит координаты x, y и z вершины. Помимо этого такие структуры могут хранить (а могут и нет) дополнительную инфу, вроде параметров освещения, метода нанесения текстуры и т.д. Для этого и был придуман FVF. С ним ты можешь решать, какую именно информацию будут хранить вершины, чтобы затем доходчиво это донести до Direct3D. Например: 2D или 3D координаты, цвет и т.д. Существуют приложения, которые хранят в FVF-структуре только координаты вершины.

На практике FVF реализуется в виде обычной структуры, в которую можно добавлять компоненты по своему усмотрению. Конечно, здесь есть свои ограничения. Например, компоненты следует указывать в определённом порядке + некоторые компоненты могут конфликтовать друг с другом (например не допускается одновременное указание 2D и 3D координат). После заполнения FVF-структуры создаётся FVF-дескриптор (FVF-descriptor), представляющий собой комбинацию флагов, описывающих твой кастомный формат вершин.
Рассмотрим пример FVF-структуры:
Пример FVF-структуры
typedef struct
{
	FLOAT x, y, z, rhw; // 2D-координаты
	FLOAT x, y, z;	// 3D-координаты
	
	FLOAT nx, ny, nz;	// Нормали
	D3DCOLOR diffuse;	// Рассеянный (diffuse) цвет
	FLOAT u, v;	// Координаты текстуры
} sVertex;

Данный код содержит структуру вершины (vertex structure), заполненную различными переменными, допустимыми форматом FVF. Порядок их указания важен! При удалении каких-либо переменных из данного примера необходимо сохранить порядок их перечисления. Как видим, единственный конфликт здесь возникает при указании 2D и 3D координат + координат нормалей. Нормали (normals) представляют собой координаты, определяющие направление. Они могут использоваться только совместно с 3D-координатами. Здесь необходимо выбрать, какие координаты (2D или 3D) выбрать, а от каких отказаться. Оба вида координат в одной FVF-структуре использовать нельзя.
Единственное отличие 2D и 3D координат - присутствие у 2D-координат отдельного параметра rhw (reciprocal of the homogeneous W; обратная величина однородного (в пространстве отсечения) W). Если в двух словах, то rhw обычно представляет собой расстояние от наблюдателя (viewpoint) до вершины, расположенной на оси Z. В большинстве случаев можно выставить rhw в 1,0 и не париться.
Обрати внимание на то, что пользовательская структура sVertex активно использует тип данных FLOAT (значение с плавающей точкой).
Вот ещё пример определения FVF-структуры:
struct CUSTOMVERTEX
{
	FLOAT x, y, z; // 3D-координаты
	DWORD color; // Цвет вершины
}

Тип данных D3DCOLOR - это по сути переменная типа DWORD, которая применяется в Direct3D для хранения цветовых значений.
Для создания цветового значения типа D3DCOLOR применяют одну из двух функций:
D3DCOLOR D3DCOLOR_RGBA(Red, Green, Blue, Alpha);
D3DCOLOR D3DCOLOR_COLORVALUE(Red, Green, Blue, Alpha);

Каждая из этих функций (вообще, это макросы) принимает 4 параметра, представляющие собой количественные значения (amount) каждого цветового компонента (color component) + цвет альфа-канала (канала прозрачности). Данные значения варьируются:
  • от 0 до 255 для каждого параметра макроса D3DCOLOR_RGBA,
  • от 0.0 до 1.0 для параметров макроса D3DCOLOR_COLORVALUE.
Для цвета альфа-канала можно выбрать любое допустимое значение. Но на практике его обычно выставляют в макс. значение 255 (или 1.0).
Сначала определяют FVF-структуру, затем заполняют её дескриптор (об этом ниже).

Создаём пользовательскую FVF-структуру (пример)

Например, нам необходимо включить в кастомную FVF-структуру только 3D-координаты вершины и данные по её рассеянному цвету (diffuse color):
Пример пользовательской (=кастомной) FVF-структуры
struct VERTEX
{
	FLOAT x, y, z;
	FLOAT tu, tv;
}

Первые 3 переменных члена - это позиция вершины.
tu и tv - переменные, описывающие метод нанесения текстуры. Direct3D также поддерживает оборачивание (wrapping) текстуры вокруг 3D-объекта. В этом случае сперва указывают координаты левого верхнего угла текстуры: tu=0.0 и tv=0.0. Затем правого нижнего: tu = 1.0 и tv=1.0 . Технически все полигоны, расположенные между этим прямоугольником, имеют нулевые текстурные координаты, что указывает Direct3D просто растянуть текстуру над ними.

Создаём FVF-дескриптор

Как только FVF-структура создана, создаём её FVF-дескриптор, указав один или несколько специальных флагов:
ФЛАГ ОПИСАНИЕ
D3DFVF_XYZ В структуру включены 3D-координаты вершины.
D3DFVF_XYZRHW В структуру включены 2D-координаты вершины.
D3DFVF_NORMAL В структуру включены данные нормали (вектора).
D3DFVF_DIFFUSE В структуру включены данные о рассеянном (diffuse) цвете вершины.
D3DFVF_SPECULAR В структуру включены данные о цвете бликов вершины.
D3DFVF_TEX0 - D3DFVF_TEX7 В структуру включены текстурные координаты.

В нашем случае определение дескриптора выглядит так:
#define VertexFVF (D3DFVF_XYZ | D3DFVF_DIFFUSE)

Указаны только флаги, необходимые нашей FVF-структуре.
Дескриптор созданной FVF-структуры затем передаётся в различные Direct3D-функции.

Вершины


Определяем вершины

Сразу после создания FVF-структуры можно приступать к работе с вершинами. Объявляем инстансы FVF-структуры и заполняем данными. В случае рендеринга треугольника, создаём 3 вершины:
CUSTOMVERTEX Vertices[] = {
	{150.0f, 50.0f, 0.5f, D3DXCOLOR_XRGB(255, 0, 255), },
	{250.0f, 2 50.0f, 0.5f, D3DXCOLOR_XRGB(0, 255, 0), },
	{50.0f, 250.0f, 0.5f, D3DXCOLOR_XRGB(255, 255, 0), },
}

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

Квады (quads)

Image
Рис.1 Треугольный список (list) не имеет общих вершин с другими полигонами. Объединяя полигоны в треугольные полосы (strips) и вентиляторы (fans), мы сокращаем число вершин, тем самым экономя ресурсы и увеличивая производительность

  • Схожи со спрайтами (sprites).
  • Представляют собой квадрат, состоящий их двух треугольных полигонов с одной общей стороной.
  • Является частным случаем полосы (strip; одного из стандартных плоских примитивов Direct3D; см. Рис.1).
  • Широко применяются в игрокодинге наряду со спрайтами.
  • В DirectX 9 для их хранения появилась специальная структура QUAD.2
Взяв за основу предварительно созданную FVF-структуру VERTEX, можно создать специальную структуру QUAD, которая будет хранить инфу о квадах:
struct QUAD
{
	VERTEX vertices;
	LPDIRECT3DVERTEXBUFFER9 buffer;
	LPDIRECT3DTEXTURE9 texture;
}

Структура QUAD полностью самодостаточна (self-contained). В нашем случае мы разместили в ней инфу о 4-х вершинах, соответствующих четырём углам квада. Для одного квада у нас есть вершинный буфер и текстура, которая его покрывает. Приведённый выше код не создаёт квад, а лишь является своеобразным шаблоном. Прежде чем создать квад и заполнить его вершинами, сперва пишут функцию, создающую вектор (vector):
VERTEX CreateVertex(float x, float y, float z, float tu, float tv)
{
	VERTEX vertex;
	vertex.x = X;
	vertex.y = Y;
	vertex.z = Z;
	vertex.tu = tu;
	vertex.tv = tv;
	return vertex;
}

Функция CreateVertex (здесь) создаёт временную структуру VERTEX и заполняет её значениями, передаваемыми в параметрах. Возвращаемое значение - структура VERTEX, содержащая 5 переменных членов. Буквально через абзац мы создадим квад. Но сперва рассмотрим тему "Вершинные буферы".

Вершинные буферы (Vertex Buffers)

  • Представляет собой место (область в памяти), где хранятся вершины (по сути - точки), образующие полигоны для последующей отрисовки средствами Direct3D.
  • Вообще, это массив (array).
  • Их может быть несколько.
При желании можно создавать отдельные буферы вершин для каждого объекта сцены. Такой подход очень распространён, т.к. в этом случае нужный объект быстро рендерится вновь и вновь путём обычного вызова функции рендеринга. Абзацем выше мы определили 3 вершины треугольника. Следующий шаг - поместить их в вершинный буфер, с которым работает Direct3D.

Создание вершинного буфера (DirectX 9)

После создания FVF-структуры вершины, её дескриптора и определения вершин создаём переменную вершинного буфера:
LPDIRECT3DVERTEXBUFFER9 buffer;

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

Майкрософтовский макрос LPDIRECT3DVERTEXBUFFER9 можно записать как обычный указатель IDirect3DVertexBuffer9* . Такие записи равнозначны.

Далее создаём сам вершинный буфер путём вызова функции CreateVertexBuffer. Вот её прототип:
Прототип функции CreateVertexBuffer
HRESULT IDirect3DDevice9::CreateVertexBuffer(
	UINT Length, // Размер создаваемого буфера в байтах
	DWORD Usage,
	DWORD FVF,
	D3DPOOL Pool,
	IDirect3DVertexBuffer9** ppVertexBuffer,
	HANDLE* pSharedHandle );

Первый параметр (Length) указывает размер вершинного буфера, который выбирается так, чтобы его было достаточно для хранения всех вершин полигонов, готовящихся к рендерингу. Часто здесь можно видеть что-нибудь вроде sizeof(CUSTOMVERTEX)*3 .
Второй параметр указывает способ доступа к вершинному буферу. Обычно здесь стоит "только для записи" или 0. Третий параметр - указатель на дескриптор предварительно созданной FVF-структуру. В нашем случае в ней есть только позиция и текстурные координаты каждой вершины.
Четвёртый параметр - пул памяти (memory pool), в котором будет создан вершинный буфер. Это может быть системная (=оперативная) память либо видеопамять видеокарты. Возможные значения являются элементами перечисления (enumeration) D3DPOOL_SYSTEMMEM:
typedef enum _D3DPOOL
{
	D3DPOOL_DEFAULT = 0,
	D3DPOOL_MANAGED = 1,
	D3DPOOL_SYSTEMMEM = 2,
	D3DPOOL_SCRATCH = 3,
	D3DPOOL_FORCE_DWORD = 0x7fffffff
}D3DPOOL;

Пятый параметр - указатель на создаваемый вершинный буфер.
Последний параметр (на 2005 год) зарезервирован. Просто передаём здесь NULL.
Вот пример вызова функции CreateVertexBuffer:
d3ddev->CreateVertexBuffer(
	4*sizeof(VERTEX),
	D3DUSAGE_WRITEONLY,
	D3DFVF_MYVERTE X,
	D3DPOOL_DEFAULT,
	&buffer,
	NULL);

Видно, что для указания объёма буфера в первом параметре стоит вызов функции sizeof(VERTEX), умноженный на 4 (число вершин). В случае отрисовки треугольника, вызов функции sizeof переменожается на 3. Самые важные здесь параметры - это первый (размер буфера) и пятый (указатель на создаваемый вершинный буфер).
Если заметил, функция CreateVertexBuffer не запрашивает указателей на сами вершины т.к. изначально создаётся пустым. На данном этапе в нём размещена группа обнулённых байтов.

Заполнение вершинного буфера вершинами

Данный шаг должен всегда следует после кода, который инициализирует или загружает массив вершин. Т.к. эти данные передаются в вершинный буфер.
Как только вершины получили свои координаты (позицию + текстурные), можно смело размещать их в вершинном буфере. Перед этим буфер блокируют методом Lock, копируют туда вершины и затем разблокируют методом Unlock (обе функции экспонированы интерфейсом IDirect3DVertexBuffer9). Данная операция требует наличия временного указателя. Вот пример:
void *temp = NULL;

buffer->Lock(0, sizeof(vertices), (void**)&temp, 0);
memcpy(temp, vertices, sizeof(vertices));
buffer->Unlock;

Прототип функции Lock выглядит так:
Прототип функции Lock
HRESULT Lock(
	UINT OffsetToLock, // Смещение от начала содержимого буфера в байтах.
			// Обычно лочат весь буфер, выставляя здесь 0.
	UINT SizeToLock, // Размер блокируемого участка буфера (начиная со смещения).
			// Обычно лочат весь буфер, выставляя здесь 0.
	VOID **ppbData, // Указатель на блокируемый буфер.
	DWORD Flags // Флаги поведения вершинного буфера (только чтение, только запись и др.).
			// Обычно 0.
);

Для копирования вершин в буфер вызываем функцию memcpy. Вот её прототип:
Прототип функции memcpy
void* memcpy(
	void* Dest, // Получатель данных
	const void* Source, // Отправитель данных.
	DWORD Size // Размер в байтах.
	);

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

Ещё раз напомним, что перед вызовом memcpy обязательно вызываем метод Lock. А после - Unlock. Если не разлочить вершинный буфер перед показом кадра, вершины не будут отрендерены.


Стадии рендеринга содержимого вершинного буфера

После инициализации вершинный буфер готов к применению в конвейере Direct3D. Для отправки содержимого вершинного буфера на экран требуется:
  • Установить матрицы вида (View), трансформации (Transformation) и проекции (Projection).
  • Назначить созданный вершинный буфер в качестве источника потока (stream source).
  • Присвоить объекту устройства Dierct3D созданную ранее FVF-структуру.
  • Вызвать функцию DrawPrimitive.
Все эти шаги обычно выполняются в главном цикле рендеринга (rendering loop) приложения. Рассмотрим каждый из них подробнее.

Назначаем (set) матрицы вида (View), трансформации (Transformation) и проекции (Projection)

Если кратко, матрица представляет собой таблицу чисел (grid of numbers), которая (в игрокодинге) выполняет роль набора инструкций для преобразований объектов. Так матрица преобразований (transformation matrix) инструктирует Direct3D, как именно надо переместить, повернуть или изменить в размерах объект.
Принцип прост: матрица сначала заполняется данными и затем отправляется в Direct3D. Подробнее об этом здесь: Базовые понятия 3D-графики.

Матрица трансформации (матрица преобразований; Transform Matrix)
Указывает Direct3D как именно изменить объект перед рендерингом. Она состоит (является произведением) из трёх других матриц: трансляции (=перемещения), вращения и масштабирования. Вот пример перемещения объекта в точку начала координат (0, 0, 0):
D3DXMATRIX g_Transform;
D3DXMatrixIdentity(&g_Transform);
D3DXMatrixTranslation(&g_Transform, 0.0f, 0.0f, 0.0f);

Ничего сверхъестественного. Но каждую создаваемую матрицу обязательно надо приводить к единичному виду путём вызова функции D3DXMatrixIdentity.
Как только матрица трансформации создана, оповещаем об этом Direct3D путём вызова метода SetTransform (экспонирован объектом устройства IDirect3DDevice9):
HRESULT SetTransform(D3DTRANSFORMSTATETYPE State, CONST D3DMATRIX *pMatrix);

Здесь первый параметр - тип (считай "функциональная должность"), который присваивается данной матрице. Возможные значения: D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION.
Второй параметр - указатель на предварительно созданную и заполненную матрицу. Вот пример:
g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_Transform);


Матрица вида (View Matrix)
Позиционирует и ориентирует в 3D-пространстве виртуальную камеру (= viewer). Именно через неё Direct3D рендерит сцену. Если эту матрицу изменять во времени (анимировать) можно получить перемещение этой камеры (как будто перемещается персонаж игры жанра 3D FPS).
Матрицу вида можно создать вручную с нуля, либо воспользоваться одной из функций Direct3D. Самая популярная - D3DXMatrixLookAtLH (LH означает леворучная картезианскя система координат):
Прототип функции D3DXMatrixLookAtLH
D3DXMATRIX *WINAPI D3DXMatrixLookAtLH (
	D3DXMATRIX *pOut, // Матрица-результат
	CONST D3DXVECTOR3 *pEye, // Положение виртуальной камеры ("глаз") в 3D-пространстве
	CONST D3DXVECTOR3 *pAt, // Вектор направления "взгляда"
	CONST D3DXVECTOR3 *pUp // Вектор, указывающий вверх (в данной системе координат)
	);

Здесь в качестве вводных параметров передаём положение камеры (часто это что-то вроде (0, -15, 0)), точку, куда она смотрит и вектор, который у нас принят за указатель вверх (обычно это (0, 1, 0)). Чтобы всю сцену сделать верх ногами, в векторе верха достаточно указать (0, -1, 0). Вот пример:
D3DXMATRIX g_View;

D3DXVECTOR3 Eye(0.0f, -5.0f, 0.0f);
D3DXVECTOR3 LookAt(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 Up(0.0f, 1.0f, 0.0f);

D3DXMatrixLookAtLH(&g_View, &Eye, &LookAt, &Up);
g_pdDevice->SetTransform(D3DTS_VIEW, &g_View);


Матрица проекции (Projection Matrix)
  • Указывает Direct3D, каким образом проецировать 3D-сцену на 2D-экран монитора. Таких способов может быть несколько.
  • Обеспечивает корректное отображение всех объектов сцены на экране путём "фокусировки" виртуальной камеры.
  • Делает ближние к объективу объекты больше, а дальние - меньше.
  • Самая сложная матрица с точки зрения её вычисления.
В Direct3D для вычисления матрицы проекции есть специальная функция D3DXMatrixPerspectiveFovLH:
Прототип функции D3DXMatrixPerspectiveFovLH
D3DXMATRIX *WINAPI D3DXMatrixPerspectiveFovLH (
	D3DXMATRIX *pOut, // Матрица-результат
	FLOAT fovy, // Соотношение сторон (Aspect ratio).
	// Вычисляется как ширина пространства вида делёная на высоту.
	// Здесь почти всегда стоит значение D3DX_PI/4.
	FLOAT Aspect, // Ширина окна делёная на высоту.
	FLOAT zn, // Z-значение ближней плоскости отсечения (Near clip plane). Обычно 1.0f .
	FLOAT zf // Z-значение дальней плоскости отсечения (Far clip plane).
	// Определяет расстояние, на которое камера может "видеть".
	// Все объекты, расположенные дальше считаются невидимыми (не показываются).
	// Для учебного приложения можно выставить значение 500.0f .
);

Вот пример:
D3DXMATRIX Projection;
FLOAT FOV = D3DX_PI / 4;

FLOAT Aspect = WindowWidth/WindowHeight;
D3DXMatrixPerspectiveFovLH(&Projection, FOV, Aspect, 1.0f, 500.0f);
g_pdDevice->SetTransform(D3DTS_PROJECTION, &Projection);


Назначаем вершинный буфер в качестве источника потока объекта устройства Direct3D. Функция SetStreamSource

Источник потока. Как только все нужные матрицы назначены, сцена почти готова к рендерингу. Direct3D полностью информирован о том, где и как расположены объекты, камера + о том, как настроены "линзы" её объектива. Осталось лишь прописать пару моментов.
Поток (stream) Direct3D - это своеобразный информационный канал, поток данных. В Direct3D есть несколько потоков (обычно 8), отсчёт которых начинается с 0. Указав на любом из них свой источник данных, мы как-будто говорим: "Эй, Direct3D! У меня тут есть вершины для рендеринга. Они жидают в потоке 1." В нашем случае в потоке 0 мы ставим ссылку на наш вершинный буфер. Для назначения вершинного буфера в качестве источника потока применяют функцию SetStreamSource, экспонированную интерфейсом IDirect3DDevice9 (DirectX 9):
Прототип функции SetStreamSource
HRESULT SetStreamSource (
	UINT StreamNumber, // Поток, которому назначаем вершинный буфер. Обычно 0.
	IDirect3DVertexBuffer9 *pStreamData, // Указатель на вершинный буфер.
	UINT OffsetInBytes, // Смещение в байтах. Если без него, то ставим 0.
	UINT Stride // Размер каждой вершины в байтах. Обычно здесь стоит что-то похожее на sizeof(CUSTOMVERTEX).
	);

Вот пример:
g_pdDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

Напомним, что здесь мы пока ничего не рендерим. Но уже скоро всё будет.

Назначаем объекту устройства Direct3D предварительно созданную FVF-структуру. Функция SetFVF

Передаётся не сама FVF-структура, а её дескриптор размера DWORD (для этого он и создан). В FVF-структуре указано, какие именно данных хранит каждая вершина, созданная на её основе. Это позволит Direct3D корректно распознать данные вершин, уже хранящиеся в потоке 0.
Для прописывания в объекте устройства Direct3D кастомной FVF-структуры служит функция SetFVF, экспонированная интерфейсом IDirect3DDevice9:
Прототип функции SetFVF
g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

В качестве единственного параметра передаётся дескриптор предварительно созданной FVF-структуры.

Выводим вершины на экран. Рендерим примитив

Наконец-то мы рендерим вершины (в нашем случае) в виде треугольника. На данном этапе вершинный буфер полностью создан, заполнен и "подключен" к объекту устройства Direct3D. Все нужные матрицы назначены. Вершинный буфер прописан в источнике потока (объекта устройства Direct3D) и указан FVF-дескриптор. Только на этом этапе можно давать Direct3D команду что-либо отрендерить.
Для рендеринга примитива (вообще примитивы Direct3D - это тема отдельной статьи) применяют функцию DrawPrimitive, также экспонированную интерфейсом IDirect3DDevice9:
Прототип функции DrawPrimitive
HRESULT DrawPrimitive (
	D3DPRIMITIVETYPE PrimitiveType, // Тип примитива
	UINT StartVertex, // Номер вершины, с которой начнём рендеринг. Обычно 0.
	UINT PrimitiveCount // Число примитивов. Для одного треугольника значение здесь 1.
	);

Возможные значения первого параметра прописаны в перечислении (enumeration) _D3DPRIMITIVETYPE:
typedef enum _D3DPRIMITIVETYPE
{
	D3DPT_POINTLIST = 1,
	D3DPT_LINELIST = 2,
	D3DPT_LINESTRIP = 3,
	D3DPT_TRIANGLELIST = 4,
	D3DPT_TRIANGLESTRIP = 5,
	D3DPT_TRIANGLE_FAN = 6,
	D3DPT_FORCE_DWORD = 0x7fffffff
}D3DPRIMITIVETYPE;

Описание каждого типа можно найти в статье Базовые понятия 3D-графики или в Интернете.

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


Определяем структуру MODEL

Сперва определим кастомную (т.е. её содержание определяет программер) структуру, которая будет хранить всю инфу о 3D-объекте, загруженном из .X-файла:
struct MODEL {
	LPD3DXMESH mesh;
	D3DMATERIAL9* materials;
	LPDIRECT3DTEXTURE9* textures;
	DWORD material_count;
};

Некоторые игрокодеры называют .X-файлы меш-файлами, что не совсем верно. .X-файлы, помимо мешей (полигональных сеток), могут хранить в себе материалы, инструкции по анимации и множество других видов данных. Поэтому наша структура называется более общим понятием MODEL. Она содержит основные объекты, необходимые для загрузки и рендеринга файла модели.
В первой строке, конечно же, стоит указатель на меш (mesh). Если в .X-файле содержится несколько мешей/объектов (.X-формат файла это позволяет), то они должны быть объединены в один меш (например утилитой conv3ds.exe из DirectX SDK 6.1).
Далее идёт указатель на объект типа D3DMATERIAL9, который указывает на массив материалов, определённых в .X-файле. Указатель LPDIRECT3DTEXTURE9 хранит данные о текстурах. Модель может иметь несколько текстур. Технически текстуры не хранятся в самом .X-файле. В нём хранятся лишь имена растровых файлов текстур, содержащиеся в одном каталоге с ним. В конце списка элементов структуры MODEL стоит счётчик числа материалов модели, используемых при рендеринге. У модели может быть много материалов, но далеко не все из них требуют текстуру. Текстуры должны быть определены отдельно от материалов. Несмотря на это, в нашей структуре MODEL счётчик текстур отсутствует. Далее на базе созданного шаблона создаётся объект структуры:
MODEL *model = (MODEL*)malloc(sizeof(MODEL));

Эта строка размещает объект структуры в памяти и возвращает свой указатель в вызвавшую её функцию LoadModel (о ней чуть позже).

Загружаем меш (Loading the Mesh)

Для создания полигональной сетки (=меша) из загруженного .X-файла модели Direct3D (DirectX 9) предоставляет специальную функцию D3DXLoadMeshFromX. Вот её прототип:
Прототип функции D3DXLoadMeshFromX
HRESULT WINAPI D3DXLoadMeshFromX(
	LPCSTR pFilename,
	DWORD Options,
	LPDIRECT3DDEVICE9 pDevice,
	LPD3DXBUFFER *ppAdjacency,
	LPD3DXBUFFER *ppMaterials,
	LPD3DXBUFFER *ppEffectInstances,
	DWORD *pNumMaterials,
	LPD3DXMESH *ppMesh );

Вот самые важные здесь параметры:
  • pFileName (имя .X-файла);
  • pDevice (объект устройства Direct3D);
  • ppMaterials (указатель на буфер материалов);
  • pNumMaterials (указатель на счётчик материалов);
  • ppMesh (указатель на создаваемый объект меша).
Перед вызовом функции D3DXLoadMeshFromX необходимо создать (проинициализировать) буфер материалов (material buffer), куда будут загружаться данные о материалах из .X-файла:
LPD3DXBUFFER matbuffer;

Вот пример вызова функции D3DXLoadMeshFromX:
result = D3DXLoadMeshFromX(
	filename, // Имя файла
	D3DXMESH_SYSTEMMEM, // Опции меша
	d3ddev, // Объект устройства
	Direct3D NULL, // Буфер смежных вершин (adjacency buffer),
	&matbuffer, // Буфер материалов
	NULL, // Спецэффекты
	&model->material_count, // Кол-во материалов
	&model->mesh // Результирующий меш
	);


Загружаем текстуры и материалы

Материалы хранятся в буфере материалов. Но перед их применением в рендеринге модели они должны быть корректно сконвертированы в материалы и тестуры формата Direct3D. Для этого материалы и текстуры копируются из буфера материалов в отдельные (individual) массивы материалов и текстур:
D3DXMATERIAL* d3dxMaterials = (LPD3DXMATERIAL)matbuffer->GetBufferPointer();
model->materials = new D3DMATERIAL9[model->material_count];
model->textures = new LPDIRECT3DTEXTURE9[model->material_count];

Здесь для получения указателя на начало массива matbuffer вызываем метод GetBufferPointer, экспонированный интерфейсом ID3DXBuffer.
Определение структуры D3DXMATERIAL выглядит так:
typedef struct D3DXMATERIAL {
	D3DMATERIAL9 MatD3D; // Имя материала
	LPSTR pTextureFilename; // Имя текстуры материала
}

Следующий шаг - итерация через список материалов и их выборка из буфера материалов. Для каждого выбранного материала назначается:
  • цвет рассеянного (ambient) света;
  • текстура (загружается в объект текстуры).
Так как оба пункта представляют собой массивы, объём каждой можели ограничен лишь объёмом памяти ПК и возможностями видеокарты. Теоретически ты можешь рендерить (средствами DirectX) модель с миллионами граней, каждой из которых присвоен свой собственный материал. Вот пример кода создания массива материалов:
for(i=0; i < model->material_count; i++)
{
	// Получаем материал
	model->materials[i] = d3dxMaterials[i].MatD3D;
	
	// Назначаем цвет рассеянного (ambient) света
	model->materials[i].Ambient = model->materials[i].Diffuse;
	model->textures[i] = NULL;
	if(d3dxMaterials[i].pTextureFilename != NULL && lstrlen(d3dxMaterials[i].pTextureFilename) > 0)
	{
		// Загружаем текстуру, указанную в .X-файле
		result = D3DXCreateTextureFromFile(d3ddev, d3dxMaterials[i].pTextureFilename, &model->textures[i]);
	}
}

Здесь, применив цикл for, итерируем через все материалы и текстуры, присваиваем их имена элементам массива d3dxMaterials. Для материалов с текстурой загружаем текстуру. По завершении выполнения цикла получим массив материалов и текстур, совместимых с загруженным мешем. По окончании загрузки материалов и текстур буфер материалов удаляют:
matbuffer->Release();


Рендерим модель

Сперва модели назначается материал и текстура. Затем вызывается функция DrawPrimitive или DrawSubset. Субсет (subset) - это группа полигонов, покрытых одной текстурой. Каждая текстура меша покрывает свой субсет. Сколько у меша текстур, столько и субсетов. Меш рендерится субсет за субсетом.
Здесь сложность в том, что необходимо итерировать (=опрашивать) модель и рендерить каждый субсет отдельно, с учётом счётчика material_count. Вот пример:
for(i=0; i < model->material_count; i++)
{
	d3ddev->SetMaterial(&maodel->materials[i]);
	d3ddev->SetTexture(0, model->textures[i]);
	model->mesh->DrawSubset(i);
}


Освобождаем память по завершении рендеринга

Очищаем созданный в начале объект ID3DXMesh а также все текстуры и материалы:
if(model->materials != NULL)
	delete[] model->materials;

if(model->textures != NULL)
{
	for(DWORD i=0; i<model->materials; i++)
	{
		if(model->textures[i])
			model->textures->Release();
	}
	
	delete[] model->textures;
}

if(&model->mesh != NULL)
	&model->mesh->Release();


Пример приложения, загружающего неанимированный меш из .X-файла.

Создадим Проект приложения, которое читает с диска .X-файл и выводит содержащуюся в нём 3D-модель на экран. За основу возьмём исходный код из статьи DirectX 9 Graphics. Начало работы (финальный вариант Проекта D3D9Init01 в конце статьи), который, в свою очередь, основан на исходном коде базового приложения Windows из статьи Создание приложений Сpp Win32. В этом примере мы продолжим размещать исходный код в отдельных файлах, увеличивая его модульность и тем самым повышая удобочитаемость и юзабилити.

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

  • Создай пустой Проект с именем DX9LoadMesh01.
Проект автоматически разместится внутри Решения с таким же именем. Весь процесс подробно расписан в статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK.

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

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта DX9LoadMesh01.
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp. Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
// Файл: WinMain.cpp 
// Базовый исходник приложения, реализующий функции для работы с окнами.
// www.igrocoder.ru 2021
// При использовании просьба указывать ссылку на источник.

#define STRICT
#define WIN32_LEAN_AND_MEAN	// Уменьшаем кол-во используемых компонентов в программе.

#include <windows.h>
#include "game.h"

// Прототип функции WinMain
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int nCmdShow);
// Объявление оконной процедуры
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

// Реализация главной функции программы
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	HWND hWnd;			// Дескриптор окна
	MSG msg;			// Структура сообщения
	CoInitialize(NULL);

	WNDCLASSEX wndClass;	// Объявляем экземпляр класса программы на базе WNDCLASSEX

   // Заполняем структуру оконного класса
	wndClass.cbSize=sizeof(wndClass);
	wndClass.style=CS_CLASSDC;
	wndClass.lpfnWndProc=WindowProc;		// Оконная процедура
	wndClass.cbClsExtra=0;
	wndClass.cbWndExtra=0;
	wndClass.hInstance=hInstance;			// Экземпляр окна
	wndClass.hIcon=LoadIcon(NULL, IDI_APPLICATION);		// Пиктограмма приложения
	wndClass.hIconSm=LoadIcon(NULL, IDI_APPLICATION);	// Малая пиктограмма приложения
	wndClass.hCursor=NULL;		// Курсор при наведении на окно
	wndClass.hbrBackground=NULL;	// Закрашиваем окно белым цветом
	wndClass.lpszMenuName=NULL;		// Дескриптор Главного меню окна. Сейчас оно не нужно
	wndClass.lpszClassName="GameClass"; // Обзываем оконный класс.
	
	// Если класс не зарегистрируется, досрочно прерываем выполнение программы
	if(!RegisterClassEx(&wndClass)) return FALSE;

	// Создание окна на основе зарегистрированного класса
	hWnd=CreateWindow(
		"GameClass",		// Класс окна.
		"My game title",			// Текст заголовка (на верхнем тулбаре).
		WS_OVERLAPPEDWINDOW, // Основной стиль окна
		0, 0,					// Координаты X и Y
		iWidth,	// Ширина окна
		iHeight,	// Высота окна
		NULL,							// Дескриптор родительского окна
		NULL,							// Дескриптор меню
		wndClass.hInstance,						// Дескриптор экземпляра приложения
		NULL);							// Дополнительные данные

  if(!hWnd)
    return FALSE;

  ShowWindow(hWnd, SW_NORMAL);
  UpdateWindow(hWnd);

  // Очищаем структуру сообщения
  ZeroMemory(&msg, sizeof(MSG));

  if(GameInit(hWnd) == TRUE) 
  {
	  while (true) 
	  {
		  if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) 
		  {
			  TranslateMessage(&msg);
			  DispatchMessage(&msg);
			  if (msg.message == WM_QUIT) break;
		  }
		  else
		  {
		   // Главный цикл программы (mainloop)
		   GameRun(hWnd);
		  }
	  }
  }

    CoUninitialize();

  //Удаляем регистрацию класса
	UnregisterClass("GameClass", hInstance);
	
	return (msg.wParam);
}

// Оконная процедура
LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch(msg)
	{
	case WM_DESTROY:			// В случае этого сообщения...
		GameEnd(hWnd);
		PostQuitMessage(0);		// Говорим Windows закрыть приложение
		return 0;
	}
		// В случае любых других сообщений...
	return DefWindowProc(hWnd, msg, wParam, lParam);
}

  • Сохрани Решение (Файл->Сохранить все).
В начале листинга в секции indude-ссылок видим подключение следующих заголовков:
  • windows.h Предоставляет базовые WINAPI-функции создания окон и работы с сообщениями. Впервые появился в MS Visual Studio ещё в конце 90-х годов XX века. В начале 2000-х у него появился более расширенный "собрат" windowsx.h. Но в экспериментальных целях мы выведем DirectX-графику путём применения "хардкорной" классики.
  • game.h Этот заголовок мы создадим ниже. В нём разместятся ссылки на другие заголовочные файлы, реализующие (в перспективе) различный функционал движка (графика, звук, ввод и т.д.). Представляет собой реализацию концепции единой точки контакта (основопологающей в игрокодинге).

Добавляем в Проект заголовочный файл dxgraphics.h

dxgraphics.h содержит объявления DirectX-функций, реализованных в dxgraphics.cpp.
ОК, приступаем.
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект DX9LoadMesh01, созданный выше.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта DX9LoadMesh01.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "dxgraphics.h".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле dxgraphics.h набираем следующий код:
dxgraphics.h
#define D3DFVF_MYVERTEX (D3DFVF_XYZ | D3DFVF_TEX1)

#ifndef _DXFUNC_H_
#define _DXFUNC_H_

#include <d3d9.h>
#include <d3dx9.h>
#include <d3dx9math.h>

// Прототипы функций
HRESULT d3d9Init(IDirect3D9 **d3d, 
                IDirect3DDevice9 **d3ddev,
                HWND hWnd, 
				DWORD iWidth,
				DWORD iHeight,
				BOOL bFullScreen
				);
LPDIRECT3DSURFACE9 LoadSurface(char *, D3DCOLOR);
LPDIRECT3DTEXTURE9 LoadTexture(char *, D3DCOLOR);

struct VERTEX
{
	float x, y, z;
	float tu, tv;
};

struct QUAD
{
	VERTEX vertices[4];
	LPDIRECT3DVERTEXBUFFER9 buffer;
	LPDIRECT3DTEXTURE9 texture;
};

void SetPosition(QUAD*, int, float, float, float);
void SetVertex(QUAD*, int, float, float, float, float, float);
VERTEX CreateVertex(float, float, float, float, float);
QUAD* CreateQuad(char*);
void DeleteQuad(QUAD*);
void DrawQuad(QUAD*);
void SetIdentity();
void SetCamera(float, float, float, float, float, float);
void SetPerspective(float, float, float, float);
void ClearScene(D3DXCOLOR);

// Объявления переменных
extern LPDIRECT3D9 d3d;
extern LPDIRECT3DDEVICE9 d3ddev;
extern LPDIRECT3DSURFACE9 backbuffer;

#endif

  • Сохрани Решение (Файл->Сохранить все).
По мере "обрастания" движка новым функционалом список подключаемых здесь заголовков будет увеличиваться. Заголовок d3dx9math.h нужен для матричных вычислений положения камеры в сцене.
Для кода инициализации Direct3D здесь объявлена отдельная функция d3d9Init аж с шестью вводными параметрами.3 Первые 5 мы уже применяли выше. Шестой - булево значение, отслеживающее использование полноэкранного режима. При bFullScreen=TRUE приложение работает в полноэкранном режиме.
В конце листинга видим объявления переменных Direct3D и объекта устройства Direct3D. Перед каждым стоит служебное слово extern, разрешающее их применение во всех исходниках данного Проекта.

Добавляем в Проект файл исходного кода dxgraphics.cpp

В файле исходного кода dxgraphics.cpp содержатся реализации DirectX-функций, объявленных в dxgraphics.h.
ОК, приступаем.
  • Убедись, что MSVC++2010 запущена и вней открыт Проект DX9LoadMesh01, созданный выше.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта DX9LoadMesh01.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл C++ (.cpp)" и в поле "Имя" введи "dxgraphics.cpp".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле dxgraphics.cpp набираем следующий код:
dxgraphics.cpp
// Файл: dxfunc.cpp
// Реализация DX-функций, объявленных в dxfunc.h.
// Сборник DirectX-функций

#include "dxgraphics.h"

// Объявления переменных
LPDIRECT3D9 d3d = NULL;
LPDIRECT3DDEVICE9 d3ddev = NULL;
LPDIRECT3DSURFACE9 backbuffer = NULL;

HRESULT d3d9Init(IDirect3D9 **d3d, 
                IDirect3DDevice9 **d3ddev,
                HWND hWnd, 
				DWORD iWidth,
				DWORD iHeight,
				BOOL bFullScreen)
{
  // Инициализируем Direct3D
 if(( *d3d = Direct3DCreate9(D3D_SDK_VERSION)) == NULL)
 {
	return E_FAIL;
 }

 // Заполняем основные (общие) параметры представления
  D3DPRESENT_PARAMETERS d3dpp;
  ZeroMemory( &d3dpp, sizeof( d3dpp ) );

  d3dpp.BackBufferWidth = iWidth;
  d3dpp.BackBufferHeight = iHeight;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
  d3dpp.hDeviceWindow = hWnd;


  // Запрос на отображение в полноэкранном режиме
  int  iRes;
  if (!bFullScreen)
      iRes=MessageBox(hWnd, "Перейти в полноэкранный режим?", "Screen", MB_YESNO | MB_ICONQUESTION);
  else
	  iRes = IDYES;

  if(iRes == IDYES)
  {
      //////////////////////////////////////////////////////////
	  // Полноэкранный режим
      //////////////////////////////////////////////////////////
	  // Установка параметров полноэкранного режима
	  d3dpp.Windowed         = FALSE;
	  d3dpp.BackBufferCount = 1;
      d3dpp.BackBufferFormat = D3DFMT_R5G6B5;
	  d3dpp.SwapEffect       = D3DSWAPEFFECT_FLIP;
      d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
      d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;  // Direct3D сам выбирает частоту показа.
	  // Обычно она равна частоте кадров.
  } 
  else 
  {
  // Доп. параметры для оконного режима
  // Получить формат пикселя
  D3DDISPLAYMODE d3ddm;
  (*d3d)->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);

  // Установка параметров оконного режима
  d3dpp.Windowed         = TRUE;
  d3dpp.BackBufferFormat = d3ddm.Format;
  d3dpp.SwapEffect       = D3DSWAPEFFECT_DISCARD;
  }

 if(FAILED((*d3d)->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, d3ddev)))
 {
	 return E_FAIL;
 }

 // Очищаем бэкбуфер в чёрный цвет
 (*d3ddev)->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);

 // Создаём указатель на бэкбуфер
 (*d3ddev)->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer);
}

LPDIRECT3DSURFACE9 LoadSurface(char *filename, D3DCOLOR transcolor)
{
	LPDIRECT3DSURFACE9 image = NULL;
	D3DXIMAGE_INFO info;
	HRESULT result;

	// Получаем из файла длину и высоту изображения
	result = D3DXGetImageInfoFromFile(filename, &info);
	if(result != D3D_OK)
		return NULL;

	// Создаём поверхность
	result = d3ddev->CreateOffscreenPlainSurface(
		info.Width,  // Ширина изображения
		info.Height,  // Высота изображения
		D3DFMT_X8R8G8B8,  // Формат цветности поверхности
		D3DPOOL_DEFAULT,  // Используемый пул памяти
		&image,  // Указатель на поверхность
		NULL);  // Зарезервирован (всегда NULL)

	if(result != D3D_OK)
		return NULL;

	// Загружаем изображение из файла в созданную поверхность
	result = D3DXLoadSurfaceFromFile(
		image,  // Поверхность-получатель
		NULL,  // Палитра-получатель
		NULL,  // Прямоугольник-получатель
		filename,  // Имя файла-источника
		NULL,  // Прямоугольник-источник
		D3DX_DEFAULT,  // Метод фильтрации изображения
		transcolor,  // Цвет прозрачности (0 если без неё)
		NULL);  // Инфо о файле изображения (обычно NULL)

	if(result != D3D_OK)
		return NULL;

	return image;
}

LPDIRECT3DTEXTURE9 LoadTexture(char *filename, D3DCOLOR transcolor)
{
	// Указатель на текстуру
	LPDIRECT3DTEXTURE9 texture = NULL;

	// Структура, куда сохраним инфу о файле растрового изображения (=битмапе)
	D3DXIMAGE_INFO info;

	// Стандартное значение, возвразаемое в ОС Windows
	HRESULT result;

	// Получаем ширину и высоту изображения из битмапа
	result = D3DXGetImageInfoFromFile(filename, &info);
	if(result != D3D_OK)
		return NULL;

	// Создаём текстуру путём загрузки битмапа
	D3DXCreateTextureFromFileEx(
		d3ddev,  // Объект устройства Direct3D
		filename,  // Имя файла битмапа
		info.Width,  // Ширина битмапа
		info.Height,  // Высота битмапа
		1,  // Кол-во мипмап-уровней (1 - значит без цепочки)
		D3DPOOL_DEFAULT,  // Тип поверхности (стандартная)
		D3DFMT_UNKNOWN,  // Формат цветности поверхности (по умолчанию)
		D3DPOOL_DEFAULT,  // Пул памяти для текстуры
		D3DX_DEFAULT,  // Фильтр изображения
		D3DX_DEFAULT,  // Мип-фильтр
		transcolor,  // Ключевой цвет для прозрачности
		&info,  // Инфо о битмапе (загружено выше из файла битмапа)
		NULL,  // Цветовая палитра
		&texture  // Указатель на конечный объект текстуры
		);

	// Проверяем, что битмап успешно загружен
	if(result != D3D_OK)
		return NULL;

	return texture;
}

void SetPosition(QUAD *quad, int ivert, float x, float y, float z)
{
	quad->vertices[ivert].x = x;
	quad->vertices[ivert].y = y;
	quad->vertices[ivert].z = z;
}

void SetVertex(QUAD *quad, int ivert, float x, float y, float z, float tu, float tv)
{
	SetPosition(quad, ivert, x , y, z);
	quad->vertices[ivert].tu = tu;
	quad->vertices[ivert].tv = tv;
}

VERTEX CreateVertex(float x, float y, float z, float tu, float tv)
{
	VERTEX vertex;
	vertex.x = x;
	vertex.y = y;
	vertex.z = z;
	vertex.tu = tu;
	vertex.tv = tv;
	return vertex;
}

QUAD *CreateQuad(char *textureFilename)
{
	QUAD *quad = (QUAD*)malloc(sizeof(QUAD));
	// Загружаем текстуру
	D3DXCreateTextureFromFile(d3ddev, textureFilename, &quad->texture);

	// Создаём для данного квада вершинный буфер
	d3ddev->CreateVertexBuffer(
		4*sizeof(VERTEX),
		0,
		D3DFVF_MYVERTEX, D3DPOOL_DEFAULT,
		&quad->buffer,
		NULL);

	// Создаём 4 угла этой "ленты", состоящей из двух треугольников.
	// У каждой вершины есть координаты X, Y, Z и текстурные координаты U, V
	quad->vertices[0] = CreateVertex(-1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
	quad->vertices[1] = CreateVertex(1.0f, 1.0f, 0.0f, 1.0f, 0.0f);
	quad->vertices[2] = CreateVertex(-1.0f, -1.0f, 0.0f, 0.0f, 1.0f);
	quad->vertices[3] = CreateVertex(1.0f, -1.0f, 0.0f, 1.0f, 1.0f);

	return quad;
}

void DeleteQuad(QUAD *quad)
{
	if(quad == NULL)
		return;

	// Особождаем вершинный буфер
	if(quad->buffer != NULL)
		quad->buffer->Release();

	// Особождаем память, ранее выделенную под текстуру
	if(quad->texture != NULL)
		quad->texture->Release();

	// Удаляем квад
	free(quad);
}

void DrawQuad(QUAD *quad)
{
	// Заполняем вершинный буфер вершинами квада
	void *temp = NULL;
	quad->buffer->Lock(0, sizeof(quad->vertices), (void**)&temp, 0);
	memcpy(temp, quad->vertices, sizeof(quad->vertices));
	quad->buffer->Unlock();

	// Рисуем треугольную ленту с текстурой, состоящую из двух треугольников
	d3ddev->SetTexture(0, quad->texture);
	d3ddev->SetStreamSource(0, quad->buffer, 0, sizeof(VERTEX));
	d3ddev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
}

void SetIdentity()
{
	// Устанавливаем положение, масштаб и вращение по умолчанию
	D3DXMATRIX matWorld;
	D3DXMatrixTranslation(&matWorld, 0.0f, 0.0f, 0.0f);
	d3ddev->SetTransform(D3DTS_WORLD, &matWorld);
}

void ClearScene(D3DXCOLOR color)
{
	d3ddev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, color, 1.0f, 0);
}

void SetCamera(float x, float y, float z, float lookx, float looky, float lookz)
{
	D3DXMATRIX matView;
	D3DXVECTOR3 updir(0.0f, 1.0f, 0.0f);

	// Перемещения камеры
	D3DXVECTOR3 cameraSource;
	cameraSource.x = x;
	cameraSource.y = y;
	cameraSource.z = z;

	// Точка направления взгляда камеры
	D3DXVECTOR3 cameraTarget;
	cameraTarget.x = lookx;
	cameraTarget.y = looky;
	cameraTarget.z = lookz;

	// Настраиваем матрицу вида (view matrix) камеры
	D3DXMatrixLookAtLH(&matView, &cameraSource, &cameraTarget, &updir);
	d3ddev->SetTransform(D3DTS_VIEW, &matView);
}

void SetPerspective(float fieldOfView, float aspectRatio, float nearRange, float farRange)
{
	// Устанавливаем перспективу, чтобы объекты в кадре выглядели не такими большими
	D3DXMATRIX matProj;
	D3DXMatrixPerspectiveFovLH(&matProj, fieldOfView, aspectRatio, nearRange, farRange);
	d3ddev->SetTransform(D3DTS_PROJECTION, &matProj);
}

  • Сохрани Решение (Файл->Сохранить все).
Естесственно, в начале листинга подключаем заголовок dxgraphics.h, где размещены объявления DirectX-функций. В листинге dxgraphics.cpp нет ни одного вызова функции. Только реализации. Все эти функции вызываются из других внешних файлов (в основном WinMain.cpp и Game.cpp, который создадим ниже). dxgraphics.cpp служит для хранения реализаций служебных DirectX-функций. В перспективе можно создать и другие пары файлов .h и .cpp с произвольными именами, соответствующие добавляемому функционалу (dxaudio.h, dxinput.h и т.д.). Ниже видим огромный блок реализации функции d3d9Init. В нём:
  • Инициализируется Direct3D
  • Заполняются параметры представления (в экземпляре структуры D3DPRESENT_PARAMETERS).
Сначала заполняются общие параметры. Затем - отдельные для оконного и полноэкранного режимов отображения. Здесь же выводится модальное диалоговое окно с запросом на переход в полноэкранный режим.
  • На основе заполненной структуры параметров представления создаётся объект устройства Direct3D.

Добавляем в Проект заголовочный файл Game.h

Game.h содержит объявления функций, специфичных для игры, реализованных в Game.cpp. ОК, приступаем.
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект DX9LoadMesh01, созданный выше.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта DX9LoadMesh01.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Game.h".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле dxfunc.h набираем следующий код:
Game.h
// Файл: Game.h 
// Объявления функций, специфичных для игры, реализации которых
// размещены в Game.cpp

#ifndef _GAME_H_
#define _GAME_H_

#include "dxgraphics.h" // Для инициализации Direct3D + подключения d3d9.h

// Заголовок приложения (выводится на тулбаре окна)
#define APPTITLE "Loading .X-file demo"

// Настройки видеорежима
#define FULLSCREEN 1
#define iWidth 640
#define iHeight 480

// Макрос асинхронного считывания нажатий клавиатуры (без Direct Input)
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1:0)
#define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1:0)

// Прототипы игровых функций
bool GameInit(HWND);  // Инициализирует игру.
void GameRun(HWND);
void GameEnd(HWND);

#endif

  • Сохрани Решение (Файл->Сохранить все).
В начале листинга присваиваем значения переменным заголовка окна (APPTITLE; подставляется в WinMain.cpp при создании окна), iWidth и iHeight, устанавливающим начальные значения ширины и высоты окна.
Ниже видим пару макросов асинхронного считывания нажатий клавиатуры средствами WinAPI. Нужны для закрытия окна приложения по нажатию Esc на клавиатуре.
Вообще, достаточно лишь одной строки #define KEY_DOWN... Но пусть будут обе.
Ниже видим триаду функций главного игрового цикла: GameInit (инициализирует игру), GameRun (рисует графику, выводит звук и т.д.) и GameEnd (удаляет ресурсы из памяти при завершении работы приложения.

Добавляем в Проект файл исходного кода Game.cpp

В заголовочном файле Game.cpp содержатся реализации функций, специфичных для игры, объявленных в Game.h. ОК, приступаем.
  • Убедись, что MSVC++2010 запущена и вней открыт Проект DX9LoadMesh01, созданный выше.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта DX9LoadMesh01.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл C++ (.cpp)" и в поле "Имя" введи "Game.cpp".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле Game.cpp набираем следующий код:
Game.cpp
// Файл: Game.cpp 
// Реализация функций, специфичных для игры, объявленных в Game.h

#include "Game.h"

#define WHITE D3DCOLOR_ARGB(0, 255, 255, 255)
#define BLACK D3DCOLOR_ARGB(0, 0, 0, 0)

// Координаты камеры вынесены в определения define. Вообще это лишь начальные значения переменных,
// которые позднее можно изменить, в том числе с помощью пользовательского ввода.
// Отдаляем камеру по оси X на 20 единиц, чтобы объект целиком входил в кадр.
#define CAMERA_X -20.0f
#define CAMERA_Y 4.0f
#define CAMERA_Z 7.0f

HRESULT result;

// Определяем структуру MODEL
struct MODEL
{
	LPD3DXMESH mesh;
	D3DMATERIAL9* materials;
	LPDIRECT3DTEXTURE9* textures;
	DWORD material_count;
};

MODEL *car;

MODEL *LoadModel(char *filename)
{
	MODEL *model = (MODEL*)malloc(sizeof(MODEL));
	LPD3DXBUFFER matbuffer;
	HRESULT result;

	// Загружаем меш из указанного .X-файла
	result = D3DXLoadMeshFromX(
		filename,  // Имя файла
		D3DXMESH_SYSTEMMEM,  // Опции меша
		d3ddev,  // Объект устройства Direct3D
		NULL,  // Буфер смежных вершин (adjacency buffer)
		&matbuffer,  // Буфер материалов
		NULL,  // Спецэффекты
		&model->material_count,  // Кол-во материалов модели
		&model->mesh  // Результирующий меш
		);

	if(result != D3D_OK)
	{
		MessageBox(NULL, "Ошибка выполнения функции D3DXLoadMeshFromX", "Error", MB_OK);
		return NULL;
	}

	// Извлекаем из буфера материалов свойства материала и имена текстур
	D3DXMATERIAL* d3dxMaterials = (D3DXMATERIAL*)matbuffer->GetBufferPointer();
	model->materials = new D3DMATERIAL9[model->material_count];
	model->textures = new LPDIRECT3DTEXTURE9[model->material_count];

	// Создаём материалы и текстуры модели
	for(DWORD i=0; i < model->material_count; i++)
	{
		// Выбираем материал
		model->materials[i] = d3dxMaterials[i].MatD3D;

		// Назначаем цвет рассеянного (ambient) света
		model->materials[i].Ambient = model->materials[i].Diffuse;

		model->textures[i] = NULL;
		if(d3dxMaterials[i].pTextureFilename != NULL && lstrlen(d3dxMaterials[i].pTextureFilename) > 0)
		{
			// Загружаем файл текстуры, указанный в .X-файле
			result = D3DXCreateTextureFromFile(d3ddev, d3dxMaterials[i].pTextureFilename, &model->textures[i]);

			if(result != D3D_OK)
			{
				MessageBox(NULL, "Не могу найти файл текстуры", "Error", MB_OK);
				return NULL;
			}
		}
	}

	// Освобождаем буфер материала т.к. больше не нужен
	matbuffer->Release();

	return model;
}

void DeleteModel(MODEL *model)
{
	// Освобождаем память от материалов
	if(model->materials != NULL)
		delete[] model->materials;

	// Освобождаем память от текстур
	if (model->textures != NULL)
	{
		for(DWORD i=0; i < model->material_count; i++)
		{
			if(model->textures[i] != NULL)
				model->textures[i]->Release();
		}
		delete[] model->textures;
	}

	// Удаляем меш (сетку) модели из памяти
	if(model->mesh != NULL)
		model->mesh->Release();

	// Удаляем структуру модели из памяти
	if(model != NULL)
		free(model);
}

void DrawModel(MODEL *model)
{
	// Рисуем каждый субсет меша
	for(DWORD i=0; i<model->material_count; i++)
	{
		// Назначаем для субсета материал и текстуру
		d3ddev->SetMaterial(&model->materials[i]);
		d3ddev->SetTexture(0, model->textures[i]);

		// Рисуем субсет меша
		model->mesh->DrawSubset(i);
	}
}

// Инициализация игры
bool GameInit(HWND hWnd)
{
	if (d3d9Init(&d3d, &d3ddev, hWnd, iWidth, iHeight, FALSE)!=S_OK)
	{
	  MessageBox(hWnd, "DirectX Initialize Error", "Error", MB_OK);
	  return FALSE;
	}

	// Устанавливаем камеру и перспективу
	SetCamera(CAMERA_X, CAMERA_Y, CAMERA_Z, 0, 0, 0); // Начальные координаты камеры определены в начале листинга.
	float ratio = (float)iWidth/(float)iHeight;
	SetPerspective(45.0f, ratio, 0.1f, 1000.0f);

	// Включаем рассеянное (ambient) освещение и Z-буферизацию
	d3ddev->SetRenderState(D3DRS_ZENABLE, TRUE);
	d3ddev->SetRenderState(D3DRS_AMBIENT, WHITE);

	car = LoadModel("car.x");
	if(car == NULL)
	{
		MessageBox(hWnd, "Ошибка загрузки .X-файла", "Error", MB_OK);
		return 0;
	}

	// Если выполнение дошло до сюда, то возвращаем ОК
	return 1;
}

// Главный цикл игрового приложения
void GameRun(HWND hWnd)
{
	// Проверяем корректность созданного объекта устройства Direct3D
	if(d3ddev == NULL)
		return;

	ClearScene(BLACK);

	// Начинаем рендеринг
	if(d3ddev->BeginScene())
	{
		// Вращаем вид
		D3DXMATRIX matWorld;
		D3DXMatrixRotationY(&matWorld, timeGetTime()/1000.0f);
		d3ddev->SetTransform(D3DTS_WORLD, &matWorld);

		// Рисуем модель машины
		DrawModel(car);

		// Завершаем рендеринг
		d3ddev->EndScene();
	}

	// Выводим содержимое бэкбуфера на экран
	d3ddev->Present(NULL, NULL, NULL, NULL);

	// При нажатии Esc завершаем работу приложения
	if(KEY_DOWN(VK_ESCAPE))
		PostMessage(hWnd, WM_DESTROY, 0, 0);
}

// Освобождаем память, стирая ранее выделененые ресурсы
void GameEnd(HWND hWnd)
{
	DeleteModel(car);
}

  • Сохрани Решение (Файл->Сохранить все).
Многие из этих функций являются авторскими функциями-обёртками (wrapper functions) DirectX-функций объявленных в dxgraphics.h. include-ссылка на данный заголовок есть в Game.h, а Game.h, в свою очередь, подключен оператором include в начале листинга Game.cpp.
В начале листинга определены словесные (=символьные) переменные для координат камеры (CAMERA_X, CAMERA_Y, CAMERA_Z). Они активно юзаются здесь же в функции GameInit и других функциях по работе с камерой. Кастомная (пользовательская) структура MODEL хранит в себе данные о сетке модели, материалах и текстурах. Авторская функция LoadModel является функцией-обёрткой DirectX-функции D3DXLoadMeshFromX, загружающей меш из .X-файла. Здесь же с помощью функции D3DXCreateTextureFromFile на основе данных из .X-файла создаётся текстура модели. В авторской функции-обёртке DrawModel размещены функции отрисовки модели и в частности вызов DirectX-функции DrawSubset.
В авторской функции-обёртке GameInit:
  • Инициализируется Direct3D (путём вызова такой же авторской функции-обёртки d3d9Init).
  • Устанавливается камера (SetCamera) + её перспектива (= направление взора, фокусное расстояние; SetPerspective).
  • Включается фоновое освещение сцены и Z-буферизация.
  • Загружается модель из файла car.x (путём вызова авторской функции-обёртки LoadModel).
В авторской функции-обёртке GameRun:
  • Очищаем сцену объекта устройства Direct3D (ClearScene).
  • Рендерим содержимое бэкбуфера между вызовами функций BeginScene и EndScene.
  • Выводим содержимое бэкбуфер на экран (Present).
Здесь же прописан выход из приложения по нажатию Esc на клавиатуре.
В авторской функции-обёртке GameEnd:
  • Удаляем объект MODEL.

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

Весь код написан. Но перед компиляцией Проект сперва надо грамотно настроить.

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

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

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

Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект DX9LoadMesh01.
В Обозревателе решений видим: "Решение "DX9LoadMesh01"", а строкой ниже жирным шрифтом название Проекта (тоже DX9LoadMesh01).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта DX9LoadMesh01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все)

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

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


Закрыть
warningОшибка LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt

В MS Visual C++ 2010 даже компиляция консольных приложений нередко завершается неудачей, а вместо исполняемого файла программист видит сообщение:
LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt.

ПРИЧИНА: MS Visual С++ 2010 "не нравится" версия .NET Framework(external link), установленная в операционной системе. В общих чертах, MS Visual С++ 2010 спрограммирована для работы под управлением .NET Framework 4.0 и сильно к нему привязана. Если точнее, к нему сильно привязана система инкрементной компоновки приложений (incremental linking), которая по умолчанию включена для всех создаваемых проектов.
Во время установки MS Visual C++ 2010 пытается установить свой "родной" .NET Framework 4.0, проверяя версию этой программной платформы, установленную в ОС на данный момент. Если версия .NET Framework ниже 4.0, то она обновляется до 4.0 и всё прекрасно компилируется. Если версия .NET Framework выше 4.0, то всё оставляется как есть: IDE успешно завершает установку, но при компиляции ВСЕХ приложений выскакивает данная ошибка. Более того, ошибка была замечена даже при наличии в системе .NET Framework версии 4.0, но отличающейся от "родной" припиской вроде "Beta" или "Release Candidate".

ВАРИАНТЫ РЕШЕНИЙ:
1. Отключить инкрементную линковку в опциях Проекта.
В главном меню MSVC++2010 выбираем: Проект->Свойства->Свойства конфигурации->Компоновщик(Linker)->Включить инкрементное построение (Incremental Linking). Данный пункт по умолчанию включен для всех новых проектов. Выставляем его в Нет (No). И жмём OK. Инкрементное построение заметно сокращает время компилирования больших проектов. Но в нашем случае его отсутствие некритично.
2. Удалить (переместить в другое место) утилиту cvtres.exe из каталога bin установленной MS Visual C++ 2010.
В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe . Официальное название данного приложения - Microsoft® Resource File To COFF Object Conversion Utility (утилита конвертации файлов двоичных ресурсов в Component Object File Format). Опытным путём установлено, что при линковке IDE "спотыкается" именно об него.
3. Удалить из системы все версии .NET Framework (включая языковые пакеты и всякие профайлеры, если есть) и саму MS Visual C++ 2010.
Всё вышеперечисленное можно без труда найти в меню "Программы и компоненты" (MS Windows Vista/7/8). Затем заново установить MS Visual C++ 2010. При этом автоматом установится .NET Framework 4.0, идущий с ней в наборе.

Третий пункт - самый долгий. На деле почти всегда хватает выполнения первых двух. Данным вопросом озадачивались ребята здесь: https://www.cyberforum.ru/cpp-beginners/thread637174.html?ysclid=l3akpmanrq(external link). После этого линковка (=компоновка) проходит идеально. Подобные "костыли" в IDE от Майков - не редкость. Можно предположить, что команда с головой ударилась в тестирование .NET-возможностей MSVC++2010, совсем забыв о Win32-направлении (либо признав его бесперспективным).


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

Удаляем файл cvtres.exe

В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe
В Win32-кодинге он особо ни на что не влияет. Зато без него линковка идёт как по маслу.

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

Да, даже на данном этапе компиляция Проекта выдаст ошибку. Библиотека libci.dll использовалась в VisualStudio когда-то очень давно, и в современных версях IDE её нет. Тем не менее компоновщик почти всегда вызывает её при компиляции, ругаясь на её отсутствие. Самый простой способ это исправить - запретить использовать libci.dll по умолчанию.
ОК, начнём.
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь в данный момент.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Командная строка (Configuration Properties -> Linker -> Command Promt).
В правой части, внизу, видим поле ввода "Дополнительные параметры".
  • Пишем в него строку: /NODEFAULTLIB:libci
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

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

Если попытаться скомпилировать Проект DX9LoadMesh01 в таком виде, то ничего не выйдет.
В dxgraphics.h можно увидеть директиву включения (#include) заголовочных файлов:
Фрагмент dxgraphics.h
...
#include <d3d9.h>
#include <d3dx9.h>
#include <d3dx9math.h>
...

На данном этапе MSVC++2010 ничего не знает об их местоположении. В статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK мы указывали пути к DirectX SDK (версии 9 и выше). Здесь всё делается аналогично. Начнём.
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект DX9LoadMesh01.
  • Убедись, что DirectX SDK 9 установлен на компьютере и ты уверенно можешь назвать полный путь к его каталогу.
В Обозревателе решений видим: "Решение "DX9LoadMesh01"", а строкой ниже жирным шрифтом название Проекта (тоже DX9LoadMesh01).
  • Жмём правой кнопкой мыши по названию Проекта DX9LoadMesh01. Во всплывающем меню выбираем пункт "Свойства".
Image
Или в Главном меню выбираем Проект->Свойства. Или нажимаем Alt+F7.
В появившемся меню свойств проекта выбираем Свойства конфигурации -> Каталоги VC++. В правой части этой страницы расположены пути ко всевозможным каталогам. Здесь нас интересуют только 2 строки: Каталоги включения и Каталоги библиотек.

Указываем каталог включений (include) DirectX SDK 9
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги включения. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
Image
В появившемся меню "Каталоги включения" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к заголовочным файлам DirectX SDK 9 (include). В нашем случае это C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Жмём "ОК".

Указываем каталог библиотек (lib) DirectX SDK 9
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги библиотек. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
  • В появившемся меню "Каталоги библиотек" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к 32-разрядным версиям файлов библиотек DirectX SDK 9 (lib). В нашем случае это C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Lib\x86. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Жмём "ОК".
  • На Странице свойств тоже жмём "ОК".
  • Сохрани Решение (File -> Save All).
Готово.

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

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

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

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

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

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

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

Прописываем библиотеки d3d9.lib, d3dx9.lib и WinMM.LIB в окне "Дополнительные зависимости" (Additional dependencies) компоновщика (Linker)

Библиотеки d3d9.lib и d3dx9.lib расположены в папке с установленным DirectX SDK 9 (в нашем случае по пути C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Lib\x86), пути к которому мы прописали выше.
Библиотека WinMM.lib отвечает за мультимедиа-возможности приложения и расположена в каталоге с установленным Windows SDK (в нашем случае здесь: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib).
Т.к. мы создаём исполняемое приложение (исполняемый .exe-файл), а не библиотеку, то после компиляции полученный объектный модуль сразу линкуется путём вызова компоновщика (=linker). Так вот, этот самый компоновщик по ранее прописанным каталогам данные библиотеки не ищет. Поэтому их необходимо указывать отдельно в окне настроек Проекта, в разделе "Компоновщик". ОК, начинаем.
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Ввод (Configuration Properties -> Linker -> Input).
  • В правой части, напротив строки "Дополнительные зависимости" жмём кнопку с чёрным треугольником.
  • В всплывающем списке жмём "Изменить".
  • В появившемся окне "Дополнительные зависимости" в верхнем поле ввода прописываем в столбик (один под другим) имена файлов трёх библиотек:
d3d9.lib
d3dx9.lib
WinMM.lib
Image
Библиотека Winmm.lib отвечает за мультимедиа-возможности приложения и расположена в каталоге с установленным Windows SDK.
  • Жмём ОК, ОК.
  • Сохрани Решение (Файл->Сохранить все).

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

Наконец, наш тестовый Проект готов к компиляции.
  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 на клавиатуре.
Image
Если весь код был введён без ошибок, после компиляции запустится приложение, отображающее окно с белым фоном, которое через секунду закрывается.
Скомпилированное .exe-приложение в нашем случае (Win7 x64) расположено по пути C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\DX9LoadMesh01\Debug .
Закрыть
noteОбрати внимание

Если попытаться запустить скомпилированный .exe-файл на компьютере без установленной MSVC++2010, то экзешник (скорее всего) "ругнётся" на отсутствующую библиотеку MSVCR100D.dll. Буква D в её имени означает Debug, т.е. данное приложение скомпилировано с отладочной конфигурацией (профилем), которая выставляется по умолчанию у каждого вновь создаваемого Проекта/Решения. При релизе игры приложение напротив, компилируют с конфигурацией Release (релиз). При этом при создании инсталлятора в дистрибутив с игрой добавляют т.н. набор библиотек времени выполнения (в нашем слчае MS Visual C++ 2010 Runtime Redistributable). Он устанавливается вместе с игрой, т.к. без него игра тупо не стартанёт. Если для Release-профиля такой набор нетрудно нагуглить (например здесь: https://www.mydigitallife.net/visual-c-2010-runtime-redistributable-package-x86-x64-ia64-free-download(external link), 4,8 Мб для 64-разрядной версии ОС), то для запуска Debug-версии на отдельном компе на него потребуется установить целую MSVC++2010. Всё так сложно в том числе с целью не допустить утечек предрелизных разработок с компов игрокодерских компаний. Релиз есть релиз. А с дебаг-версиями, как правило, работают только сами игрокодеры, на своих компах отлавливая ошибки.
Конфигурации Debug и Release легко переключаются на странице свойств открытого Проекта (в MSVC++2010 в главном меню выбираем Проект->Свойства, в верхней части диалога видим всплывающий список "Конфигурация").

Программа ругнётся на отсутствующий .X-файл.

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

В Game.cpp есть такой код:
Game.cpp
...
	car = LoadModel("car.x");
	if(car == NULL)
	{
		MessageBox(hWnd, "Ошибка загрузки .X-файла", "Error", MB_OK);
		return 0;
	}
...

Здесь загружается модель из .X-файла car.x. По умолчанию программа ищет данный файл в каталоге со своим исполняемым файлом.
Организуем .X-файл c 3D-моделью. Вот лишь несколько способов:
  • Найти в Интернете.
Это сложно, т.к. по запросу .X-файлы моделей ищется всё, что угодно, только не DirectX-файлы моделей. Куда проще найти файлы моделей формата .3ds (3D Studio Max) и грамотно сконвертировать их в .X-формат. По этой теме на Игрокодере есть хорошая статья Anim8or. Моделинг и экспорт в .x формат, в которой интересен только последний абзац "Конвертируем Car01.3ds в Car01.x с изменением масштаба".
  • Создать модель в любом 3D-редакторе, грамотно экспортировав его в файл формата .x .
Создание моделей в 3D-редакторе - сложная задача. Опять же, читай статью на Игрокодере Anim8or. Моделируем автомобиль и экспортируем в .X-файл. Или найди уроки по 3DS Max в Интернете. Напомним, вся соль заключается в грамотном конвертировании модели в формат .X. Под 3DS Max в Интернете есть несколько бесплатных плагинов-конвертеров. Установка, настройка и работа с одним из них (Panda X-converter) подробно описана в статье на Игрокодере 3D Studio Max 7: Установка и настройка экспорта .X-файлов.
  • Взять из примеров DirectX SDK.
Наверное самый простой способ. В недрах DX SDK 9 по пути по умолчанию c:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Direct3D\Tutorials\Tut06_Meshes\ расположен файл модели tiger.x.
Когда получишь заветный .X-файл любым из способов:
  • Помести его в каталог с исполняемым файлом нашего приложения (в нашем случае путь до него такой: C:\Users\<Имя пользователя >\Documents\Visual Studio 2010\Projects\DX9LoadMesh01\Debug).
  • Переименуй его в car.x .
  • Снова запусти приложение DX9LoadMesh01.exe .
На экране появится окно с запросом на переход в полноэкранный режим. При выборе любого варианта в окне появится вращающаяся 3D-модель из .X-файла.
Закрыть
noteОбрати внимание

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


Ссылка на исходные коды примера

Исходные коды примера (ZIP-архив с проектом для MSVC++2010) забираем здесь: https://disk.yandex.ru/d/TLBw0tXTkvCnNg(external link)

Заключение

Значение этой статьи для любого начинающего игрокодера трудно переоценить. 3D-графика - самый сложный элемент DirectX. Кроме того, в исходном коде примера мы разнесли код по авторским функциям-обёрткам с "говорящими" названиями. В WinMain.cpp прописаны вызовы основных трёх функций главного игрового цикла (GameInitm GameRun, GameEnd). Исходный код данного примера можно взять за основу при создании фреймворка игрового приложения/движка, добавив в него требуемый функционал. Конечно, данный подход не является единственно верным, но помогает сориентироваться в бездне различных авторских подходов, являя собой пример грамотно организованного каркаса будущей игры.

Источники


1. Thorn A. DirectX 9 graphics: the definitive guide to Direct3D. - Wordware Publishing, 2005
2. Harbour J.S. Beginning Game Programming. - Thomson Course Technology, 2005
3. Фленов М.Е. Искусство программирования игр на C++ . - СПб.: БХВ-Петербург, 2006


Последние изменения страницы Четверг 09 / Июнь, 2022 01:42:11 MSK

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

No records to display

Search Wiki Page

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

Категории

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