netlib.narod.ru< Назад | Оглавление | Далее >

Переходим к рисованию

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

Использование вершин

Direct3D предоставляет вам свободу определять вершины различными способами. Например, если вы используете двухмерную графику, вы можете задавать координаты в двухмерной системе координат экрана (преобразованные координаты).

С другой стороны, если вы используете локальную или мировую систему координат, вы можете задавать координаты в трехмерном пространстве (непреобразованные координаты). А как обстоит дело с цветами и текстурами? Вы можете выбрать включать ли эту информацию в описание ваших вершин.

Как вы можете отслеживать всю эту информацию и гарантировать, что Direct3D знает, что вы делаете? Встречайте настраиваемый формат вершин.

Настраиваемый формат вершин

Настраиваемый формат вершин (flexible vertex format или, для краткости, FVF) используется для конструирования произвольных структур данных вершины, которые будут применяться в вашем приложении. С FVF вы можете выбрать, какую информацию использовать для описания ваших вершин, — такую как трехмерные координаты, двухмерные координаты, цвет и т.д.

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

Приведенный ниже фрагмент кода содержит структуру данных вершины, использующую различные переменные, допустимые для FVF (по крайней мере те, которые я буду использовать в этой книге). Переменные в структуре перечислены именно в том порядке, в котором они должны располагаться в ваших собственных структурах; если вы убираете какую-нибудь из переменных, убедитесь, что порядок остается тем, который показан:

typedef struct {
    FLOAT    x, y, z, rhw; // Двухмерные координаты
    FLOAT    x, y, z;      // Трехмерные координаты
    FLOAT    nx, ny, nz;   // Нормаль
    D3DCOLOR diffuse;      // Рассеиваемый цвет
    FLOAT    u, v;         // Координаты текстуры
} sVertex;

Как видите, единственные конфликтующие переменные — это координаты, в том числе и нормаль. Нормаль (normal) — это набор координат, определяющий направление, который может использоваться только совместно с трехмерными координатами. Вам необходимо выбрать, какой набор координат (двухмерные или трехмерные) оставить, а какой убрать. Если вы используете двухмерные координаты, то не можете включать трехмерные, и наоборот.

Единственным реальным различием между двухмерными и трехмерными координатами является добавление переменной rhw, которая является аналогом однородного W. Говоря человеческим языком, это значение обычно представляет расстояние от точки просмотра до вершины вдоль оси Z. В большинстве случаев вы спокойно можете присваивать переменной rhw значение 1.0.

Также обратите внимание, что структура sVertex использует данные типа FLOAT (числа с плавающей точкой), а что такое D3DCOLOR? D3DCOLOR — это значение типа DWORD, которое применяется в Direct3D для хранения значений цвета. Чтобы создать значение цвета типа D3DCOLOR можно воспользоваться одной из двух функций: D3DCOLOR_RGBA или D3DCOLOR_COLORVALUE:

D3DCOLOR D3DCOLOR_RGBA(Red, Green, Blue, Alpha);
D3DCOLOR D3DCOLOR_COLORVALUE(Red, Green, Blue, Alpha);

Каждая функция (в действительности это макросы) получает четыре параметра, каждый из которых задает величину отдельной цветовой составляющей, включая значение альфа-компоненты (прозрачность). Для макроса D3DCOLOR_RGBA эти значения должны находиться в диапазоне от 0 до 255, а для макроса D3DCOLOR_COLORVALUE — в диапазоне от 0.0 до 1.0 (быть десятичными дробями). Если вы используете сплошные цвета (непрозрачные), для альфа-компоненты всегда задавайте значение 255 (или 1.0).

В качестве примера предположим, что в вашу структуру данных вершины необходимо включить только трехмерные координаты и рассеиваемую составляющую цвета:

typedef struct {
    FLOAT    x, y, z;
    D3DCOLOR diffuse;
} sVertex;

Следующим этапом конструирования вашего FVF является создание дескриптора FVF с использованием комбинации флагов, перечисленных в таблице 2.3.


Таблица 2.3. Флаги дескриптора настраиваемого формата вершин



Флаг Описание

D3DFVF_XYZ Включены трехмерные координаты
D3DFVF_XYZRHW Включены двухмерные координаты
D3DFVF_NORMAL Включена нормаль (вектор)
D3DFVF_DIFFUSE Включена рассеиваемая составляющая цвета
D3DFVF_TEX1 Включен один набор координат текстуры


