BoxDepth = 0.4; // Eintauchtife der Kiste
Lektion 7 Delphi und DirectX Lektion 5

DirectX 8 und Delphi

Lektion 6: Vollbild oder Fenstermodus, Hintergrund mit Textur

von Jürgen Rathlev

In dieser Lektion soll unserer 3D-Szene mit der würfelförmigen Kiste ein Rundumhintergrund hinzugefügt werden. Die dazu erforderlichen Texturen (Wolkenhimmel und Wasseroberfläche) sind den Beispielen aus dem Microsoft DirectX-SDK entnommen.
Als erstes wollen wir aber unsere 3D-Initialisierung so erweitern, dass die Darstellung wahlweise im Vollbildmodus (wie bisher) oder im Fenstermodus (neu) erfolgen kann. Die letztere Alternative bietet vor allem bei der Programmentwicklung und Fehlersuche Vorteile, da nur hier der integerierte Delphi-Debugger einwandfrei arbeitet. Im Deklarationsteil sind einige Variablen hinzuzufügen:
type
  TSample3DForm = class(TForm)
  ...
  private
    { Private-Deklarationen }
    // Direct3D Interfaces (siehe Lektion 5)
    lpd3d            : IDIRECT3D8;
    lpd3ddevice      : IDirect3DDevice8;
    D3dDevCaps       : TD3DCaps8;
    HwVertexProcess,
    FullScreen       : boolean;
    ...
	
procedure TSample3DForm.FormCreate(Sender: TObject);
begin
  ...
  FullScreen:=false;     // setze auf"true" für Vollbildmodus
  ...
Durch Setzen der Variable FullScreen auf true oder false beim Programmstart kann zwischen den beiden Darstellungen gewählt werden. In der Routine zur Initialisierung der 3D-Grafik beibt der Teil für den Vollbildmodus unverändert. Für den Fenstermodus wird die aktuelle Einstellung über GetAdapterDisplayMode abgefragt.
Außerdem ist eine Abfrage eingefügt, ob die Grafikkarte das Hardware-Vertexprocessing unterstützt. Entsprechend wird die DirectX-Schnittstelle initialisiert.
procedure TSample3DForm.D3DInit;
var
  ...
  d3ddm : TD3DDISPLAYMODE;
  vp    : integer;
begin
  ...
  with d3dpp do begin
    ...
    Windowed:=not FullScreen;
    if FullScreen then begin
      // Vollbild
      BackBufferWidth   := 640;
      BackBufferHeight  := 480;
      BackBufferFormat  := D3DFind16BitMode;
      BackBufferCount:=1;         // 1 Backbuffer
      end
    else begin
      // Fenster
      hr:=lpd3d.GetAdapterDisplayMode(D3DADAPTER_DEFAULT,d3ddm);
      if failed(hr) then FatalError(hr,'Fehler beim Ermitteln des Dislaymodes');
      BackBufferFormat := d3ddm.Format;
      end;
    end;  
  ...
  // Hardware T&L?
  hr:=lpd3d.GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,D3dDevCaps);
  if FAILED(hr) then FatalError(hr,'Fehler beim Abfragen der DevCaps');
  HwVertexProcess:=D3dDevCaps.DevCaps and D3DDEVCAPS_HWTRANSFORMANDLIGHT <> 0;

  if HwVertexProcess then vp:=D3DCREATE_HARDWARE_VERTEXPROCESSING
  else vp:=D3DCREATE_SOFTWARE_VERTEXPROCESSING;

  //Erstellen des D3D-Device
  hr:=lpd3d.CreateDevice(D3DADAPTER_DEFAULT,
                         D3DDEVTYPE_HAL,
                         Handle,
                         vp,
                         d3dpp,
                         lpd3ddevice);
  ...
Nachfolgend soll beschrieben werden, wie ein Rundumhintergrund in die Szene eingefügt wird. Wir benutzen dazu wieder den Würfel, machen ihn aber sehr groß und legen die Texturen auf die Innenseiten. In den Beispielen des Microsoft DirectX-SDK sind geeignete Bitmaps für alle Seiten enthalten (skybox_...bmp). Sie bilden einen Wolkenhimmel mit einer Wasseroberfläche. Da unsere Kiste auf dem Wasser schwimmen soll, legen wir den Boden des Würfels in den Koordinatennullpunkt. Da die Kiste etwas eintauchen soll, muss der Z-Buffer verwendet werden. DirectX berechnet dann automatisch die Entfernung der Vertizes vom Beobachter und verdeckt unsichtbare Bereiche. In diesem Beispiel soll sich im Gegensatz zur vorangegangenen Lektion nicht die Kiste sondern der Beobachter in einem Kreis um den Nullpunkt bewegen.
Als erstes definieren wir einige Konstanten am Anfang des Programms, mit denen sich später sehr einfach einige Einstellungen ändern lassen:
const
  Fovy = Pi/4;            // Öffnungswinkel des Sichtkegels (Field of view)
  ViewDist = 15;          // Abstand des Betrachters vom Nullpunkt
  ViewHeight = 5;         // Höhe des Betrachters über der Wasseroberfläche
  Delta = 0.3;            // Winkelinkrement für Animation
  CubeScale = 1.5;        // Skalierungsfaktor für Kiste
  SkyScale = 100;         // Skalierungsfaktor für Hintergrundbox
