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

Сегодняшняя программа основана на заготовке, которую мы создали в прошлом выпуске. Она (программа) очень проста (в плане функциональности). Всё что она делает - выводит на экран треугольник чёрного цвета.

Треугольник задаётся тремя точками. В Direct3D эти точки называются вершинами (vertex, множественное число - vertices). Большинство объектов с которыми мы будем работать состоят из треугольников, которые в свою очередь состоят из вершин.

Для хранения вершин нам понадобится следующая структура:

struct vertex
{
  float x,y,z;
  unsigned long color;
}

Добавьте этот код перед определением WinMain.

Вершина состоит из четырёх полей: три координаты и цвет.

Вершинные буферы (vertex buffers) в Direct3D

Для вывода графики на экран в Direct3D используются вершинные буферы.

Работают они следующим образом. Допустим, мы создали какой-нибудь сложный объект (10000 вершин?). Все эти вершины помещаются в одно место в памяти - вершинный буфер. Соответсвенно, так как вся геометрия объекта хранится в одном участке памяти, то доступ к каждой вершине довольно быстрый. Интерфейс вершинного буфера - IDirect3DVertexBuffer9:

IDirect3DVertexBuffer9* vb = NULL;
Эту строчку нужно добавить к объявлениям остальных интерфейсов Direct3D.

Почти весь нижеследующий код расположен сразу после вызова метода IDirect3D9::CreateDevice.

Создание вершинных буферов

Для создания вершинного буфера (получения указателя) используется метод

IDirect3DDevice9::CreateVertexBuffer (create - создавать):

HRESULT CreateVertexBuffer(
  UINT Length,
  DWORD Usage,
  DWORD FVF,
  D3DPOOL Pool,
  IDirect3DVertexBuffer9** ppVertexBuffer,
  HANDLE* pSharedHandle
);

Length - длина (размер):
Размер вершинного буфера задаётся в байтах. Давайте посчитаем количество байт для хранения треугольника с вершинами типа vertex: x - 4 байта, y - 4 байта, z - 4 байта, color - 4 байта. length = (x+y+z+color)*3 = 48 байт.

Usage - использование:
Данный аргумент позволяет более эффективно использовать вершинный буфер. Пока что мы будем передавать константу D3DUSAGE_WRITEONLY. Эта константа говорит, что мы можем только писать данные в буфер. Т.е. если мы поместили вершины в буфер, то уже не сможем посмотреть их значения.

FVF - Flexible Vertex Format (гибкий формат вершин):
Данный аргумент определяет формат вершин. Подробности ниже. Мы будем использовать вершины с тремя координатами (x,y,z) и цветом (color). Этому соответствует логическое поразрядное ИЛИ двух констант - D3DFVF_XYZ|D3DFVF_DIFFUSE.

Pool:
Параметр определяет класс памяти в которой будет размещён буфер (например, в RAM или в AGP). Мы позмолим DirectX самому выбрать класс памяти за нас. Для этого будем использовать константу D3DPOOL_DEFAULT.

ppVertexBuffer:
Указатель на интерфейс вершинного буфера. В этот аргумент передаём адрес интерфейса IDirect3DVertexBuffer9.

pSharedHandle:
Данный параметр используется только с Vista. Мы будем всегда передавать NULL.

Ну и собственно, вызов метода:

dev->CreateVertexBuffer( 3* sizeof(vertex), D3DUSAGE_WRITEONLY,
                         D3DFVF_XYZ|D3DFVF_DIFFUSE, D3DPOOL_DEFAULT,
                         &vb,NULL);

Render states - состояния рендеринга

Программист может контролировать каким образом Direct3D выводит графику на экран посредством состояний рендеринга. Для этого используется метод IDirect3DDevice9::SetRenderState:

HRESULT SetRenderState(
  D3DRENDERSTATETYPE State,
  DWORD Value
);

State - состояние: Одна из переменных состояния устройства, которая будет изменена.

Value - значение:
Новое значение переменной, хранящей состояние устройства.

Добавьте в код следующую строку:

dev->SetRenderState(D3DRS_LIGHTING, false);
Мы устанавливаем состояние для переменной D3DRS_LIGHTNING, которая включает освещение Direct3D. Мы устанавливаем эту переменную в false, чтобы можно было использовать цвет вершин (определённый нами тип vertex).

Попробуйте вместо (или вместе) вышеприведённого состояния задать следующее:

dev->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
Или

dev->SetRenderState(D3DRS_FILLMODE,D3DFILL_POINT);
В последнем случае, вам придётся очень хорошо приглядеться, чтобы что-нибудь увидеть (ну или приблизить изображение к камере).

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

Теперь нам нужно создать массив вершин:

vertex vertices[] =
{
  { -2, -1, 0, 0xff000000},
  {  0,  2, 0, 0xff000000},
  {  2, -1, 0, 0xff000000},
};

