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

Direct3D mit Delphi unter DirectX 8

Erzeugen von bewegten Oberflächen

Die Wasseroberfläche der vorigen Lektion sieht natürlich noch recht langweilig aus. Um eine sich bewegende Fläche mit Wellen zu erzeugen, müssen wir die Fläche in sehr viel kleinere Dreiecke zerlegen und die Höheninformation (y-Richtung in DirectX) während der Animation geeignet verändern. Wir ersetzen deshalb die untere Seite des Einheitswürfels durch ein Quadrat, das wir in beiden Richtungen (x und z) in Streifen zerlegen, so dass wir auf der Fläche ein Gitter aus kleinen Quadraten erhalten. Die Anzahl der Gitterpunkte wird über eine Programmkonstante (SurfCount) vorgegeben. Je mehr Gitterpunkte wir verwenden, um so naturgetreuer sieht es nachher aus. Der Rechenaufwand für die Neuberechnung der y-Koordinaten unserer Vertizes steigt jedoch quadratisch mit der Anzahl der Gitterlinien pro Richtung an. Da diese Berechnung für jedes Bild der Animation durchgeführt werden muss, hängt die sinnvolle Anzahl von der Geschwindigkeit des verwendeten Rechners ab. Auf einem Athlon 1200 kann man noch gut mit einer Anzahl von 201×201 Gitterpunkten arbeiten. Viel mehr sind ohnehin nicht möglich, solange wir uns auf 16-bit Indizes beschränken (siehe weiter unten).
Wenn wir unser Einheitsquadrat (-1 <= x <= 1, -1 <= z <= 1) in jeder Richtung in n Streifen unterteilen, erhalten wir (n+1)2 Vertizes. Jedes Gitterquadrat wird wie die Seitenflächen bei unserem Würfel in zwei Dreiecke unterteilt, so dass sich insgesamt 2*(n+1)2 Dreiecke mit 6*(n+1)2 Eckpunkten (Vertizes) ergeben. Da viele Eckpunkte von mehreren Dreiecken gemeinsam benutzt werden, wollen wir unsere Dreieck über einen Indexbuffer definieren. Dies spart Speicherplatz und erleichtert uns außerdem das Verändern der y-Koordinate der Vertizes während der Animation.
Ich übernehme wieder das Programm der vorigen Lektion und stelle nachfolgend die erforderlichen Änderungen dar. Zunächst ist die Initialsierung der Variablen zu erweitern:

const
  ...
  SurfCount = 180;        // Anzahl der Streifen für das Gitter der
                          // Wasseroberfläche (x und z)
                          // (SurfCount+1)^2 < 32768
  SkyScale = 50;          // Skalierungsfaktor für Hintergrundbox
  BoxDepth = 0.5;         // Eintauchtiefe der Kiste
  ...

type
  TSample3DForm = class(TForm)
    ...
  private
    WaterVB,                             // Vertexbuffer für Wasserfläche
    CubeVB: IDirect3DVertexBuffer8;    // Vertexbuffer für Würfel
    WaterIB: IDirect3DIndexBuffer8;     // Indexbuffer für Wasseroberfäche

    WVertex: array of TMyVertex;      // Vertizes des Gitters für die Oberfläche
    WIndex: array of word;           // Indexbuffer für die Dreiecke der Oberfläche
    WSize,                               // Anzahl der Streifen
    WvCount,                             // Anzahl der Gitter-Vertizes
    WiCount: integer;                 // Anzahl der Indizes
    ...
    procedure GenerateSurface(np: integer);  // Gitter erzeugen
    procedure ChangeSurface(y: single);     // Oberfläche verändern
  ...

procedure TSample3DForm.FormCreate(Sender: TObject);
begin
  ...
  WaterVB := nil; WaterIB := nil;
  WvCount := 0; WiCount := 0;
  ...