Чтобы описать дескриптор FVF вы комбинируете соответствующие флаги в определении (подразумевается, что вы используете трехмерные координаты и рассеиваемую составляющую цвета):

#define VertexFVF (D3DFVF_XYZ | D3DFVF_DIFFUSE)

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

Использование буфера вершин

После того, как вы создали вашу структуру данных вершины и дескриптор, вы создаете объект, который содержит массив вершин. Direct3D предоставляет вам для работы два объекта: IDirect3DVertexBuffer9 и IDirect3DIndexBuffer9. В этой книге я буду использовать объект IDirect3DVertexBuffer9, который хранит вершины, используемые для рисования списков треугольников, полос треугольников и вееров треугольников.

При работе со списками треугольников объект IDirect3DVertexBuffer9 хранит как минимум по три вершины для каждого рисуемого полигона (вершины упорядочены по часовой стрелке). У полосы треугольников для рисования первого полигона используется три вершины, а для рисования каждого последующего полигона используется одна дополнительная вершина. Для веера треугольников задается одна центральная вершина, а для рисования каждого полигона хранятся две дополнительные вершины.

ПРИМЕЧАНИЕ
Полигон может использовать одну, две или три вершины, в зависимости от того, что именно вы рисуете. Для точек требуется одна единственная вершина, для линий необходимо две, а для треугольников — три вершины. В этой книге мы в основном будем иметь дело с треугольными полигонами.

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

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


Рис. 2.11. Вы можете хранить простой полигон, такой как квадрат, различными способами

Рис. 2.11. Вы можете хранить простой полигон, такой как квадрат (с четырьмя вершинами и двумя полигонами), различными способами. В зависимости от того, как хранятся вершины, для описания квадрата требуется до шести вершин


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

Вы создаете буфер вершин, используя инициализированный объект IDirect3DDevice9:

HRESULT IDirect3DDevice9::CreateVertexBuffer(
    UINT Length,      // количество вершин, умноженное на
                      // размер структуры данных вершины
    DWORD Usage,      // 0
    DWORD FVF,        // дескриптор FVF
    D3DPOOL Pool,     // D3DPOOL_MANAGED
    IDirect3DVertexBuffer **ppVertexBuffer, // буфер вершин
    HANDLE *pHandle); // укажите NULL

В вызове CreateVertexBuffer есть только один параметр, значение которого вы, возможно, решите изменить — это флаг Usage, который сообщает Direct3D как будет использоваться память, предназначенная для хранения вершин. Вполне безопасно всегда присваивать Usage значение 0, но, если вы хотите несколько увеличить производительность, присвойте Usage значение D3DCREATE_WRITEONLY. Сделав так вы проинформируете Direct3D, что не планируете читать данные вершин из буфера и все должно быть хорошо, если в соответствии с этим выбрать место хранения. Обычно это означает, что данные вершин будут размещены в памяти видеокарты (читай: памяти с более быстрым доступом).

Вот небольшой пример создания буфера вершин, содержащего четыре вершины (построенный на основании созданного ранее в разделе «Настраиваемый формат вершин» формата вершин, который содержит только трехмерные координаты и рассеиваемую составляющую цвета):

// g_pD3DDevice = ранее инициализированный объект устройства
// sVertex      = ранее определенная структура данных вершины
// VertexFVF    = ранее определенный дескриптор FVF 
IDirect3DVertexBuffer9 *pD3DVB = NULL;

// Создание буфера вершин
if(FAILED(g_pD3DDevice->CreateVertexBuffer(sizeof(sVertex) * 4,
                                D3DCREATE_WRITEONLY, VertexFVF,
                                D3DPOOL_MANAGED, &pD3DVB, NULL))) {
    // Произошла ошибка
}

 

ПРИМЕЧАНИЕ
Когда вы создаете собственный буфер вершин, убедитесь, что указали требуемый размер буфера (первый параметр в вызове CreateVertexBuffer). В показанном выше примере выделяется память для хранения четырех вершин типа sVertex. 4 означает четыре экземпляра, а sVertex — это структура, которую вы используете для хранения данных ваших вершин.

 

ПРИМЕЧАНИЕ
Как обычно, убедитесь, что по завершении работы вы освобождаете COM-объект буфера вершин, вызвав его метод Release.

Блокировка буфера вершин

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

HRESULT IDirect3DVertexBuffer9::Lock(
    UINT  OffsetToLock, // смещение до начала блокируемой области, в байтах
    UINT  SizeToLock,   // сколько байт блокировать, 0 = все
    VOID  **ppbData,    // указатель на указатель (для доступа к данным)
    DWORD Flags);       // 0