Die Konstante Fovy lässt sich mit der Brennweitenverstellung eines Zoomobjektivs vergleichen. Große Werte bewirken eine Sicht wie durch ein Weitwinkelobjektiv, kleine Werte entsprechen einem Teleobjektiv. Mit Delta kann die Geschwindigkiet der Animation an den jeweiligen Rechner angepasst werden. Es ist ein Maß für die Winkeländerung bei der kreisförmigen Bewegung des Beobachters. Bei langsamen Rechnern wählt man einen etwas größeren Wert.
Der Initialsierungsteil des Programms muss um einige neue Variablen für die verschiedenen Texturen erweitert werden:
type
  TSample3DForm = class(TForm)
  ...
  private
    ...
    // Unsere Texturen
    SkyTop,SkyBottom,
    SkyFront,SkyBack,
    SkyLeft,SkyRight,
    CubeTexture   : IDIRECT3DTEXTURE8;

    // Rotationswinkel für den Beobachter in Grad
    RotY   : single  ;
    ...

procedure TSample3DForm.FormCreate(Sender: TObject);
begin
  ...	
  SkyTop:=nil; SkyBottom:=nil;
  SkyFront:=nil; SkyBack:=nil;
  SkyLeft:=nil; SkyRight:=nil;
  ...
In der Szeneninitialisierung gibt es ebenfalls einige Änderungen. Da wir mehrere Texturen laden müssen, definieren wir uns eine kleine lokale Unterroutine. Außerdem müssen wir für das Rendern den Z-Buffer einschalten. Den Vertexbuffer für den Würfel können wir sowohl für die Kiste als auch für den Hintergrund verwenden. Hier werden beim Rendern nur unterschiedliche Weltkoordinaten mit SetTransform(D3DTS_WORLD,..) verwendet.
// Initialisieren der Szenenobjekte
procedure TSample3DForm.D3DInitScene;
var
  hr            : HRESULT;
  vbVertices    : pByte;
  ProjMatrix    : TD3DXMATRIX;

  // Textur laden
  function LoadTexture (Filename : string) : IDIRECT3DTEXTURE8;
  var
    hr         : HRESULT;
  begin
    if assigned(lpd3ddevice) then with lpd3ddevice do begin
      hr:=D3DXCreateTextureFromFile(lpd3ddevice,pchar(MPath+Filename),Result);
      if FAILED(hr) then FatalError(hr,'Fehler beim Laden der Textur: '+Filename);
      end;
    end;

begin
  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    ...
    // Lighting abschalten, da unsere Vertices nocht keine Normalenvektoren besitzten
    SetRenderState(D3DRS_LIGHTING,0);
    // Z-Buffer beim Rendern einschalten
    SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE);

    // Erstelle eine Projektionsmatrix
    D3DXMatrixPerspectiveFovLH(ProjMatrix,  // Resultierende Matrix
                               Fovy,        // Öffnungswinkel des Sichtkegels
                               640/480,     // Seitenverhältnis der Ansicht
                               1.0,         // Mindeste Nähe
                               1000.0);     // Maximal sichtbare Entfernung
    SetTransform(D3DTS_PROJECTION,ProjMatrix );

    // Texturen laden
    CubeTexture:=LoadTexture ('ieap-kiste-g.bmp');
    SkyTop:=LoadTexture ('skybox_top.bmp');
    SkyBottom:=LoadTexture ('skybox_bottom.bmp');
    SkyFront:=LoadTexture ('skybox_front.bmp');
    SkyBack:=LoadTexture ('skybox_back.bmp');
    SkyLeft:=LoadTexture ('skybox_left.bmp');
    SkyRight:=LoadTexture ('skybox_right.bmp');
    ...