Gitter und Index werden als dynamische Arrays angelegt. Ihre Größe wird erst während der Programmlaufzeit festgelegt.
Es folgt jetzt die Routine, mit der wir das Gitter für die Wasseroberfläche erstellen. Die Anzahl der Streifen, in die unser Einheitsquadrat je Seite zerlegt werden soll, wird über die Variable np an die Routine übergeben. Daraus berechnen sich die Größen für unsere Vertex- und Indexliste. Über die Unterroutine MakeVertex werden die einzelnen Vertizes mit Orts- und Textur-Koordinaten versehen. Die Textur soll später über die gesamte Quadratfläche gelegt werden. Außerdem kann hier schon der Vertexbuffer erzeugt werden.
Anschließend wird jedes Gitterquadrat in zwei Dreiecke zerlegt. Die Indizies der zugehörigen Eckpunkte werden in einer Indexliste notiert. Für das spätere Zeichnen mit DrawIndexedPrimitive müssen wir uns eine Indexbuffer erzeugen (CreateIndexBuffer). Wir setzen seine Eigenschaften auf D3DUSAGE_WRITEONLY (nur schreiben) und D3DFMT_INDEX16 (max. 65536 Vertizes). Anschließend wird die Indexliste in den IndexBuffer von DirectX kopiert. Die Funktionsweise ist hierbei genauso, wie beim Kopieren der Vertizes in den Vertexbuffer. Da die Wasseroberfläche sich bewegen soll, müssen die Höheninformationen (y) der Gitterpunkte während der Animation (siehe Routine ChangeSurface weiter unten) für jedes Bild neu berechnet und die Vertizes in den in den Vertexbuffer kopiert werden.

procedure TSample3DForm.GenerateSurface (np : integer);
var
  n2, i, j: integer;
  hr: HRESULT;
  BPtr: pByte;

  function MakeVertex(ax, ay, az: single; AColor: cardinal; au, av: single): TMyVertex;
  begin
    with result do begin
      x := ax; y := ay; z := az;
      color := AColor;
      tu := au; tv := av;
      end;
    end;

begin
  WvCount := 0; WiCount := 0; WSize := np;
  n2 := np div 2;
  // Größe der Arrays für das Punktraster festlegen
  WvCount := succ(np)*succ(np);
  SetLength(WVertex, WvCount);    // dyn. Array definieren
  SetLength(WIndex, 6*np*np);
  // Erzeuge das quadratische Raster
  for i:=0 to np do
    for j:=0 to np do
      WVertex[i*succ(np)+j] := MakeVertex((j-n2)/n2, 0, (i-n2)/n2, $FF0000, j/np, 1-i/np);

  // Die Dreiecke werden über Indizes definiert
  // (jedes Quadrat wird zwei Dreiecke zerlegt)
  for i:=0 to np-1 do begin
    for j:=0 to np-1 do begin
      // 1. Dreieck
      WIndex[WiCount] := i*succ(np)+j+1;
      WIndex[WiCount+1] := succ(i)*succ(np)+j;
      WIndex[WiCount+2] := i*succ(np)+j;
      // 2. Dreieck
      WIndex[WiCount+3] := i*succ(np)+j+1;
      WIndex[WiCount+4] := succ(i)*succ(np)+j+1;
      WIndex[WiCount+5] := succ(i)*succ(np)+j;
      inc(WiCount, 6);
    end;
  end;

  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    //Vertex-Buffer erzeugen
    hr := CreateVertexBuffer(WvCount*SizeOf(TMyVertex),
      D3DUSAGE_WRITEONLY or D3DUSAGE_DYNAMIC,
      D3D8T_CUSTOMVERTEX,  // Unser Vertextyp
      D3DPOOL_DEFAULT,
      WaterVB);
    if FAILED(hr) then FatalError(hr, 'Fehler beim Erstellen des Vertex-Buffers '+
    'für die Wasserfläche');

    // Index-Buffer erzeugen
    hr := CreateIndexBuffer(WiCount*SizeOf(word),
      D3DUSAGE_WRITEONLY,
      D3DFMT_INDEX16,
      D3DPOOL_DEFAULT,
      WaterIB);
    if FAILED(hr) then FatalError(hr, 'Fehler beim Erstellen des Index-Buffers');

    // Index Buffer kopieren
    with WaterIB do begin
      hr := Lock(0, WiCount*SizeOf(word), BPtr, 0);
      if FAILED(hr) then FatalError(hr, 'Fehler beim Locken des Index-Buffers');
      Move(WIndex[0], BPtr^, WiCount*SizeOf(word));
      Unlock;
    end;
  end;
end;

Die nachfolgende Routine wird während der Animation für jedes zu erzeugende Bild aufgerufen. Die Variable y wird in der Render-Routine aus der Rotationsbewegung des Beobachters abgeleitet. Sie schwankt im Bereich [-0,3..0,3] und ist ein Maß für die Wellenhöhe auf unserer Wasseroberfläche. In jeder horizontalen Richtung (x und z) wird der Höhe eine Kosinusfunktion unterschiedlicher Periode überlagert, so dass eine verhältnismßig realistische Wellenbewegung entsteht. Anschließend werden die neu berechneten Vertizes in den Vertexbuffer kopiert.

