Михаил Краснов - Графика DirectX в Delphi
Сразу после запуска приложения до появления на экране окна нашего приложения, копируем во вспомогательный объект класса TBitmap содержимое рабочего стола:
wrkBitmap := TBitmap.Create; wrkBitmap.Height := 480; wrkBitmap.Width := 640;
BitBlt(wrkBitmap.Canvas.Handle, 0, 0, 640, 480, GetDC (GetDesktopWindow), 0, 0, SRCCOPY);
Поверхность фона создается "длинным" способом. При этом не загружаем ничего из растра:
ZeroMemory (ddsd, SizeOf(ddsd), 0); with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
dwWidth := 640;
dwHeight := 480; end;
hRet := FDD.CreateSurface(ddsd, FDDSBackGround, nil);
if Failed(hRet) then ErrorOut(hRet, 'Create Back Surface');
// Копируем содержимое wrkBitmap на фоновую поверхность
hRet := DDCopyBitmap (FDDSBackGround, wrkBitmap.Handle, 0, 0,
wrkBitmap.Width, wrkBitmap.Height);
if Failed(hRet) then ErrorOut(hRet, 'DDCopyBitmap'); wrkBitmap.Free; // wrkBitmap больше не требуется
Дальше все происходит традиционно: на первичную поверхность выводится содержимое поверхности фона, поверх которого накладывается полупрозрачный образ. Чтобы добиться зловещего эффекта призрачного колебания, первоначальный массив образа искажается по синусоиде:
function TfrmDD.Rotate (const pictOriginal : TWordArray) : TWordArray;
var
i, j, k : Integer;
begin
ZeroMemory (SResult, SizeOf (Result)); for j := 0 to 255 do
for i := 0 to 255 do begin
k := trunc (sin (Angle + j * 3 * Pi / 255) * 10); // Сдвиг точек
if (i - k >= 0) and (i - k <= 255) then // Помещается ли в растр
Result [i, j] := pictOriginal [i - k, j ] ;
end;
Angle := Angle +0.2; // Периодичный сдвиг
if Angle > 2 * Pi then Angle := Angle - 2 * Pi;// Избежать переполнения
end;
Пользователь может перемещать курсор, попытаться выполнить привычные действия, видя знакомое содержимое экрана, но реакции на его действия, конечно, не последует. Чтобы у него не возникало паники, закрываем приложение по нажатию любой клавиши, иначе у пользователя может возникнуть ощущение того, что система зависла.
Выбор объектов
В этом разделе мы познакомимся с простейшим способом организации выбора и выяснением, какой объект находится в определенной точке экрана, например под курсором. В простейших проектах, конечно, можно всего-навсего анализировать координаты нужной точки и перебирать все отображаемые объекты, чтобы выделить из них нужный. Но если объектов присутствует очень много, а сами они бесформенные или имеют сложную форму, то на перебор и анализ может уйти слишком много времени.
В таких случаях используется выбор по цвету, заключающийся в том, что объекты раскрашиваются в различные цвета, анализ цвета нужной точки дает ответ на вопрос: "Что в настоящий момент находится под курсором".
Рассмотрим пример из проекта каталога Ех20. На экране перемещаются три одинаковых образа, при этом образ, находящийся под курсором, перекрашивается (рис. 3.9).
Поскольку образы выводятся совершенно одинаковые, мы не можем напрямую различать их по цвету. Действуем так: на вспомогательной поверхности DDSDoubie отображаем образы такой же формы, что и на экране, но разные по цвету (в моем примере это три круга чистых цветов: красного, зеленого i синего). Выводятся они с теми же координатами, что и на экране. Перед тем, как отобразить сферы на экране, анализируем цвет нужного пиксела на вспомогательной поверхности:
function TfrmDD.UpdateFrame : HRESULT;
var
ddbltfx : TDDBLTFX; // Для очистки экрана
wrkl : Integer; // Рабочая переменная
begin
Result := DD_FALSE;
ZeroMemory (@ddbltfx, SizeOf(ddbltfx));
ddbltfx.dwSize := SizeOf(ddbltfx); ddbltfx.dwFillColor := 0;
// Закрашиваем, очищаем обе поверхности
FDDSBack.Blt(nil, nil, nil, DDBLT_COLORFILL or DDBLT_WAIT, @ddbltfx);
FDDSDouble.'Blt(nil, nil, nil, DDBLT_COLORFILL or DDBLT_WAIT, Sddbltfx);
ThisTickCount := GetTickCount;
// Пауза для смены положения сфер
if ThisTickCount - LastTickCount > 10 then begin
Angle := Angle + 0.02;
if Angle > 2 * Pi then Angle := Angle - 2 * Pi; LastTickCount := GetTickCount;
end;
// Выводим три сферы на вспомогательную поверхность
FDDSDouble.BltFast (0, 140 - trunc (sin (Angle) * 100),
FDDSImageRed, nil, DDBLTFAST_WAIT);
// Красная, соответствует первому образу
FDDSDouble.BltFast (230, 140 - trunc (sin (Angle + Pi / 4) * 100),
FDDSImageGreen, nil, DDBLTFAST_WAIT);
// Зеленая, для второго образа
FDDSDouble.BltFast (440, 140 - trunc (sin (Angle + Pi / 2) * 100),
FDDSImageBlue, nil, DDBLTFAST_WAIT);
// Синяя для третьего
wrkl := Select (mouseX, mouseY); // Выбор элемента под курсором
if wrkl = -1 then begin // Произошла авария
Result := RestoreAll;
Exit;
end;
if wrkl =1 // Под курсором первая сфера, ее выводим помеченной
then FDDSBack.BltFast (0, 140 - trunc (sin (Angle) * 100),
FDDSImageSelect, nil, DDBLTFAST_WAIT)
// Под курсором не первая сфера, ее выводим обычной
else FDDSBack.BltFast (0, 140 - trunc (sin (Angle) * 100),
FDDSImageSphere, nil, DDBLTFAST_WAIT);
// Аналогично с двумя оставшимися сферами
if wrkl = 2
then FDDSBack.BltFast (220, 140 - trunc (sin (Angle + Pi / 4) * 100),
FDDSImageSelect, nil, DDBLTFAST_WAIT)
else FDDSBack.BltFast (220, 140 - trunc (sin (Angle + Pi / 4) * 100),
FDDSImageSphere, nil, DDBLTFAST_WAIT);
if wrkl = 3
then FDDSBack.BltFast (440, 140 - trunc (sin (Angle + Pi / 2) * 100),
FDDSImageSelect, nil, DDBLTFAST_WAIT)
else FDDSBack.BltFast (440, 140 - trunc (sin (Angle + Pi / 2) * 100),
FDDSImageSphere, nil, DDBLTFAST_WAIT);
// Вывод указателя курсора
FDDSBack.BltFast (mouseX, mouseY, FDDSMouse, nil,
DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);
if Failed (FlipPages)
then Result := RestoreAll
else Result := DD_OK;
end;
Теперь посмотрим функцию выбора:
function TfrmDD.Select (const X, Y : Integer) : Integer;
var
desc : TDDSURFACEDESC2;
Red, Green, Blue : Byte;
Pixel : Word;
begin
Result := -1;
ZeroMemory (@desc, SizeOf(desc));
desc.dwSize := SizeOf(desc) ;
if Failed (FDDSDouble.Lock (nil, desc, DDLOCK_WAIT, 0))
then Exit; // Закрыть не удается, выходим
Pixel := PWord (Integer (desc.IpSurface) + У * desc.lPitch + X * 2)^;
Blue := Pixel and $1F; // Цветовые компоненты пиксела
Green := (Pixel shr 5) and $3F; Red := (Pixel shr 11) and $1F; FDDSDouble.Unlock (nil);
if Blue <> 0 then Result := 3 else // Анализируем результат if Green <> 0 then Result := 2 else
if Red <> 0 then Result := 1 else Result := 0;
end;
Конечно, для этого конкретного примера можно делать выбор просто по координате, но я надеюсь, что сумел достичь данной иллюстрацией понимания, как поступать в случаях, когда подобный анализ получается слишком длинным.
В рассмотренном примере фон не используется. Но если он потребуется, то учтите, что на вспомогательную поверхность его выводить совершенно не нужно.
В некоторых стратегических играх вы можете заметить, что на экране можно выбирать "спрятавшиеся" объекты, закрытые каким-то элементом пейзажа, деревом или горой. На вспомогательной поверхности они не рисуются, поэтому так и происходит.
Также часто возникают ситуации, когда выбор осуществляется неточно, в некотором районе объекта. Это происходит потому, что для повышения скорости работы на вспомогательную поверхность выводятся окрашенные прямоугольники, а не силуэт объекта.
Лупа
В данном разделе мы рассмотрим два любопытных примера, посвященных организации лупы. Задача сама по себе занимательна, но вдобавок мы узнаем кое-что интересное и загадочное.
Запустите проект, располагающийся в каталоге Ех21. По экрану перемещается "лупа", кружок, в пределах которого выводится увеличенный участок фона (рис. 3.10).
В качестве фона в примерах этого раздела я использую, с любезного разрешения автора, работы художника, имя которого присутствует в левом нижнем углу растрового изображения. Псевдоним автора - Beardo, а адрес ею страницы http://home5.swipnet.se/~w-57902/images/art/.
Изобразить увеличенный участок фона - задача не из трудных, мы хорошо усвоили метод Bit поверхности. Проблема состоит в том, чтобы вывести не прямоугольную, а именно круглую лупу. Посмотрим, как это сделано в данном примере.
Поверхность, связанная с лупой, называется FDDSZoom, для нее установлен цветовой ключ - черный цвет. Размер поверхности - 100x100 пикселов.
Все точки этой поверхности, находящиеся за пределами круга "лупы", окрашиваются черным:
function TfrmDD.Circle : HRESULT;
var
desc : TDDSURFACEDESC2;
i, j : Integer;
hRet : HRESULT; begin
ZeroMemory (@desc, SizeOf(desc));
desc.dwSize := SizeOf (desc);
hRet := FDDSZoom.Lock (nil, desc, DDLOCK_WAIT, 0); if Failed (hRet) then begin Result := hRet;
Exit;
end;
for i := 0 to 99 do // Цикл по всем точкам поверхности
for j := 0 to 99 do
// Выделяем точки, располагающиеся за пределами круга "лупы"
if sqr (i - 50} + sqr (j - 50) > 50 * 50 then // Заполняем черным
PWord (Integer(desc.IpSurface) + j * desc.lPitch + i * 2)^ := 0;
Result := FDDSZoom.Unlock (nil);
end;
При отображении цветовой ключ позволяет ограничить вывод растянутой поверхности именно кругом:
// Квадрат, задающий степень увеличения
SetRect (wrkRect, mouseX + 25, mouseY + 25, mouseX + 75, mouseY + 75);
// Растягиваем участок фона
FDDSZoom.Bit (nil, FDDSBackGround, SwrkRect, DDBLT_WAIT, nil);
Circle; // Заполняем черным часть квадрата
// Выводим с цветовым ключом
FDDSBack.BltFast (mouseX, mouseY, FDDSZoom, nil,
DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);
Выглядит просто и эффектно, но в решении содержится проблема: оно подходит только для черного цвета. Если в качестве ключа использовать любой другой цвет, то на точки, заполненные цветом ключа "вручную", прозрачность распространяться не будет: прозрачными окажутся только участки этого же цвета, но окрашенные вызовом метода поверхности. Разрешить означенную проблему мне не удалось, поскольку плохо понятно, как DirectDraw удается различать такие участки.
Черный цвет для использования его в качестве ключа подходит для этого фона, но пример будет некрасиво работать с фоном, подобным рис. 3.11, где присутствует масса участков именно черного цвета.