Вы указываете смещение от начала буфера до той области к которой хотите получить доступ (в байтах), а также размер блокируемой области в байтах (0, если блокируете весь буфер). Затем вам надо предоставить функции указатель на указатель, который вы будете использовать для доступа к буферу вершин (приведенный к типу VOID). Вот пример вызова, который блокирует весь буфер вершин:

// pD3DVB = ранее инициализированный объект буфера вершин
BYTE *Ptr;

// Блокировка пямати буфера вершин и получение указателя на нее
if(FAILED(pD3DVB->Lock(0, 0, (void**)&Ptr, 0))) {
    // Произошла ошибка
}

 

ВНИМАНИЕ!
Буфер вершин при создании которого был указан флаг D3DCREATE_WRITEONLY недоступен для чтения, а доступен только для записи. Если вы планируете читать данные из буфера вершин (как это делают некоторые функции библиотеки D3DX), то в вызове CreateVertexBuffer должны присвоить параметру Usage значение 0.

После того, как вы завершили доступ к буферу вершин для каждого вызова Lock вызывайте IDirect3DVertexBuffer9::Unlock:

HRESULT IDirect3DVertexBuffer9::Unlock();

Разблокировка буфера вершин гарантирует, что Direct3D может безопасно начать его использование, зная, что вы не будете модифицировать никакие содержащиеся в буфере данные.

ВНИМАНИЕ!
Всегда минимизируйте время, проходящее между вызовами Lock и Unlock. Чем быстрее вы завершите работу с заблокированным буфером вершин, тем лучше, поскольку Direct3D ждет, пока вы не закончите свои дела, чтобы получить доступ к содержащимся в буфере данным.

Заполнение данных вершин

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

Продолжая мой пример и используя определенный ранее формат вершин (который содержит трехмерные координаты и рассеиваемую составляющую цвета), я создаю локальный набор данных вершин в массиве:

sVertex Verts[4] = {
    { -100.0f,  100.0f, 100.0f, D3DCOLOR_RGBA(255,255,255,255) },
    {  100.0f,  100.0f, 100.0f, D3DCOLOR_RGBA(255,  0,  0,255) },
    {  100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(  0,255,  0,255) },
    { -100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(  0,  0,255,255) }
};

Заблокируйте буфер вершин, получив тем самым указатель на память буфера вершин, и скопируйте локальные данные вершин (и по завершении разблокируйте буфер):

// pD3DVB = ранее инициализированный объект буфера вершин
BYTE *Ptr;

// Блокируем память буфера вершин и получаем указатель на нее
if(SUCCEEDED(pD3DVB->Lock(0, 0, (void**)&Ptr, 0))) {

    // Копируем локальные вершины в буфер вершин
    memcpy(Ptr, Verts, sizeof(Verts));

    // Разблокируем буфер вершин
    pD3DVB->Unlock();
}

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

Поток вершин

Direct3D позволяет вам передавать вершины визуализатору через несколько каналов, называемых потоки вершин (vertex stream). Объединяя несколько потоков вершин в единый поток вы можете добиться потрясающих результатов, но в данной книге я использую только один поток, поскольку сложности использования нескольких потоков выходят за рамки этой книги.

Чтобы указать, какие данные вершин будут передаваться в поток, вы используете функцию IDirect3DDevice9::SetStreamSource:

HRESULT IDirect3DDevice9::SetStreamSource(
    UINT StreamNumber,  // 0
    IDirect3DVertexBuffer9* pStreamData, // Объект буфера вершин
    UINT OffsetInBytes, // Смещение (в байтах) данных вершин
                        // в буфере вершин.
    UINT Stride);       // Размер структуры данных вершины

Все, что необходимо делать для установки источника потока вершин — вызвать данную функцию, передав ей указатель на объект буфера вершин и указав количество байт, используемых для хранения структуры данных вершины (с помощью sizeof).

Если в буфере вершин вы храните более одной группы полигонов (например, несколько полос треугольников), вы можете в параметре OffsetInBytes указать смещение в байтах до начала требуемой группы вершин. Например, если я использую буфер вершин для хранения двух полос треугольников (первая начинается со смещения 0, а вторая — со смещения 6), я могу нарисовать вторую полосу треугольников, указав соответствующее смещение (в рассматриваемом примере — 6).

Для моего примера работы с буфером вершин, приведенного в предыдущем разделе, вы должны использовать следующий код:

// g_pD3DDevice = ранее инициализированный объект устройства
// pD3DVB       = ранее инициализированный буфер вершин
if(FAILED(g_pD3DDevice->SetStreamSource(0, pD3DVB,
                                        0, sizeof(sVertex)))) {
    // Произошла ошибка
}

Вершинные шейдеры

Для выполнения заключительного этапа в использовании вершин для рисования графики вам необходимо познакомиться с концепцией вершинных шейдеров. Вершинный шейдер (vertex shader) — это механизм, выполняющий загрузку и обработку вершин; в это входит модификация координат вершины, применение цветов и тумана, и обработка прочих компонентов данных вершины.

Существует две формы вершинных шейдеров. Это может быть фиксированный (fixed) вершинный шейдер (в который встроена вся функциональность, необходимая в подавляющем большинстве случаев) или программируемый (programmable) вершинный шейдер (в котором вы пишете собственную процедуру для модификации данных вершины перед визуализацией на экране).

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

Чтобы использовать фиксированный вершинный шейдер для ваших вершин, вам надо передать свой дескриптор FVF вершины в функцию IDirect3DDevice9::SetFVF:

HRESULT IDirect3DDevice9::SetFVF(
    DWORD FVF); // Дескриптор FVF вершины

Использовать показанную функцию очень просто:

// g_pD3DDevice = ранее инициализированный объект устройства
// VertexFVF    = ранее определенный дескрипторx FVF вершины
if(FAILED(g_pD3DDevice->SetFVF(VertexFVF))) {
    // Произошла ошибка
}

Вы указали данные о вершинах. Теперь надо указать различные преобразования, необходимые для размещения вершины (определенной в ее локальном пространстве) в мировой системе координат. Конечно, это необходимо делать только в том случае, если вы используете трехмерные координаты.

Преобразования

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

Мировое преобразование

Вершинам, которые определены в локальном пространстве, требуется преобразование в соответствующие координаты мирового пространства. Например, если вы создаете из вершин куб (в локальном пространстве) и хотите поместить его в выбранное вами место в мире, то вы применяете к нему мировое преобразование (как показано на рис. 2.12).


Рис. 2.12. Куб, созданный в локальном пространстве, необходимо перед визуализацией ориентировать в мировом пространстве

Рис. 2.12. Куб, созданный в локальном пространстве, необходимо перед визуализацией ориентировать в мировом пространстве


Используйте старого знакомого, библиотеку D3DX, которая поможет вам создать матрицу мирового преобразования. Для ориентации объекта вам потребуется сконструировать три матрицы вращения (по одной для каждой оси), матрицу перемещения и матрицу масштабирования.

D3DXMATRIX matWorld;
D3DXMATRIX matRotX, matRotY, matRotZ;
D3DXMATRIX matTrans;
D3DXMATRIX matScale;

// Создаем матрицы вращения
D3DXMatrixRotationX(&matRotX, XAngle);
D3DXMatrixRotationY(&matRotY, YAngle);
D3DXMatrixRotationZ(&matRotZ, ZAngle);

// Создаем матрицу перемещения
D3DXMatrixTranslation(&matTrans, XPos, YPos, ZPos);

// Создаем матрицу масштабирования
D3DXMatrixScaling(&matScale, XScale, YScale, ZScale);

Затем вы комбинируете все эти матрицы в матрицу мирового преобразования. Комбинировать матрицы необходимо в следующем порядке: масштабирование, поворот относительно оси X, поворот относительно оси Y, поворот относительно оси Z, и затем перемещение.

// Делаем матрицу matWorld единичной
D3DXMatrixIdentity(&matWorld);

// Комбинируем все матрицы в матрицу мирового преобразования
D3DXMatrixMultiply(&matWorld, &matWorld, &matScale);
D3DXMatrixMultiply(&matWorld, &matWorld, &matRotX);
D3DXMatrixMultiply(&matWorld, &matWorld, &matRotY);
D3DXMatrixMultiply(&matWorld, &matWorld, &matRotZ);
D3DXMatrixMultiply(&matWorld, &matWorld, &matTrans);

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

HRESULT IDirect3DDevice9::SetTransform(
    D3DTRANSFORMSTATETYPE State,     // D3DTS_WORLD
    CONST D3DMATRIX       *pMatrix); // Устанавливаемая мировая матрица

Обратите внимание, что второй параметр — это указатель на структуру D3DMATRIX, но, к счастью, вы можете здесь использовать объект D3DXMATRIX, который создали. Значение D3DTS_WORLD в первом параметре сообщает Direct3D, что матрица используется для мирового преобразования и все, что будет рисоваться в дальнейшем, должно быть ориентировано с использованием этой матрицы.

