Direct3D mit Delphi unter DirectX 8
Vollbild oder Fenstermodus, Hintergrund mit Textur
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 BoxDepth = 0.4; // Eintauchtife der Kiste
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 noch 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.