DirectX Графика в проектах Delphi

       

Отрезки



Для рисования отрезков в Direct3D предусмотрены два типа примитивов: независимые отрезки и связанные отрезки. Начнем постижение этой темы с первого из этой пары типа примитивов.
Для построения независимых отрезков первым аргументом метода DrawPrimitive указывается константа D3DРТ_LINELISТ. По считываемым попарно из потока вершинам строятся отдельные, несвязанные, отрезки прямой.
Несложный пример из каталога Ех1б является иллюстрацией на эту тему. На экране строятся два отрезка красного цвета, параллельные друг другу. Координаты вершин хранятся в четырехэлементном массиве пользовательского типа TCUSTOMVERTEX. Массивы заполняются тривиальным образом: значения полей первых двух элементов определяют начало и конец первого отрезка, последние два элемента массива относятся ко второму отрезку.
Обратите внимание, что собственно при построении примитивов последним аргументом передается не количество вершин, а количество примитивов:

hRet := FD3DDevice. DrawPrimitive (D3DPT_LINELIST, 0, 2) ;

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

Константа D3DРТ_LINELISТ является признаком другого примитива - группы связанных отрезков. В этом случае вершины, считываемые из потока, задают характеристики вершин, последовательно соединяемых отрезками прямой.
В проекте каталога Ех17 создается пятиугольник (рис. 7.9), в построении которого используется пять связанных отрезков.

Рис. 7.9. Простой пример использования связанных отрезков

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

for i := 0 to 5 do
with VPoints [i] do begin
X := 150 + cos (Angle +1*2* Pi /5) * Radius;
Y := 150 + sin (Angle +i*2*Pi/5) * Radius;
end;

Обращаю внимание на параметры метода воспроизведения примитивов:

hRet := FD3DDevice.DrawPrimitive(D3DPT_LINESTRIP, 0, 5);


Надеюсь, остальной код вопросов у вас не вызывает.
Теперь нам стоит обсудить, как воспроизводить одновременно несколько независимых групп примитивов. Организовать такое воспроизведение можно разными способами: хранить вершины в одном буфере, либо использовать отдельные буферы для каждой группы вершин.
Разберем первый вариант на примере проекта каталога Ех18. На экране вращаются два многоугольника: пятиугольник и квадрат (рис. 7.10).





Рис. 7.10. Независимые группы примитивов

Массив vpoints хранит координаты 11 вершин: первые 6 связаны с пятиугольником, оставшиеся предназначены для построения квадрата.
Квадрат и Пентагон вращаются в противоположные стороны с различными скоростями:

for i := 0 to 5 do // Первыми хранятся координаты вершин Пентагона
with VPoints [i] do begin
X := 150 + cos (Angle + i * 2 * Pi / 5) * Radius;
Y := 150 + sin (Angle +i*2*Pi/5) * Radius;
end;
for i := 0 to 4 do // Координаты вершин квадрата
with VPoints [6 + i] do begin
// Скорость вращения квадрата удвоена
X := 150 + cos (- 2 * Angle - i * Pi / 2) * Radius / 2;
Y := 150 + sin (- 2 * Angle - i * Pi / 2) * Radius / 2;
end;

Собственно при построении к методу Drawprimitive обращаемся дважды, поскольку строим две независимые фигуры. Обратите внимание на значение второго аргумента метода:

hRet := FD3DDevice.DrawPrimitive(D3DPT_LINESTRIP, 0, 5);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3DDevice.DrawPrimitive(D3DPT_LINESTRIP, 6, 4);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;

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





Рис. 7.11. Сетка проекции трехмерной поверхности

Пример построен по очень простому алгоритму: 22 отрезка соединяют узлы сетки, угловые точки которой разбросаны случайно:

if for i := 0 to 10 do begin // Первый набор отрезков сетки
with VPoints [i * 2] do begin // Начало отрезка
X := XI + i * (X2 - XI) / 10; // Разбиение на 10 точек
Y := Yl + i * (Y2 - Yl) /10;
end;
with VPoints [i * 2 + 1] do begin // Конец отрезка
X := ХЗ + i * (X4 - X3) / 10;
Y := Y3 + i * (Y4 - Y3) / 10;
end;
end;
for i := 0 to 10 do begin // Второй набор отрезков сетки
with VPoints [i * 2 + 22] do begin
X := XI + i * (X3 - XI) / 10;
Y := Yl + i * (Y3 - Yl) / 10;
end;
with VPoints [i * 2 + 1 + 22] do begin
X := X2 + i * (X4 - X2) / 10;
Y := Y2 + i * (Y4 - Y2) / 10;
end;
end;

Угловые точки перемещаются с течением времени, отскакивая от границ области вывода. В примере после нажатия клавиши <Пробел> координаты этих точек заново инициализируются. Стоит сказать, что некоторые комбинации положений порождают очень интересные "поверхности".

Треугольник
Если первым аргументом метода DrawPrimitive указана константа D3DTP_ TRIANGLELIST, то каждая триада вершин, считываемых из потока, задает три вершины независимого треугольника.
Посмотрите проект каталога Ех21, простейший пример на тему построения треугольника. При работе программы на экране вращается треугольник красного цвета. Программа написана по той же схеме, что и предыдущие примеры: задаются координаты трех вершин треугольника, вызывается метод DrawPrimitive с соответствующими аргументами:

hRet := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

Треугольник является базовой фигурой для построений. Именно с его помощью и осуществляется большинство построений в Direct3D. Квадраты, прямоугольники и вообще все остальные фигуры рисуются из треугольников.
Посмотрите проект каталога Ех22, где из отдельных независимых треугольников строится пятиконечная звезда, плавно изменяющаяся в размерах по ходу своего вращения (рис. 7.12).



Рис. 7.12. Десять независимых треугольников образуют невыпуклый многоугольник



Код этого примера также не должен вызывать у вас трудностей при изучении, поэтому ограничусь лишь замечанием о том, что первые пять соприкасающихся треугольников образуют внутренний Пентагон, вторая половина примитивов создает лучи звезды.
Попутно с рассмотрением примитивов Direct3D отвлечемся немного на некоторые важные вопросы.
При необходимости, содержимое экрана воспроизведения может быть легко записано в растр или любой другой стандартный графический формат. Точно так же, как мы поступали с приложениями, использующими DirectDraw, для этого потребуется в канву вспомогательного объекта класса TBitmap скопировать с помощью функции BitBit содержимое канвы формы и вызвать метод записи в файл.
С созданием видео тоже проблем возникать не должно, поскольку в рассмотренном нами способе кадры создаваемого фильма представляют собой список объектов класса TBitmap, а при копировании в его канву содержимого формы, как я только что сказал, в 16-битном и выше режимах проблем не возникает.
Значение пикселов канвы формы согласуется с выводом, производимым DirectX, что можно применять для простейшего выбора объектов, похожего на использованный нами в DirectDraw. Если в этом примере при нажатии кнопки мыши требуется определить, что находится под курсором, то обработчик нужного события можно записать так:

procedure TfrmD3D.FormMouseDown(Sender: TObject; Button:
TMouseButton; Shift: TShiftState; X, Y: Integer);
var
R, G, В : Byte;
begin
R := GetRValue (Canvas.Pixels [X, Y]);
G := GetGValue (Canvas.Pixels [X, Y]);
В := GetBValue (Canvas.Pixels [X, Y] ) ;
if R = 0
then ShowMessage ('Под курсором звездочка')
else ShowMessage ('Под курсором фон')
end;

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



Color := D3DCOLOR_XRGB(0, 0, 254);

А внутренний Пентагон по- прежнему будем заполнять чистым синим цветом. Столь малая разница в оттенках зрителем совершенно не будет ощущаться и позволит нам точнее разделять две группы объектов для выбора:

if R = О then begin
if В = 255 // Чистым синий - у лучей звезды
then ShowMessage ('Под курсором луч')
else ShowMessage ('Под курсором Пентагон')
end
else ShowMessage ('Под курсором фон');

Аналогично, если надо различать выбор для каждого отдельного луча звезды, окрашиваем их в индивидуальные оттенки, по значению которых и ориентируемся в выборе пользователя. Таким образом, можно предлагать для выбора очень много комплексных или одиночных объектов, предел на их количество - чувствительность зрителя.
Позже мы вернемся к теме выбора объектов, а сейчас немного поговорим на тему закрашивания примитивов.
Посмотрите работу примера из каталога Ех23, возвращающего нас к предыдущему проекту с одиночным треугольником. Небольшое отличие в коде данного примера заключается в том, что вершины треугольника окрашены в различные чистые цвета. Интересно же в примере то, что цвета вершин треугольника интерполируются, отчего при окрашивании получается красивый градиентный переход (рис. 7.13).



Рис. 7.13. По умолчанию в DirectSD установлена закраска Гуро

Direct3D по умолчанию назначает закраску Гуро - быстрый алгоритм интерполяции цветов вершин треугольника. Также зарезервирована возможность использования закраски Фонга, но пока этот способ системой не поддерживается.
Поменять схему окрашивания или тонирования примитивов возможно с помощью знакомого уже метода задания режимов воспроизведения. Например, чтобы отказаться от интерполяции цветов, надо записать следующую строку:

FD3DDevice.SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FIAT);

В этом случае цвет первой вершины треугольника будет определять цвет всего примитива.
Вторым аргументом для указанного режима могут использоваться также константы D3DSHADE_COURAUD и D3DSHADE_PHONG. Второй случай пока аналогичен отказу от интерполяции.Еще одним режимом воспроизведения, на который необходимо обязательно обратить внимание, является режим D3DRS_FiLLMODE. По умолчанию действует твердотельный режим, примитивы выводятся заполненными. Этому режиму соответствует константа DSDFILL^SOLID. Для установления проволочного, каркасного режима воспроизведения необходимо вторым аргументом метода setRenderState задавать другую константу:



FD3DDevice.SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

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



Рис. 7.14. Иллюстрация проволочного режима воспроизведения

Если для этого режима использовать константу D3DFiLL_POiNT, то при воспроизведении станут выводиться только точки вершин примитивов.
Продолжаем изучать примитивы DirectSD. Группе связанных треугольников соответствует флаг DSDPTJTRIANGLESTRIP. Первые три вершины задают первый треугольник, вторая, третья и четвертая определяют второй треугольник, третья, четвертая и пятая - третий и т. д. Получается лента соприкасающихся треугольников (рис. 7.15).



Рис. 7.15. Принцип построения ленты треугольников по данным потока

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

VPoints : Array [0..Level + 1] of TCUSTOMVERTEX;

Для построения диска берем попарно точки, лежащие на внутренней и внешней границах диска:

i := 0;
repeat
with VPoints [i] do begin // Внутренняя граница диска
X := 150 + cos (Angle + i * 2 * Pi / Level) * Radius / 2;
Y := 150 + sin (Angle + i * 2 * Pi / Level) * Radius / 2;
Color := D3DCOLOR_XRGB(255, 0, 0); // Красного цвета
end;
with VPoints [i + 1] do begin // Внешняя граница диска
X := 150 + cos (Angle + i * 2 * Pi / Level) * Radius;
Y := 150 + sin (Angle + i * 2 * Pi / Level) * Radius;
Color := D3DCOLOR_XRGB(0, 0, 255); // Синего цвета
end;
Inc (i, 2); // Переходим к следующей паре вершин
until i > Level;

Окончательное решение задачи можете посмотреть в каталоге Ех25, результат работы которого в проволочном режиме представлен на рис. 7.16.





Рис. 7.16. Диск строится лентой треугольников

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

type
TRGB = packed record // Запись цвета
R, G, В : BYTE;
end;
const
Size =2; // Размер отдельного квадратика, "пиксела"
Fade =4; // Степень затухания пламени
NumX = 150; // Количество квадратиков по горизонтали
NumY = 150; // Количество квадратиков по вертикали
var
Fire : Array [L.NumX, L.NumY + 1] of TRGB; // Цвета узлов сетки
PreF : Array [L.NumX] of TP.GB; // Вспомогательный массив первой строки
Angle : Single = 0.0; // для движения падающей точки
ParticleX : Integer =0; // Координаты точки
ParticleY : Integer = NumY;

Следующая пользовательская функция выводит один квадрат, цвета углов которого задаются текущими значениями элементов массива Fire:

function TfrmDSD.DrawPix(const inX, inY : Integer) : HRESULT;
var
pVertices : PByte;
hRet : HRESULT;
begin
with VPoints [0] do begin // Левый нижний угол квадрата
X := inX * Size;
Y := 300 - inY * Size; // Переворачиваем ось Y
Color := D3DCOLOR_XRGB(Fire[inX, inY + 1].R, Fire[inX, inY + 1].G,
Fire[inX, inY + 1].B);
end;
with VPoints [1] do begin // Левый верхний угол квадрата
X := inX * Size;
Y := 300 - (inY + 1) * Size;
Color := D3DCOLOR_XRGB(Fire[inX, inY].R, Fire[inX, inY].G,
Fire[inX, inY].B); end; with VPoints [2] do begin // Правый нижний угол квадрата
X := (inX + 1) * Size;
Y := 300 - inY * Size;
Color := D3DCOLOR_XRGB(Fire[inX + 1, inY + 1].R, Fire[inX + 1,
inY + 1].G, Fire[inX + 1, inY + 1].B);
end;
with VPoints [3] do begin // Правый верхний угол квадрата
X := (inX + 1) * Size;
Y := 300 - (inY + 1) * Size;
Color := D3DCOLOR_XRGB(Fire[inX + 1, inY].R, Fire[inX + 1, inY].G,
Fire[inX + 1, inY].B);
end;
hRet := FD3DVB.Lock(0, SizeOf(VPoints), pVertices, 0];
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Move (VPoints, pVertices^, SizeOf(VPoints));
hRet := FD3DVB.Unlock;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
Result := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
end;



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

procedure TfrmD3D.DrawFire;
i, j : Integer;
f : Byte;
begin
// Инициализация последней строки экрана
for i := 2 to NumX-1 do begin
f := random(255) ;
PreF[i].R := 255;
PreF[i].G := trunc (f / 1.4);
PreF[i] . := f div 2;
end;
// Заполняем в массиве Fire последнюю строку
// усредненными значениями соседних элементов
PreF '" for i := 2 to NumX - 1 do begin
Fire[i, 1}.R := (PreF[i - 1] .R 4- PreF[i 4- 1} .R + PreF[i] .R) div 3; $; Fire[i, 1].G := (PreF[i - 1] .G + PreF[i + 1] .G + PreF[i] .G) div 3; Fire[i, 1].B := (PreF[i - 1].B + PreF[i + 1].B + PreF[i].B) div 3; end;
// Смешивание, усреднение значений пикселов по экрану for j := NumY - 1 downto 2 do for i := 2 to NumX - 1 do begin
Fire[i,j].R := (Fire[i-1, j].R + Fire[i+1, j].R + Fire[i,j].R +
Fire[i-1, j-1].R + Fire[i+1, j-1].R +
Fire[i, j-1].R) div 6;
Fire[i,j].G := (Fire[i-1, j].G + Fire[i+1, j].G + Fire[i,j].G +
Fire[i-1, j-1].G + Fire[i+l, j-1].G +
Fire[i, j-1].G) div 6;
Fire[i,j].B := (Fire[i-1, j].B + Fire[i+1, j].B +
Fire[i,j].B + Fire[i-1, j-1].B + Fire[i+1, j-1].B +
Fire[i, j-1].B) div 6;
end;
// Квадратик, соответствующий падающей частице for j := ParticleY - 1 to ParticleY do
for j := ParticleX - 1 to
ParticleX do begin
Fire[i, j].R := 255;
Firefi, j].G := 0;
Fire[i, j].B := 0;
end;
// Вывод квадратиков содержимого экрана
for j := 2 to NumY - 1 do
for i := 2 to NumX - 1 do
DrawPix (i - 1, j - 1) ;
// Затухание оттенков по мере подъема языков пламени
for j := NumY downto 2 do
for i := 1 to NumX do begin
if Fire[i, j - 1J.R >= Fade
then Firefi, j].R = Firefi, j - 1].R- Fade
else Firefi, j].R = 0;
if Firefi, j - 1].G >= Fade
then Firefi, j].G = Firefi, j - 1].G - Fade
else Firefi, j].G = 0;
if Firefi, j - 1].B >= Fade
then Firefi, j].B = Firefi, j - 1].B - Fade
else Firefi, j].B = 0;
end;
end;



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



Рис. 7.17. Принцип использования вершин потока для примитива D3DPT TRIANGLEFAN

По такой схеме удобно строить конусы и пирамиды, а для плоскостных построений - выпуклые многоугольники, эллипсы и окружности. Приведу тривиальный пример на этот случай (проект из каталога Ех27).
Значение константы Level задает степень разбиения полного круга, количество вершин нам требуется на пару больше этого значения:

const
Level = 255;
var
VPoints : Array [0..Level + 1] of TCUSTOMVERTEX;

Нервая вершина массива хранит координаты центральной точки круга, все детальные равномерно располагаются на его границе:

const
Step = 2 * Pi / Level;
with VPoints [0] do begin // Первая точка - центр круга
х := 150;
Y := 150;
Color := D3DCOLOR_XRGB(0, 0, 0);
end;
If for i := 1 to Level + 1 do // Точки на краю круга
with VPoints [i] do begin
X := 150 + cos (Angle + i * Step) * Radius;
Y := 150 + sin (Angle + i * Step) * Radius;
Color := D3DCOLOR_XRGB(0, trunc(i * 255 / Level), 0);
end;

Для каждой вершины последовательно увеличивается вес зеленой составлявшей цвета. Для последней точки он принимает максимальное значение при произвольной величине константы Level. Градиент зеленого цвета я взял для того, чтобы получить в итоге некое подобие экрана радара (рис. 17.8).



Рис. 7.18. Пример использования примитива D3DPT TRIANGLEFAN

Оконные приложения, использующие Direct3D, безболезненно переживают ситуации потери фокуса и восстановления, но осталась еще одна исключительная ситуация - спящий режим. Возврат из этого режима гарантированно приведет к потере способности воспроизведения нашим приложением.
Для предупреждения таких исключений можно воспользоваться методом TestcooperativeLevel объекта устройства. Метод возвращает значение D3DERR_DEvicELOST в ситуации, когда устройство вывода недоступно, например, в спящем состоянии. Другое, кроме успешного, возвращаемое методом значение - DSDERF^DEVICENOTRESET, соответствует ситуации, когда устройство, в принципе, готово, но воспроизведение невозможно.
На примере этого проекта рассмотрим, как пользоваться данным методом, чтобы оконные приложения смогли пережить спящий режим. Код обработчика цикла ожидания приведите к следующему виду:

if FActive then begin Inc (Frames);
// Определяем состояние устройства
hRet := FD3DDevice.TestcooperativeLevel;
if hRet = D3DERR_DEVICELOST
// Сейчас устройство не готово, воспроизведение невозможно
then Exit
// Выход из спящего режима
else if Failed(hRet) then begin
// Заново инициализируем систему InitDSD;
InitPoints;
end;
// Воспроизведение осуществляем без проверки исключений
Render;
...

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


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