Die Routine zum Rendern der Szene muss als erstes den sich ändernden Standort des Beobachters festlegen SetTransform(D3DTS_VIEW,..). Anschließend werden nacheinander Die Oberseite und die Seitenwände des Hintergrundwürfels mit den verschiedenen Texturen gezeichnet. Der Standardwürfel wird dabei um den Faktor SkyScale vergrößert. Für das Rendern der Textur muss hier D3DCULL_CW gesetzt werden, da die Innenseiten des Würfels sichtbar sein sollen.
// Rendern der Szene
procedure TSample3DForm.D3DRender;
var
  WorldMatrix,
  ViewMatrix,
  TempMatrix     : TD3DXMATRIX;
begin
  RotY:=RotY+Delta;   // Rotation des Beobachters

  if assigned(lpd3ddevice) then with lpd3ddevice do begin
    ...
      // Hier erstellen wir unsere SichtMatrix
      // Der Beobachter bewegt sich in einer Höhe von ViewHeight
      // kreisförmig im Abstand "ViewDist" um die Kiste
      D3DXMatrixLookAtLH (ViewMatrix,D3DXVECTOR3(ViewDist*sin(Pi180*RotY),
                                                 ViewHeight,
                                                 ViewDist*cos(Pi180*RotY)),
                                     D3DXVECTOR3(0.0,0.0,0.0),
                                     D3DXVECTOR3(0.0,1.0,0.0));

      SetTransform(D3DTS_VIEW,ViewMatrix);

      // Vertex Typ einstellen
      SetVertexShader(D3D8T_CUSTOMVERTEX);

      // Stream auf Vertexbuffer für Würfel setzen
      SetStreamSource(0,CubeVB,sizeof(TMyVertex));

      // Berechne die Welt-Matrix für den Hintergrund (Seiten und oben)
      D3DXMatrixScaling(WorldMatrix,SkyScale,SkyScale,SkyScale);
      SetTransform(D3DTS_WORLD,WorldMatrix);

      // Hintergrund (Seiten): Textur auswählen und zeichnen
      SetRenderState(D3DRS_CULLMODE,D3DCULL_CW);
      SetTexture(0,SkyFront);         // Vorderseite des Hintergrunds
      DrawPrimitive(D3DPT_TRIANGLELIST,0,2);

      SetTexture(0,SkyLeft);         // linke Seite des Hintergrunds
      DrawPrimitive(D3DPT_TRIANGLELIST,6,2);

      SetTexture(0,SkyBack);         // Hinterseite des Hintergrunds
      DrawPrimitive(D3DPT_TRIANGLELIST,12,2);

      SetTexture(0,SkyRight);        // rechte Seite des Hintergrunds
      DrawPrimitive(D3DPT_TRIANGLELIST,18,2);

      // Hintergrund (oben): Textur auswählen und zeichnen
      SetTexture(0,SkyTop);          // Oberseite des Hintergrunds
      DrawPrimitive(D3DPT_TRIANGLELIST,24,2);
      ...
Der Boden wird auf die Höhe des Koordinatennullpunkts angehoben und mit gleichen Skalierung wie vor gezeichnet.
      ...
      // Berechne die Welt-Matrix für den Hintergrund (Boden)
      // setze dabei y auf 0 (Wasseroberfläche)
      D3DXMatrixScaling(WorldMatrix,SkyScale,0,SkyScale);
      SetTransform(D3DTS_WORLD,WorldMatrix);
      // Hintergrund (unten): Textur auswählen und zeichnen
      SetTexture(0,SkyBottom);
      DrawPrimitive(D3DPT_TRIANGLELIST,30,2);
      ...
Als letztes kommt noch die Kiste dran. Sie wird mit CubeScale skaliert und um 0,4 einheiten angehoben (Eintauchtiefe). Der eingeschaltete Z-buffer sorgt dafür, dass die eingetauchten Teile der Kiste vom Wasser verdeckt werden. Für die Textur muss hier wieder D3DCULL_CCW gesetzt werden, da die Außenseiten sichtbar sein sollen.
      // 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,0);

      // Skaliere die Kiste
      D3DXMatrixScaling(TempMatrix,CubeScale,CubeScale,CubeScale);
      D3DXMatrixMultiply(WorldMatrix,WorldMatrix,TempMatrix);
      SetTransform(D3DTS_WORLD,WorldMatrix);

      // Zeichnen der Kiste
      DrawPrimitive(D3DPT_TRIANGLELIST,0,12);
      ...
Es empfiehlt sich, an den verschiedenen Parametern Veränderungen vorzunehmen, um den Einfluss auf die 3D-Szene beurteilen zu können (z.B. verschiedene Sichtwinkel, Betrachterhöhen, Ein- und Ausschalten des Z-Buffers, etc.).
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.

Lektion 5 Delphi und DirectX Lektion 7