In diesem Kapitel werden Sie zum ersten Mal etwas lernen, was Sie in Ihrer Endanwendung auch sehen werden. Dies ist zwar nur eine Farbe, in der Ihre Anwendung gefärbt wird, aber immerhin. In diesem Kapitel lernen Sie den letzten Teil des Grundgerüstes, der Bestandteil jeder DirectX-Anwendung ist.
DirectX arbeitet mit zwei Speicherplätzen für den Bildschirm, dem sogenannten Frontbuffer und dem Backbuffer. Der Frontbuffer ist der Speicherbereich, der auf dem Bildschirm sichtbar ist. Der Backbuffer ist von der selben Grösse, wie der Frontbuffer, und kann mit Ihm ausgetauscht werden, so dass der Backbuffer zum Frontbuffer wird und umgekehrt. In dem Sie Ihre 3D-Szene immer jeweils auf dem Backbuffer generieren und mit dem Frontbuffer austauschen, können Sie flimmerfreie Animationen erzeugen, da dass Vertauschen der beiden Buffer über Zeiger gesteuert wird und deshalb sehr schnell geht. In diesem Kapitel lernen Sie, wie Sie Zugriff auf den Backbuffer bekommen, diesen in eine Farbe tauchen und anschliessend mit dem Frontbuffer vertauschen.
Alle Renderbefehle müssen zwischen folgenden beiden Befehlen liegen:
d3ddev8.BeginScene; // Render-Befehle d3ddev8.EndScene; |
d3ddev8 ist hierbei das aus den vorigen Kapiteln verwendete IDirect3DDevice8. Diese Befehlsfolge können Sie ruhig mehrmals hintereinander benutzen. Diese Befehle haben noch keinen Einfluss auf den Frontbuffer. In den meisten Anwendungen wird es allerdings nur einmal nötig sein, diese Befehlsfolge pro Frame zu verwenden.
Mit dem Clear-Befehl Ihres Devices löschen Sie Ihren Backbuffer um ihn von Grund auf neu beschreiben zu können. Dieser Befehl ist also nicht zwingend notwendig. Wenn Sie die Funktion jedoch nicht verwenden und eine Animation zeigen, dann werden Ihre Objekte, die Sie bewegen, eine Art Spur hinter sich her ziehen, weil die Bereiche, in denen das Objekt gezeigt wird, nicht gelöscht werden. Der Befehl sieht so aus:
function Clear(const Count : LongWord; pRects : PD3DRect; const Flags : LongWord; const Color : TD3DColor; const Z : Single; const Stencil : LongWord) : HResult; |
Count: Gibt die Anzahl der Rechtecke an, die im Array pRects gespeichert ist. Dies hat folgenden Sinn. Sie können bestimmen, ob der ganze Bildschirm gelöscht werden soll, oder nur bestimmte Bereiche. Entscheiden Sie sich für das Erstere, dann muss dieser Parameter 0 sein und pRects NIL. Wollen Sie jedoch, dass nur bestimmte Bereiche gelöscht werden, dann bestimmen Sie mit diesem Parameter, wieviele der Rechtecke aus dem Array pRects gelöscht werden sollen.
pRects: Erstes Element eines Arrays des Typs D3DRect. Die Koordinaten, die Sie in diesem Record angeben, entsprechen den Bildschirmkoordinaten. Möchten Sie alles sichtbare löschen, können Sie als Parameter auch einfach NIL übergeben. Dann muss Count 0 sein.
Flags: Eine oder mehrere Konstanten der D3DClearFlags, mit denen Sie angeben, was genau Sie löschen möchten. Denn neben der sichtbaren Oberfläche gibt es noch den sogenannten Z-Buffer und den Stencil-Buffer. Die letzten beiden werden wir in einem späteren Kapitel noch ausführlicher behandeln. Beachten Sie, dass mindestens ein Flag angegeben sein muss.
Color: Ein Farbwert, mit der der zu löschende Bereich überdeckt wird. TD3DColor ist dabei vom Typ Longword. Sie übergeben also Delphi im Prinzip eine 32 Bit-Zahl, die für eine Farbe steht. Die Jedi-Header bieten Funktionen, mit denen Sie bequem die gewünschten Farbwerte übergeben können.
Z: Dieser Parameter wird dem Z-Buffer übergeben. Er bestimmt, wie weit Sie in der Tiefe noch Objekte erkennen können. Z kann zwischen 0.0 und 1.0 betragen, wobei 0.0 die nächste Distanz zum Betrachter ist und 1.0 die entfernteste.
Stencil: Dieser Wert bestimmt, wieviel Sie vom Stencil-Buffer löschen möchten. Stencil kann zwischen 0 und 2n-1 liegen, wobei n die Bittiefe des Stencil-Buffers angibt.
Nachdem Sie alle Dinge, die Sie darstellen wollen, gerendert und mit der EndScene-Funktion das Rendern abgeschlossen haben, bringen Sie mit dem .Present-Befehl den Inhalt des Backbuffers auf den Bildschirm und erhalten einen neuen Backbuffer für die nächste Renderprozedur.
function Present(pSourceRect, pDestRect : PRect; const hDestWindowOverride : HWND; pDirtyRegion : PRgnData) : HResult; |
pSourceRect: Mit diesem Parameter bestimmen Sie in Form eines Rechtecks, welcher Teil des Backbuffer-Bildes auf den Bildschirm gebracht werden soll. Es gelten, wie auch beim Clear-Befehl die normalen Bildschirmkoordinaten. Übersteigen die angegebenen Koordinaten den Fenster- oder Bildschirmrahmen, werden die Koordinaten angepasst. Es entsteht also keine Fehlermeldung. Möchten Sie den ganzen Inhalt auf den Bildschirm bringe, dann geben Sie NIL an. Vorraussetzung, um diesen Parameter nutzen zu können, ist, dass das Device mit dem SwapEffect D3DSWAPEFFECT_COPY oder D3DSWAPEFFECT_COPY_VSYNC erstellt worden ist. Ansonsten muss der Parameter NIL sein.
pDestRect: Hier geben Sie in Form eines Rechtecks an, in welchem Teil Ihrer Anwendung das Bild, dass Sie mit pSourceRect eingegrenzt haben, dargestellt werden soll. Übersteigen die angegebenen Koordinaten den Fenster- oder Bildschirmrahmen, werden die Koordinaten angepasst. Es entsteht also keine Fehlermeldung. Sie können, wie bei pSourceRect entweder Bildschirmkoordinaten oder NIL angeben. Letzteres ist zwingend notwendig, wenn Sie einen anderen SwapEffect benutzen als D3DSWAPEFFECT_COPY oder D3DSWAPEFFECT_COPY_VSYNC.
hDestWindowOverride: Das Handle des Fensters oder der visuellen Komponente, in der das gerenderte Bild angezeigt werden soll. Geben Sie 0 an, wenn Sie das Handle aus den D3DPresent_Parameters benutzen wollen. Wenn Sie aber zum Beispiel Ihre 3D-Welt innerhalb eines Panels oder Labels anzeigen lassen wollen, so können Sie auch das Handle dieser Komponenten angeben.
pDireyRegion: Dieser Parameter wird von DirectX 8.0 zur Zeit noch nicht genutzt, und sollte deshalb 0 sein.
Nachfolgend möchte ich Ihnen zeigen, wie typische Rahmen(-bedingungen) in der Praxis aussehen können. Beachten Sie auch hier wieder, dass ich die Variablenvorgaben aus den vorherigen Kapiteln verwende. Fangen wir mit dem ersten an:
d3ddev8.BeginScene; d3ddev8.Clear( 0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0); // Render-Anweisungen d3ddev8.EndScene; d3ddev8.Present(nil, nil, 0, nil); |
So könnte zum Beispiel Ihre Renderprozedur aussehen. In diesem Fall wird der gesamte Backbufferinhalt auf die gesamte zur Verfügung stehende Fläche des Fensters bzw. Bildschirms gebracht. Da wir bisher nichts konkretes Rendern, sehen wir ein schwarze Fläche. Die Farbübergabe lässt sich am praktischsten mit der Funktion D3DCOLOR_XRGB erledigen, bei der Sie einen Rot-, Grün und Blau-Farbwert zwischen 0 und 255 angeben.
Hier ist ein Beispiel, wie Sie nur bestimmte Bereiche Ihres Backbuffers löschen:
var recht: Array[0..1] of TD3DRect; precht: Array of PD3DRect; begin setlength(precht, 2); recht[0].x1:= 100; recht[0].y1:= 120; recht[0].x2:= 300; recht[0].y2:= 320; recht[1].x1:= 400; recht[1].y1:= 420; recht[1].x2:= 500; recht[1].y2:= 520; precht[0]:= @recht[0]; // precht[0] zeigt auf recht[0] precht[1]:= @recht[1]; // precht[1] zeigt auf recht[1] d3ddev8.Clear( 2, precht[0], D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0); d3ddev8.BeginScene; // Render-Anweisungen d3ddev8.EndScene; d3ddev8.Present(nil, nil, 0, nil); end; |
Da der .Clear-Befehl des Direct3DDevices das erste Element eines Arrays mit einem Zeiger auf ein D3DRect haben möchte, müssen wir zuerst die gewünschten Koordinaten in einem Array vom Typ D3DRect angeben, und diese dann dem jeweiligen Zeiger übergeben. Nun werden nur der Bereiche von (100/120) bis (300/320) und der Bereich von (400, 420) bis (500, 520) gelöscht. Objekte, die sich ausserhab dieser Felder bewegen, ziehen eine Spur Ihrer eigenen Farbe hinter sich her. Nebenbei zeigt dieses Code-Sample, dass der .Clear-Befehl auch vor .BeginScene verwendet werden kann.
Und so könnte ein Programm aussehen, dass nur einen bestimmten Bereich des Backbuffers auf den Bildschirm bringt:
var flaeche: TRect; pflaeche: PRect; begin flaeche.left:= 100; flaeche.top:= 300; flaeche.right:= 400; flaeche.bottom:= 600; pflaeche:= @flaeche; d3ddev8.Clear( 0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0); d3ddev8.BeginScene; // Render-Anweisungen d3ddev8.EndScene; d3ddev8.Present(pflaeche, pflaeche, 0, nil); end; |
Das Prinzip ist hierbei im Grunde das selbe. Zuerst werden die Daten einem Record vom Typ TRect übergeben, und anschliessend der Zeiger auf dieses Record einem typisiertem Pointer (pflaeche). In diesem Beispiel liegt der Bereich, der aus dem Backbuffer geholt wird, am gleichen Ort, wie der Bereich, in dem der Inhalt dargestellt wird. Dies hat folgenden Vorteil: wenn das Fenster einer laufenden DirectX-Anwendung vergrössert oder verkleinert, wird auch automatisch ihre 3D-Szene dem neuen Fenster angepasst und dem entsprechend skaliert. Wenn Sie dies jedoch nicht wollen, können Sie mit diesem Code angeben, dass Ihre 3D-Szene immer gleich gross angezeigt werden soll, egal wie gross oder klein Ihr Fenster ist. Vergessen Sie nicht, beim initialisieren des Devices D3DSWAPEFFECT_COPY oder D3DSWAPEFFECT_COPY_VSYNC als SwapEffect anzugeben.
Wenn Sie bereits versucht haben, die Beispiele aus den Kapiteln direkt umzusetzen, dann werden Sie sich vielleicht auch schon gefragt haben, in welchen Prozeduren diese einzelnen Schritte am besten aufgehoben sind.
In den meisten DirectX-Anwendungen werden Sie wahrscheinlich wollen, dass das DirectX von Beginn an aktiv ist. Hier bietet es sich an, die Initialisierung in der OnCreate oder OnActivate-Prozedur vorzunehmen, womit garantiert wäre, dass dies auch nur genau einmal geschieht. Ihre eigentliche Anwendung, also das, was wir hier erstmal als Rahmen bezeichnet haben, sollten Sie in einer anderen Prozedur auslagern. Wenn Sie eine Animationen, Bewegungen etc. in Ihrer Anwendung benutzen wollen, ist es notwendig, dass kontinuirlich Frame auf Frame das Bild generiert wird. Zum eine könnten Sie dafür den Delphi-Standardtimer benutzen. Doch damit erhalten sie nicht unbedingt die optimalste Geschwindigkeit. Für den Anfang empfehle ich Ihnen die sogenannte OnIdle-Prozedur, die von Delphi automatisch genau dann aufgerufen wird, wenn keine anderen Operationen anfallen. Über einen kleinen Trick sorgen Sie dafür, dass diese Prozedur immer und immer wieder aufgerufen wird:
type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private procedure OnIdle(Sender: TObject; var done: boolean); { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin // Initialisierungsbefehle Application.OnIdle:= OnIdle; end; procedure TForm1.OnIdle(Sender: TObject; var done: boolean); begin done:= false; // Rahmen und Renderanweisungen end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin // Close-Anweisungen end; |
Entscheidend sind folgende Dinge: sie müssen dem Event OnIdle eine Prozedur zuweisen, die aufgerufen werden soll. In diesem Fall ist das die gleichnamige Prozedur OnIdle. Normalerweise wird diese immer jeweils nur einmal aufgerufen, nachdem festgestellt wurde, dass sich die Anwendung in einer Leerlaufphase befindet. Damit OnIdle immer wieder aufgerufen wird, muss die Variable done der OnIdle-Prozedur auf false gestellt werden. Das Resultat: wir haben einen kontinuirlichen sehr zügig aufeinanderfolgenden Selbstaufruf, der für unsere Zwecke völlig ausrecht.
Falls Sie stattdessen DirectXGraphics kontrolliert und nicht kontinuirlich verwenden möchten, ist es natürlich auch möglich die entscheidenen Befehle in irgendeiner anderen Prozedur unterzubringen.
Wenn Sie bis hier hin gekommen sind, dann haben Sie die grösste Durststrecke bis zu Ihrer Direct3D-Anwendung überwunden. Denn im Gegensatz zu den letzten Kapiteln, werden Sie in den nun folgenden Abschnitten Dinge lernen, von denen Sie am PC auch optisch etwas haben. Also, nochmal kurz durchatmen, und weiter gehts. Denn jetzt beginnt es, wirklich interessant zu werden.