DirectX 8 Graphics. Создание и рендеринг объектов
Здесь рассмотрим, как именно Direct3D рисует графику, изучим различные способы применения вершин для рисования полигонов.1 Начнём с вершин.
Для компиляции примеров на понадобится:
- MS Visual C++ 2010 Express Edition,
- Microsoft DirectX SDK 8.
Содержание
- DirectX 8 Graphics. Создание и рендеринг объектов
- Применение вершин (Using vertices)
- Гибкий формат вершин (Flexible Vertex Format, FVF)
- Вершинные буферы (Vertex Buffers)
- Создаём вершинный буфер (Vertex Buffer; DirectX 8) и его дескриптор
- Блокируем вершинный буфер (Locking the Vertex Buffer)
- Заполняем вершинный буфер вершинами
- Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 8)
- Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 9)
- Вершинные шейдеры (Vertex Shaders; DirectX 8)
- Трансформации (Transformations)
- Цвета и материалы (Colors and Materials; DirectX 8)
- Начало и конец прорисовки сцены (Beginning and Ending the Scene; DirectX 8)
- Рендеринг полигонов (Rendering Polygons; DirectX 8)
- Вывод сцены на экран (Presenting Scene; DirectX 8)
- Источники
- Применение вершин (Using vertices)
Применение вершин (Using vertices)
Direct3D позволяет определить вершину множеством различных способов. К примеру, если ты используешь только 2D-графику, можно просто указать координаты 2D экранных (трансформированных) координатах. С другой стороны, если ты используешь локальные (local) и мировые (world) координаты, то можешь указать координаты в 3D-проекции (нетрансформированные координаты). Вдобавок, Direct3D позволяет включать (в том числе) в каждую вершину данные об используемом цвете или текстуре. Как же отслеживать всю эту инфу и быть уверенным, что Direct3D понимает, что ты делаешь в данный момент?Встречаем гибкий формат вершин (Flexible Vertex Format).
Гибкий формат вершин (Flexible Vertex Format, 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 (значение с плавающей точкой).
Тип данных 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.
Создаём пользовательскую FVF-структуру (пример)
Например, нам необходимо включить в кастомную FVF-структуру только 3D-координаты вершины и данные по её рассеянному цвету (diffuse color):typedef struct { FLOAT x, y, z; D3DCOLOR diffuse; }sVertex;
Создаём FVF-дескриптор
Как только FVF-структура создана, создаём её FVF-дескриптор, указав один или несколько специальных флагов:ФЛАГ | ОПИСАНИЕ |
---|---|
D3DFVF_XYZ | В структуру включены 3D-координаты вершины. |
D3DFVF_XYZRHW | В структуру включены 2D-координаты вершины. |
D3DFVF_NORMAL | В структуру включены данные нормали (вектора нормали). |
D3DFVF_DIFFUSE | В структуру включены данные о рассеянном (diffuse) цвете вершины. |
D3DFVF_TEXT | В структуру включены текстурные координаты. |
В нашем случае определение дескриптора выглядит так:
#define VertexFVF (D3DFVF XYZ | D3DFVF DIFFUSE)
Указаны только флаги, необходимые нашей FVF-структуре.
Вершинные буферы (Vertex Buffers)
После создания FVF-структуры вершины и её дескриптора, создаём объект, содержащий массив (array) вершин. Для выполнения этой задачи Direct3D (DirectX 8) предлагает два шаблонных объекта :
- IDirect3DVertexBuffer8
- IDirect3DIndexBuffer8.
- Список треугольников (triangle list);
- Полоса треугольников (triangle strip);
- Вентилятор треугольников (triangle fan).
При создании списка треугольников (triangle list) объект IDirect3DVertexBuffer8 хранит как минимум 3 вершины для каждого отрисовываемого полигона (напомним, вершины при этом указываются по часовой стрелке). При создании полосы треугольников (triangle strip) первый полигон образуется тремя вершинами, а последующие - путём добавления в пространстве ещё одной вершины. Что касается вентилятора треугольников (triangle fan), то здесь сначала сохраняется всего 1 вершина, а каждый полигон дорисовывается, путём добавления ещё двух вершин.
Обрати внимание
С точки зрения Direct3D полигон может состоять из 3-х, 2-х (отрезок) и даже из одной вершины, в зависимости от того, что ты рисуешь. В то же время в классической геометрии грань фигуры может состоять из более чем трёх вершин (например гранью куба является квадрат, у которого 4 вершины). В терминологии 3D-моделирования принято приравнивать грань фигуры к полигону, который в большинстве систем состоит ровно из трёх вершин. Системы, оперирующие четырёхугольными полигонами встречаются нечасто. Одной из таких была игровая консоль Sega Saturn, увидевшая свет в 1995 г. Её создатели хотели как лучше, а на деле лишь прибавили головной боли разработчикам игр, которые ранее никогда не работали с четырёхугольными полигонами. В данной статье рассматриваются только треугольные полигоны.
Создаём вершинный буфер (Vertex Buffer; DirectX 8) и его дескриптор
Вершинный буфер создаётся на базе предварительно проинициализированного объекта IDirect3DDevice8. С помощью функции CreateVertexBuffer:HRESULT IDirect3DDevice8::CreateVertexBuffer ( UINT Lenght, // Число байт, задействованное под все FVF-структуры. DWORD Usage, // Выставляем D3DCREATE_WRITEONLY DWORD FVF, // Дескриптор FVF D3DPOOL Pool, // Выставялем D3DPOOL_MANAGED IDirect3DVertexBuffer **ppVertexBuffer // Указатель на вершинный буфер. );
Здесь единственный изменяемый параметр - это флаг Usage. Обычно здесь всегда стоит значение D3DCREATE_WRITEONLY. Изменяют его очень редко. Другой возможный вариант данного флага - D3DCREATE_SOFTWAREPROCESSING. Ранее в этой статье мы создали пользовательскую FVF-структуру, включающую в себя 3D-координаты и данные по diffuse-цвету вершины, а также прописали её (струкутры) дескриптор. Выглядело это так:
#define VertexFVF (D3DFVF_XYZ | D3DFVF_DIFFUSE) typedef struct { FLOAT x, y, z; D3DCOLOR diffuse; }sVertex;
Создадим на основе данной структуры вершинный буфер, содержащий 4 вершины:
// g_pD3DDevice - предварительно проинициализированный объект устройства. // sVertex - созданная в предыдущем примере пользовательская FVF-структура. // VertexFVF - дескриптор FVF-структуры IDirect3DVertexBuffer8 *pD3DVB = NULL; // Создаём вершинный буфер if(FAILED(g_pD3DDevice->CreateVertexBuffer(sizeof(sVertex) * 4, D3DCREATEWRITEONLY, VertexFVF, D3DPOOL_MANAGED, &pD3DVB))) { // Ошибка. }
Обрати внимание
По завершении работы с вершинным буфером не забудь его удалить путём вызова макроса RELEASE.
Блокируем вершинный буфер (Locking the Vertex Buffer)
Перед добавлением вершин в вершинный буфер, необходимо сперва заблокировать (lock) память, которую этот самый буфер использует. Это сделает участок памяти с вершинами доступным для редактирования. С помощью функции Lock мы получаем доступ к памяти вершинного буфера и извлекаем (retrieve) его (буфера) указатель. Вот прототип функции IDirect3DVertexBuffer8::Lock:HRESULT IDirect3DVertexBuffer8::Lock ( UINT OffsetToLock, // Смещение блокировки буфера в байтах. UINT SizeToLock, // Сколько байт залочить (0 - все байты). BYTE** ppbData. // Указатель на указатель (для доступа к данным буфера). DWORD Flags // 0 );
Пример её применения:
// pD3DVB - предварительно проинициализированный вершинный буфер. BYTE *Ptr; // Блокируем (Lock) память вершинного буфера и получаем указатель на него: if(FAILED(pD3DVB->Lock(0, 0, (BYTE**)&Ptr, 0))) { // Ошибка. }
По завершении работы с вершинным буфером каждый залоченный буфер разблокируем отдельной командой Unlock:
HRESULT IDirect3DVertexBuffer8::Unlock();
Вызывается без параметров. Очень важно, чтобы на каждую команду Lock приходилась своя команда Unlock. То есть в одном исходнике число вызовов команды Lock должно совпадать с числом вызовов команды Unlock.
Заполняем вершинный буфер вершинами
Итак, на данном этапе мы:- Создали собственную FVF-структуру для хранения данных вершин.
- Создали дескриптор по её применению.
- Создали на основе нашей FVF-структуры вершинный буфер.
- Заблокировали (Lock) его, подготовив для заполнения данными.
Для примера предварительно создадим набор из четырёх вершин, представив его в виде массива:
sVertex Verts[4] = { {-100.0f, 100.0f, 100.0f, D3DCOLOR_RGBA(2 55, 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)} };
Теперь мы блокируем (Lock) вершинный буфер, получая указатель на его участок памяти, и затем копируем вершины из массива:
// pD3DVB - предварительно проинициализированный вершинный буфер. BYTE *Ptr; // Блокируем вершинный буфер и получаем указатель на него. if(SUCCEEDED(pD3DVB->Lock(0, 0, (BYTE**)&Ptr, 0))) { // Копируем массив с вершинами Verts в вершинный буфер memcpy(Ptr, Verts, sizrof(Verts)); }
По завершении мы разблокируем (Unlock) вершинный буфер:
pD3DVB->Unlock();
Готово! Вершинный буфер создан и заполнен вершинами.
Следующий шаг - назначить ему (вершинному буферу) отдельный поток-источник (stream source) и вершинный шейдер (vertex shader).
Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 8)
Direct3D (начиная с версии 8) позволяет отправлять вершины рендереру посредством нескольких потоков, называемых вершинными потоками (vertex streams). Данная тема весьма обширна и инфу по ней нетрудно найти инете (почти вся на англ. языке). Для простоты изложения в данной статье мы будем использовать всего 1 поток.Для привязки заполненного вершинного буфера определённому потоку данных устройства (device data stream) применяют функцию IDirect3DDevice8::SetStreamSource:
HRESULT IDirect3DDevice8::SetStreamSource( UINT StreamNumber, // 0 IDirect3DVertexBuffer8* pStreamData, // Объект вершинного буфера. UINT Stride // Размер FVF-структуры вершины. );
При успешном выполнении возвращаемое значение D3D_OK. В случае неудачи возвращается D3DERR_INVALIDCALL или другие значения.2
Здесь:
- StreamNumber - номер потока. 0 - первый поток, -1 - максимально возможный номер потока.
- Stride (от англ. "шаг") - шаг компонента (в байтах).
// g_pD3DDevice - объект устройства Direct3D. // pD3DVB - предварительно проинициализированный вершинный буфер. if(FAILED(g_pD3DDevice->SetStreamSource(0, pD3DVB, sizeof(sVertex)))) { // Ошибка }
Здесь в параметре Stride мы указали размер всего массива sVertex.
Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 9)
Функция IDirect3DDevice9::SetStreamSource привязывает вершинные буферы к потокам данных устройства:3HRESULT IDirect3DDevice9::SetStreamSource( UINT StreamNumber, IDirect3DVertexBuffer9 * pStreamData, UINT OffsetInBytes, UINT Stride );
Здесь:
- StreamNumber - Номер потока. На современных видеокартах их поддерживается по несколько штук.
- pStreamData - Указатель на интерфейс вершинного буфера, который будет привязан к потоку.
- OffsetInBytes - смещение от начала потока к началу вершинных данных. Задаётся в байтах.
- Stride. Данный параметр должен быть равным размеру (занимаемому объёму памяти) вершины либо размеру её FVF-структуры.
Вершинные шейдеры (Vertex Shaders; DirectX 8)
- Это механизм, обеспечивающий загрузку и обработку вершин;
- Включают в себя изменение координат вершин, применение цвета и т.н. затуманивания (fogging) + ряд других операций.
- Бывают двух видов: фиксированный (fixed; предустановленный; здесь весь необходимый функционал встроен в него) и программируемый (programmable; здесь любую процедуру можно изменять).
HRESULT IDirect3DDevice8::SetVertexShader(DWORD Handle); // Указатель на кастомную (или не очень) FVF-структуру для хранения вершин.
В нашем случае пример её применения выглядит так:
// g_pD3DDevice8 - предварительно проинициализированный объект устройства Direct3D. // VertexFVF - предварительно созданный дескриптор на кастомную FVF-структуру/ if(FAILED(g_pD3DDevice->SetVertexShader(VertexFVF))) { // Ошибка. }
Здесь мы указали вершинному шейдеру используемый формат FVF-структуры. Следующий шаг - назначить различные трансформации, необходимые для позиционирования вершин (изначально расположенных в локальном пространстве) в мировое пространство. Само собой, всё нижеизложенное справедливо только для 3D-координат!
Трансформации (Transformations)
Когда имеешь дело с 3Р-объектами (вершины, полиогоны и т.д.), изначально они создаются в локальной системе координат (local space). Для корректного позиционирования при рендеринге они предварительно должны пройти через несколько матриц преобразований (мировая, вида и проекции). Подробнее об этом читай раздел "Трансформации" в статье Базовые понятия 3D-графики.Каждая трансформация требует создание специальной матрицы (всего их 3), которая представляет соответствующие значения ориентации или проекции вершин/объектов в пространстве. Рассмотрим каждую из этих матриц подробнее.
Мировая трансформация (The World Transformation)
Вершины, изначально определённые в локальном пространстве, должны быть сориентированы в мировом. К примеру, ты создал бокс (box, куб) из вершин в локальном пространстве и хочешь разместить его в определённом месте в мире. Для этого к соответствующим координатам каждой его вершины необходимо применить мировую матрицу (См. Рис.2). Для её создания применяют вспомогательную библиотеку D3DX (DirectX 8). Только что созданная мировая матрица (World matrix) сама по себе бесполезна. Перед применением к вершинам она последовательно комбинируется (=перемножается) с несколькими другими матрицами, отражающими положение объекта в 3D-пространстве:
- матрица вращения по оси X (X Rotation matrix);
- матрица вращения по оси Y (Y Rotation matrix);
- матрица вращения по оси Z (Z Rotation matrix);
- матрица трансляции (=перемещения; Translation matrix);
- матрица масштабирования (Scale matrix).
D3DXMATRIX matWorld; // Результирующая матрица мира. D3DXMATRIX matRotX, matRotY, matRotZ; D3DXMATRIX matTrans; D3DXMATRIX matScale; // Создаём матрицы вращения. По одной на каждую ось. D3DXMatrixRotationX(&matRotX, XAngle); D3DXMatrixRotationY(&matRotY, XAngle); D3DXMatrixRotationZ(&matRotZ, XAngle); // Создаём матрицу трансляции (=перемещения). D3DXMatrixTranslation(&matTrans, XPos, YPosm ZPos); // Создаём матрицу масштабирования. D3DXMatrixScaling(&matScale, XScale, YScale, ZScale);
Далее просто комбинируем (=перемножаем) 3 матрицы вращения, матрицы трансляции и масштабирования в одну мировую матрицу (World matrix).
Важно!
Для создания корректной мировой матрицы её нужно комбинировать с матрицами-компонентами в строго определённом порядке:
- матрица масштаба (Scale matrix),
- матрица вращения по оси X,
- матрица вращения по оси Y,
- матрица вращения по оси Z,
- матрица трансляции (Translation matrix).
В коде это выглядит так:
// Задаём матрицу matWorld в качестве единичной (identity matrix). D3DXMatrixIdentity(SmatWorld); // Поочерёдно комбинируем все матрицы-компоненты с мировой матрицей трансформации. D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatScale); D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotX); D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotY); D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotZ); D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatTrans);
Мы почти закончили. Осталось только указать Direct3D использовать полученную матрицу в качестве мировой путём вызова функции SetTransform. Вот её прототип:
HRESULT IDirect3DDevice8::SetTransform( D3DTRANSFORMSTATETYPE State, // Обычно здесь стоит D3DTS_WORLD. CONST D3DMATRIX *pMatrix // Устанавливаем указанную мировую матрицу. );
Здесь второй параметр является указателем на структуру D3DMATRIX. Но в нашем случае сюда можно подставить указатель на созданную выше результирующую мировую матрицу.
Установка первого параметра в D3DTS_WORLD говорит Ditect3D, что выбранная матрица используется для мировой трансформации (world transformation). И всё, что будет отрисовываться после этого, должно быть ориентировано данной мировой матрицей.
Если у нас два и более объектов, которые необходимо сориентировать в мировом пространстве, то на каждый из них надо создавать отдельную мировую матрицу (в соответствии с текущим положением данных объектов в пространстве) и затем так же по отдельности на каждый объект вызывать отдельную функцию SetTransform, назначающую объекту конкретную мировую матрицу.
Трансформация вида (The View Transformation)
Если кратко, то трансформация вида выполняет роль виртуальной камеры (часто её называют вьюпортом, от англ. "viewport"). Создав матрицу, содержащую смещения ранее установленных мировых координат объектов, мы размещаем сцену с объектами перед "объективом" вьюпорта. С помощью трансформации вида все вершины должны быть сориентированы вокруг центра мира точно так же, как и их позиция относительно вьюпорта.Для создания трансформации вида создают матрицу, отражающую текущую позицию и поворот вьюпорта. Здесь порядок комбинирования матриц-компонентов с матрицей вида следующий:
- матрица трансляции,
- матрица вращения по оси Z,
- матрица вращения по оси Y,
- матрица вращения по оси X.
D3DXMATRIX matView; D3DXMATRIX matRotX, matRotY, matRotZ; D3DXMATRIX matTrans; // Создаём матрицы вращения (с отрицательными значениями). D3DXMatrixRotationX(&matRotX, -XAngle); D3DXMatrixRotationY(&matRotY, -YAngle); D3DXMatrixRotationZ(&matRotZ, -ZAngle); // Создаём матрицу трансляции (с отрицательными значениями). D3DXMatrixTranslation(&matTrans, -XPosm -YPos, -ZPos); // Устанавливаем матрицу вида в качестве единичной. D3DMatrixIdentity(&matView); // Поочерёдно комбинируем каждую из матриц-компонентов с матрицей (трансформации) вида. D3DXMatrixMultiply(&matView, &matView, &matTrans); D3DXMatrixMultiply(&matView, &matView, &matRotZ); D3DXMatrixMultiply(&matView, &matView, &matRotY); D3DXMatrixMultiply(&matView, &matView, &matRotX);
Перед началом использования результирующей матрицы вида назначим её с помощью всё той же функции SetTransform. В этот раз первый параметр выставляем в D3DTS_VIEW:
// g_pD3DDevice - предварительно проинициализированный объект устрйоства Direct3D. if(FAILED(g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView))) { // Ошибка. }
Как видишь, назначить матрицу вида вершинам совсем не трудно. Куда сложнее её создать. Для упрощения данного процесса в D3DX есть отличная функция D3DXMatrixLookAtLH, которая создаёт матрицу вида буквально за один вызов. Вот её прототип:
D3DXMATRIX* D3DXMatrixLookAtLH( D3DXMATRIX* pOut, // Результирующая (output) матрица вида. CONST D3DXVECTOR3* pEye, // Координаты вьюпорта. CONST D3DXVECTOR3* pAt, // Координаты точки наблюдения (по направлению луча "зрения"). CONST D3DXVECTOR3* pUp // Определяет, где у нас вверх. );
В коде видим незнакомые объекты D3DXVECTOR3. На деле они очень схожи с объектами типа D3DXMATRIX с той лишь разницей, что их векторные "собратья" содержат всего 3 элемента. В нашем случае - 3 координаты. Объект D3DXVECTOR3 так и называют - векторный объект.
Вектор pEye представляет собой координаты самого вьюпорта. pAt - координаты цели, куда устремлён "взгляд" вьюпорта. pUp - это вектор, указыващий, в какой стороне у вьюпорта находится верх. Обычно его устанавливают в (0,1,0), что означает положительное направление оси Y. Но, в свете того, что вьюпорт может вращаться в разные стороны (подобно тому, как обычный наблюдатель может поворачивать свою голову из стороны в сторону), направление "вверх" можно выбирать любое и по любой из осей (в зависимости от игры). Вот пример использования функции D3DXMatrixLookAtLH:
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(SmatView, SvecVP, SvecTP, SvecUp);
Здесь предполагается, что вьюпорт находится в точке с координатами XPos, YPos, ZPos и что смотрит он в начало координат.
Трансформация проекции (The Projection transformation)
Завершает череду преобразований трансформация проекции, которая преобразует (нетрансформированные) 3D-координаты вершин в (трансформированные) 2D-координаты. На основе последних Direct3D выводит графику на экран. Представь, будто трансформация проекции "сплющивает" 3D-изображение и выводит на проский экран монитора (См. Рис.3). Здесь вступают в "игру" следующие аспекты (См. Рис.4):
- Соотношение сторон вьюпорта (Aspect Ratio of viewport);
- Поле видимости (Field of View, FOV);
- Ближняя плоскость отсечения (Near clipping plane);
- Дальняя плоскость отсечения (Far clipping plane).
D3DXMATRIX* D3DMatrixPerspectiveFovLH( D3DXMATRIX* pOut, // Результирующая матрица. FLOAT fovy, // Поле видимости, в радианах. FLOAT Aspect, // Соотношение сторон (Aspect Ratio) FLOAT zn, // Z-значение ближней (near) плоскости отсечения. FLOAT zf // Z-значение дальней (far) плоскости отсечения. );
Параметр fovy указывает ширину вида проекции. Т.е. чем больше значение, тем больше объектов сцены видно в кадре одновременно. В случае слишком больших или слишком малых значений возникает эффект искажения краёв кадра (т.н. линза "рыбий глаз"). Типичное значение здесь D3DX_PI/4, что соответствует 1/4 числа пи.
Параметр Aspect представляет собой соотношение сторон видимой области. Если игра идёт в окне 400х400, то Apect выставляем 1:1 или 1:0 (т.к. окно квадратное). В случае окна 400х200 выставляем Aspect в 2:1 или 2:0. Для расчёта соотношения сторон ширину окна (в пикселях) делят на его высоту. Иногда просто применяют формулу с переменными, вроде этой:
FLOAT Aspect = (FLOAT)WindowWidth / (FLOAT)WindowHeight;
Параметры zn и zf представляют собой значения для ближней и дальней плоскостей отсечения (clipping plane) и измеряются в тех же самых единицах, что определены в FVF-формате 3D-вершин. Типичные значения:
- для параметра zn: 1.0;
- для параметра zf: 1000.0 .
После создания матрицы проекции, она устанавливается в качестве таковой спецфункцией IDirect3DDevice8::SetTransform, где в качестве параметра STATE указываем D3DTS_PROJECTION. Вот пример:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D. D3DXMATRIX matProj; // Создаём матрицу проекции. D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 1000.0f); // Назначаем созданную матрицу в качестве матрицы проекции. if(FAILED(g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj))) { // Ошибка. }
Цвета и материалы (Colors and Materials; DirectX 8)
В начале этой статьи, при изучении гибкого формата вершин (FVF), мы определяли цвет как один из параметров вершины. В отличие от вершин, полигоны имеют дополнительные цветовые атрибуты.
Запомни!
Цвет, применяемый к полигону, называется материалом (material).
Перед рендерингом полигона средствами Direct3D программер может назначить ему материал. Если материал не назначен, то полигон окрашивается в цвета своих вершин (if any).
каждый материал содержит несколько цветовых значений, описывающих его. В Direct3D цветовые значения материала хранятся в специальной структуре D3DMATERIAL8:
typedef struct _D3DMATERIAL8 { D3DCOLORVALUE Diffuse; // Цветовой компонент рассеяного (diffuse) света. D3DCOLORVALUE Ambient; // Цветовой компонент света подсветки (Ambient). D3DCOLORVALUE Specular; // Цветовой компонент блика (Specular). D3DCOLORVALUE Emissive; // Цветовой компонент излучаемого света (Emissive). float Power; // Чёткость бликов. } D3DMATERIAL8;
На практике:
- В большинстве случаев имеют дело лишь с одним компонентом - Diffuse.
- Компоненту Ambient часто присваивают то же значение, что и у компонента Diffuse.
- Компонент Specular изменяется от 0.0 до 1.0 (при Power = 0.0).
В нашем случае мы применяем к грани (face) полигона Diffuse-компонент. Если применить цвет материала к полигону, у которого уже заданы цвета вершин, то можно заметить (часто нежелательные) изменения в цвете полигона. Поэтому лучше использовать что-то одно: либо цвета вершин, либо цвет материала полигона.
При работе с цветовыми компонентами материала, цвета каждого компонента присваиваются напрямую, без использования макроса D3DCOLOR_RGBA. При этом каждому цветовому компоненту компонента материала соответствует своя буква, по цвету: r - red (красный), g - green (зелёный), b - blue (синий), a - alpha (цвет прозрачности). Например, при создании материала жёлтого цвета структура материала может выглядеть так:
D3DMATERIAL8 d3dm; // Очищаем структуру материала. ZeroMemory(&d3dm, sizeof(D3DMATERIAL8)); // Заполняем компоненты Diffuse и Ambient жёлтым цветом. d3dm.Diffuse.r = d3dm.Ambient.r = 1.0f; // r - red (красный) d3dm.Diffuse.g = d3dm.Ambient.g = 1.0f; // g - green (зелёный) d3dm.Diffuse.b = d3dm.Ambient.b = 0.0f; // b - blue (синий) d3dm.Diffuse.a = d3dm.Ambient.a = a.0f; // a - alpha (альфа-компонент; цвет прозрачности)
Как только структура материала создана, необходимо указать Direct3D использовать её при рендеринге полигона с помощью функции IDirect3DDevice8::SetMaterial, принимающей всего 1 параметр - указатель на созданную структуру материала:
IDirect3DDevice8::SetMaterial(CONST D3DMATERIAL8 *pMaterial);
После единственного её вызова все полигоны будут рендериться только с этим материалом. В нашем случае созданная выше структура материала назначается так:
g_pD3DDevice->SetMaterial(&d3dm);
Очищаем вьюпорт (Clearing the viewport; DirectX 8)
Для подготовки бэкбуфера к рендерингу его необходимо очистить (например от графики, которая осталась в нём от рендеринга предыдущего кадра). Делается это с помощью функции IDirect3DDevice8::Clear:
HRESULT IDirect3DDevice8::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 - предварительно проинициализированный объект устройства Direct3D. if(FAILED(g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_RGBA(0,0,192,255), 1.0f, 0)) { // Ошибка }
Начало и конец прорисовки сцены (Beginning and Ending the Scene; DirectX 8)
Прежде чем что-либо отрендерить, необходимо сперва скомандовать Direct3D подготовиться к прорисовке с помощью функции IDirect3DDevice8::BeginScene, не имеющей параметров:HRESULT IDirect3DDevice8::BeginScene();
По окончании рендеринга сцены информируем Dierct3D о том, что мы закончили прорисовку:
HRESULT IDirect3DDevice8::EndScene();
Между вызовами функций BeginScene и EndScene нельзя включать функцию Clear. Обычно это делают перед вызовом функции BeginScene.
Единственное, что может находиться между вызовами функций BeginScene и EndScene - это вызовы функции, непосредственно рендерящие полигоны.
Рендеринг полигонов (Rendering Polygons; DirectX 8)
Наконец-то можно рендерить полигоны! Типичный кадр игры проделывает следующие операции:- очищает бэкбуфер;
- начинает рендеринг сцены (BeginScene);
- назначает материал;
- рендерит полигоны;
- заканчивает рендеринг сцены (EndScene).
HRESULT IDirect3DDevice8::DrawPrimitive( D3DPRIMITIVETYPE PrimitiveType, // Тип примитива. UINT StartVertex, // Вершина, с которой начинаем прорисовку (обычно 0). UINT PrimitiveCount // Кол-во примитивов. );
Параметр PrimitiveType может принимать одно из следующих значений (См. Рис.1):
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
D3DPT_POINTLIST | Рисует все вершины в виде отдельных пикселей на экране. |
D3DPT_LINELIST | Рисует все вершины в виде отдельных отрезков на экране. |
D3DPT_LINESTRIP | Рисует ломаную линию, состоящую из отрезков, соединённых друг с другом. |
D3DPT_TRIANGLELIST | Рисует отдельные полигоны, используя по 3 вершины на каждый из них. |
D3DPT_TRIANGLESTRIP | Рисует ленту (strip) из полигонов. Первый полигон строится из 3 вершин. Последующие полигоны строятся путём добавления 1 вершины к ребру предыдущего полигона. |
D3DPT_TRIANGLEFAN | Рисует полигоны в виде вентилятора. Одна из вершин первого полигона является общей для всех последующих. |
Параметр PrimitiveCount устанавливается в общее число примитивов (полигонов), которое хотим нарисовать. Вызов функции DrawPrimitive обязательно обрамляется функциями BeginScene и EndScene. Иначе вызов DrawPrimitive вызовет ошибку.
Допустим, у нас есть вершинный буфер с 6 вершинами, из которого выстраиваем 2 треугольных полигона в форме квадрата. Вот пример такого построения:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D. // pD3DVB - готовый вершинный буфер. // sVertex - предварительно созданная FVF-структура (гибкого формата вершин). // VertexFVF - FVF-дескриптор. // Назначаем вершинный поток (vertex stream) и шейдер (shader). g_pD3DDevice->SetStreamSource(0, pD3DVB, sizeof(sVertex)); g_pD3DDevice_>SetVertexShader(VertexFVF); if(SUCCEEDED(g_pD3DDevice->BeginScene ())) { // Рендерим полигоны. if(FAILED(g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2))) { // Ошибка. } } // Заканчиваем прорисовку. g_pD3DDevice->EndScene();
Вывод сцены на экран (Presenting Scene; DirectX 8)
- Завершающий шаг.
- Подразумевает смену (flip) буферов, в результате которого подготовленный бэкбуфер с полигонами выводится на экран.
- Выполняется функцией IDirect3DDevice8::Present.
HRESULT IDirect3DDevice8::Present( CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion );
Здесь можно смело выставить все параметры в NULL, тем самым указав Direct3D обновить весь текущий экран. Другие параметры нужны для вывода изображения небольшими порциями. Вот пример её применения:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D. if(FAILED(g_pD3DDevice->Present(NULL, NULL, NULL, NULL))) { // Ошибка. }
Для создания более реалистичных сцен используй несколько различных вершинных буферов (по одному на каждый 3D-объект сцены).
Источники
1. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press, 2002
2. Документация DirectX SDK 8.1
Последние изменения страницы Вторник 24 / Май, 2022 23:19:40 MSK
Последние комментарии wiki