// Höhenwerte der Vertizes verändern (-0.3 < y < 0.3)
procedure TSample3DForm.ChangeSurface(y: single);
var
  hr: HRESULT;
  i, j: integer;
  BPtr: pByte;
begin
  for i:=0 to WSize do
    for j:=0 to WSize do
      WVertex[i*succ(WSize)+j].y := y*(cos(157*i/WSize+0.2*y)+cos(91*j/WSize-0.2*y));

  if assigned(WaterVB) then with WaterVB do begin
    hr := Lock(0, WvCount*SizeOf(TMyVertex), BPtr, D3DLOCK_DISCARD);
    if FAILED(hr) then FatalError(hr, 'Fehler beim Locken des Vertex-Buffers '+
    'für die Wasserfläche');
    Move(WVertex[0], BPtr^, WvCount*SizeOf(TMyVertex));
    Unlock;
  end;
end;

Bei der Szeneninitialisierung ist nur die Erzeugung der Oberfläche zu ergänzen:

procedure TSample3DForm.D3DInitScene;
...
begin
  ...
    // Wasseroberfläche erzeugen
    GenerateSurface(SurfCount);
  ...

Das Rendern der Szene wird um die Animation der Wasseroberfläche erweitert. Zunächst wird aus dem Drehwinkel des Beobachters eine Größe y (s.o.) berechnet, mit der die Vertizes der Wasseroberfläche verändert werden. Das Zeichnen des Hintergrunds bleibt bis auf den Boden unverändert. Für das Zeichnen des Bodens müssen wir über SetStreamSource den Vertexbuffer WaterVB auswählen. Die Textur wird aus der vorherigen Lektion übernommen. Außerdem muss der Indexbuffer initialisiert werden. Das Zeichnen erfolgt über die Funktion DrawIndexedPrimitive (siehe dazu Dokumentation zu DirectX).
Als letztes wird wie bisher die Kiste gezeichnet (StreamSource wieder auf CubeVB). Damit sie sich etwas im Seegang bewegt, wird sowohl ihre Eintauchtiefe als auch ihre Stellung zum Beobachter ständig verändert. Durch D3DXMatrixRotationYawPitchRoll wird eine Dreh- und eine kleine Taumelbewegung überlagert.

procedure TSample3DForm.D3DRender;
var
  WorldMatrix,
  ViewMatrix,
  TempMatrix: TD3DXMATRIX;
  y: single
begin
  RotY := RotY+Delta;   // Rotation des Beobachters
  y := 0.3*sin(0.2*RotY);

  // Erzeuge eine sich bewegenden Wasseroberfläche
  ChangeSurface(y);
  ...
      SetTexture(0, SkyBottom);
      SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

      // Stream auf Vertexbuffer mit Wasseroberfläche
      SetStreamSource(0, WaterVB, SizeOf(TMyVertex));
      // Indexbuffer wählen
      SetIndices(WaterIB, 0);
      // Oberfläche aus isolierten Dreiecken aufbauen
      DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, WvCount, 0, WiCount div 3);

      //Kiste zeichen
      SetStreamSource(0, CubeVB, SizeOf(TMyVertex));
      // Textur für Kiste auswählen
      SetTexture(0, CubeTexture);
      SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
      // Setze die Welt-Matrix für die Kiste etwas nach oben
      D3DXMatrixTranslation(WorldMatrix, 0, 1-BoxDepth+y, 0);
      // und lasse sie etwas gegenüber dem Hintergrund sich drehen und
      // um die vertikale Achse taumeln
      D3DXMatrixRotationYawPitchRoll(TempMatrix, 0.5+0.5*Pi180*RotY,
        0.1*sin(Pi180*RotY*1.5), 0.2*cos(Pi180*RotY*1.5));
      D3DXMatrixMultiply(WorldMatrix, WorldMatrix, TempMatrix);
      // Skaliere die Kiste
      D3DXMatrixScaling(TempMatrix, CubeScale, CubeScale, CubeScale);
      D3DXMatrixMultiply(WorldMatrix, WorldMatrix, TempMatrix);
      SetTransform(D3DTS_WORLD, WorldMatrix);

      // Zeichnen der Kiste
      DrawPrimitive(D3DPT_TRIANGLELIST, 0, 12);
  ...

Auch bei diesem Beispiel ist es interessant zu beobachten, welchen Einfluss die verschiedenen am Programmanfang definierten Parameter auf die Szene haben. Hier sollte man ein wenig herumprobieren.
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.