lektion 2 Delphi und DirectX

DirectX 8 und Delphi

Lektion 1: Direct3D Initialisierung

von Jürgen Rathlev

Grundlage für die nachfolgende Einführung in DirectX8 mit Delphi bilden die Tutorials von www.snorre-dev.com. Die dort beschriebenen Beispiele wurden von mir nach Delphi 5 umgesetzt. Sie sollten aber auch mit anderen Delphi-Versionen funktionieren. Nützlich ist es außerdem, sich die DirectX-SDK von Microsoft herunterzuladen. Neben einigen Beispielen (in C++ und VB) enthält dieses Paket eine umfangreiche Dokumentation zu DirectX. Einige der darin enthaltenen Beispiele habe ich nach Delphi umgesetzt.

Um DirectX unter Delphi einzusetzen, benötigt man entsprechende Interface-Units. Zum Glück haben sich die Leute des Jedi Projects die Arbeit gemacht, diese zu erstellen und allen Interessierten zur Verfügung zu stellen. Man kann sich die Units und die erforderliche DLL dort bei Jedi Graphix herunterladen. Da leider mehrere leicht unterschiedliche Versionen im Umlauf sind, empfehle ich die auf dieser Seite bereitgestellte Version zu verwenden. Sie wurde mit den nachfolgenden Beispielen zusammen getestet.

Wie wir in den nachfolgenden Beispielen sehen, bringt meiner Meinung nach der Einsatz von Delphi gerade für den Einsteiger einige Vorteile, da er sich hier im Gegensatz zu VisualC++ nicht mit der am Anfang etwas unübersichtlichen Verwaltung von Fenster-Handles und Botschaftsroutinen herumschlagen muss. Ein wenig Erfahrung bei der Programmentwicklung unter Delphi ist allerdings schon erforderlich.

Fangen wir also an! Als erstes erzeugen wir eine neue Delphianwendung. Der Form geben wir den Namen "Sample3DForm". Der uses-Zeile fügen wir die benötigten DirectX-Units hinzu:

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Direct3D8, d3dx8;
In der Deklaration von TSample3DForm benötigen wir unter private einige Variablen und Methoden:
type
  TSample3DForm = class(TForm)
  ...   // hier werden von Delphi Komponeneten und Ereignisse eingetragen
  private
    // Das Direct3D Interface, es wird zum Initialisieren und Schließen von D3D benötigt
    lpd3d            : IDIRECT3D8;

    // Das D3DDevice wird zum Rendern benutzt und spiegelt den Bildschirm mit allen
    // Funktionen wieder. Wenn wir das D3DRender Interface erstellen, verändern oder 
	// das Bild rendern, so machen wir das über dieses Interface
    lpd3ddevice      : IDirect3DDevice8;

    red,green,blue   : byte;   // Hintergrundfarbe

    procedure FatalError(hr : HResult; FehlerMsg : string);
    function D3DFind16BitMode : TD3DFORMAT;
    procedure D3DInit;
    procedure D3DShutdown;
    procedure D3DInitScene;
    procedure D3DKillScene;
    procedure D3DRender;
  end;
Bevor wir die D3D-Methoden mit Leben füllen, müssen wir uns um die Initialisierung und Deinitialisierung unserer Objekte kümmern. Durch Doppelklick auf OnCreate im Objektinspektor erzeugen wir den Rumpf für die Initialisierung und fügen nachfolgende Zeilen ein:
// Initialisieren aller Methoden und Variablen
procedure TSample3DForm.FormCreate(Sender: TObject);
begin
  lpd3d:=nil;
  lpd3ddevice:=nil;
  red:=0; green:=0; blue:=0;
  end;
Entsprechend verfahren wir für die Deinitialisierung mit OnClose:
procedure TSample3DForm.FormClose(Sender: TObject;
  var Action: TCloseAction);
begin
  // Lösche die D3D Scene bevor wir D3D beenden
  D3DKillScene;
  // Lösche D3D
  D3DShutdown;
  end;
Die Initialisierung unserer 3D-Objekte wird im OnShow-Ereignis vorgenommen:
procedure TSample3DForm.FormShow(Sender: TObject);
begin
  D3DInit;     // Initialisieren von D3D
  D3DInitScene;
  D3DRender;   // Zeichne unsere Grafiken
  end;
Damit das Programm auf Tastatureingaben (ESC für Programmende) reagiert, fügen wir noch ein OnKeyDown-Ereignis ein:
procedure TSample3DForm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key=VK_ESCAPE then close;
  end;
Jetzt können wir daran gehen, die 3D-Umgebung zu initialisieren. Mit der Funktion Direct3DCreate8 wird ein Direct3D-Interfaceobjekt erzeugt. Alle DirectX Strukturen werden immer mit ZeroMemory(...) überschrieben, um unangenehme Nebeneffekte durch ältere Aufrufe zu vermeiden.
Im Record d3dpp werden alle wichtigen Parameter, die zur Erstellung des Devices nötig sind, gesetzt. Um ein geeinetes Backbufferformat zu ermiiteln, wird die weiter unten beschriebene Funktion D3DFind16BitMode aufgerufen.
Anschließend erstellen wir mit CreateDevice endgültig unsere DirectX Schnittstelle. Der Parameter D3DCREATE_SOFTWARE_VERTEXPROCESSING ist für ältere Karten bestimmt, die noch keine Harwareunterstützung für Vertexprocessing haben. Das lässt sich mit D3DCREATE_HARDWARE_VERTEXPROCESSING auch abstellen:
// Mit dieser Funktion initialisieren wir D3D
procedure TSample3DForm.D3DInit;
var
  hr    : HRESULT;
  d3dpp : TD3DPRESENTPARAMETERS;
begin
  //Erstelle Direct3D! Muß immer als erstes erstellt werden
  //Immer D3D_SDK_VERSION als Version setzen
  lpd3d:=Direct3DCreate8(D3D_SDK_VERSION);
  if(lpd3d=nil) then FatalError(0,'Fehler beim Erstellen von Direct3D!');

  // Setze D3DPRESENT_PARAMETERS auf 0, sonst könnten wir probleme mit älteren
  // Eintragungen bzw. unkontrollierbaren Ergebnissen bekommen!
  // Sollten wir bei allen DirectX Strukturen machen
  ZeroMemory(@d3dpp,sizeof(d3dpp));

  with d3dpp do begin
    // Hiermit werden alte Frames gelöscht, denn wir brauchen sie nicht
    SwapEffect:=D3DSWAPEFFECT_DISCARD;
    hDeviceWindow:=Handle;      // Dies ist unser HWND von TForm
    BackBufferCount:=1;         // 1 Backbuffer

    Windowed          := FALSE;
    BackBufferWidth   := 640;
    BackBufferHeight  := 480;
    BackBufferFormat  := D3DFind16BitMode;
    end;

  //Nachdem wir die D3DPRESENT_PARAMETERS Struktur ausgefüllt haben, sind wir
  // endlich so weit unser D3D Device zu erstellen
  hr:=lpd3d.CreateDevice(D3DADAPTER_DEFAULT,
                         D3DDEVTYPE_HAL,
                         Handle,
                         D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                         d3dpp,
                         lpd3ddevice);
  if FAILED(hr) then FatalError(hr,'Fehler beim Erzeugen des 3D-Device');
  end;
Beim Programmende müssen alle Ressourcen auch wieder freigegeben werden:
// *** D3DShutdown hier werden die Resourcen von D3D wieder freigegeben
procedure TSample3DForm.D3DShutdown;
begin
  if assigned(lpd3ddevice) then lpd3ddevice:=nil;
  if assigned(lpd3d) then lpd3d:=nil;
  end;
Die nachfolgende beiden Routinen werden erst in der nächsten Lektion mit Leben erfüllt und bleiben hier zunächst leer:
procedure TSample3DForm.D3DInitScene;
begin
  end;

procedure TSample3DForm.D3DKillScene;
begin
  end;
Folgende kleine Routine ist für die Fehlerbehandlung zuständig. Sie gibt eine Fehlermeldung auf dem Bildschirm aus und beendet dann das Programm:
// Fataler Fehler. Meldung und Programmende
procedure TSample3DForm.FatalError(hr : HResult; FehlerMsg : string);
var
  s : string;
begin
  if hr<>0 then s:=D3DXErrorString(hr)+#13+FehlerMsg else s:=FehlerMsg;
  D3DKillScene;
  D3DShutdown;
  MessageDlg(s,mtError,[mbOK],0);
  close;
  end;
Jetzt müssen wir noch prüfen, ob unsere Grafikhardware überhaupt den von uns gewünschten Grafikmodus unterstützt. Mit dem Parameter D3DADAPTER_DEFAULT wählen wir die primäre Grafikkarte aus. Mit D3DDEVTYPE_HAL stellen wir ein, dass wir die Hardwarebeschleunigung (hardware application layer) einsetzen wollen. Die nächsten zwei Parameter beschreiben den Farbmodus des Bildschirms und des Backbuffers. Hier wählen wir den 16-bit Modus (rot 5 Bits, grün 6 oder 5 Bits, blau 5 Bits und ohne oder mit 1 Bit Alphakanal). Man kann auch mit anderen Farbmodi experimentieren (siehe dazu die DirectX-SDK):
function TSample3DForm.D3DFind16BitMode : TD3DFORMAT;
var
  hr    : HRESULT;
begin
  hr:=lpd3d.CheckDeviceType (D3DADAPTER_DEFAULT,
                             D3DDEVTYPE_HAL,
                             D3DFMT_R5G6B5,      // Format Primary
                             D3DFMT_R5G6B5,      // Format Buffer
                             FALSE);
  if (SUCCEEDED(hr)) then begin
    Result:=D3DFMT_R5G6B5; exit;
    end;

  hr:=lpd3d.CheckDeviceType (D3DADAPTER_DEFAULT,
                             D3DDEVTYPE_HAL,
                             D3DFMT_X1R5G5B5,
                             D3DFMT_X1R5G5B5,
                             FALSE);
  if (SUCCEEDED(hr)) then begin
    Result:=D3DFMT_X1R5G5B5; exit;
    end;
  FatalError(0,'Der erforderliche D3D Modus wird von Ihrer Karte nicht unterstützt');
  Result:=0;
  end;
Jetzt können wir endlich beginnen etwas auf den Bildschirm zu schreiben. Die Render-Routine wird in den nachfolgenden Lektionen noch ergänzt. Hier erzeugen wir zunächst nur einen schwarzen Bildschirm.
Die nachfolgenden Routinen sind alle Methoden von lpd3ddevice (IDirect3DDevice8). Clear wird benutzt, um den Buffer zu löschen. Mit BeginScene starten wir den eigentlichen Code, in dem das Bild aufgebaut wird (siehe nächste Lektion). Mit EndScene schließen wir den Bildaufbau ab. Zum Schluß bringen wir den Backbuffer mit Present auf den Bildschirm.
procedure TSample3DForm.D3DRender;
begin
  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    Clear(0,           // Wieviel Rechtecke löschen? 0 Löscht alle
          nil,         //Pointer zu den Rechtecken. NULL = Ganzer Bildschirm
          D3DCLEAR_TARGET,
          D3DCOLOR_XRGB(red,green,blue), //Hintergrundfarbe
          1,           // Lösche ZBuffer ( Wir haben momentan noch keinen )
          0 );

    if SUCCEEDED(BeginScene) then begin
      // Hier wird später alles stehen, was gerendert werden soll

      EndScene;
      end;

   // Zeige Resultate auf dem Bildschirm
    Present(nil,nil,0,nil);
    end;
  end;
Die Quelltexte der Beispiele stehen zum Download zur Verfügung. Die Zip-Datei enthält alle Lektionen. Zum Ausführen einer der Lektionen muss in den Projekt-Optionen von Delphi als Bedingung einer der Werte Lesson1, Lesson2, ... definiert werden.

Delphi und DirectX Lektion 2