Глава 8 Подробнее о библиотеке Direct3D

Частичная прозрачность
Альфа-составляющая цвета
Размытие при движении
Работа с переменным числом вершин
Текстура
Текстурные координаты
Альфа-составляющая текстуры
Мультитекстурирование
Цветовой ключ текстур
Спрайты в Direct3D
Что вы узнали в этой главе

Материал данной главы более подробно освещает вопросы, связанные с программированием графики с применением библиотеки Direct3D, и предлагает описание основных приемов ее использования.
Примеры к главе располагаются в каталоге \Examples\Chapter08.
Частичная прозрачность
В этом небольшом разделе нам предстоит подойти к изучению одного из важнейших механизмов Direct3D.
Взгляните на работу примера из каталога Ex01: на фоне звездного неба в разных направлениях вращаются полупрозрачные квадрат и треугольник (рис. 8.1).

Рис. 8.1. Пример на тему частичной прозрачности примитивов

Нововведение в примере, помимо эффекта полупрозрачности, заключается в присутствии разнородных объектов, примитивов различных типов. Вершины объектов хранятся в трех различных массивах:

var
VPoints : Array [0..500] of TCUSTOMVERTEX; // 501 точка
VTriangle : Array [0..2] of TCUSTOMVERTEX; // Отдельный треугольник
VQuad : Array [0..3] of TCUSTOMVERTEX; // Вершины квадрата

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

function TfrmD3D.InitPoints : HRESULT;
var
hRet : HRESULT;
i : Integer; wrkDWORD : DWORD;
begin
Randomize;
// Инициализация массива точек
for i := 0 to High (VPoints) do begin
wrkDWORD := random (200) + 40; // Интенсивность трех весов with
VPoints [i] do begin
X := random (ScreenWidth) ; // Координаты - случайно по всему
Y := random (ScreenHeight) ; // экрану
Z := 0.0;
RHW := 1.0;
Color := D3DCOLOR_XRGB (wrkDWORD, wrkDWORD, wrkDWORD);
end;
end;
// Инициализация массива вершин треугольника
for i := 0 to High (VTriangle) do with VTriangle [i] do begin
Z := 0.0;
RHW := 1.0;
end;
// Цвета вершин треугольника
VTriangle [0]. Color := D3DCOLOR_XRGB (255, 0, 0) ;
VTriangle [1]. Color := D3DCOLOR_XRGB (0, 255, 0) ;
VTriangle [2]. Color := D3DCOLOR_XRGB (0, 0, 255);
// Инициализация массива вершин серого квадрата
for i := 0 to High (VQuad) do with VQuad [i] do begin Z := 0.0;
RHW := 1.0;
I Color := D3DCOLOR_XRGB (100, 100, 100);
end;
// Создаем буфер вершин, размер опирается на размер массива точек
hRet := FD3DDevice.CreateVertexBuffer(SizeOf(VPoints),
D3DUSAGE_WRITEONLY, D3DFVF_CUSTOMVERTEX,
D3DPOOL_DEFAULT, FD3DVB);
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Связываем поток
hRet := FDSDDevice.SetStreamSource(0, FD3DVB, SizeOf(TCUSTOMVERTEX));
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Задаем шейдер вершин
Result := FD3DDevice.SetVertexShader(D3DFVF_CUSTOMVERTEX);
end;

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

function DrawPoints : HRESULT;
function Draw2DTriangle (const inX, inY : Single) : HRESULT;
function TfrmD3D.Draw2DQuad (const inX, inY : Single) : HRESULT;

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

Result:=FD3DDevice.DrawPrimitive(D3DPT_POINTLIST, 0, High (VPoints) + 1);

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

const
HalfPi = Pi / 2; // Небольшая оптимизация
function TfrmD3D.Draw2DQuad (const inX, inY : Single) : HRESULT;
var
pVertices : PByte;
hRet : HRESULT;
begin
with VQuad [0] do begin // Левый нижний угол квадрата
X := inX + Radius * cos (Angle / 2 - HalfPi);
Y := inY + Radius * sin (Angle / 2 - HalfPi);
end;
with VQuad [1] do begin // Левый верхний угол квадрата
X := inX + Radius * cos (Angle /2);
Y := inY + Radius * sin (Angle / 2);
end;
with VQuad [2] do begin // Правый нижний угол квадрата
X := inX + Radius * cos (Angle / 2 + Pi);
Y := inY + Radius * sin (Angle / 2 + Pi);
end;
with VQuad [3] do begin // Правый верхний угол квадрата
X := inX + Radius * cos (Angle / 2 + HalfPi);
Y := inY + Radius * sin (Angle / 2 + HalfPi);
end;
// Заполняем вершинный буфер данными о вершинах квадрата
hRet := FD3DVB.Lock(0, SizeOf(VQuad), pVertices, 0);
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Move (VQuad, pVertices", SizeOf(VQuad));
hRet := FD3DVB.Unlock;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Квадрат образован двумя связанными треугольниками
Result := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
end;

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

function TfrmD3D.Render : HRESULT;
var
hRet : HRESULT;
begin
if FD3DDevice = nil then begin Result := E_FAIL;
Exit;
end;
// Очистка экрана и окрашивание его в черный цвет
hRet := FD3DDevice.Clear(0, nil, D3DCLEAR_TARGET, 0, 0.0, 0);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FDSDDevice.BeginScene;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Первыми рисуются точки фона
hRet := DrawPoints;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Включаем режим полупрозрачности
with FDSDDevice do begin
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(True));
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
end;
// Полупрозрачный квадрат
hRet := Draw2DQuad (HalfScreenWidth, HalfScreenHeight);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Полупрозрачный треугольник
hRet := Draw2DTriangle (HalfScreenWidth, HalfScreenHeight);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Режим полупрозрачности больше не нужен
FDSDDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(False));
hRet := FDSDDevice.EndScene;
if FAILED(hRet) then begin
Result := hRet;
Exit ;
end;
Result := FDSDDevice.Present(nil, nil, 0, nil) ;
end;

Чтобы примитив оказался полупрозрачным, необходимо, во-первых, включить режим D3DRS_ALPHABLENDENABLE. Вторым аргументом для этого режима
задается преобразованная в DWORD (или cardinal) булева константа. Для простоты можно использовать просто нуль или единицу. И во-вторых, следует задать параметры источника и приемника. Для простейшего случая, когда все примитивы одинаково полупрозрачны, этим параметром назначается константа DSDBLEND ONE.

Совет
В случае одинаковой полупрозрачности примитивов достаточно задавать параметр приемника, значение для DSDRS^SRCBLEND по умолчанию установлено В D3DBLEND_ONE.
При включенном режиме ОЗОЕЗ_АЪРНАВЬЕЫОЕЫАВЬЕ примитивы как бы рисуются на кальке: в местах наложения листов кальки плотность цветовых компонентов увеличивается.
Для оптимизации сразу же после того, как нарисованы все полупрозрачные примитивы, режим полупрозрачности необходимо отключить.

Альфа-составляющая цвета
Рассмотренный в предыдущем разделе пример полупрозрачности является частным случаем работы с четвертой составляющей цвета, так называемым альфа-компонентом или альфа-составляющей цвета. Этот компонент позволяет регулировать степень прозрачности. По умолчанию установлено значение 255, т. е. примитивы совершенно не прозрачны. Нулевое значение имитирует полную прозрачность. Все промежуточные значения соответствуют градации прозрачности.
Рассмотрим простой пример на эту тему (проект каталога Ех02), где на фоне непрозрачного красного квадрата вращается сложная геометрическая композиция зеленого цвета, которую для простоты назовем звездой (рис. 8.2).

Рис. 8.2. В этом примере степень прозрачности примитива меняется со временем

Звезда сначала абсолютно прозрачна, но со временем становится плотнее. Цвета квадрата и звезды накладываются точно, образуя оттенки желтого, пока звезда не станет совершенно непрозрачной.
Вспомогательная переменная Alpha последовательно принимает значения от 0 до 255 и используется в функции рисования звезды, при задании цветовой составляющей вершин:

for i := 0 to High (VStar) do
VStar [i].Color := D3DCOLOR_ARGB(Alpha, 0, 255, 0) ;

Обратите внимание, что теперь для задания цвета вершины используется функция D3DCOLOR_ARGB, аргументом которой является четверка чисел. Первый аргумент - значение альфа-компонента.
При перерисовке кадра режим альфа-смешения устанавливается только на время работы функции рисования звезды из соображений оптимальности:

with FDSDDevice do begin
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(True));
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
end;
hRet := DrawStar; if FAILED(hRet) then begin
Result := hRet;
Exit; end; FDSDDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (False));

Для режима регулируемой прозрачности значение степени прозрачности источника определяется текущим значением альфа-составляющей цвета вершин. Режиму соответствует константа D3DBLEND_SRCALPHA. В данном режиме каждый чистый цвет источника имеет коэффициент прозрачности, равный значению альфа-составляющей. Для режима смешения цветов режим приемника задается константой DSDBLEND^NVSRCALPHA так, что суммарно получается единица - итоговый пиксел совершенно непрозрачный:

- D3DBLEND__SRCALPHA: коэффициент смешения (Aj, AS, As, AS);
- D3DBLEND_INVSRCALPHA: Коэффициент смешения (1-Aj, 1~ AS, 1-AS, 1~AS).

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

type
TRGB = packed record // Запись тройки цветов
R, G, В : Byte;
end;
TDrip = class // Класс отдельного пятна
PosX, PosY : Integer; // Координаты на экране
Ring_Color : TRGB; // Цвет пятна
Ring_Radius : Integer; // Текущий радиус пятна
end;
const
Level =36; // Уровень разбиения круга
Max Drips = 120; // Максимум присутствия пятен в окне
Max_Ring_Radius = 100.0; // Максимальный радиус пятна var
VCircle : Array [0..Level + 1] of TCUSTOMVERTEX; // Вершины круга
First_Drip, New_Drip : Integer; // Счетчики пятен
Drips : Array [0..Max_Drips - 1] of TDrip; // Массив пятен

При каждом нажатии кнопки мыши создается новое пятно. Координатами его центра выступают текущие координаты курсора:

procedure Create_Drip(const inX, inY : Integer; const R, G, В : Byte);
begin
// Создание нового пятна
Drips [New_Drip] := TDrip.Create;
with Drips[New_Drip] do begin
Ring_Color.R := R;
Ring_Color.G := G;
Ring_Color.B := B;
Ring_Radius := 0;
PosX := inX;
PosY := inY;
end;
// Увеличение счетчиков
New_Drip := (New_Drip + 1) mod Max_Drips;
// Достигнут предел по количеству пятен на экране
if New_Drip = First_Drip
then First_Drip := (First_Drip + 1) mod Max_Drips;
end;
procedure TfrmD3D.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
Create_Drip(X,Y,random(lSO)+105,random(150)+105, random(150)+105);
end;

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

function TfrmD3D.DrawCircle (const inX, inY, inRadius : Integer;
const Ring_Color : TRGB) : HRESULT;
const
Step = 2 * Pi / Level;
var
pVertices : PByte;
hRet : HRESULT; i : Integer;
begin
// Первая точка - центр круга
with VCircle [0] do begin
X := inX;
Y := inY;
// Точка центра совершенно непрозрачна
Color := D3DCOLOR_ARGB(255, Ring_Color.R, Ring_Color.G, Ring_Color.B);
end;
// Точки края круга абсолютно прозрачны
for i := 1 to Level + 1 do
with VCircle [i] do begin
X := VCircle [0].X + cos (i * Step) * inRadius;
Y := VCircle [0].Y + sin (i * Step) * inRadius;
Color := D3DCOLOR_ARGB(0, Ring_Color.R, Ring_Color.G, Ring_Color.B);
end;
hRet := FD3DVB.Lock(0, SizeOf(VCircle), pVertices, 0) ;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Move (VCircle, pVerticesA, SizeOf(VCircle));
hRet := FD3DVB.Unlock;
if Failed (hRet) then begin
Result := hRet;
Exit ;
end;
Result := FD3DDevice.DrawPrimitive(D3DPTJTRIANGLEFAN, 0, Level);
end;

При перерисовке кадра отображается круг для каждого существующего пятна:

i := First_Drip;
// Цикл по всем присутствующим на экране пятнам
while i <> New_Drip do begin
// Каждое пятно увеличивается в размерах
Drips [ i ]. Ring_Radius := Drips [i] .Ring_Radius + 1;
DrawCircle (Drips [i] . PosX,
Drips [i] .PosY,
Drips [i] .Ring_Radius,
Drips [i] .Ring_Color) ;
// Пятно достигло максимального размера, поэтому исчезает с экрана
if Drips [i] .Ring_Radius > Max_Ring_Radius
then First_Drip := (First_Drip + 1) mod Max_Drips;
i := (i+1) mod Max_Drips;
end;

Посмотрите работу данного примера для случаев, когда альфа-компоненты для центра и краев круга одинаковы и равны небольшой константе, или когда для точки центра таким значением берется 255, а для точек края - значение радиуса круга.
Размытие при движении
Манипулированием альфа-смешением можно легко получить эффект размытия при движении, когда быстро движущиеся объекты оставляют за собой след. Обычно такой эффект называется motion blur. Для получения эффекта в местах предыдущего положения объекта рисуют его полупрозрачную копию. Более простой способ создания эффекта - манипулирование цветовой интенсивностью.
Оба способа используются в проекте каталога Ех04, где на экране быстро вращаются два круга: желтый оставляет полупрозрачный след, а за красным тянется постепенно угасающий след (рис. 8.3).

Рис. 8.3. Эффект размытия при движении

Рисование каждого круга сводится к выводу ряда близко расположенных примитивов:

wrkGreen := 5;
for i := 1 to 10 do begin // 10 зеленых треугольников фона
wrkGreen := wrkGreen + 25;
hRet := DrawTriangle (ScreenWidth - (i + 1) * (ScreenWidth div 11),
ScreenWidth div 6, wrkGreen);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
end;
wrkAngle := Angle;
wrkAlpha := 5; // Круги рисуются, начиная с самого прозрачного
with FDSDDevice do begin
SetRenderState(D3DRS__ALPHABLENDENABLE, DWORD(True));
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
end;
for i := 1 to 10 do begin // 10 желтых кругов различной
wrkAngle := wrkAngle +0.04; // прозрачности
wrkAlpha := wrkAlpha + 25;
hRet := DrawYellowCircle (wrkAngle, wrkAlpha);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
end;
FDSDDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(False));
wrkAngle := Angle + Pi;
wrkRed := 5; // Степень насыщенности красного
for i := 1 to 10 do begin // 10 красных кругов
wrkAngle := wrkAngle + 0.04;
wrkRed := wrkRed + 25;
hRet := DrawRedCircle (wrkAngle, wrkRed);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
end;

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

Работа с переменным числом вершин
Мы уже хорошо освоились в построениях фигур с помощью Direct3D и в этом небольшом разделе попробуем развить наши навыки и узнать некоторые новые для нас вещи.
Как выяснилось из многочисленных предыдущих примеров, при использовании FVF-флага DSDFVF_XYZRHW в своих построениях мы опираемся на систему координат, ассоциированную с окном. Теперь нам предстоит постичь смысл еще одного флага: D3DFVF__XYZ. При его применении система координат экрана воспроизведения выглядит так: центру окна, независимо от его размеров, соответствует точка с координатами (0, 0), правому верхнему углу окна - (1, 1), левому нижнему углу - (-1, -1).
Пока мы ограничимся такой трактовкой этого флага, а позже узнаем о его особенностях кое-что дополнительно. Сейчас же для нас важно, что при использовании этого FVF-флага мы не сможем окрашивать вершины так, как привыкли это делать, и временно можем использовать только черно-белые картинки.
На примере проекта каталога Ех05 попробуем закрепить знания об этом флаге и попутно решим еще одну задачу: научимся работать с переменным числом вершин.
Самое простое решение задачи состоит, конечно, в том, чтобы задавать размер буфера вершин максимальным. Но такой прием приводит к неэффективному расходу памяти.
Во время работы программы на экране появляются узоры, образуемые отрезками, соединяющими равномерно расположенные точки на окружности. Число узлов меняется с течением времени случайно (рис. 8.4).

Рис. 8.4. Пример с произвольным числом вершин

Текущее значение переменной numpoints хранит число узлов. В периодически вызываемой функции initve не используется массив вершин, а применяется единственная переменная - указатель на структуру TCustomVertex. В этой структуре, в отличие от предыдущих примеров, отсутствует поле color, а описание формата данных вершины сократилось до одной константы:

const
D3DFVF CUSTOMVERTEX = D3DFVF XYZ;

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

function TfrmD3D.InitVB : HRESULT;
const
Pi2 = 2 * Pi; // Для сокращения числа операций
var
Vertices : ^TCustomVertex; // Указатель на запись вершины
i, j, k : Byte;
hRet : HRESULT;
begin
numPoints := random (7) + 3; // Генерация количества узлов
k := 0; // Подсчет количества отрезков, образующих узор
for i := 1 to numPoints do
for j := i + 1 to numPoints do begin Inc(k);
end;
numbines := k; // Используется в DrawPrimitive
// Создание буфера вершин нужного размера
hRet := FD3DDevice.CreateVertexBuffer(2 * k * SizeOf(TCustomVertex), 0,
D3DFVF__CUSTOMVERTEX, D3DPOOL_DEFAULT, FD3DVB) ;
if Failed(hRet) then begin
Result := hRet; Expend;
// Заполнение буфера
hRet := FD3DVB.Lock(0,2 * k * SizeOf(TCustomVertex), PByte(Vertices), 0);
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Перебор точек узлов
for i := 1 to numPoints do
for j := i + 1 to numPoints do begin
// Начало отрезка, точка на окружности радиусом 0.5
Vertices.X := 0.5 * cos(Pi2 * i / numPoints);
Vertices.Y := 0.5 * sin(Pi2 * i / numPoints);
Vertices.Z := 0;
Inc(Vertices); // Сдвигаем указатель
// Конец отрезка
Vertices.X :=. 0.5 * cos(Pi2 * j / numPoints);
Vertices.Y := 0.5 * sin(Pi2 * j / numPoints);
Vertices.Z := 0; Inc(Vertices);
end;
hRet := FD3DVB.Unlock;
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Заново устанавливаем поток
hRet := FDSDDevice.SetStreamSource(0, FD3DVB, SizeOf(TCUSTOMVERTEX));
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Задаем вершинный шейдер
Result := FDSDDevice.SetVertexShader(D3DFVF_CUSTOMVERTEX);
end;

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

function TfrmDSD.InitVB : HRESULT;
var
Vertices : ^TCustomVertex;
hRet : HRESULT;
begin
hRet := FD3DDevice.CreateVertexBuffer(3 * SizeOf(TCustomVertex), 0,
D3DFVF_CUSTOMVERTEX,D3DPOOL_DEFAULT, FD3DVB);
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3DVB.Lock(0, 3 * SizeOf(TCustomVertex), PByte(Vertices), 0);
if Failed(hRet) then begin
Result := hRet;
Exit ;
end;
Vertices.X =0.0;
Vertices.Y = 0.0;
Vertices.Z = 0;
Inc(Vertices);
Vertices.X = 0.0;
Vertices.Y = 0.5;
Vertices.Z = 0;
Inc(Vertices) ;
Vertices.X =0.5;
Vertices.Y = 0.5;
Vertices.Z =0;
hRet := FD3DVB.Unlock;
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3DDevice.SetStreamSource(0, FD3DVB, SizeOf(TCUSTOMVERTEX));
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Result := FDSDDevice.SetVertexShader(D3DFVF_CUSTOMVERTEX);
end;

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

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

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

FD3DDevice.SetRenderState(D3DRS CULLMODE, D3DCULL NONE);

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

Текстура
Теперь нам предстоит изучить одну из важнейших тем - использование растровых образов. В Direct3D имеется несколько типов (стилей) текстур. Мы изучим текстуру, подобную наклеиваемым обоям.
Как всегда, для изучения нового понятия нам потребуется познакомиться с новыми типами объектов и интерфейсов. И как обычно для этой книги, знакомство осуществим на конкретном примере. Сейчас им послужит проект каталога Ех06. Работа примера очень проста: на экране выводится содержимое растрового файла - картинка с изображением дискеты (рис. 8.5).

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

Кратко смысл программы можно описать так: на два связанных треугольника, образующих квадрат, накладывается квадратная текстура.
В списке переменных добавилась еще одна, связанная с используемым СОМ-объектом:

FD3Texture : IDIRECT3DTEXTURE8;

В начале работы ее значением устанавливается nil, а при завершении работы перед окончательным освобождением памяти вызывается метод _Reiease этого объекта.
Формат данных вершины, помимо пространственных координат, содержит еще две, связанные с наложением текстуры:

type
TCUSTOMVERTEX = packed record
X, Y, Z : Single;
U, V : Single; // Новая пара координат в формате вершины
end;
const
D3DFVF_CUSTOMVERTEX = D3DFVF_XYZ or D3DFVF_TEX1; // Новая константа

Итак, для наложения текстуры на объект для каждой вершины должны указываться текстурные координаты. Сейчас мы используем двумерную текстуру. Она представляет собой прямоугольный массив данных. Для такой текстуры в формате вершин необходимо задавать две координаты, обычно называемые U и V. Первая из этих координат ассоциирована с горизонтальной осью текстуры, вторая, V-координата - с вертикальной. То есть для вершины, связываемой с левым нижним углом текстуры, оба эти значения должны быть нулевыми, а для вершины, к которой приклеивается правый верхний угол текстуры, оба эти значения должны быть единичными. Обращаю внимание, что эти координаты никак не связаны с пространственными координатами вершин и примитива, сам примитив не обязан иметь единичные размеры.
Константа DSDFVFJTEXI является указанием на то, что координаты текстуры задаются именно парой чисел, максимальным может быть девять координат.
При инициализации буфера вершин используем тот же прием, что и в предыдущем примере, т. е. обходимся без вспомогательного массива. Но инициализация буфера производится один раз, в самом начале работы приложения.
Квадрат располагаем в центре экрана:

function TfrmD3D.InitVB : HRESULT;
var
Vertices : ^TCustomVertex;
hRet : HRESULT;
begin
// Буфер вершин на четыре вершины квадрата
hRet := FD3DDevice.CreateVertexBuffer(4 * SizeOf(TCustomVerrex), 0,
D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, FD3DVB);
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Устанавливаем поток
hRet := FD3DDevice.SetStreamSource(0, FD3DVB, SizeOf(TCustomVertex));
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Задаем шейдер вершин
hRet := FD3DDevice.SetVertexShader(D3DFVF_CUSTOMVERTEX);
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Заполняем буфер данными
hRet := FD3DVB.Lock(0, 4 * SizeOf(TCustomVertex), PByte(Vertices), 0),
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Левый нижний угол квадрата
Vertices.X = -0.5; // Координата на листе
Vertices.Y = -0.5;
Vertices.Z = 0; // Левый нижний угол текстуры
Vertices.U = 0;
Vertices.V = 0;
Inc(Vertices); // Переходим к следующей вершине
Vertices.X = -0.5; // Левый верхний угол квадрата
Vertices.Y = 0.5;
Vertices.Z = 0;
Vertices.U = 0;
Vertices.V = 1;
Inc(Vertices);
Vertices.X = 0.5; // Правый нижний угол квадрата
Vertices.Y = -0.5;
Vertices.Z = 0;
Vertices.U = 1;
V ertices.V = 0;
I nc(Vertices) ;
Vertices.X =0.5; // Правый верхний угол квадрата
Vertices.Y = 0.5;
V ertices.Z = 0;
V ertices.U = 1;
V ertices.V = 1;
R esult := FD3DVB.Unlock;
end;
Текстура создается с помощью отдельной функции, единственным аргументом которой является имя файла-прототипа:

function TfrmD3D.InitTexture (const FileName : String) : HRESOLT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT; // Вспомогательная запись
dwDstPitch : DWORD; // Шаг поверхности текстуры
X, Y : DWORD;
Bmp : tBitmap;
R, G, В : Byte;
begin
Bmp := TBitmap.Create;
Bmp.LoadFromfile (FileName);
// Создание объекта текстуры
hRet := FD3DDevice.CreateTexture (Bmp.Width, Bmp.Height, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, FD3Texture);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Запираем поверхность текстуры FD3Texture.LockRect(0, d3dlr, nil, 0);
dwDstPitch := d3dlr.Pitch; // Запоминаем шаг поверхности
// Заполняем поверхность данными из растра
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do begin
R := GetRValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y]);
G := GetGValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y]);
В := GetBValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y]);
PDWORD (DWORD(d3dlr.pBits)+Y*dwDstPitch + X * 4)^:=
D3DCOLOR_XRGB(R,G, B);
end;
Bmp.Free;
// Отпираем поверхность текстуры
Result := FD3Texture.UnlockRect(0);
end;

Первые два аргумента метода CreateTexture объекта устройства - ширина и высота создаваемой текстуры. Каждое из этих чисел должно быть степенью двойки. Это очень важное правило, не пропустите его. Растр может быть любого размера, но поверхность текстуры произвольные размеры иметь не может. Если необходимо использовать растр, размеры которого не равны степени двойки, его следует масштабировать, используя те же приемы, которые мы рассмотрели в нескольких примерах на тему применения DirectDraw.
Следующие два параметра метода для нас не важны, а вот на пятый аргумент, формат пиксела, надо обратить внимание. Выбор формата поверхности текстуры оставлен за разработчиком, который сам должен решить, какое значение из предлагаемого набора для него наиболее всего подходит. Для текстур, представляющих собой обычные растры, самым удобным является 32-битный формат, в котором на каждый пиксел приходится четверка чисел. Константа, соответствующая такому формату - D3DFMT_А8R8G8В8. Конечно, можно использовать и другие форматы, например "5-6-5", но при манипуляции с пикселами поверхности необходимо учитывать сделанный выбор.
Последними аргументами метода CreateTexture являются константа, отражающая пожелание разработчика о месте расположения поверхности текстуры, и собственно имя создаваемого объекта.
После того как объект текстуры создан, необходимо заполнить его поверхность. Как видим из кода, порядок действий здесь похож на манипуляции, производимые с поверхностями в DirectDraw: поверхность вначале запирается, и мы получаем информацию о ее шаге и адресе, по которому она располагается. После того как поверхность заполнена, она должна быть разблокирована.
Вторым аргументом метода LockRect, запирающего поверхность текстуры, должна передаваться величина типа ТD3DLОСКЕD_RЕСТ, вспомогательная запись из двух полей: шаг, ширина поверхности и адрес поверхности в памяти.
Заполняем поверхность текстуры тривиальным образом, сообразно с пикселами загруженного растра. Ось Y при этом переворачиваем, присутствующее здесь преобразование типа в DWORD совсем не обязательно, его я осуществляю только для того, чтобы предотвратить ненужные предупреждения компилятора.
Адресация ячейки пиксела текстуры аналогична тому, что мы производили в DirectDraw: опираемся на шаг поверхности, который не обязательно равен ее ширине. Значение X умножается на 4, т. е. на размер одной ячейки. Число это обусловлено выбранным форматом пиксела.
После того как поверхность текстуры заполнена и разблокирована, необходимо задать параметры ее использования и назначить текущей. Из соображений оптимизации рекомендуется устанавливать текстуру только на время непосредственного ее использования:

with FD3DDevice do begin
SetTexture(0, FD3Texture); // Задаем текущую текстуру
SetTextureStageState(0, D3DTSS_COLOROP, D3DTA_TEXTURE);
end;
// Квадрат, покрытый текстурой
hRet := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Делаем текстуру недействительной
FD3DDevice.SetTexture(0, nil);

Чтобы задать текущую текстуру, необходимо вызвать метод SetTexture объекта устройства, вторым аргументом передается нужный объект текстуры или nil в случае, если текстура больше не используется. Представленное за этим действие следует понимать как задание правил операций с цветом при работе с текстурой, значение цвета для каждого пиксела определяется содержимым поверхности текстуры.
Методы SetTexture и SetTextureStageState должны вызываться в установленном состоянии воспроизведения, после вызова метода BeginScene. Также помните о том, что блоки установок могут содержать вызовы этих методов.
Итак, текстура является приклеенным к примитиву образом, который масштабируется и поворачивается вслед за ним. В проекте каталога Ех07 квадрат, покрытый текстурой, вращается, а нажатием клавиш <Insert> и <Delete> можно манипулировать его размерами (рис. 8.6).

Рис. 8.6. Текстура поворачивается и растягивается вслед за положением вершин примитива

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

Текстурные координаты
Быстро выводить растровое изображение с помощью DirectDraw мы уже давно научились. Теперь же должны посмотреть все возможности, которые предоставляются нам Direct3D, и то, что сделать раньше мы могли, только затрачивая титанические усилия.
Например, если в предыдущих примерах единичные значения текстурных координат заменить на 3, образ будет повторяться 9 раз. А если и нулевые значения изменить на -3, мы получим 36 образов, уменьшенных в размерах.
Теперь посмотрите проект каталога Ех08. Текстура здесь накладывается на квадрат, образованный четырьмя десятками независимых треугольников: полный круг разделен на четыре четверти, в пределах каждой из которой строится десять независимых треугольников. Первая вершина каждого треугольника - центр итогового квадрата. Остальные вершины лежат на его границе.
Вот часть кода, посвященная верхней четверти квадрата:

for i := 10 downto 1 do begin
Vertices. := 0; // Центр экрана
Vertices.У := 0;
Vertices.Z := 0;
Vertices.U := 0.5; // Центр текстуры
Vertices.V := 0.5;
Inc(Vertices);
// Вершины перечисляем по часовой стрелке,
// движемся с левого верхнего угла квадрата
Vertices.X = 0.5 - i / 10;
Vertices.Y =0.5; // Верхний край, значение Y не меняется
Vertices.Z = 0;
Vertices.U = 1 - i / 10; // Х-координата текстуры
Vertices.V =1.0; // Y-координата текстуры
Inc(Vertices);
Vertices.X =0.5- (i- 1)/10; //По часовой стрелке,
Vertices.Y =0.5; // точка слева
Vertices.Z = 0;
Vertices.U = 1 - (i - 1) / 10;
Vertices.V = 1.0;
Inc(Vertices);
end;

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

Рис. 8.7. Этапы разрушения стены

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

for i := 10 downto 1 do begin
Vertices.X := CenterX + Radius * Wl [i] ; // Точка разлома картинки
Vertices.Y := CenterY + Radius * Wl [i];
Vertices.Z := 0;
Vertices.U := CenterX + 0.5; // CenterX находится в точке [-0.5; 0.51
Vertices.V := CenterY +0.5;
Inc(Vertices);
// Точки, расположенные на границе квадрата
Vertices.X =0.5-1/10-1- Radius * Wl [i] ;
Vertices.Y = 0.5 + Radius * Wl [i];
Vertices.Z = 0;
Vertices.U =1-1/10;
Vertices.V = 1.0;
Inc(Vertices) ;
Vertices.X = 0.5 - (i - 1) / 10 + Radius * Wl [i];
Vertices.Y = 0.5 + Radius * Wl [i] ;
Vertices.Z = 0;
Vertices.U = 1 - (i - 1) /10;
Vertices.V = 1.0;
Inc(Vertices);
end;

В программе предусмотрен режим пошагового разрушения, а по нажатии клавиши <Enter> картинка собирается заново:

procedure TfrmD3D.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
i : Integer;
begin
if Key = VK_ESCAPE then Close else
// Пошаговое разрушение
if Key = VK_INSERT then Radius := Radius +0.05 else
// Пошаговое движение в обратном направлении
if Key = VK_DELETE then Radius := Radius - 0.05 else
// Пробел - быстрое разрушение
if Key = VK_SPACE then Moving := True else
// Ввод - картинка собирается заново
if Key = VK_RETURN then begin
Moving := False; // Прекратить движение
Radius := 0; // Картинка собирается
CenterX := random -0.5; // Координаты точки разлома
CenterY := random - 0.5;
for i := 1 to 10 do begin // Коэффициенты скорости движения
repeat // треугольников, все ненулевые
Wl [i] := random- 0.5; until Wl [i] о 0.0;
repeat
W2 [i] := random- 0.5; until W2 [i] <> 0.0;
repeat
W3 [i] := random - 0.5; until W3 [i] <> 0.0;
repeat
W4 [i] := random - 0.5; until W4 [i] <> 0.0;
end;
end;
end;

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

Рис. 8.8. Эту технику живописи отличают крупные мазки

В массиве некоторого предопределенного размера хранятся точки, разбросанные в пределах области вывода.

tуре
TXY = packed record // Координаты точки на плоскости
X, У : Single;
end;
const
SIDES = 20; // Уровень детализации круга
К, SIZE = 5500; // Количество точек
var
points : Array [O..SIZE-1] of TXY; // Массив точек
Radius : Single = 0.03; // Размер отдельной точки

Массив заполняется в начале работы значениями из интервала [-1.0; 1.0]:

procedure TfrmD3D.FormCreate(Sender: TObject) ;
var
hRet : HRESULT;
i : Integer;
begin
Randomize;
for i := 0 to SIZE - 1 do begin // Заполнение массива точек
Points[i].X := random * 2 - 1.0;
Points[i].Y := random * 2 - 1.0;
end;
hRet := InitDSD;
if Failed (hRet) then ErrorOut ('InitD3D', hRet);
hRet := InitVB; // Буферы вершин под (SIDES + 1) вершину
if Failed (hRet) then ErrorOut ('InitVB', hRet);
hRet := InitTexture ('../Mandrill.bmp');
if Failed (hRet) then ErrorOut ('InitTexture1, hRet);
end;

При рисовании отдельного мазка текстурные координаты всех вершин одинаковы и связаны с координатами точки:

function TfrmD3D.DrawCircle (const inX, inY : Single) : HRESULT;
const
Step = 2 * Pi / SIDES;
var
Vertices : ATCustomVertex; hRet : HRESULT;
i : Integer; begin
hRet := FD3DVB.Lock(0, (SIDES + 1) * SizeOf(TCustornVertex),
PByte(Vertices), 0) ;
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Первая точка, точка центра мазка
Vertices.X := inX;
Vertices.Y := inY;
Vertices.Z := 0.0;
Vertices.U := (inX +1.0) / 2;
Vertices.V := (inY + 1.0) / 2;
Inc(Vertices);
// Точки, лежащие на краю круга
for i := 0 to SIDES do begin
Vertices.X := inX + sin(i * Step) * Radius; // По часовой стрелке
Vertices.Y := inY + cos(i * Step) * Radius;
Vertices.Z := 0;
Vertices.U := (inX + 1.0) / 2;
Vertices.V := (inY + 1.0) / 2;
Inc(Vertices); end;
hRet := FD3DVB.Unlock; if Failed(hRet) then begin
Result := hRet;
Exit;
end;
// Связанные треугольники выстраиваются в полньм круг
Result := FDSDDevice.DrawPrimitive(D3DPTJTRIANGLEFAN, О, SIDES);
end;

Пользуемся мы этой функцией отдельно для каждого элемента массива:

for i := 0 to SIZE - 1 do begin
hRet := DrawCircle (Points [i].X, Points [i].Y);
Kif FAILED (hRet) then begin
Result := hRet;
Exit;
end;
end;

Размер мазков регулируется. Можно также получить новый набор точек, чтобы подобрать самую эффектную картинку:

procedure TfrmD3D.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
i : Integer;
begin
if Key = VK_ESCAPE then Close else
if Key = VK_INSERT then Radius := Radius + 0.005 else
if Key = VKJ3ELETE then Radius := Radius - 0.005 else
if Key = VKJ3PACE then begin // Заново генерируем набор точек
for i := 0 to SIZE - 1 do begin
Points[i].X := random * 2 - 1.0;
Points[i].Y := random * 2 - 1.0;
end;
end;
end;

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

procedure TfrmD3D.FormMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
i : Integer;
begin
if Down then begin // Нажата ли кнопка мыши
// Сравниваем с предыдущим расположением курсора
if (X о LastX) and (Y <> LastY) then begin
for i := 1 to 20 do begin // Берется 20 точек облачка
// вокруг курсора
NumPoints := (NumPoints + 1) mod SIZE;
// Масштабируем точки для системы координат D3DFVF_XYZ
Points[NumPoints].X := ((X + random (7) - 3)/ ClientWidth) * 2 - 1.0;
Points[NumPoints].Y := ((ClientHeight -
(Y + random (7) - 3)) / ClientHeight) * 2 - 1.0;
LastX := X;
LastY := Y;
end;
end;
end;
end;

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

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

Рис. 8.9. Постепенно проступающий образ: пример использования альфа-составля ющей текстуры

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

PDWORD (DWORD(d3dlr.pBits) + У * dwDstPitch + X * 4)^ :=
D3DCOLOR_ARGB(0, R, G, В);
То есть все точки образа текстуры первоначально задаются совершенно прозрачными.
При воспроизведении квадрата с наложенной текстурой разрешаем работу с альфа-составляющей привычным для нас способом. Помимо этого необходимо отметить, что значение альфа для каждого пиксела текстуры определяется содержимым ее образа:

with FDSDDevice do begin
SetTexture(0, FD3Texture); // Устанавливаем текстуру
// Операции с цветом пикселов текстуры
SetTextureStageStatefO, D3DTSS__COLOROP, D3DTAJTEXTURE);
// Операции с альфа-компонентом пикселов текстуры
SetTextureStageStatefO, D3DTSS_ALPHAOP, D3DTA_TEXTURE);
// Разрешаем работу с альфа-составляющей
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (True));
// Параметры альфа-смешения
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
end;
// Квадрат, покрытый текстурой
hRet := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Выключаем текстуру и альфа-смешение
with FDSDDevice do begin
SetTexture(0, nil) ;
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (False));
end;

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

procedure TfrmD3D.FormMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
i : Integer; wrkX, wrkY : DWORD;
begin
if Down then begin // Нажата ли кнопка мыши
FD3Texture.LockRect(0, d3dlr, nil, 0);
dwDstPitch := d3dlr.Pitch;
for i := 1 to 50 do begin //50 точек в районе курсора
repeat // Генерируем точку в пределах окна
wrkX := DWORD (X + random (7) - 3);
wrkY := DWORD (ClientHeight - Y + random (7) - 3);
until (wrkX < DWORD (ClientWidth)) and (wrkY < DWORD (ClientHeight))
and (wrkX > 0) and (wrkY > 0);
PDWORD (DWORD(d3dlr.pBits) + wrkY * dwDstPitch + wrkX * 4)^ :=
// Альфа-составляющую для точек задаем равной 255
PDWORD (DWORD(d3dlr.pBics) + wrkY * dwDstPitch + wrkX * 4)" +
SFF000000;
end;
FD3Texture.UnlockRect(0);
end;
end;

Несколько небольших замечаний:

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

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

Мультитекстурирование
Для помещения на объект нескольких текстур одновременно можно воспользоваться простейшим способом альфа-смешения: воспроизводить несколько раз один и тот же полупрозрачный объект с наложением различных текстур.
Сначала попробуем добиться того, чтобы на экране просто присутствовали несколько текстур одновременно. Это сделано в проекте каталога Ех13, где экран покрыт одним образом, а курсор оставляет за собой след, в котором просвечивает другой образ (рис. 8.10).

Рис. 8.10. Симпатичный пример на тему присутствия двух текстур на экране

В коде появилось два объекта, связанных с текстурами, FD3Texturei и FD3Texture2, второй заполняется согласно содержимому загружаемого растрового изображения, а первый - с помощью отдельной функции черно-белой клеткой:

function TfrmDSD.InitTexturel : HRESULT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
X, Y : DWORD;
begin
hRet := FD3DDevice.CreateTexture (256, 256, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, FD3Texturel);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3Texturel.LockRect(0, d3dlr, nil, 0);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
dwDstPitch := dSdlr.Pitch;
for X := 0 to 255 do
for Y := 0 to 255 do // Клетка 16x16
if ((X shr 4) and 1) xor ((Y shr 4) and 1) = 0
then PDWORD (DWORD(dSdlr.pBits) + Y * dwDstPitch + X * 4)^ := $FF000000
else PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch -f X * 4)" := $FFFFFFFF;
Result := FDSTexturel.UnlockRect(0) ;
end;

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

with FD3DDevice do begin if First
then SetTexture (0, FD3Texture2) // Картинка внизу, фон
else SetTexture(0, FDSTexturel); // Внизу клетки
SetTextureStageState(0, D3DTSS_COLOROP, D3DTAJTEXTURE);
SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTAJTEXTURE);
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (True));
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
end;
// Квадрат, закрытый первьм растром, будет фоном
hRet := FD3DDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
if First // Накладываем вторую текстуру из нашей пары
then FD3DDevice.SetTexture(0, FDSTexturel)
else FD3DDevice.SetTexture(0, FD3Texture2);
hRet := FD3DDevice.DrawPrimitive(D3DPTJTRIANGLESTRIP, 0, 2);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
with FD3DDevice do begin SetTexture(0, nil);
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (False));
end;

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

procedure TfrmD3D.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key = VK_ESCAPE then Close else
if Key = Ord ('!') then First := True else // Клетки сверху
if Key = Ord ('2') then First := False; // Клетки снизу
end;

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

procedure TfrmD3D.FormMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
d3dlr : TD3DLOCKED_RECT; dwDstPitch : DWORD;
i : Integer;
wrkX, wrkY : DWORD;
begin
// Добавилась проверка положения курсора
if Down and (X > 0) and (X < ClientWidth)
and (Y > 0) and (Y < ClientHeight) then begin
if First // Определяемся, в какой текстуре вносятся изменения
then FD3Texturel.LockRect(0, d3dlr, nil, 0)
else FD3Texture2.LockRect(0, d3dlr, nil, 0);
dwDstPitch := d3dlr.Pitch; for i := 1 to 50 do begin
repeat
wrkX := DWORD (X + random (7) - 3);
wrkY := DWORD (ClientHeight - Y + random (7) - 3);
until (wrkX < DWORD (ClientWidth)) and
(wrkY < DWORD (ClientHeight));
// Значение альфа-составляющей пикселов сбрасываем в ноль
PDWORD (DWORD(d3dlr.pBits) + wrkY * dwDstPitch + wrkX * 4)^ :=
PDWORD (DWORD(d3dlr.pBits) + wrkY * dwDstPitch + wrkX * 4)^ -
$FF000000;
end;
if First
then FD3Texturel.UnlockRect(0)
else FD3Texture2.UnlockRect(0);
end;
end;

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

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

Рис. 8.12. Наложение текстуры с использованием цветового ключа

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

FD3TextBrick : IDIRECT3DTEXTURE8; // Кирпичная кладка
FD3TextBmp : IDIRECT3DTEXTURE8; // Дерево

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

function TfrmD3D.InitTexture (const FileName : String;
const keyR, keyG, keyB : Byte) : HRESULT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
X, Y : DWORD; Bmp : tBitmap;
R, G, В : Byte;
begin
Bmp := TBitmap.Create;
Bmp.LoadFromfile (FileName);
hRet := FDSDDevice.CreateTexture (Bmp.Width, Bmp.Height, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOLJ4ANAGED, FDSTextBmp);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FDSTextBmp.LockRect(0, d3dlr, nil, 0) ;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
dwDstPitch := d3dlr.Pitch;
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do begin
R := GetRValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y]);
G := GetGValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y]);
В := GetBValue (Bmp.Canvas.Pixels [X, DWORD (Bmp.Height - 1) - Y[):
//Сравнение цвета пиксела с цветовым ключом
if (R = keyR) and (G = keyG) and (B = keyB)
then PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^:=
D3DCOLOR_ARGB(0, R, G, В) // Такой пиксел
// должен стать прозрачным
else PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^ :=
D3DCOLOR_ARGB(255, R, G, B); // Все остальные пикселы
// непрозрачны
end;
Bmp. Free ;
Result := FDSTextBmp.UnlockRect(0);
end;
Кирпичная кладка создается "вручную", дополнительные растры не используются:

function TfrmDSD.MakeBrick : HRESULT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
X, Y, wrkStep : DWORD;
begin
wrkStep := 0;
hRet := FDSDDevice.CreateTexture (128, 128, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, FD3TextBrick);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3TextBrick.LockRect(0, d3dlr, nil, 0);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
dwDstPitch := d3dlr.Pitch;
for Y := 0 to 127 do for X := 0 to 127 do
// Горизонтальные полоски - через каждые 10 пикселов
if Y mod 10 = 0 then begin // Полоски сероватого цвета
PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^ :=
D3DCOLOR_XRGB(200 + random (30), 200+ random (30), 200+ random (30)) ;
Inc (wrkStep); // Сдвиг для вертикальных полосок
end else
// Вертикальные полоски сдвигаются через каждый ряд кладки
if (X + wrkStep) mod 20 = 0
then PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^ :=
D3DCOLOR_XRGB(200 + random (30), 200+ random (30), 200+ random (30))
// Собственно кирпичи
else PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^ :=
D3DCOLOR_XRGB{150 + Random(lOO), 80, 10);
Result := FD3TextBrick.UnlockRect(0);
end;

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

Спрайты в DirectSD
Итак, мы знаем все, чтобы познакомиться с альтернативным DirectDraw способом создания настоящих 2О-приложений, являющихся частным случаем ЗВ-графики. Спрайты теперь представляются в виде примитивов, на которые накладывается текстура.
В проекте каталога Ех15 реализована несложная иллюстрация такого подхода, во время работы ее по экрану меланхолично проплывают рыбки (рис. 8.13).

Рис. 8.13. Пример создания спрайтов в DirectSD

Как обычно для многих примеров этой книги, массив объектов предопределенного размера содержит объекты отдельных изображений:

type
TFish = class // Отдельная рыбка
private
FDSTexture : IDIRECT3DTEXTURE8;
public
PosX, PosY, StepX : Single; // Позиция на экране и шаг перемещения
Scale : Single; // Масштабный множитель
function RotateTexture : HRESULT; // Поворот текстуры
function Draw : HRESULT; // Собственно отображение на экране
procedure Move; // Движение по экрану
constructor Create (const FileName : String; const ir.R, inG, inB : Byte);
destructor Destroy; override;
end;
const
NumFish = 10; // Количество рисуемых рыбок
var
Fishes : Array [0..NumFish-1] of TFish; // Массив объектов

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

constructor TFish.Create (const FileName : String;
const inR, inG, inB : Byte);
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
X, Y : DWORD;
Bmp, wrkBmp : TBitmap;
R, G, В : Byte;
begin
Bmp := TBitmap.Create;
Bmp.LoadFromflie (FileName);
wrkBmp := TBitmap.Create;
wrkBmp.Width := 128;
wrkBmp.Height := 128;
// Масштабирование исходного растра
wrkBmp.Canvas.StretchDraw (Rect (0, 0, 128, 128), Bmp);
hRet := frmDSD.FD3DDevice.CreateTexture (wrkBmp.Width, wrkBmp.Height,
0, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, FDSTexture);
if FAILED(hRet) then begin
if Failed (hRet) then frmDSD.ErrorOut ('InitTexture', hRet);
Exit;
end;
hRet := FD3Texture.LockRect(0, d3dlr, nil, 0);
if FAILED(hRet) then begin
if Failed (hRet) then frmDSD.ErrorOut ('InitTexture', hRet);
Exit;
end;
dwDstPitch := d3dlr.Pitch; for Y := 0 to wrkBmp.Height - 1 do
for X := 0 to wrkBmp.Width - 1 do begin
R := GetRValue(wrkBmp.Canvas.Pixels[X, DWORD (wrkBmp.Height-1)-Y]);
G := GetGValue(wrkBmp.Canvas.Pixels[X, DWORD (wrkBmp.Height-1)-Y]);
В := GetBValue(wrkBmp.Canvas.Pixels[X, DWORD (wrkBmp.Height-1)-Y]);
// Пикселы предопределенного цвета делаются прозрачными
if (R = inR) and (G = inG) and (B = inB)
then PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)^ :=
D3DCOLOR__ARGB(0, R, G, B)
else PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4)" :=
D3DCOLOR_ARGB(255, R, G, B);
end;
hRet := FD3Texture.UnlockRect(0) ;
if FAILED(hRet) then begin
if Failed (hRet) then frmD3D.ErrorOut ('InitTexture', hRet);
Exit;
end;
Bmp. Free ; wrkBmp.Free ;
end;

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

for i := 0 to NumFish - 1 do begin // Инициализация массива объектов
case random (4) of
0 : Fishes [i] := TFish.Create ('Fishl.bmp', 0, 255, 0);
1 : Fishes [i] := TFish.Create ('Fish2.bmp', 255, 0, 0) ;
2 : Fishes [i] := TFish.Create ('Fish3.bmp', 0, 255, 0);
3 : Fishes [i] := TFish.Create ('Fish4.bmp', 0, 255, 0);
end;
with Fishes [i] do begin PosX := random - 0.5;
PosY := (random (60) - 30) / 100;
StepX := (random - 0.5) / 10;
if StepX < 0 then RotateTexture; // Требуется поворот Scale := (random (60) + 40) / 100;
end;
end;

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

function TFish.RotateTexture : HRESULT;
var
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
pDst, pDstl : PDWORD;
X, У : DWORD;
wrkDW : DWORD;
begin
FD3Texture.LockRect(0, d3dlr, nil, 0);
dwDstPitch := d3dlr.Pitch;
for Y := 0 to 127 do
for X := 0 to 63 do begin //До половины ширины образа
// Переставляем содержимое двух пикселов
pDst := PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch + X * 4);
pDstl := PDWORD (DWORD(d3dlr.pBits) + Y * dwDstPitch +
(127 - X) * 4);
wrkDW := pDsf\-pDst^;
pDstlA; pDst^;:= wrkDW;
end;
Result := FD3Texture.UnlockRect(0) ;
end;

Буфер вершин инициализируется с размером под четыре вершины, поскольку для изображения рыбки этот буфер заполняется координатами четырех сторон квадрата. Размер стороны квадрата - scale:

function TFish.Draw : HRESULT;
var
Vertices : ATCustomVertex;
hRet : HRESULT;
begin
hRet := frraD3D.FD3DVB.Lock(0, 4 * SizeOf(TCustomVertex), PByte(Vertices), 0);
if Failed(hRet) then begin
Result := hRet;
Exit;
end;
Vertices.X = -0.5 + PosX; // Левый нижний угол квадрата
Vertices.Y = -0.5 + PosY;
Vertices.Z = 0;
Vertices.U = 0;
Vertices.V = 0;
Inc(Vertices);
Vertices.X = -0.5 + PosX; // Левый верхний угол квадрата
Vertices.Y = -0.5 + Scale + PosY;
Vertices.Z = 0;
Vertices.U = 0;
Vertices.V = 1;
Inc(Vertices);
Vertices.X = -0.5 + Scale + PosX; // Правый нижний угол квадрата
Vertices.Y = -0.5 + PosY;
Vertices.Z = 0;
Vertices.U = 1;
Vertices.V = 0;
Inc(Vertices) ;
Vertices.X = -0.5 + Scale + PosX; // Правый верхний угол квадрата
Vertices.Y = -0.5 + Scale + PosY;
Vertices.Z = 0;
Vertices.U = 1;
Vertices.V = 1;
frmD3D.FD3DVB.Unlock;
with frmD3D.FD3DDevice do begin
SetTexture(0, FD3Texture);
SetTextureStageState(0, D3DTSS_COLOROP, D3DTA_TEXTURE);
SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTAJTEXTURE);
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (True));
SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2) ;
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD (False));
end;
Result := frmD3D.FD3DDevice.SetTexture(0, nil);
end;

Через некоторый промежуток времени для каждого объекта вызывается метод, связанный с перемещением:

procedure TFish.Move; begin
PosX := PosX + StepX;
if (PosX < -1.5) or (PosX > 1.5) then begin // Уход за границу экрана
RotateTexture; // Переворачиваем образ
StepX := -StepX; // Меняем направление на противоположное
end;
end;

Как видим, код значительно сократился, и программировать стало гораздо удобнее, но видеокарты, поддерживающие только 20-акселерацию, не смогут ускорить работу таких приложений.

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