Если вам необходимо расположить в мире несколько объектов, просто создайте матрицу мирового преобразования для размещения каждого из них (в требуемой ориентации) и затем снова вызовите SetTransform, убедившись перед переходом к следующему мировому преобразованию, что все требуемые объекты нарисованы.

Преобразование вида

Фактически преобразование вида действует как камера (называемая точка просмотра — viewpoint). Создавая матрицу, которая содержит смещения для ориентации вершин в мировом пространстве, вы выравниваете сцену относительно точки просмотра. Все вершины должны быть ориентированы относительно центра мира (с использованием преобразования вида) точно в те же относительные позиции, в которых они находятся относительно точки просмотра.

Чтобы создать преобразование вида, вы строите матрицу из перемещения и поворота точки просмотра, располагая преобразования в следующем порядке: перемещение, поворот вокруг оси Z, поворот вокруг оси Y и поворот вокруг оси X. Трюк здесь заключается в том, что вы используете для местоположения и поворота противоположные значения. Например, если точка просмотра расположена в координатах X = 10, Y = 0, Z = –150, вы используете значения X = –10, Y = 0, Z = 150.

Вот как выглядит код построения матрицы преобразования вида:

D3DXMATRIX matView;
D3DXMATRIX matRotX, matRotY, matRotZ;
D3DXMATRIX matTrans;

// Создаем матрицы вращения (с противоположными значениями)
D3DXMatrixRotationX(&matRotX, -XAngle);
D3DXMatrixRotationY(&matRotY, -YAngle);
D3DXMatrixRotationZ(&matRotZ, -ZAngle);

// Создаем матрицу перемещения (с противоположными значениями)
D3DXMatrixTranslation(&matTrans, -XPos, -YPos, -ZPos);

// Инициализируем единичную матрицу matView
D3DXMatrixIdentity(&matView);

// Комбинируем все матрицы в матрицу преобразования вида
D3DXMatrixMultiply(&matView, &matView, &matTrans);
D3DXMatrixMultiply(&matView, &matView, &matRotZ);
D3DXMatrixMultiply(&matView, &matView, &matRotY);
D3DXMatrixMultiply(&matView, &matView, &matRotX);

Чтобы Direct3D использовал созданную вами матрицу преобразования вида, снова воспользуйтесь функцией IDirect3DDevice9::SetTransform, но на этот раз в параметре State укажите значение D3DTS_VIEW:

// g_pD3DDevice = ранее инициализированный объект устройства
if(FAILED(g_pD3DDevice->SetTransformat(D3DTS_VIEW, &matView))) {
    // Произошла ошибка
}

Как видите, установить преобразование вида просто; основную проблему представляет конструирование матрицы вида. Чтобы сделать жизнь проще, D3DX предоставляет функцию, которая за один вызов инициализирует матрицу преобразования вида:

D3DXMATRIX* D3DXMatrixLookAtLH(
    D3DXMATRIX        *pOut, // Итоговая матрица преобразования вида
    CONST D3DXVECTOR3 *pEye, // Координаты точки просмотра
    CONST D3DXVECTOR3 *pAt,  // Координаты цели
    CONST D3DXVECTOR3 *pUp); // Верхнее направление

Разобраться с первого взгляда в функции D3DXMatrixLookAtLH не так-то просто. Вы видите обычный указатель для возврата вычисленной матрицы, но что это за три объекта D3DXVECTOR3? D3DXVECTOR3 во многом похож на объект D3DXMATRIX, за исключением того, что содержит лишь три значения (называемые x, y и z) — в данном случае три значения координат. Такой объект D3DXVECTOR3 называется объектом вектора (vector object).

Параметр pEye представляет координаты точки просмотра, а pAt представляет координаты цели, то есть той точки, на которую направлен взгляд. Параметр pUp — это вектор, задающий направление вверх для точки просмотра. Обычно для pUp задаются значения 0, 1, 0 (это означает, что направление вверх совпадает с положительным направлением оси Y), но поскольку точка просмотра может наклоняться (точно так же, как вы наклоняете голову из стороны в сторону), направление вверх может указывать в любом направлении вдоль любой из осей.

Чтобы применить функцию D3DXMatrixLookAtLH вы можете воспользоваться следующим фрагментом кода (подразумевается, что точка просмотра имеет координаты XPos, YPos, ZPos, а взгляд направлен на начало координат):

D3DXMATRIX matView;
D3DXVECTOR3 vecVP, vecTP, vecUp(0.0f, 1.0f, 0.0f);
vecVP.x = XPos;
vecVP.y = YPos;
vecVP.z = ZPos;
vecTP.x = vecTP.y = vecTP.z = 0.0f;
D3DXMatrixLookAtLH(&matView, &vecVP, &vecTP, &vecUp);

Преобразование проекции

Последним выполняется преобразование проекции, преобразующее трехмерные вершины (непреобразованные) в двухмерные координаты (преобразованные), которые Direct3D использует для рисования вашей графики на экране. Думайте о преобразовании проекции, как о способе расплющивания трехмерной графики на экране вашего дисплея (как показано на рис. 2.13).


Рис. 2.13. Преобразование проекции позволяет видеть объекты, определенные с использованием трехмерных координат, на плоском двухмерном экране

Рис. 2.13. Преобразование проекции позволяет видеть объекты, определенные с использованием трехмерных координат, на плоском двухмерном экране


Имея дело с преобразованием проекции следует учесть множество аспектов, таких как соотношение размеров порта просмотра, поле зрения, а также ближнюю и дальнюю плоскости отсечения.

Что за отсечение? При рисовании трехмерной графики некоторые объекты могут оказаться расположены слишком близко к точке просмотра или слишком далеко от нее. Вы указываете Direct3D, когда можно отбросить эти объекты (для увеличения скорости). Чтобы сконструировать матрицу проекции и определить область пространства, в которой объекты видимы и не могут быть отсечены, мы будем использовать функцию D3DXMatrixPerspectiveFovLH:

D3DXMATRIX* D3DXMatrixPerspectiveFovLH(
    D3DXMATRIX *pOut,  // Итоговая матрица
    FLOAT      fovy,   // Угол поля зрения в радианах
    FLOAT      Aspect, // Соотношение размеров
    FLOAT      zn,     // Координата Z ближней плоскости отсечения
    FLOAT      zf);    // Координата Z дальней плоскости отсечения

 

ПРИМЕЧАНИЕ
Функция D3DXMatrixPerspectiveFovLH создает матрицу перспективной проекции для левосторонней системы координат. Если вы используете правостороннюю систему координат, воспользуйтесь функцией D3DXMatrixPerspectiveFovRH (у нее те же параметры, что и у версии для левосторонней системы).

Параметр fovy указывает ширину проектируемого вида, поэтому чем большее число вы укажете, тем больше будете видеть. Но это обоюдоострый меч, поскольку если вы используете слишком маленькие или слишком большие значения, представление становится искаженным. Обычным значением для fovy является D3DX_PI/4, то есть одна четвертая пи.

Следующий важный параметр — Aspect, который является соотношением размеров области просмотра. Если у вас есть окно размером 400 × 400 пикселов, соотношение размеров будет 1 : 1 или 1.0 (потому что это квадрат). Если у вас окно размером 400 × 200 точек (высота в два раза меньше ширины) соотношение размеров будет 2 : 1 или 2.0. Чтобы вычислить это значение разделите ширину окна на его высоту:

FLOAT Aspect = (FLOAT)WindowWidth / (FLOAT)WindowHeight;

Параметры zn и zf — это координаты местоположения ближней и дальней плоскостей отсечения, измеренные в тех же единицах, которые используются для описания местоположения вершин в трехмерном пространстве. Обычно для ближней и дальней плоскостей отсечения задаются значения 1.0 и 1000.0 соответственно. Эти два значения означают, что полигоны, расположенные менее чем на 1.0 единицу (или далее, чем на 1000.0 единиц) относительно точки просмотра не будут рисоваться. Вы можете указать для параметра zf большее значение, если в вашем проекте необходимо рисовать обьекты, удаленные более чем на 1000 единиц от камеры.

После создания матрицы проекции вы устанавливаете ее с помощью функции IDirect3DDevice9::SetTransform, указывая в параметре State значение D3DTS_PROJECTION:

// g_pD3DDevice = ранее инициализированный объект устройства
D3DXMATRIX matProj;

// Создаем матрицу преобразования проекции
D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 1000.0f);

// Устанавливаем матрицу проекции для Direct3D
if(FAILED(g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj))) {
    // Произошла ошибка
}

Материалы и цвета

Вы уже видели, как можно задать цвета в данных вершин, но на уровне полигонов можно также задавать специальные параметры цвета. Цвета, применяемые для граней объекта, называются материалами (materials). Перед тем, как рисовать полигон с помощью Direct3D, вы можете назначить материал, который будет использоваться (если вы решите не использовать материалы, можно использовать цвета вершин, если они заданы).

