Создание в Delphi эффекта пламени c использованием DirectX

Автор статьи: AlexTank
Сайт Автора: нет
E-mail Автора: aipatov@mediacorp.ru
Дата публикации: 25.11.2005

 Существует мнение, что демки необходимо делать на ассемблере или, в крайнем случае, на Си. Якобы, хорошая демо должна иметь маленький размер, чего нельзя добиться в Delphi или Visual Basic . На самом деле, это не так. Главное в демке ни размер, ни алгоритмы, ни язык, на котором она написана, а выполняемые ей действия.

В этой статье я опишу графический эффект, который, наверное, делали все демо-дизайнеры – эффект пламени. Создавать его будем в Delphi с использованием DirectX и WinAPI.

Итак, начнем. Первое, что необходимо сделать – это создать в Delphi новой проект (Меню: File/New/Application). Так как мы не будем использовать VCL , то удалим из проекта форму (Project/Remove from Project). Откроем файл проекта (Project/View Source) и перепишем его:

program Project1;

uses
  windows,messages,directdraw;

{$R *.res}

type tacc=array[0..307200] of byte;


var
    WindowClass: TWndClass; 
    Instance: HWnd;
    Handle: HWnd; 
    msg: TMsg; 
    DDResult : HResult;
    DDrawObject : IDirectDraw;
    PrimarySurface : IDirectDrawSurface; 
    SecondarySurface : IDirectDrawSurface;  
    DDPal: IDirectDrawPalette;
    SurfaceDescription: TDDSurfaceDesc;  
    BackCaps: TDDSCaps;  
    palen:array[0..255] of PaletteEntry;  
    b1,b2:^tacc;

begin
  InitApp;
  InitDirectDraw; 
  while GetMessage( msg, 0, 0, 0) do
  begin
    translatemessage(msg);
    dispatchmessage(msg);
  end;
end.

В секции uses мы прописали directdraw. Этот модуль для работы с библиотекой DirectDraw, которого нет в стандартной поставке Delphi. Я использую адаптацию C++ заголовков, сделанную Erik Unger. Затем идет описание глобальных переменных, необходимых в работе программы.
В основной части программы вызываются процедуры: InitApp - для создание окна, и InitDirectDraw - для инициализации DirectDraw. Далее идет цикл заставляющий обрабатывать сообщения окна.

Первое, что должна делать программа – это создать окно в Windows.

procedure InitApp;
begin
  instance := GetModuleHandle(nil);
  with WindowClass do
  begin
    style := CS_HRedraw or CS_VRedraw;
    lpfnWndProc := @windowproc;
    Hinstance := Instance;
    LpszClassName := 'DXF';
    hbrBackground:=0;
    hIcon := 0;
  end;
  RegisterClass(WindowClass);
  Handle:=CreateWindowEx(0, 'DXF', 'Flame', WS_POPUP,
      5, 5, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
      0, 0, instance, nil);
  UpdateWindow(Handle);
  ShowWindow(Handle, CmdShow);
  SetTimer(Handle, 1, 1, nil);
end;

Сначала, мы заполняем структуру WindowsClass, необходимую для регистрации класса окна. Затем, с помощью RegisterClass, регистрируем класс в Windows. После этого, создаем окно и выводим его на экран.
В конце процедуры мы создадим таймер срабатывающий каждую миллисекунду.

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

function windowproc (Hwn,msg,wpr,lpr: longint): longint; stdcall;
begin
  result := defwindowproc(hwn,msg,wpr,lpr);
  case msg of
    WM_DESTROY:
    begin
       FreeAll;
      Halt;
    end;

    WM_SETCURSOR:
      SetCursor(0);

    WM_KEYDOWN:
    begin
      if wpr=VK_ESCAPE then  begin  
        FreeAll;                   
        Halt;
       end;
    end;

    WM_TIMER:
      redraw;
  end;
end;

При получении сообщения WM_DESTROY или нажатии кнопки Esc программа вызывает FreeAll, отвечающею за освобождение системных ресурсов, занятых нами, и завершает работу. При сообщении WM_TIMER, вызывается процедура redraw, выполняющая вывод очередного кадра на экран.

Но перед тем, как выводить что-либо на экран нужно инициализировать DirectDraw, и создать необходимые поверхности и буферы, а также установить палитру.

procedure InitDirectDraw;
var I: Integer;
begin
  DDResult := DirectDrawCreate (nil, DDrawObject, nil);
  DDResult := DDrawObject.SetCooperativeLevel(handle, DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN);
  DDResult := DDrawObject.SetDisplayMode(640, 480, 8);

  with SurfaceDescription do
  begin
    dwSize := sizeof(SurfaceDescription);
    dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
    ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or DDSCAPS_FLIP or DDSCAPS_COMPLEX;
    DwBackBufferCount := 1;
  end;
  DDResult := DDrawObject.CreateSurface (SurfaceDescription,PrimarySurface,nil);

  BackCaps.dwCaps := DDSCAPS_BACKBUFFER;
  PrimarySurface.GetAttachedSurface(BackCaps, SecondarySurface);

  for i:=0 to 25 do              
  begin
    palen[i].peRed := (i)*10;
    palen[i].peGreen := 0;
    palen[i].peBlue := 0;
  end;
  for i:=26 to 85 do      
  begin
    palen[i].peRed := 255;
    palen[i].peGreen := round((i-25)*4.2);
    palen[i].peBlue := 0;
  end;
  for i := 86 to 141 do         
  begin
    palen[i].peRed := 255;
    palen[i].peGreen := 255;
    palen[i].peBlue := round((i-86)*4.51);
  end;
  for i := 142 to 255 do     
  begin
    palen[i].peRed := 255;
    palen[i].peGreen := 255;
    palen[i].peBlue := 255;
  end;
  DDrawObject.CreatePalette(DDPCAPS_8BIT or DDPCAPS_ALLOW256, @palen, DDPal, nil);
  PrimarySurface.SetPalette(DDPal);

  new(b1);
  new(b2);
end; 

Во-первых, создаем объект DirectDraw. Устанавливаем приложению эксклюзивный полноэкранный уровень и разрешения экрана 640x480x8.
Во-вторых, создадим первичную и вторичную поверхности.
Эффект пламени проще и быстрей реализовать используя палитровые поверхности, поэтому создадим палитру с плавным переходом цветов: черный – красный – желтый – белый.
Последнее, что необходимо сделать – это создать два буфера для генерации пламени: b1 и b2.

Теперь можно рассмотреть и генерацию кадров.

procedure redraw;

var i, j, k: Integer;
    swp: Pointer;
    diracc:^tacc;

begin
  i := 0;
  while (i<640) do
  begin
    b1[i+0] := random(2)*255;
    b1[i+640] := random(2)*255;
    inc(i, random(5));
  end;

  for i := 1280 to 307200 do
    b2[i] := ((b1[i-1]+b1[i+1]+b1[i]+b1[i-639]+b1[i-641]+b1[i-1279]+b1[i-1280]+b1[i-1281]) shr 3);

  try
    SecondarySurface.Lock(nil, SurfaceDescription, DDLOCK_SURFACEMEMORYPTR or DDLOCK_WRITEONLY or DDLOCK_WAIT, 0);
    diracc := SurfaceDescription.lpSurface;
    j := 0;
    k := 306560;
    for i := 0 to 477 do
    begin
      Move(b2[k], diracc[j], 640);
      inc(j, SurfaceDescription.lPitch);
      dec(k, 640);
    end;
  finally
    SecondarySurface.Unlock(nil);
  end;

  swp := b1;
  b1 := b2;
  b2 := swp;

  PrimarySurface.Flip(nil, DDFLIP_WAIT);
end;

Первый цикл – это создание так называемых "hotspots", случайных точек максимального цвета в начале первого буфера.
Алгоритм, используемый мной, заключается в том, что я ставлю точку во второй буфер равною среднее значение суммы точек первого буфера, которые окружаю точку находящейся выше на линию данной (Y координата меньше на 1). Эти и занимается второй цикл.
Далее все просто. Лочим вторичную поверхность, для прямого доступа к ее памяти. Копируем на нее содержимое второго буфера (только в зеркальном отражении по оси X, иначе гореть будет сверху вниз).Меняем местами b1 и b2, а также первичную и вторичную поверхности. И кадр выведен на экран.

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

procedure FreeAll;
begin
  Dispose(b2);
  Dispose(b1);
  DDPal._Release;
  pointer(DDPal) := nil;
  SecondarySurface._Release;
  pointer(SecondarySurface) := nil;
  PrimarySurface._Release;
  pointer(PrimarySurface) := nil;
  DDrawObject._Release;
  pointer(DDrawObject) := nil;

end;