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

Direct3D mit Delphi unter DirectX 8

Die ersten bewegten Objekte

Die in der Lektion 2 erzeugten Objekte (Dreieck und Quadrat) sollen jetzt in Bewegung versetzt werden. Sie sollen sich jeweils um eine vertikale Achse drehen. Dazu müssen wir die ganze Szene in eine echte perspektivische Projektion umsetzen.
Zuerst wollen wir die Objekte in etwas geänderter Form definieren:

type
// Unsere Struktur, in der wir die Dreiecke speichern
  TMyVertex = record
    x, y, z: single;   // Position des Vertex
    color: dword;    // Farbe des Vertex
  end;

  TMyVertices = array [0..6] of TMyVertex;

Die Koordinaten der Eckpunkte werden hier in nicht transformierter Form angegeben. Die Transformation erfolgt erst später beim Rendern der Szene. Die Konstante D3D8T_CUSTOMVERTEX wird daher auf einen anderen Wert gesetzt (D3DFVF_XYZ für die Koordinaten x,y,z und D3DFVF_DIFFUSE für die Farbinformation). Außerdem wollen wir alle Objekte in einem Vertexbuffer unterbringen. Die ersten drei Vertizes gehören zum Dreieck, die restlichen vier zum Quadrat. Daher müssen wir hier auch nur eine Variable für den Vertexbuffer MyVB definieren. Die Initialisierung in FormCreate wird wie in Lektion 2 beschrieben vorgenomme.

const
// Mit D3DFVF_DIFFUSE sagen wir DX, das unsere Struktur eine Farbe hat.
//  D3DFVF_XYZ bedeutet, das es sich um ein untransformiertes Vertex handelt
  D3D8T_CUSTOMVERTEX = D3DFVF_XYZ or D3DFVF_DIFFUSE;

  MyVertices: TMyVertices = (
    (x:   0.0; y:   1.0; z:  0.0; color: $FF0000FF ), // x, y, z, color
    (x:   1.0; y:  -1.0; z:  0.0; color: $0000FF00 ),
    (x:  -1.0; y:  -1.0; z:  0.0; color: $00FF0000 ),

    (x:  -1.0; y:  -1.0; z:  0.0; color: $FF0000FF ),
    (x:  -1.0; y:   1.0; z:  0.0; color: $0000FF00 ),
    (x:   1.0; y:  -1.0; z:  0.0; color: $00FF0000 ),
    (x:   1.0; y:   1.0; z:  0.0; color: $00FFFFFF ));

type
  TSample3DForm = class(TForm)
    ...
  private
    ...
    // Buffer, der unsere Vertizes enthält
    MyVB: IDirect3DVertexBuffer8;
  ...
  end;

Bei der Initialisierung der Szene muss also auch nur ein Vertexbuffer erzeugt werden. Für das spätere Rendern sind hier allerdings einige weitere Einstellungen vorzunehmen: D3DRS_CULLMODE legt fest, wie die Rückseiten der Dreicke ermittelt und angezeigt werden. Mit D3DCULL_NONE sind beide Seiten sichtbar (In der Lektion 4 werden auch andere Möglichkeiten erläutert). D3DRS_LIGHTING schaltet eine Beleuchtung der Dreiecksflächen ein oder aus. Da wir noch keine Lichtquelle verwenden, setzen wir diese Option sicherheitshalber auf Aus (Sie würde hier sowieso nicht funktionieren, da unsere Vertizes nicht mit Normalenvektoren versehen sind – dazu mehr in einer späteren Lektion).
Außerdem müssen wir für die später erforderliche Koordinatentransformation der perspektivischen Ansicht die Grundeinstellungen vornehmen. Dazu sind die Koordinaten der Kamera (Ort des Auges des Betrachters – x = 0, y = 0, z = -6) und eines Zielpunktes (Mitte unserer darzustellenden Objekte – x = 0, y = 0 z = 0), sowie eine Angabe darüber erforderlich, was in unserer Welt oben ist (hier die pos. Y-Achse – x = 0, y = 1, z = 0). Mit D3DXMatrixLookAtLH wird die zugehörige Viewmatrix erstellt.
Die erforderliche Projektionsmatrix erzeugen wir mit D3DXMatrixPerspectiveFovLH. Hier sind anzugeben: Der Winkel des Sichtfeldes in rad, das Seitenverhältnis, sowie die Minimal- und Maximalentfernung für den sichtbaren Bereich.

procedure TSample3DForm.D3DInitScene;
var
  hr: HRESULT;
  vbVertices: pByte;
  ViewMatrix, matProj: TD3DXMATRIX;
begin
  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    // Hier wird der Vertex Buffer erstellt, der groß genug ist,
    // um alle Vertizes zu enthalten.
    hr := CreateVertexBuffer (sizeof(TMyVertices),
      D3DUSAGE_WRITEONLY, // Nur Schreibzugriffe
      D3D8T_CUSTOMVERTEX, // Unser Vertex
      D3DPOOL_MANAGED,
      MyVB);              // Pointer zu unserem Buffer

    if FAILED(hr) then FatalError(0, 'Fehler beim Erstellen des Vertex Buffers');

    // Nun kopieren wir unsere Vertizes in den Buffer
    // Wir müssen es zuvor mit Lock festhalten, um es bearbeiten zu können
    with MyVB 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 Vertex-Buffers');
      // Hier wird der Vertexbuffer kopiert.
      Move(MyVertices, vbVertices^, SizeOf(TMyVertices));
      // Und wieder loslassen
      Unlock;
    end;

    // Einstellungen für die Dreiecksrückseiten und die Beleuchtung
    SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
    SetRenderState(D3DRS_LIGHTING, 0);

    // Hier erstellen wir unsere SichtMatrix. Denkt einfach es ist
    // eure Kamera, von der aus wir sehen. Als erstes setzen wir die Kamera
    // um 6 Einheiten zurück auf der Z-Achse.
    D3DXMatrixLookAtLH(ViewMatrix, D3DXVECTOR3(0.0, 0.0, -6.0),
                                   D3DXVECTOR3(0.0, 0.0, 0.0),
                                   D3DXVECTOR3(0.0, 1.0, 0.0));

    // Da sich unsere Kamera nicht bewegt legen wir sie einfach fest
    SetTransform(D3DTS_VIEW, ViewMatrix);

    D3DXMatrixPerspectiveFovLH(matProj,   // Resultierende Matrix
      D3DX_PI/4, // Sichtwinkel
      640/480,   // Seitenverhätnis
      1.0,       // Mindeste Nähe
      100.0);    // Maximal sichtbare Entfernung

    // Unsere Projektion wird sich niemals bewegen, also setzen wir sie fest
    SetTransform(D3DTS_PROJECTION, matProj );
  end;
end;

Jetzt müssen wir noch unsere Render-Routine erweitern. Da unsere beiden Objekte rotieren sollen, werden bei jedem Aufruf der Routine die zu den Drehachsen gehörenden Winkel erhöht. Der nachfolgende Teil entspricht weitgehend dem Beispiel aus Lektion 2. Vor dem Zeichnen der Objekte müssen allerdings noch die erforderliche Koordinatentransformationen vorgenommen werden. Wir erzeugen uns jeweils eine Rotationsmatrix um die gewünschten (sich ändernden) Winkel mit D3DXMatrixRotationYawPitchRoll oder D3DXMatrixRotationY und multiplizieren sie mit einer Translationsmatrix D3DXMatrixTranslation, um Drehen und Verschieben zu kombinieren. Die so erzeugte Matrix wird als Weltmatrix für die im Hintergrund ablaufende Koordinatentransformation beim Zeichnen verwendet.
Da DrawPrimitive den Index des ersten Vertex des jeweiligen Objekts entsprechend anpassen (0 für das Dreieck, 3 für das Quadrat).

procedure TSample3DForm.D3DRender;
var
  matWorld,
  rot_matrix,                     //Our rotation matrix
  trans_matrix: TD3DXMATRIX;    //Our translation matrix
begin
  rot_triangle_X := rot_triangle_X+0.03;
  rot_triangle_Y := rot_triangle_Y+0.02;
  rot_triangle_Z := rot_triangle_Z+0.01;
  rot_square := rot_square+0.01;

  if assigned(lpd3ddevice) then 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(0,0,0), //Hintergrundfarbe schwarz
      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,MyVB, SizeOf(TMyVertex));

      // Die Rotation um alle Achsen.
      D3DXMatrixRotationYawPitchRoll(rot_matrix, rot_triangle_Y,
        rot_triangle_X, rot_triangle_Z);
      // Verschiebe das Dreieck um 1.2 nach links
      D3DXMatrixTranslation(trans_matrix, -1.2, 0.0, 0.0);
      // Kombiniere das Teil mit der Welt
      D3DXMatrixMultiply(matWorld, rot_matrix, trans_matrix);
      SetTransform(D3DTS_WORLD, matWorld);

      // Hier zeichnen wir 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);

      // Rotation um die Y Achse
      D3DXMatrixRotationY(rot_matrix, rot_square);
      D3DXMatrixTranslation(trans_matrix, 1.2, 0.0, 0.0);
      D3DXMatrixMultiply(matWorld, rot_matrix, trans_matrix);
      SetTransform(D3DTS_WORLD, matWorld);

      // Jetzt zeichnen wir das Viereck. Man beachte, das der Offset
      // nun auf 3 steht!
      DrawPrimitive(D3DPT_TRIANGLESTRIP, 3, 2);

      EndScene;
    end;

    // Zeige Resultate auf dem Bildschirm
    Present(nil, nil, 0, nil);
  end;
end;

Als letztes müssen wir nun noch Bewegung in unser Programm bringen. Dazu können wir z. B. ein Timer-Objekt aus den Delphi-Komponeneten in unser Projekt einbauen und im OnTimer-Ereignis die Render-Routine aufrufen.
Eine schnellere Bewegung lässt sich erreichen, wenn unser Programm im Exklusivmodus läuft. Dazu müssen wir für das Application.OnIdle-Ereignis eine kleine Routine schreiben. OnIdle wird vom System immer ausgeführt, wenn auf irgendwelche Eingaben (z. B. von der Tastatur) gewartet wird. Wir fügen in der Deklaration von TSample3DForm unter private folgende Zeilen ein:

...
    Animate: boolean;
    ...
    procedure MyIdleHandler(Sender: TObject; var Done: Boolean);
    ...

Im OnCreate-Ereignis erfolgt die Zuweisung unserer Routine an OnIdle:

...
  Animate := true;
  Application.OnIdle := MyIdleHandler;
  ...

Mit der Boolean-Variablen Animate können wird unsere Animation über die Tastatur (Leertaste) stoppen und wieder starten. Wenn wir die Variable Done in MyIdleHandler unverändert (true) lassen, übergibt das System nach Verlassen der Routine die Steuerung an den Message-Handler, um andere Anwendungen zum Zuge kommen zu lassen. Wird dieser Wert auf false gesetzt, wird dies nicht gemacht, so dass unser Programm im Exklusivmodus läuft.

procedure TSample3DForm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key=VK_ESCAPE then Close;
  if Key=VK_SPACE then Animate := not Animate;
end;

procedure TSample3DForm.MyIdleHandler (Sender: TObject; var Done: Boolean);
begin
  if Animate then D3DRender;
  Done := false;
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.