У каждого материала есть набор описывающих его цветовых компонентов. В Direct3D описывающие материал цветовые составляющие хранятся в структуре:

typedef struct _D3DMATERIAL9 {
    D3DCOLORVALUE Diffuse;  // Рассеиваемая составляющая цвета
    D3DCOLORVALUE Ambient;  // Фоновая составляющая цвета
    D3DCOLORVALUE Specular; // Отражаемая составляющая цвета
    D3DCOLORVALUE Emissive; // Испускаемая составляющая цвета
    float         Power;    // Контрастность бликов
} D3DMATERIAL9;

В реальной жизни вы в большинстве случаев будете иметь дело только с одной составляющей: Diffuse. Значение составляющей Ambient обычно то же самое, что у Diffuse, а для Specular вы можете задать значения 0.0 или 1.0 (установив значение Power равным 0.0). Я предлагаю вам немного поэкспериментировать со значениями, чтобы понять какой эффект производит каждый из компонентов.

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

Имея дело с цветовыми составляющими материала, вы задаете значение каждого компонента непосредственно, а не используете макросы наподобие D3DCOLOR_RGBA. Не беспокойтесь — каждая компонента цвета представляется первой буквой ее английского названия (r для красного, g для зеленого, b для синего и a для альфа) и диапазон значений тот же (от 0.0 до 1.0). Если вы хотите создать материал желтого цвета, то инициализация структуры данных материала должна выглядеть так:

D3DMATERIAL9 d3dm;

// Очищаем структуру данных материала
ZeroMemory(&d3dm, sizeof(D3DMATERIAL9));

// Задаем для компонентов Diffuse и Ambient желтый цвет
d3dm.Diffuse.r = d3dm.Ambient.r = 1.0f; // красный
d3dm.Diffuse.g = d3dm.Ambient.g = 1.0f; // зеленый
d3dm.Diffuse.b = d3dm.Ambient.b = 0.0f; // синий
d3dm.Diffuse.a = d3dm.Ambient.a = 1.0f; // альфа

Инициализировать структуру материала вы можете так, как хотите, но, после того как структура инициализирована, необходимо до визуализации полигонов сказать Direct3D, чтобы он использовал ее. Эту задачу выполняет функция IDirect3DDevice9::SetMaterial, которая в единственном параметре получает указатель на структуру данных вашего материала:

IDirect3DDevice9::SetMaterial(CONST D3DMATERIAL9 *pMaterial);

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

g_pD3DDevice->SetMaterial(&d3dm);

Очистка порта просмотра

Вам необходимо очистить вторичный буфер для подготовки его к рисованию, стерев графику, которая может быть в нем. Эта задача выполняется функцией IDirect3DDevice::Clear:

HRESULT IDirect3DDevice9::Clear(
    DWORD         Count,    // 0
    CONST D3DRECT *pRects,  // NULL
    DWORD         Flags,    // D3DCLEAR_TARGET
    D3DCOLOR      Color,    // Цвет для очистки
    float         Z,        // 1.0f
    DWORD         Stencil); // 0

Единственный параметр, которому надо уделить внимание сейчас, это Color, определяющий цвет, которым будет заполнен вторичный буфер. Значение цвета может формироваться с помощью обычных макросов D3DCOLOR_RGBA или D3DCOLOR_COLORVALUE, с которыми вы уже познакомились. Предположим, вы хотите очистить вторичный буфер, заполнив его светло-синим цветом:

// g_pD3DDevice = ранее инициализированный объект устройства
if(FAILED(g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET,
                    D3DCOLOR_RGBA(0,0,192,255), 1.0f, 0))) {
    // Произошла ошибка
}

 

ПРИМЕЧАНИЕ
Флаг D3DCLEAT_TARGET используется чтобы сообщить Direct3D, что именно вы хотите очистить (экран или окно). Есть несколько доступных для использования полезных флагов и с некоторыми из них вы вскоре познакомитесь.

Начало и завершение сцены

Перед тем, как вы будете что-либо визуализировать, необходимо сообщить Direct3D о том, что надо подготовиться к визуализации. Для этой цели используется функция IDirect3DDevice::BeginScene (у которой нет параметров):

HRESULT IDirect3DDevice9::BeginScene();

Когда вы завершаете визуализацию сцены, вам необходимо уведомить об этом Direct3D, воспользовавшись функцией EndScene:

HRESULT IDirect3DDevice9::EndScene();

Вы не должны помещать вызов функции Clear между BeginScene и EndScene; эта функция должна вызываться до BeginScene. Между вызовами функций начала и завершения сцены можно помещать только вызовы функций, рисующих полигоны.

Визуализация полигонов

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

Вы рисуете объект IDirect3DVertexBuffer9 с помощью следующей функции (конечно, после того как вы установили источник данных вершин и задали их формат):

HRESULT IDirect3DDevice9::DrawPrimitive(
    D3DPRIMITIVETYPE PrimitiveType,   // Рисуемые примитивы
    UINT             StartVertex,     // Начальная вершина (0)
    UINT             PrimitiveCount); // Количество рисуемых примитивов

Первый параметр, PrimitiveType, сообщает Direct3D какие именно фигуры надо рисовать (список некоторых из них приведен в таблице 2.4). Параметр StartVertex позволяет указать, с какой именно вершины следует начать рисование (обычно указывается 0). В параметре PrimitiveCount вы указываете сколько именно примитивов (полигонов) следует нарисовать.


Таблица 2.4. Типы примитивов в параметре DrawPrimitive



Тип Описание

D3DPT_POINTLIST Рисуем все вершины как пикселы
D3DPT_LINELIST Рисуем набор изолированных линий, каждая из которых использует две вершины
D3DPT_LINESTRIP Рисуем набор линий, каждая из которых (кроме первой) связана с предыдущей
D3DPT_TRIANGLELIST Рисуем отдельные полигоны, используя три вершины для каждого из них
D3DPT_TRIANGLESTRIP Рисуем полосу треугольников, используя для первого из них три вершины, а для каждого последующего — две вершины из предыдущего треугольника и одну дополнительную
D3DPT_TRIANGLEFAN Рисуем веер из полигонов, используя первую вершину в качестве центра (все остальные полигоны присоединены к ней)


Используемый тип примитива зависит от того, как вы заполняли буфер данными вершин. Если вы использовали по три вершины для каждого полигона, укажите тип D3DPT_TRIANGLELIST. Если вы применяете более эффективный способ, например, полосу треугольников, укажите тип D3DPT_TRIANGLESTRIP.

Следует помнить только об одной вещи — перед тем, как визуализировать полигоны, вы должны начать сцену с помощью функции IDirecr3DDevice9::BeginScene; иначе вызов функции DrawPrimitive приведет к ошибке.

Предположим, вы создали буфер вершин, содержащий шесть вершин, образующих два треугольных полигона, которые формируют квадрат. Их визуализация (вместе с вызовами функций BeginScene и EndScene, а также установкой источника данных вершин и формата данных вершин) будет выглядеть примерно так:

// g_pD3DDevice = ранее инициализированный объект устройства
// pD3DVB       = ранее инициализированный буфер вершин
// sVertex      = ранее созданная структура данных вершины
// VertexFVF    = ранее объявленный дескриптор FVF

// Устанавливаем источник потока данных и шейдер
g_pD3DDevice->SetStreamSource(0, pD3DVB, 0, sizeof(sVertex));
g_pD3DDevice->SetFVF(VertexFVF);

if(SUCCEEDED(g_pD3DDevice->BeginScene())) {

    // Визуализируем полигоны
    if(FAILED(g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2))) {
        // Произошла ошибка
    }

    // Завершаем сцену
    g_pD3DDevice->EndScene();
}

Показ сцены

Наконец-то вы готовы переключить вторичный буфер и экранную поверхность, чтобы показать пользователю созданную вами графику с помощью следующей функции:

HRESULT IDirect3DDevice9::Present(
    CONST RECT    *pSourceRect,
    CONST RECT    *pDestRect,
    HWND          hDestWindowOverride,
    CONST RGNDATA *pDirtyRegion);

Абсолютно спокойно можете указывать для вех параметров функции IDirect3DDevice9::Present значения NULL, что сообщит Direct3D о необходимости обновить весь экран (поскольку функция может обновлять и отдельные фрагменты экрана), как показано в следующем фрагменте кода:

// g_pD3DDevice = ранее инициализированный объект устройства
if(FAILED(g_pD3DDevice->Present(NULL, NULL, NULL, NULL))) {
    // Произошла ошибка
}

Вот и все! Чтобы создавать более реалистичные сцены вы используете несколько различных буферов вершин для рисования различных трехмерных объектов в вашем мире. Другим способом увеличения реализма вашей графики является использование наложения текстур.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz