Home » Tutorials » Grafik und Spiele » Direct3D mit Delphi unter DirectX 8

Direct3D mit Delphi unter DirectX 8

Zeichnen einfacher Objekte

Wir wollen jetzt auf der in der Lektion 1 erzeugten Zeichenfläche ein Dreieck und ein Quadrat anzeigen. Die Angaben zu den Eckpunkten aller Objekte werden in DirectX als Vertizes gespeichert. Welche Information in einem Vertex enthalten sein sollen, ist in weiten Grenzen frei wählbar (siehe dazu die DirectX-SDK). Für uns genügen die Angaben zu den Koordinaten und den Farbwerten der Eckpunkte. Außerdem definieren wir zwei Arrays, die die Vertizes unserer beiden Objekte enthalten sollen.

type
  TMyVertex = record
    x,y,z,rhw : single;   // Position des Vertex
    color     : dword;    // Farbe des Vertex
    end;

  TTriangleVertex = array [0..2] of TMyVertex;
  TQuadratVertex  = array [0..3] of TMyVertex;

Die Koordinaten sollen hier in transformierter Form (d.h. in Pixeln) angegeben werden. Entsprechend wird die Konstante D3D8T_CUSTOMVERTEX gesetzt (D3DFVF_XYZRHW für die Koordinaten x,y,z,rwh und D3DFVF_DIFFUSE für die Farbinformation). Wir benötigen sie weiter unten in CreateVertexBuffer, um das Vertexformat festzulegen. In den späteren Beispielen werden wir stattdessen untransformierte Koordinaten verwenden, die erst durch Festlegung eines Sichtpunktes und einer Projektion in Bildschirmkoordinaten von DirectX umgerechnet werden.
Die Größe RHW (reciprocal of homogeneous W) ist in der DirectX-SDK unter den Stichworten „Transformed and Lit Vertices“, „What Are Depth Buffers?“ und „Eye-Relative vs. Z-Based Depth“ näher beschrieben. Sie wird nur bei transformierten Koordinaten benötigt und entspricht einer auf den Ort des Auges bezogenen Tiefeninformation ähnlich der Information im z-Buffer. Wir setzen sie hier einfach auf 1. Bei der Angabe der Raumkoordinaten ist zu beachten, dass anders als in der Mathematik üblich als Koordinatensystem kein Rechtssystem (x nach rechts, y nach oben z nach vorn) sondern ein Linkssystem (x nach rechts, y nach oben, z nach hinten) verwendet wird.
DirectX benutzt als Elementarfläche immer das Dreieck, so dass das Quadrat durch zwei Dreiecke dargestellt werden muss. Außerdem müssen in der Deklaration von TSample3DForm unter private zwei Variablen für die Vertexbuffer definiert werden.

const
// Beschreibung des Vertextyps: Mit D3DFVF_DIFFUSE sagen wir DX, das unsere
// Struktur eine Farbe hat. D3DFVF_XYZRHW bedeutet, dass es sich um ein transformiertes
// Vertex handelt
  D3D8T_CUSTOMVERTEX = D3DFVF_XYZRHW or D3DFVF_DIFFUSE;

  // Dreieck
  TriangleVertex : TTriangleVertex = (
    (x: 175.0; y:  50.0; z: 0.5; rhw: 1.0; color: $000000FF), // x, y, z, rhw, Farbe
    (x: 300.0; y: 300.0; z: 0.5; rhw: 1.0; color: $FFFF0000),
    (x:  50.0; y: 300.0; z: 0.5; rhw: 1.0; color: $00FFFFFF));

  // Quadrat
  // Leider werden keine Vierecke von D3D unterstützt so wie in OpenGL, also müssen wir
  // doch alles aus Vertices erstellen
  QuadratVertex : TQuadratVertex = (
    (x: 350.0; y: 300.0; z: 0.5; rhw: 1.0; color: $000000FF), // x, y, z, rhw, Farbe
    (x: 350.0; y:  50.0; z: 0.5; rhw: 1.0; color: $00FF0000),
    (x: 590.0; y: 300.0; z: 0.5; rhw: 1.0; color: $0000FFFF), // x, y, z, rhw, Farbe
    (x: 590.0; y:  50.0; z: 0.5; rhw: 1.0; color: $0000FF00));

type
  TSample3DForm = class(TForm)
    ...
  private
    ...
    // Die Buffer, die unsere Vertizes enthalten
    dxtriangle, dxsquare: IDirect3DVertexBuffer8;
  ...
  end;

Unter FormCreate werden sie initialisiert:

// Initialisieren aller Variablen
procedure TSample3DForm.FormCreate(Sender: TObject);
begin
  lpd3d := nil;
  lpd3ddevice := nil;
  dxtriangle := nil;
  dxsquare := nil;
  red := 0; green := 0; blue := 0;
end;

Wir wollen für jedes der beiden Objekte einen eigenen Vertex-Buffer verwenden. Die Bereitstellung der Buffer erfolgt in der Routine D3DInitScene durch Aufruf von CreateVertexBuffer. Wir setzen ihn auf D3DUSAGE_WRITEONLY, weil wir in den Vertexbuffer nur schreiben wollen. Als nächstes, geben wir unser Vertexformat (siehe oben) an. Mit dem D3DPOOL geben wir an, wie bzw. wo wir unser Vertex ablegen wollen. Wir legen fest, dass DirectX den Speicher der Grafikkarte automatisch verwalten soll (D3DPOOL_MANAGED). Die letzte Variable ist ein Pointer vom Typ Byte-Array. Dieser Pointer zeigt auf den von DirectX bereitgestellten Vertexbuffer.
Zum Kopieren eines Vertex-Arrays in diesen Buffer muss dieser zuerst für andere Zugriffe gesperrt werden (Lock). Das Kopieren geschieht mit der Delphi-Prozedur Move. Anschließend wird der Buffer wieder freigegeben (Unlock).

procedure TSample3DForm.D3DInitScene;
var
  hr: HRESULT;
  vbVertices: pByte;
begin
  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    // Hier wird der Vertex Buffer für das Dreieck erstellt
    hr := CreateVertexBuffer (sizeof(TTriangleVertex),
      D3DUSAGE_WRITEONLY, // Nur Schreibzugriffe
      D3D8T_CUSTOMVERTEX, // Unser Vertex
      D3DPOOL_MANAGED,
      dxtriangle);       // Pointer zu unserem Dreieck

    if FAILED(hr) then FatalError(0, 'Fehler beim Erstellen des Vertex Buffers '+
    'für unser Dreieck');

    // Hier wird der Vertex Buffer für das Quadrat erstellt
    hr := CreateVertexBuffer (sizeof(TQuadratVertex),
      D3DUSAGE_WRITEONLY,
      D3D8T_CUSTOMVERTEX,
      D3DPOOL_MANAGED,
      dxsquare);

    if FAILED(hr) then FatalError(0, 'Fehler beim Erstellen des Vertex Buffers '+
    'für unser Viereck');

    // Nun kopieren wir unsere Vertizes in den Buffer
    // Wir müssen es zuvor mit Lock festhalten, um es bearbeiten zu können
    with dxtriangle do begin
      hr := Lock(0, // Offset, an dem wir beginnen
        0, // Größe des locks ( 0 für alles )
        vbVertices, // Wenn erfolgreich dann hier ablegen
        0); // sonstige Flags
      if FAILED(hr) then FatalError(0, 'Fehler beim Locken des Buffers für die Dreiecke');
      // Hier wird der Vertexbuffer kopiert.
      Move(TriangleVertex, vbVertices^, SizeOf(TTriangleVertex));
      // Und wieder loslassen
      Unlock;
    end;

    // Dasselbe Spiel mit dem Viereck
    with dxsquare do begin
      hr := Lock(0,0,vbVertices,0);
      if FAILED(hr) then FatalError(0, 'Fehler beim Locken des Buffers für die Quadrate');
      Move(QuadratVertex, vbVertices^, SizeOf(TQuadratVertex));
      Unlock;
    end;
  end;
end;

Bei Beendigung des Programm müssen die Vertexbuffer wieder freigegeben werden:

procedure TSample3DForm.D3DKillScene;
begin
  dxtriangle := nil;
  dxsquare := nil;
end;

Das Zeichnen der beiden Objekte erfolgt in der Routine D3DRender (siehe auch Lektion 1). Mit SetVertexShader wird DirectX mitgeteilt, welches Vertexformat wir benutzen (siehe oben). Anschießend wird der Stream 0 als Quelle für die nachfolgende Zeichenoperation DrawPrimitive festgelegt. Er zeigt auf den Vertexbuffer des Dreiecks. Für das Quadrat geschieht dies entsprechend. In DrawPrimitive wird als erster Parameter ein Wert (D3DPT_TRIANGLELIST) übergeben, der festlegt, wie aus der Liste der Eckpunkte die Dreiecke zu erzeugen sind (siehe DirectX-SDK unter „D3DPRIMITIVETYPE“).
D3DPT_TRIANGLELIST: unabhängige Liste von Vertextripeln
D3DPT_TRIANGLESTRIP: mit einer Seite in Form eines Streifens aneinanderhängende Dreiecke
D3DPT_TRIANGLEFAN: der erste Punkt ist allen Dreiecken gemeinsam (fächerartige Struktur – siehe Lektion 4)
Der zweite Parameter ist die Indexnummer des Vertex, mit dem begonnen werden soll, der dritte gibt die Anzahl der zu zeichnenden „Primitiven“ (Dreiecke) an.

procedure TSample3DForm.D3DRender;
begin
  with lpd3ddevice do begin
    Clear(0,       // Wieviel Rechtecke löschen? 0 Löscht alle
      nil,         // Pointer zu den Rechtecken. nil = 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
      // Vertex Shader sind wirklich komplex, aber es lassen sich damit gute Effekte
      // erzielen. Genauere Beschreibungen in der SDK, denn alles hier niederschreiben
      // sprengt den Rahmen eines Tutorials
      SetVertexShader(D3D8T_CUSTOMVERTEX);

      // Die D3D Renderfunktionen lesen aus Streams. Hier sagen wir DX welchen Stream
      // es verwenden soll
      SetStreamSource(0,dxtriangle,sizeof(TMyVertex));

      // Hier zeichnen wir nun endlich unser Dreieck. Wir übergeben DirectX, was wir
      // zeichen wollen, 0 für den Index des 1. Vertex, 1 für die Anzahl der Vertizes
      DrawPrimitive(D3DPT_TRIANGLELIST,0,1);

      // Jetzt setzen wir den Stream auf unser Quadrat
      SetStreamSource(0,dxsquare,sizeof(TMyVertex));

      // Hier werden die 2 Dreiecke gezeichnet
      DrawPrimitive(D3DPT_TRIANGLESTRIP,0,2);

      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.