DirectX 8 und DelphiLektion 8: Erzeugen von bewegten Oberflächenvon Jürgen Rathlev |
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 201x201 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.
|