Tutorial Terrain1: Unterschied zwischen den Versionen
K (fehlendes p im source code) |
BenBE (Diskussion | Beiträge) K (Rechtschreibung korrigiert) |
||
Zeile 1: | Zeile 1: | ||
− | =Heightmaps- | + | =Heightmaps - Außenlandschaften mit System= |
==Vorwort== | ==Vorwort== | ||
Zeile 15: | Zeile 15: | ||
hierauf nicht weiter eingehen, da ihr das ja bereits hinter euch habt. Für | hierauf nicht weiter eingehen, da ihr das ja bereits hinter euch habt. Für | ||
dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke | dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke | ||
− | verwendet(OpenGL12.pas). Dazu habe ich eine für 3DNow! optimierte | + | verwendet (OpenGL12.pas). Dazu habe ich eine für 3DNow! optimierte |
Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt | Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt | ||
(ich hab alle Dateien den Sources hinzugefügt). Zum Laden der Textur kommt | (ich hab alle Dateien den Sources hinzugefügt). Zum Laden der Textur kommt | ||
noch die GlAux Library zum Einsatz. Ich war einfach zu faul, die Dinger von | noch die GlAux Library zum Einsatz. Ich war einfach zu faul, die Dinger von | ||
− | Hand an OpenGl zu senden.Den GL Initialisierungsteil solltet ihr verstehen | + | Hand an OpenGl zu senden. Den GL Initialisierungsteil solltet ihr verstehen |
können (wenn nicht, ist dies hier eh das falsche Tutorial für euch). | können (wenn nicht, ist dies hier eh das falsche Tutorial für euch). | ||
− | Gerendert wird im OnIdle Event der Anwendung, aber schaut sie euch am Besten | + | Gerendert wird im OnIdle-Event der Anwendung, aber schaut sie euch am Besten |
einfach einmal an. | einfach einmal an. | ||
Zeile 97: | Zeile 97: | ||
</pascal> | </pascal> | ||
− | Soweit das Framework. Wenn OpenGL noch nicht initialisiert wurde,wird | + | Soweit das Framework. Wenn OpenGL noch nicht initialisiert wurde, wird |
abgebrochen. Ansonsten werden die Puffer geleert und die Einheitsmatrix | abgebrochen. Ansonsten werden die Puffer geleert und die Einheitsmatrix | ||
− | geladen. Nun wird die Kamera gedreht. Die Winkel werden im OnMouseDown, | + | geladen. Nun wird die Kamera gedreht. Die Winkel werden im OnMouseDown-, |
− | OnMouseMove und OnMouseUp Event verändert und berechnet. Dann wird die | + | OnMouseMove- und OnMouseUp-Event verändert und berechnet. Dann wird die |
Skybox gerendert. Es folgt noch die Fehlerprüfung und ein Framecounter. | Skybox gerendert. Es folgt noch die Fehlerprüfung und ein Framecounter. | ||
Doch wie zeichnen wir jetzt die Skybox? Zuerst sollten wir festlegen, wie | Doch wie zeichnen wir jetzt die Skybox? Zuerst sollten wir festlegen, wie | ||
die Texturen in unserem Textur Array abgelegt werden. Ich fand diese | die Texturen in unserem Textur Array abgelegt werden. Ich fand diese | ||
− | Sortierung sinnvoll: Norden, Osten, Süden, Westen, Oben und Unten.Mit diesen | + | Sortierung sinnvoll: Norden, Osten, Süden, Westen, Oben und Unten. Mit diesen |
Texturen wird ein Würfel der Kantenlänge 4 bemalt. Eine Länge von 2 wäre | Texturen wird ein Würfel der Kantenlänge 4 bemalt. Eine Länge von 2 wäre | ||
unpraktisch, da ich die nahe Clipping Plane auf eine Entfernung von 1 | unpraktisch, da ich die nahe Clipping Plane auf eine Entfernung von 1 | ||
Zeile 215: | Zeile 215: | ||
</pascal> | </pascal> | ||
− | Bevor wir anfangen können | + | Bevor wir anfangen können zu rendern, müssen wir die Daten aus dem Bitmap |
− | auch noch laden. Ein guter Zeitpunkt dafür wäre im OnShow Ereignis, in dem | + | auch noch laden. Ein guter Zeitpunkt dafür wäre im OnShow-Ereignis, in dem |
bereits OpenGl initialisiert wird. Fügen wir dort also eine neue Prozedur | bereits OpenGl initialisiert wird. Fügen wir dort also eine neue Prozedur | ||
ein, die ein Bitmap öffnet und die für uns interessanten Daten in den | ein, die ein Bitmap öffnet und die für uns interessanten Daten in den | ||
Zeile 324: | Zeile 324: | ||
zusammen kommt. Um das Licht zu aktivieren, habe ich die | zusammen kommt. Um das Licht zu aktivieren, habe ich die | ||
Initialisierungsfunktion SetupGl etwas erweitern müssen. Zu beachten ist, | Initialisierungsfunktion SetupGl etwas erweitern müssen. Zu beachten ist, | ||
− | dass im | + | dass im OnIdle-Event das Licht mittels glDisable(GL_LIGHTING) vor dem Aufruf |
von PaintSkyBox deaktiviert werden muss und vor dem Aufruf von | von PaintSkyBox deaktiviert werden muss und vor dem Aufruf von | ||
PaintHeightMap wieder aktiviert. Nachdem Aufruf von PaintSkyBox wird der | PaintHeightMap wieder aktiviert. Nachdem Aufruf von PaintSkyBox wird der | ||
− | Tiefenpuffer geleert. Würde das nicht | + | Tiefenpuffer geleert. Würde das nicht geschehen, würden sich Skybox und |
Landschaft überschneiden (Zum Ausprobieren: Den Tiefenpuffer nach | Landschaft überschneiden (Zum Ausprobieren: Den Tiefenpuffer nach | ||
PaintSkyBox nicht wieder leeren und sich einmal im Kreise drehen. Na, | PaintSkyBox nicht wieder leeren und sich einmal im Kreise drehen. Na, |
Version vom 28. Dezember 2005, 16:19 Uhr
Inhaltsverzeichnis
Heightmaps - Außenlandschaften mit System
Vorwort
Hi,
dies Tutorial wird euch in die Basics des Terrain Rendering einweisen. Ich werde euch zeigen, wie man am Besten mit Heightmaps umgeht und wie man sie rendert. Daneben werde ich euch auch verraten, wie man der Szene etwas mehr Atmosphäre verleiht, was durch den Einsatz einer Skybox erreicht wird.
Vorarbeit
Bevor irgendetwas passiert, müssen wir OpenGl initialisieren. Ich möchte hierauf nicht weiter eingehen, da ihr das ja bereits hinter euch habt. Für dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke verwendet (OpenGL12.pas). Dazu habe ich eine für 3DNow! optimierte Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt (ich hab alle Dateien den Sources hinzugefügt). Zum Laden der Textur kommt noch die GlAux Library zum Einsatz. Ich war einfach zu faul, die Dinger von Hand an OpenGl zu senden. Den GL Initialisierungsteil solltet ihr verstehen können (wenn nicht, ist dies hier eh das falsche Tutorial für euch). Gerendert wird im OnIdle-Event der Anwendung, aber schaut sie euch am Besten einfach einmal an.
Wer mit Matrizen umgehen kann, beherrscht das Wichtigste an der 3D-Programmierung. Wer nicht zu den Genies zählt, sollte nicht sofort aufgeben und sich viele Beispiele ansehen und viel mit diesen herumspielen. Letztendlich siegt die Erfahrung doch über das Wissen :)
Etwas Atmosphäre bitte
Widmen wir uns zunächst einmal dem Himmel. Wenn gerade brauchbares Wetter ist, dann schaut einfach mal hoch (Solltet ihr jetzt die Decke eines Zimmers erblicken, dann seid mal nicht so faul und bewegt euch kurz auf die Straße). Nun, was seht ihr? Ich sehe blauen Himmel, Wolken,... Jedem sollte der Anblick bestens bekannt sein. Um die Bilder einer Skybox zu erstellen, kann man einige Programme zu Hilfe nehmen. Ich benutze meistens Terragen, da es kostenlos ist und gute Ergebnisse liefert. Sehr bekannt ist auch Bryce. Was wir brauchen sind sechs Bilder. Eines von oben, eines von unten und je eines für die 4 Himmelsrichtungen. Macht also 6 Seiten, wie die eines Würfels. Ich habe mir die Arbeit bereits gemacht, ihr müsst euch also nicht abrackern und auch eine Skybox erstellen. An der Texturauflösung solltet ihr nicht sparen. Hier ist wirklich einmal klotzen angesagt, damit die Sache nicht klobig aussieht. Ich habe eine Auflösung von 512x512 gewählt. Bei solchen Texturen ist der Einsatz von Kompression empfehlenswert, bei so wenigen Bildern jedoch nicht wirklich nötig.
Was wir benötigen ist: Sechs geladene Texturen und ein unbelichteter Würfel. Die IDs der Texturen werden in einem Array gehalten, damit wir sie so einfach wie möglich erreichen:
SkyBoxTexturen : Array[0..5] of TGlUInt;
Nun müssen wir die Bilder nur noch anzeigen:
procedure TSkyBoxForm.ApplicationEventsIdle(Sender: TObject; var Done: Boolean); procedure PaintSkyBox; begin ... end; (*PaintSkyBox*) var Error : LongInt; begin Done := True; if not OpenGLInitialized then Exit; Done := False; glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT); glLoadIdentity; glRotatef(XRotation, 1, 0,0); glRotatef(YRotation, 0, 1,0); PaintSkyBox; //Error Handler Error := glgetError; if Error <> GL_NO_ERROR then begin MessageBeep(65535); Caption := gluErrorString(Error) end; //FrameCounter Inc(Frames); if GetTickCount - StartTick >=1000 then begin Caption := Format('Sky Box Demo FPS: %f', [Frames/(GetTickCount - StartTick)*1000]); Frames := 0; StartTick := GetTickCount end; SwapBuffers(Canvas.Handle) end; (*ApplicationEventsIdle*)
Soweit das Framework. Wenn OpenGL noch nicht initialisiert wurde, wird abgebrochen. Ansonsten werden die Puffer geleert und die Einheitsmatrix geladen. Nun wird die Kamera gedreht. Die Winkel werden im OnMouseDown-, OnMouseMove- und OnMouseUp-Event verändert und berechnet. Dann wird die Skybox gerendert. Es folgt noch die Fehlerprüfung und ein Framecounter.
Doch wie zeichnen wir jetzt die Skybox? Zuerst sollten wir festlegen, wie die Texturen in unserem Textur Array abgelegt werden. Ich fand diese Sortierung sinnvoll: Norden, Osten, Süden, Westen, Oben und Unten. Mit diesen Texturen wird ein Würfel der Kantenlänge 4 bemalt. Eine Länge von 2 wäre unpraktisch, da ich die nahe Clipping Plane auf eine Entfernung von 1 gesetzt habe (Folglich verschwindet ein solcher am Ursprung aufgehängter Würfel). Zum Zeichnen definieren wir uns ein Array, das am Ende das Rendering erleichtert: Jede Wand hat 4 Vertices, mit je 3 Koordinaten:
procedure PaintSkyBox; const QuadPosition : Array[0..5] of Array[0..3] of Array[0..2] of Single = (((2.005,2.005,-1.995),(2.005,-2.005,-1.995),(-2.005,-2.005,-1.995), (-2.005,2.005,-1.995)), //Nordseite ((1.995,2.005,2),(1.995,-2.005,2),(1.995,-2.005,-2), (1.995,2.005,-2)), //Ostseite ((-2.005,2.005,1.995),(-2.005,-2.005, 1.995),(2.005,-2.005,1.995), (2.005,2.005,1.995)), //Südseite ((-1.995,2.005,-2),(-1.995,-2.005,-2),(-1.995,-2.005,2), (-1.995,2.005,2)), //Westseite ((-2,2,-2),(-2,2,2),(2,2,2), (2,2,-2)), //Oberseite ((2,-2,-2),(2,-2,2),(-2,-2,2),(-2,-2,-2))); //Unterseite
Man könnte mir jetzt vorwerfen, dass dies kein echter Würfel ist, sondern nur fast. Die etwas abgearteten Werte sind alle nahe an 2. Sie sind nötig, da OpenGl sich ein klein wenig verrechnet, was bei einem Wert von genau 2.0 Streifen erzeugt. Um dies zu umgehen, werden die vier Himmelrichtungen etwas näher an den Betrachter gezogen und gleichzeitig etwas gestreckt. Jetzt wollen wir noch Texturkoordinaten pro Vertex und einige andere Variablen definieren:
TexturePos : Array[0..3] of Array[0..1] of Single = ((1,1),(1,0),(0, 0),(0, 1)); var Side, Vertex : Integer; begin ... end; (*PaintSkyBox*)
Nun müssen wir unsere mühsam eingetippten Daten OpenGl verständlich übermitteln. Wir gehen also alle Seiten durch, aktivieren eine Textur, übergeben Vertices sowie Texturkoordinaten. Normalen sind nicht nötig, da Skyboxes nicht beleuchtet werden:
for Side := 0 to 5 do begin //Textur aktivieren glBindTexture(GL_TEXTURE_2D, SkyBoxTexturen[Side]); glBegin(GL_QUADS); //Vertieces und Tex Coords übergeben for Vertex := 3 downto 0 do begin glTexCoord2fv(@TexturePos[Vertex][0]); glVertex3fv(@QuadPosition[Side][Vertex][0]) end; glEnd() end
Das Ergebnis:
Das System der Heightmaps
Jetzt haben wir bereits ein nettes kleines Programm. Doch das eigentliche Ziel ist noch in weiter Ferne. Heightmaps sind Graustufenbilder. Je weißer ein Pixel ist, desto höher ist der Punkt in der Landschaft. Oft haben die Maps eine Größe von 2nx2n, aber an sich spielt das keine Rolle. Auf alle Fälle wird die Map in viele kleine Dreiecke aufgeteilt, die dann auf dem Bildschirm angezeigt werden.
Die Sache mit dem Bitmap
Als erstes brauchen wir ein Bitmap mit einer Landschaft. Dieses lässt sich mit einem besseren Malprogramm erzeugen oder mit einem Generator. Ich werde hier ein Heightmap einer echten Landschaft verwenden, welches ich irgendwo im Netz gefunden habe:
Da das Auslesen der Pixeldaten direkt aus einem Bitmap eine wirklich sehr, sehr langsame Angelegenheit ist, sollten wir die Daten zuerst in einem wesentlich schnelleren Delphi Array zwischenspeichern. Die Größe des Arrays wählen wir (86+1)x(86+1), ein Viertel der Größe des Bitmaps. Das sollte uns eigentlich genügen, allein schon da höhere Auflösungen mehr Rechenzeit beanspruchen. Wir sollten auch überlegen, auf welche Größe wir unsere Karte strecken wollen (eigentlich ist das nicht nötig, will man aber die Auflösung der Umgebung später doch einmal erhöhen, ohne dabei die Größe der Map auch gleich mit zu verändern, ist dies durchaus sinnvoll). Fügen wir also unserem Code folgende Zeilen hinzu:
... MapSize = 1024; MapResolution = 86; type TMapDaten = Array[0..MapResolution, 0..MapResolution] of Byte; TSkyBoxForm = class(TForm) ... private ... MapDaten : TMapDaten; ...
Bevor wir anfangen können zu rendern, müssen wir die Daten aus dem Bitmap auch noch laden. Ein guter Zeitpunkt dafür wäre im OnShow-Ereignis, in dem bereits OpenGl initialisiert wird. Fügen wir dort also eine neue Prozedur ein, die ein Bitmap öffnet und die für uns interessanten Daten in den MapDaten ablegt:
procedure LoadHeightMap; var Bmp : TBitMap; X,Z : LongInt; begin Bmp := TBitMap.Create; try Bmp.LoadFromFile('YU14H.bmp'); //Bitmap in MapDaten laden... for X := 0 to MapResolution do for Z := 0 to MapResolution do MapDaten[X,Z] := Trunc(Bmp.Canvas.Pixels [Trunc(X/MapResolution*Bmp.Width), Trunc(Z/MapResolution*Bmp.Height)] / clWhite * 255) except MessageDlg('Fehler beim laden der Heightmap', mtError, [mbOk], 0) end; Bmp.Free end; (*LoadHeightMap*)
Ich denke, das meiste erklärt sich bereits von selbst. Die Zeile MapDaten[X,Z] := ... bedarf möglicherweise etwas genaueren Erläuterungen. Da jeder Eintrag in MapDaten genau ein Byte groß ist, dürfen die eingesetzten Werte nur im Bereich 0 bis 255 liegen. Wenn wir uns aber die Farbe eines Pixels aus dem Bitmap holen, sind in ihm Werte für Blau, Rot und Grün enthalten (Für jede Farbe wird 1 Byte verwendet. Das sind dann 3 Byte, nicht 1). Durch Teilen durch clWhite erhalten wir den Weißheitsgrad in Prozent. Multiplizieren wir den erhaltenen Wert mit 255, landen wir im gewünschten Bereich zwischen 0 und 255. Da der Wert jetzt aber noch ein Single ist, wir aber ein Byte brauchen, müssen wir den Teil hinter dem Komma abschneiden, was die Funktion Trunc erledigt. Beim Zugriff auf das Pixel mittels Bmp.Canvas.Pixles[...] wird ähnlich verfahren.
Rendern der Heightmap
Wie bereits erwähnt, wird die Heightmap mit Dreiecken gerendert (Vierecke die zwar naheliegend wären, sind eher unpraktisch, aber das wird sicher erst in fortgeschritteneren Tutorials ersichtlich). Wir werden immer vier aneinanderliegende Punkte aus den frisch erzeugten MapDaten entnehmen und daraus zwei Dreiecke zaubern.
Es dürfte also kein größeres Problem vor uns liegen. Grid abgehen, Dreiecke
zeichnen, nächster Gridpunkt...
procedure PaintHeightMap; const VertPos : Array[0..3] of Array[0..1] of Byte = ((0,0),(0,1),(1,1),(1,0)); var X,Z,I : LongInt; Vertieces : Array[0..3] of TVertex; procedure PaintTriangle(V1, V2, V3 : Integer); var Normale : TVertex; begin Inc(Triangles); //Flächennormale berechnen Normale := VectorCrossProduct( VectorSubtract(Vertieces[V1], Vertieces[V2]), VectorSubtract(Vertieces[V2], Vertieces[V3])); NormalizeVector(Normale); //Dreieck zeichnen glNormal3fv(@Normale[0]); glBegin(GL_TRIANGLES); glVertex3fv(@Vertieces[V1][0]); glVertex3fv(@Vertieces[V2][0]); glVertex3fv(@Vertieces[V3][0]); glEnd() end; (*PaintTriangle*) begin for X := 0 to MapResolution - 1 do for Z := 0 to MapResolution - 1 do begin //Alle Vertieces erzeugen for I := 0 to 3 do begin Vertieces[I][0] := (X + VertPos[I][0])/MapResolution*MapSize; Vertieces[I][2] := (Z + VertPos[I][1])/MapResolution*MapSize; Vertieces[I][1] := MapDaten[X + VertPos[I][0],Z + VertPos[I][1]] end; PaintTriangle(0,1,3); PaintTriangle(1,2,3) end end; (*PaintHeightMap*)
Was passiert genau? Wie bereits gesagt, werden die Gridpunkte der Reihe nach abgegangen. An jedem werden die Orte der vier anliegenden Vertices berechnet und schließlich die zwei Dreiecke gezeichnet. Als Parameter werden der Funktion PaintTriangle die drei für das zu zeichnende Dreieck relevanten Vertices übergeben. PaintTriangle selbst berechnet zuerst die Flächennormale, damit auch eine halbwegs realistische Darstellung des Lichts zusammen kommt. Um das Licht zu aktivieren, habe ich die Initialisierungsfunktion SetupGl etwas erweitern müssen. Zu beachten ist, dass im OnIdle-Event das Licht mittels glDisable(GL_LIGHTING) vor dem Aufruf von PaintSkyBox deaktiviert werden muss und vor dem Aufruf von PaintHeightMap wieder aktiviert. Nachdem Aufruf von PaintSkyBox wird der Tiefenpuffer geleert. Würde das nicht geschehen, würden sich Skybox und Landschaft überschneiden (Zum Ausprobieren: Den Tiefenpuffer nach PaintSkyBox nicht wieder leeren und sich einmal im Kreise drehen. Na, gesehen?).
Der Lohn der ganzen Aktion:
Workshop
Um euer Hirn noch ein wenig zu belasten, habe ich hier noch ein paar Ideen zur Erweiterung des kleinen Programms:
- Die Landschaft mit einer Textur überziehen, damit sie nicht ganz so langweilig grau aussieht
- Die Normalen nicht für einzelne Polygone berechnen, sondern für jedes
Vertex. Dadurch sieht die Landschaft am Ende nicht so eckig aus, sondern wird rund. Tipp: Idealerweise werden alle Normalen vorberechnet und der Typ TMapDaten um einen Vektor für eben diese Normalen erweitert.
Ausblick auf die nächsten Tutorials
Ich hoffe jetzt natürlich, dass euch dieses schnuckelige Tutorial interessiert hat, es ist im Übrigen mein erstes :-). Das Thema Heightmaps ist noch lange nicht abgehandelt. Als nächstes werde ich ein Tutorial schreiben, das erklärt, wie man echte Schatten in die Landschaft bekommt Wenn ich die Zeit finde, werde ich euch vielleicht auch das Prinzip von Level of Details näherbringen, mit dem erreicht werden kann, dass nicht immer gleich die komplette Map mit vollen Details gerendert wird, sondern nur der Teil, der in direkter Nähe zur Kamera ist und der Teil, der wegen seiner starken Höhendifferenzen beim Rendern einen höheren Detailgrad verlangt als der Rest.
Euer
- Nico Michaelis aka DelphiC
|
||
Vorhergehendes Tutorial: - |
Nächstes Tutorial: - |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |