Программирование графических процессоров с использованием Direct3D и HLSL

Вывод простейших примитивов


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

C++

struct MYVERTEX1 { FLOAT x, y, z, rhw; DWORD color; }; MYVERTEX1 data1[100];

struct MYVERTEX2 { FLOAT x, y, z; FLOAT n1, n2, n3; }; MYVERTEX2 data2[100];

Pascal

MyVertex1 = packed record x, y, z, rhw: Single; color: DWORD; end;

MyVertex2 = packed record x, y, z: Single; n1, n2, n3: Single; end;

var data1: array [0..99] of MyVertex1; data2: array [0..99] of MyVertex2;

Тем самым данные о вершинах будут храниться в массиве, и иметь строго определенный формат (тип). В процессе визуализации сцены необходимо указать графической библиотеке, что собой представляет одна вершина примитива. Это реализуется с помощью механизма флагов, называемого FVF (Flexible Vertex Format – гибкий формат вершин). Существует порядка двух десятков флагов FVF для определения формата вершины. Самые распространенные и наиболее используемые флаги FVF для определения формата вершины представлены в следующей таблице:

D3DFVF_DIFFUSEиспользуется цветовая компонента вершины
D3DFVF_NORMALвершина содержит нормали
D3DFVF_TEX1задает текстурные координаты вершины
D3DFVF_PSIZEопределяет размер частицы
D3DFVF_XYZвершина содержит три координаты
D3DFVF_XYZRHWпреобразованный формат вершин

Комбинация конкретных флагов дает возможность сообщить системе, с каким форматом (типом) вершин она имеет дело в данный момент времени.
Так, например,
(D3DFVF_XYZ OR D3DFVF_NORMAL) – вершина содержит нормали и координаты, (D3DFVF_XYZRHW OR D3DFVF_DIFFUSE) - цветная преобразованная вершина, где OR – операция логического "или". Установка формата вершин осуществляется с помощью вызова метода SetFVF интерфейса IDirect3DDevice9. Программно это реализуется так:

C++
#define MY_FVF (D3DFVF_XYZRHW|D3DFVF_DIFFUSE) … device->SetFVF(MY_FVF);
Pascal
const MY_FVF = D3DFVF_XYZRHW or D3DFVF_DIFFUSE; … device.SetFVF(MY_FVF);

Следует особо остановиться на флаге D3DFVF_XYZRHW. Этот флаг позволяет описывать вершины, определенные на плоскости. Он служит для графической системы индикатором того, что построение сцены происходит на плоскости в координатах окна (формы) вывода. При этом формат вершины должен задаваться четверкой чисел x,y,z,w, две последних из которых, как правило, не используются, однако должны присутствовать при описании вершины.
В качестве примера попытаемся вывести на экран 100 точек, случайно "разбросанных" на форме. Для этого необходимо проделать следующие шаги:
  1. Описать структуру, в которой будут храниться координаты точек;
  2. Объявить массив нужной размерности (в нашем случае на 100 элементов) и заполнить его данными (в нашем случае случайными координатами).
  3. Вызвать метод для вывода этого массива на экран.



Первые два шага нам реализовать уже не составляет особого труда. Третий же шаг может быть реализован помощью вызова метода DrawPrimitiveUP интерфейса IDirect3DDevice9. Прототип данного метода выглядит так:
DrawPrimitiveUP( Тип выводимого примитива, Количество примитивов, Массив данных, Размер "шага в байтах" от одной вершины до другой )
Первый параметр указывает на тип выводимого примитива и задается одной из следующих констант: D3DPT_POINTLIST, D3DPT_LINELIST, D3DPT_LINESTRIP, D3DPT_TRIANGLELIST, D3DPT_TRIANGLESTRIP, D3DPT_TRIANGLEFAN. Второй аргумент задает количество выводимых примитивов. Третий параметр является указателем на область в памяти, где располагаются данные о вершинах (в нашем случае это заполненный массив).


И последний параметр задает, сколько байтов отводится для хранения одной вершины.
Возвращаясь к нашему примеру, вывод ста случайных точек на плоскости можно осуществить, например, с помощью следующего программного кода:
C++
struct MYVERTEX { FLOAT x, y, z, rhw; }data[100];
#define MY_FVF (D3DFVF_XYZRHW);
// заполнение массива data "случайными" точками …
// процедура вывода сцены device->SetFVF(MY_FVF); device->DrawPrimitiveUP( D3DPT_POINTLIST, 100, data, sizeof(MYVERTEX) );
Pascal
type MyVertex = packed record x, y, z, rhw: Single; end;
const MY_FVF = D3DFVF_XYZRHW;
var data: array [0..99] of MyVertex;
// заполнение массива data "случайными" точками …
// процедура вывода сцены device.SetFVF(MY_FVF); device.DrawPrimitiveUP( D3DPT_POINTLIST, 100, data, SizeOf(MyVertex) );

Остановимся теперь на процедуре непосредственного вывода результатов на экран. Процесс непосредственного воспроизведения примитивов рекомендуют обрамлять двумя действиями. Перед отображением необходимо вызвать метод BeginScene, а после воспроизведения – метод EndScene интерфейса IDirect3DDevice9. Первый метод информирует устройство вывода (видеокарту), что следует подготовиться к воспроизведению результатов. Второй метод сообщает устройству о том, что процесс воспроизведения для текущего кадра закончен и теперь можно осуществлять переключение буферов рендеринга. Таким образом, процедура воспроизведения сцены должна выглядеть приблизительно следующим образом:
C++VOID Render() { device->BeginScene(); device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); device->SetFVF(…); device->DrawPrimitiveUP(…); device->EndScene(); device->Present( NULL, NULL, NULL, NULL ); }
Pascalprocedure Render; begin device.BeginScene; device.Clear(0,nil,D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0,0); device.SetFVF(…); device.DrawPrimitiveUP(…); device.EndScene; device.Present(nil, nil, 0, nil); end;

Рассмотрим оставшиеся типы примитивов, которые имеются в наличии у библиотеки Direct3D.


  1. Вывод отрезков
    • Для построения независимых отрезков первым аргументом метода DrawPrimitiveUP необходимо указать константу D3DPT_LINELIST. При этом следует заметить, что количество выводимых примитивов (второй параметр метода) будет в два раза меньше количества точек в массиве.
    • Для построения связных отрезков первым аргументом метода DrawPrimitiveUP указывается константа D3DPT_LINESTRIP. При этом количество выводимых примитивов будет на единицу меньше количества точек в исходном массиве ладных.

  2. Вывод треугольников
    • Если первым аргументом метода DrawPrimitiveUP указывается константа D3DPT_TRIANGLELIST, то каждая триада (тройка) вершин задает три вершины одного (независимого) треугольника. Количества выводимых примитивов (треугольников) будет в три раза меньше чем размер массива данных.
    • Если первым аргументом метода DrawPrimitiveUP указывается константа D3DPT_TRIANGLESTRIP, то речь идет о группе связных треугольников. Первые три вершины задают первый треугольник, вторая, третья и четвертая определяют второй треугольник, третья, четвертая и пятая – третий и т.д. В результате получается лента соприкасающихся треугольников. Следует заметить, что использование связных треугольников самый экономный и эффективный способ построений.
    • Если первым аргументом метода DrawPrimitiveUP указывается константа D3DPT_TRIANGLEFAN, то строится группа связных треугольников, но данные здесь трактуются немного иначе. Треугольники связаны подобно раскрою зонтика или веера. По такой схеме удобно строить конусы и пирамиды в пространстве, выпуклые многоугольники и эллипсы в плоскости.


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


В предыдущих примерах фигурировали бинарные примитивы (белые фигуры на черном фоне). Чтобы окрасить примитивы нужно задать цвет каждой вершины примитива. Это проделывается в два этапа. Сначала мы должны изменить тип вершины, а затем добавить в комбинацию флагов FVF значение D3DFVF_DIFFUSE.
C++struct MYVERTEX { FLOAT x, y, z, rhw; DWORD color; }
#define MY_FVF (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)
Pascaltype MyVertex = packed record x, y, z, rhw: Single; color: DWORD; end;
const MY_FVF = D3DFVF_XYZRHW or D3DFVF_DIFFUSE;
<


Далее каждой вершине необходимо присвоить некоторый цвет. Это можно проделать с помощью макроса-функции D3DCOLOR_XRGB(r, g, b), где в качестве параметров передается тройка основных цветов (веса) – красного, зеленого и синего. При этом, код, отвечающий за визуализацию сцены, не претерпит никаких изменений.
Очень часто в компьютерной графике возникает задача вывода объекта, состоящего из треугольников, причем каждый треугольник имеет общие вершины с другими треугольниками. Простейшим примером такой задачи можно назвать построение триангуляции Делоне множества точек на плоскости, представленной на следующем рисунке.


С одной стороны данная триангуляция имеет 10 треугольников, каждый из которых содержит 3 вершины. Таким образом, для хранения 30 вершин при расходах 20 байт на каждую вершину, необходимо 600 байт машинной памяти. С другой стороны, можно хранить отдельно сами вершины и список номеров (индексов) вершин для каждого треугольника. В этом случае расходы на машинную память будут следующими: 9 вершин по 20 байт на каждую потребует 180 байт, 10 триплетов индексов вершин, например, по 2 байта на индекс потребуют 60 байт. Итого для такой организации хранения данных модели – в нашем случае триангуляции, необходимо 240 байт, что в два с половиной раза меньше чем при организации хранения вершин всех десяти треугольников. Использование дополнительного массива, который будет хранить только лишь номера вершин модели, позволит избежать повторения данных при задании вершин. Так, например, представленная триангуляция может быть описана следующим образом.


10 треугольников = {
1: (0,3,2),
2: (0,1,3),
3: (2,3,4),
4: (3,6,4),
5: (1,5,3),
6: (3,5,6),
7: (1,7,5),
8: (5,7,6),
9: (6,7,8),
10: (4,6,8)
} = 2 байта/индекс*
3 индекса*
10 треугольников =
60 байт

Программно этот подход может быть реализован с помощью вызова метода DrawIndexedPrimitiveUP интерфейса IDirect3DDevice9, который имеет 8 параметров. Первый параметр задает тип выводимых примитивов и представляет собой одну константу из уже известного набора.


Второй параметр определяет минимальный вершинный индекс и, как правило, он устанавливается в значение ноль. Третий параметр определяет количество вершин в модели. Четвертый параметр задает количество выводимых примитивов. Пятый аргумент метода представляет собой указатель на массив, содержащий набор индексов. Шестой аргумент определяет формат индексных данных и может принимать два значения: D3DFMT_INDEX16 и D3DFMT_INDEX32. Т.е. под один индекс может быть отведено либо 16, либо 32 бита машинной памяти. Предпоследний, седьмой аргумент метода представляет собой указатель на массив, содержащий набор вершин модели. И последний параметр определяет количество байтов, необходимых для хранения одной вершины. Таким образом, обобщив эти строки можно записать формат вызова данного метода:
DrawIndexedPrimitiveUP( 1. тип выводимых примитивов, 2. минимальный индекс вершин, 3. количество вершин, 4. количество выводимых примитивов, 5. указатель на массив индексов, 6. формат индексов, 7. указатель на массив вершин, 8. количество байтов для хранения одной вершины )
Предположим, что для каждой вершины представленной выше триангуляции, задан свой цвет, тогда ее визуализация с использованием массива индексов может выглядеть следующим образом:
C++
struct MYVERTEX { FLOAT x, y, z, rhw; DWORD color; }
#define MY_FVF (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)
WORD indices[] = {0,3,2, 0,1,3, 2,3,4, 3,6,4, 1,5,3, 3,5,6, 1,7,5, 5,7,6, 6,7,8, 4,6,8};
MYVERTEX points[] = { { 119, 354, 0, 0, D3DCOLOR_XRGB(255,0,0) }, { 47, 248, 0, 0, D3DCOLOR_XRGB(0,255,0) }, … } … device-> DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, 9, 10, indices, D3DFMT_INDEX16, points, sizeof(MYVERTEX) );
Pascal
type MyVertex = packed record x, y, z, rhw: Single; color: DWORD; end;
const MY_FVF = D3DFVF_XYZRHW or D3DFVF_DIFFUSE; indices: array [0..29] of Word = (0,3,2, 0,1,3, 2,3,4, 3,6,4, 1,5,3, 3,5,6, 1,7,5, 5,7,6, 6,7,8, 4,6,8);
var points : array [0..8] of MyVertex; … points[0].x := 119; points[0].y := 354; points[0].color := D3DCOLOR_XRGB(255,0,0); …
device.DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, 9, 10, indices, D3DFMT_INDEX16, points, SizeOf(MyVertex));
<


Результат построения триангуляции " с цветом" показан на следующем рисунке.


Вообще говоря, функции вывода примитивов DrawPrimitiveUP и DrawIndexedPrimitiveUP являются довольно медленными с эффективной точки зрения, т.к. производят довольно много лишних операций. Для хранения вершин мы использовали обычные переменные (в нашем случае массивы), хотя для таких ситуаций предназначен специальный буфер вершин. Чтобы начать работы с этим буфером вершин необходимо проделать следующие шаги:
  1. Объявить переменную, в которой будет храниться адрес буфера вершин. Программно это будет выглядеть так.
    C++LPDIRECT3DVERTEXBUFFER9 VBuffer = NULL;
    Pascalvar VBuffer: IDirect3DVertexBuffer9;

  2. Воспользовавшись методом CreateVertexBuffer интерфейса IDirect3DDevice9 создать буфер вершин.
  3. Заполнить буфер данными о вершинах. Этот шаг реализуется в три приема. Вначале необходимо запереть буфер, т.к. заполнение его может производиться только в закрытом состоянии. Достигается это вызовом метода Lock интерфейса IDirect3DVertexBuffer9. Второй шаг состоит в непосредственном копировании данных с помощью стандартной функции Win32API – memcpy(). И третий шаг заключается в отпирании буфера вершин с помощью метода Unlock интерфейса IDirect3DVertexBuffer9. Таким образом, этот шаг можно реализовать следующим образом:
    C++
    LPDIRECT3DVERTEXBUFFER9 VBuffer = NULL; VOID* pBuff; … device->CreateVertexBuffer(sizeof(points), 0, MY_FVF, D3DPOOL_DEFAULT, &VBuffer, NULL ); VBuffer->Lock( 0, sizeof(points), (void**)&pBuff, 0 ); memcpy( pBuff, points, sizeof(points) ); VBuffer-> Unlock();
    Pascal
    var VBuffer: IDirect3DVertexBuffer9; pBuff: Pointer; … device.CreateVertexBuffer(SizeOf(points), 0, MY_FVF, D3DPOOL_DEFAULT, VBuffer, nil); VBuffer.Lock(0, SizeOf(points), pBuff, 0); Move( points, pBuff^, SizeOf(points) ); VBuffer.Unlock;

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


    Третий параметр задает формат вершин буфера через набор FVF флагов. Четвертый параметр определяет месторасположение буфера вершин. Значение D3DPOOL_DEFAULT говорит о том, что библиотека сама позаботится о размещении буфера в памяти. Пятый аргумент задает адрес переменной, в которую будет помещен результат вызова метода, т.е. эта переменная будет хранить адрес буфера вершин. И Шестой параметр не используется, является зарезервированным в настоящее время и всегда должен быть пустым.
    Рассмотрим теперь значения параметров метода Lock. Первый параметр определяет смещение от начала буфера, с которого будет производиться запирание области (значение 0 указывает на то, что запирается весь буфер с самого начала). Второй аргумент задает размер запираемой области в байтах. Третий параметр возвращает адрес запираемой области. И последний аргумент задает набор флагов способа запирания и, как правило, всегда равен нулю.
  4. И последний шаг – вывод примитивов на экран. Библиотека Direct3D позволяет выводить данные в несколько потоков. Созданный буфер вершин является примером одного такого потока. Вначале вывода сцены на экран необходимо связать наш буфер вершин с одним из потоков данных. Это реализуется с помощью вызова метода SetStreamSource интерфейса IDirect3DDevice9. Этот метод имеет четыре параметра. Первый из них определяет номер потока вывода. Если в программе используется только один буфер вершин, то этот параметр должен быть 0. Второй параметр содержит указатель на переменную, ассоциированную с буфером вершин. Третий – определяет смещение от начала буфера, с которого нужно производить считывание данных. Четвертый параметр задает размер одной вершины в байтах. Далее необходимо указать формат выводимых вершин. Это проделывается с помощью вызова метода SetFVF интерфейса IDirect3DDevice9. И затем производится непосредственный вывод примитивов с помощью функции DrawPrimitive интерфейса IDirect3DDevice9. Метод DrawPrimitive имеет три параметра: первый – тип выводимых примитивов, второй – индекс начальной выводимой вершины и третий определяет количество выводимых примитивов.


    Программно шаг вывода одного треугольника выглядит так:
    C++
    device->SetStreamSource( 0, VBuffer, 0, sizeof(MYVERTEX) ); device->SetFVF( MY_FVF ); device->DrawPrimitive( D3DPT_TRIANGLELIST , 0, 1 );
    Pascal
    device.SetStreamSource(0, VBuffer, 0, SizeOf(MyVertex)); device.SetFVF(MY_FVF); device.DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);


Библиотека Direct3D имеет богатый набор по способу отображения выводимых примитивов. Программист может указать нужный ему способ отображения полигонов с помощью задания режима воспроизведения. Существует три режима воспроизведения полигонов:
  1. режим вершинной модели определяет вывод только вершин полигонов без ребер и без закраски внутренних точек примитива и реализуется с помощью вызова метода SetRenderState(D3DRS_FILLMODE, D3DFILL_POINT) интерфейса IDirect3DDevice9;
  2. режим каркасной модели задает вывод только ребер полигонов без закраски внутренних точек примитива и реализуется с помощью вызова метода SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME) интерфейса IDirect3DDevice9;
  3. режим сплошной модели определяет вывод полигонов с закрашенными внутренними точками и реализуется с помощью вызова метода SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID) интерфейса IDirect3DDevice9.

Кроме того, сплошная модель вывода подразделяется на два вида закрашивания: плоское заполнение либо закрашивание с интерполяцией. Первое реализуется через вызов метода SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT), второе через SetRenderState(D3DRS_SHADEMODE, D3DSHADE_ GOURAUD).
Данные режимы воспроизведения можно представить с помощью следующей схемы.

Содержание раздела