Цвет задаётся в шестнадцатеричном формате: 3-4-ая цифры - красный канал, 5,6-ая - зелёный, 7-8-ая - синий. Минимальное значение - 00, максимальное - ff (255). Попробуйте различные значения.

Матрицы преобразования координатных пространств

В Direct3D для матриц есть своя структура. Посмотрите на её определение (сравните с нашим классом vector3):

typedef struct _D3DMATRIX {
  union {
    struct {
      float _11, _12, _13, _14;
      float _21, _22, _23, _24;
      float _31, _32, _33, _34;
      float _41, _42, _43, _44;
    };
    float m[4][4];
  };
} D3DMATRIX;

Нам понадобится три переменные типа D3DMATRIX для: матрицы преобразования координат из объектного пространства в мировое, матрицы преобразования координат из мирового пространство в пространство камеры и проекционной матрицы:

D3DMATRIX matWorld;
D3DMATRIX matCam;
D3DMATRIX matProj;

Заполним эти матрицы:

matWorld._11=1;   matWorld._12=0;  matWorld._13=0;  matWorld._14=0;
matWorld._21=0;   matWorld._22=1;  matWorld._23=0;  matWorld._24=0;
matWorld._31=0;   matWorld._32=0;  matWorld._33=1;  matWorld._34=0;
matWorld._41=0;   matWorld._42=0;  matWorld._43=0;  matWorld._44=1;

matCam._11=1; matCam._12=0; matCam._13=0;  matCam._14=0;
matCam._21=0; matCam._22=1; matCam._23=0;  matCam._24=0;
matCam._31=0; matCam._32=0; matCam._33=1;  matCam._34=0;
matCam._41=0; matCam._42=0; matCam._43=10; matCam._44=1;

matProj._11=1; matProj._12=0; matProj._13=0;  matProj._14=0;
matProj._21=0; matProj._22=1; matProj._23=0;  matProj._24=0;
matProj._31=0; matProj._32=0; matProj._33=1;  matProj._34=1;
matProj._41=0; matProj._42=0; matProj._43=-1; matProj._44=0;

Мы ещё не рассматривали матрицу преобразования перспективной проекции - matProj. Эта матрица предназначена для перехода из трёхмерного пространства (пространства камеры) в двухмерное (фоновый буфер и далее экран пользователя). Подробно перспективная проекция будет рассмотрена в одном из следующих уроков раздела Математика.

Как вы могли заметить, матрицы очень просты. Единственное, я отодвинул координатное пространство камеры на 10 единиц в отрицательном направлении по оси z - элемент matCam._43.

И ещё одно, в DirectX пространство, которое мы называли пространством камеры, называется пространство вида или пространством точки обзора (view space).

Как вы знаете, дальше необходимо все точки объекта (в нашем случае треугольника) умножить сначала на матрицу matWorld, затем на матрицу matCam и в последнюю очередь на матрицу matProj. Но делать мы этого не будем, а доверим эту ответственную задачу Direct3D (хотя можем и проделать все вычисления самостоятельно - никто не запрещает). Но сначала Direct3D должен узнать, какие матрицы ему использовать. Для этого предназначен метод IDirect3DDevice9::SetTransform:

HRESULT SetTransform(
  D3DTRANSFORMSTATETYPE State,
  CONST D3DMATRIX * pMatrix
);

State (состояние) - тип матрицы:
Аргумент указывает какая матрица задаётся. Значения могут быть следующими: D3DTS_WORLD (матрица преобразования в мировое координатное пространство), D3DTS_VIEW (матрица преобразования в пространство камеры), D3DTS_PROJECTION (матрица проекции из пространства камеры на плоскость).

pMatrix:
Указатель на матрицу

Теперь можно указать матрицы преобразования:

dev->SetTransform( D3DTS_WORLD, &matWorld);
dev->SetTransform( D3DTS_VIEW, &matCam);
dev->SetTransform( D3DTS_PROJECTION, &matProj );

Заполнение вершинных буферов

Объявите переменную, в которой будет храниться адрес на участок памяти с вершинами:

void* vb_vertices;
Буферы вершин заполняются следующим образом:

1. Замыкание буфера, чтобы никто не получил доступ к буферу, пока идёт его заполнение.

HRESULT Lock(
  UINT OffsetToLock,
  UINT SizeToLock,
  VOID ** ppbData,
  DWORD Flags
);

OffsetToLock (offset - смещение, lock - замыкание, закрытие):
Задаётся в байтах и указывает с какого байта нужно замыкать буфер.

SizeToLock (size - размер):
Задаётся в байтах и указывает - сколько байт в буфере будет замкнуто. Чтобы замкнуть весь буфер, в аргументы OffsetToLock и SizeToLock нужно передать 0.

ppbData:
Указатель (void*) на участок памяти в котором будут храниться вершины.

Flags:
Флаги замыкания. Пока будем передавать 0.

2. Копирование данных в буфер. Здесь нужно заполнить вершинный буфер данными.

3. Размыкание буфера, чтобы его могли использовавть другие интерфейсы.

Теперь код:

vb->Lock(0,sizeof(vertices),(void**)&vb_vertices,0);
memcpy(vb_vertices,vertices,sizeof(vertices));
vb->Unlock();

IDirect3DDevice9::SetStreamSource

Следущий шаг - привязать вершинные буферы к потокам данных устройств.

HRESULT SetStreamSource(
  UINT StreamNumber,
  IDirect3DVertexBuffer9 * pStreamData,
  UINT OffsetInBytes,
  UINT Stride
);

StreamNumber:
Номер потока.

pStreamData:
Указатель на интерфейс вершинного буфера, который будет привязан к потоку.

OffsetInBytes:
Смещение от начала потока к началу вершинных данных. Задаётся в байтах.

Stride:
Данный параметр должен быть равным размеру вершины.

FVF (flexible vertex formats)

FVF - аббревиатура от Flexible Vertex Format (гибкий формат вершин). В Direct3D используется довольно большое количество различных форматов вершин. FVF позволяет довольно удобно из различных констант "сконструировать" свой формат.

Мы уже задавали FVF при создании вершинного буфера. Теперь нужно указать формат вершин и для устройства.

У наших вершин два компонента: координаты и цвет. В FVF эти вершины могут выглядеть вот так: D3DFVF_XYZ|D3DFVF_DIFFUSE.

dev->SetStreamSource(0,vb,0,sizeof(vertex));
dev->SetFVF(D3DFVF_XYZ|D3DFVF_DIFFUSE);

Основной цикл

Напоминаю, что весь предыдущий код расположен перед основным циклом. Внутри же цикла мы добавим только одну строчку, которая располагается между вызовами методов IDirect3DDevice9::BeginScene и IDirect3DDevice9::EndScene:

dev->BeginScene();
dev->DrawPrimitive(D3DPT_TRIANGLELIST,0,1);
dev->EndScene();

Метод IDirect3DDevice9::DrawPrimitive рисует геометрические примитивы. В Direct3D три типа примитивов: точки, прямые линии и треугольники. В подавляющем большинстве случаев мы будем рисовать именно треугольники. Прототип этой функции:

HRESULT DrawPrimitive(
  D3DPRIMITIVETYPE PrimitiveType,
  UINT StartVertex,
  UINT PrimitiveCount
);

PrimitiveType:
Тип рисуемых примитивов. Мы будем использовать в основном следующие значения: D3DPT_TRIANGLELIST, D3DPT_TRIANGLEFAN, D3DPT_TRIANGLESTRIP. Все три константы позволяют рисовать треугольники, правда, немножко разными способами.

StartVertex:
StartVertex позволяет указать номер вершины в выводном потоке (stream source), с которой начнётся рисование.

PrimitiveCount:
Из имени данного аргумента понятно его предназначение - количество рисуемых примитивов. Так как в нашей сегодняшней программе нам нужен один треугольник, то мы указываем единицу.

Вот собственно и всё на сегодня.

Полный код сегодняшней программы можно найти в разделе Листинги.

Заключение

Если после данного выпуска, у вас в голове окончательно всё перемешалось, значит вы на верном пути. DirectX - не самая простая штука в изучении, так что всё нормально

На данный момент вы должны хорошо усвоить предназначение матриц преобразования (кроме matProj). Всё остальное: FVF, потоки данных устройств, вершинные буферы - вы поймёте позже.

Упражнения

Упражнения довольно простые, много времени не займут. Настоятельно рекомендую выполнить все шесть.

1. Поменяйте местами вторую и третью вершины треугольника в массиве vertices. Посмотрите что получилось.

2. Постройте куб. Вам понадобятся 12 треугольников (36 вершин). При этом установите переменную состояния рендеринга D3DRS_FILLMODE в D3DFILL_WIREFRAME. Не забудьте изменить последний параметр метода IDirect3DDevice9::DrawPrimitive.

3. Разместите объектное пространство в котором определён куб в координатах (50,0,0). Поверните камеру в соответствующую сторону. sin(90) = 1, cos(90) = 0.

4. Поверните и камеру и объект на 45 градусов вокруг оси y. cos(45) = 0,707. sin(45) = 0,707.

5. Переместите камеру (пространство камеры) в точку (-25,0,-25). Объект (треугольник) поместите в начало мировых координат и разверните в сторону камеры.

6. Переверните треугольник используя matWorld. cos(180) = -1, sin(180) = 0.