Tutorial Terrain1: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
K (Workshop: Einrückung repariert)
(Hier und da sprachlich aufgebohrt und etwas aktualisiert, Verweise eingepflegt, usw.)
Zeile 7: Zeile 7:
 
dies Tutorial wird euch in die Basics des Terrain Rendering einweisen. Ich
 
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
 
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
+
rendert. Daneben werde ich euch auch verraten, wie man mithilfe einer
Atmosphäre verleiht, was durch den Einsatz einer Skybox erreicht wird.
+
[[Skybox]] der Szene etwas mehr
 +
Atmosphäre verleiht.
  
 
==Vorarbeit==
 
==Vorarbeit==
  
Bevor irgendetwas passiert, müssen wir OpenGl initialisieren. Ich möchte
+
Für dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke
hierauf nicht weiter eingehen, da ihr das ja bereits hinter euch habt. Für
+
verwendet (OpenGL12.pas). Dazu eine für 3DNow! optimierte
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
 
Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt
(ich hab alle Dateien den Sources hinzugefügt). Zum Laden der Textur kommt
+
(alle Dateien liegen den Sources bei). 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, was ihr besser nicht nachmacht,
Hand an OpenGl zu senden. Den GL Initialisierungsteil solltet ihr verstehen
+
weils schrecklich alt ist und lang nicht mehr gepflegt wurde. Als
können (wenn nicht, ist dies hier eh das falsche Tutorial für euch).
+
moderne Alternative wäre da z.B. [[Glbitmap_loader|Glbitmap]] zu nennen.  
Gerendert wird im OnIdle-Event der Anwendung, aber schaut sie euch am Besten
+
Gerendert wird im OnIdle-Event der Anwendung, aber schaut bei Fragen
einfach einmal an.
+
doch einfach selbst hinein.
 
 
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===
 
===Etwas Atmosphäre bitte===
Zeile 33: Zeile 27:
 
Widmen wir uns zunächst einmal dem Himmel. Wenn gerade brauchbares Wetter
 
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
 
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).
+
erblicken, dann hop hop, raus an die Frische Luft! Wenns geht Laptop gleich
Nun, was seht ihr? Ich sehe blauen Himmel, Wolken,... Jedem sollte der
+
mitnehmen, an der frischen Luft machts gleich dreimal soviel Spaß!).
Anblick bestens bekannt sein. Um die Bilder einer Skybox zu erstellen, kann
+
Nun, was seht ihr? Ich sehe blauen Himmel, Wolken,... Nix unbekanntes
man einige Programme zu Hilfe nehmen. Ich benutze meistens Terragen, da es
+
jedenfalls. Das wollen wir doch gleich mal in OpenGl nachbauen... Wir
kostenlos ist und gute Ergebnisse liefert. Sehr bekannt ist auch Bryce. Was
+
nehmen einen Würfel (die schon angekündigte [[Skybox]]),
 +
den wir von innen anschauen und bekleben den mithübschen Bildchen,
 +
die unseren Himmel darstellen.
 +
 
 +
Zum erzeugen entsprechender Bilder können wir uns einer Kamera oder einiger
 +
kleiner Softwaretools bedienen. Ich mache letzteres und verweise mal
 +
auf die Programme Terragenund Bryce. Was
 
wir brauchen sind sechs Bilder. Eines von oben, eines von unten und je eines
 
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
+
für die 4 Himmelsrichtungen. Macht zusammen, ganz nach Adam Riese, 6.  
habe mir die Arbeit bereits gemacht, ihr müsst euch also nicht abrackern und
+
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.
+
könnt für erste Gehversuche meine Bilder nehmen,. An der Texturauflösung  
Hier ist wirklich einmal klotzen angesagt, damit die Sache nicht klobig
+
solltet ihr nicht zu sehr sparen, immerhin ist die [[Skybox]] statisch und man
aussieht. Ich habe eine Auflösung von 512x512 gewählt. Bei solchen Texturen
+
hat sie doch recht häufig vor der Nase. Also ruhig klotzen statt kleckern, damits
ist der Einsatz von Kompression empfehlenswert, bei so wenigen Bildern
+
keine klötzchen gibt.  
jedoch nicht wirklich nötig.
+
Ich habe ürsprünglich eine Auflösung von 512x512 gewählt, wobei da ruhig mit
 +
der Zeit gegangen werden darf und mit jeder Grafikkartengeneration ist schließlich
 +
mehr Platz im Speicher. Trotzdem kann der Einsatz von [[Textur#Kompressionen|Texturkompression]]
 +
nicht schaden.
  
Was wir benötigen ist: Sechs geladene Texturen und ein unbelichteter Würfel.
+
Auf dem Einkaufszettelchen steht also:  
Die IDs der Texturen werden in einem Array gehalten, damit wir sie so
+
* ein unbelichteter Würfel.
einfach wie möglich erreichen:
+
* Sechs geladene [[Texture|Texturen]].
  
 +
Um das Verständnis meines Codes ein bischen zu erhöhen, gibts ein wenig
 +
von dem Framework, in das wir unsere [[Skybox]] einpflegen wollen
 
<pascal>
 
<pascal>
 
SkyBoxTexturen : Array[0..5] of TGlUInt;
 
SkyBoxTexturen : Array[0..5] of TGlUInt;
</pascal>
+
...
 
 
Nun müssen wir die Bilder nur noch anzeigen:
 
 
 
<pascal>
 
 
procedure TSkyBoxForm.ApplicationEventsIdle(Sender: TObject;
 
procedure TSkyBoxForm.ApplicationEventsIdle(Sender: TObject;
 
   var Done: Boolean);
 
   var Done: Boolean);
Zeile 97: Zeile 98:
 
</pascal>
 
</pascal>
  
Soweit das Framework. Wenn OpenGL noch nicht initialisiert wurde, wird
+
Wenn OpenGL noch nicht initialisiert wurde, wird
abgebrochen. Ansonsten werden die Puffer geleert und die Einheitsmatrix
+
abgebrochen. Ansonsten werden die [[glClear|Puffer geleert]] und die [[glLoadIdentity|Einheitsmatrix]]
geladen. Nun wird die Kamera gedreht. Die Winkel werden im OnMouseDown-,
+
geladen, die [[Tutorial_Kamera1|Kamera]] gedreht, die Skybox gemalt, auf Fehler geprüft und
OnMouseMove- und OnMouseUp-Event verändert und berechnet. Dann wird die
+
die Leistung gemessen.
Skybox gerendert. Es folgt noch die Fehlerprüfung und ein Framecounter.
 
  
Doch wie zeichnen wir jetzt die Skybox? Zuerst sollten wir festlegen, wie
+
Und wie zeichnen wir jetzt die [[Skybox]]? Zuerst sollten wir festlegen, wie
die Texturen in unserem Textur Array abgelegt werden. Ich fand diese
+
die [[Textur|Texturen] in unserem Array abgelegt werden. Und weil ich hier
Sortierung sinnvoll: Norden, Osten, Süden, Westen, Oben und Unten. Mit diesen
+
der Capo bin entscheide ich unumstürzlich: Norden, Osten, Süden,  
Texturen wird ein Würfel der Kantenlänge 4 bemalt. Eine Länge von 2 wäre
+
Westen, Oben und Unten. Mit diesen
unpraktisch, da ich die nahe Clipping Plane auf eine Entfernung von 1
+
[[Textur|Texturen]] wird ein Würfel der Kantenlänge 4 bemalt.  
gesetzt habe (Folglich verschwindet ein solcher am Ursprung aufgehängter
+
Eine Länge von 2 wäre bescheiden, da in der Demo die nahe Clipping Plane  
Würfel). Zum Zeichnen definieren wir uns ein Array, das am Ende das
+
eine Entfernung von 1 zum Betrachter hat und mir ist es im echten Leben noch
Rendering erleichtert: Jede Wand hat 4 Vertices, mit je 3 Koordinaten:
+
nie passiert, dass der Himmel abgeschnitten erscheint. Warum also sollte ich das
 +
dann in einem OpenGl_Programm provozieren? Zum Zeichnen  
 +
definieren wir uns ein Array, welches die 4 Vertices jeder Wand des Würfels
 +
beschreibt.
  
 
<pascal>
 
<pascal>
Zeile 128: Zeile 131:
 
</pascal>
 
</pascal>
  
Man könnte mir jetzt vorwerfen, dass dies kein echter Würfel ist, sondern
+
Was erzählt jetzt der für einen Mist? Das ist kein anständiger Würfel, der kann
nur fast. Die etwas abgearteten Werte sind alle nahe an 2. Sie sind nötig,
+
doch wohl nicht die Ecken alle so unsystematisch wählen. Oder doch? Schau mer
da OpenGl sich ein klein wenig verrechnet, was bei einem Wert von genau 2.0
+
nochmal genauer hin:
Streifen erzeugt. Um dies zu umgehen, werden die vier Himmelrichtungen etwas
+
Die etwas abgearteten Werte sind alle nahe an 2. Sie sind nötig,
 +
da OpenGl sich ein klein wenig "verrechnet", was bei genauen Werten Streifen erzeugen
 +
kann. Um dies zu umgehen, werden die vier Himmelrichtungen etwas
 
näher an den Betrachter gezogen und gleichzeitig etwas gestreckt. Jetzt
 
näher an den Betrachter gezogen und gleichzeitig etwas gestreckt. Jetzt
wollen wir noch Texturkoordinaten pro Vertex und einige andere Variablen
+
noch Texturkoordinaten pro Vertex und einige andere Variablen
 
definieren:
 
definieren:
  
Zeile 145: Zeile 150:
 
</pascal>
 
</pascal>
  
Nun müssen wir unsere mühsam eingetippten Daten OpenGl verständlich
+
Und jetzt hat unsere Arraysammelwut doch noch etwas gutes: Das Zeichnen
übermitteln. Wir gehen also alle Seiten durch, aktivieren eine Textur,
+
geht in einer for-Schleife ganz einfach von der Hand
übergeben Vertices sowie Texturkoordinaten. Normalen sind nicht nötig, da
 
Skyboxes nicht beleuchtet werden:
 
  
 
<pascal>
 
<pascal>
 +
    glBegin(GL_QUADS);
 
     for Side := 0 to 5 do
 
     for Side := 0 to 5 do
 
     begin
 
     begin
 
       //Textur aktivieren
 
       //Textur aktivieren
 
       glBindTexture(GL_TEXTURE_2D, SkyBoxTexturen[Side]);
 
       glBindTexture(GL_TEXTURE_2D, SkyBoxTexturen[Side]);
      glBegin(GL_QUADS);
+
       //Ecken und Texturkoordinaten übergeben
       //Vertieces und Tex Coords übergeben
 
 
       for Vertex := 3 downto 0 do
 
       for Vertex := 3 downto 0 do
 
       begin
 
       begin
Zeile 162: Zeile 165:
 
         glVertex3fv(@QuadPosition[Side][Vertex][0])
 
         glVertex3fv(@QuadPosition[Side][Vertex][0])
 
       end;
 
       end;
      glEnd()
 
 
     end
 
     end
 +
    glEnd()
 
</pascal>
 
</pascal>
  
Das Ergebnis:
+
Und voilà. Schon fertig
  
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_skyboxdemo.jpg]]
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_skyboxdemo.jpg]]
Zeile 172: Zeile 175:
 
==Das System der Heightmaps==
 
==Das System der Heightmaps==
  
Jetzt haben wir bereits ein nettes kleines Programm. Doch das eigentliche
+
Das ist jetzt alles schön und toll, aber durch eine 3D-Landschaft kann man
Ziel ist noch in weiter Ferne. Heightmaps sind Graustufenbilder. Je weißer
+
wandeln. In Unserer Skybox dürfen wir uns nur drehen, wie langweilig. Drum
ein Pixel ist, desto höher ist der Punkt in der Landschaft. Oft haben die
+
wollen wir uns um eine etwas "tiefergehende", weniger flache Landschaft bemühen.  
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
+
Heightmaps sind Graustufenkarten einer Landschaft, topologische Karten sozusagen.  
Bildschirm angezeigt werden.
+
Je weißer ein Pixel ist, desto höher ist der Punkt in der Landschaft. Die Punkte
 +
wollen wir mit Dreiecken verbinden, die wir schließlich zeichnen wollen.
  
 
===Die Sache mit dem Bitmap===
 
===Die Sache mit dem Bitmap===
  
Als erstes brauchen wir ein Bitmap mit einer Landschaft. Dieses lässt sich
+
Also her mit so einer Karte! Die könnten wir mit einem Generator wie
mit einem besseren Malprogramm erzeugen oder mit einem Generator. Ich werde
+
[[Perlin Noise]] erzeugen oder, für die ganz wilden, mit einem  
hier ein Heightmap einer echten Landschaft verwenden, welches ich irgendwo
+
Bildbearbeitungsprogramm selbst zeichnen.
im Netz gefunden habe:
+
Ich packe dagegen eine Heightmap einer echten Landschaft aus, welches ich irgendwo
 +
im Netz aufgetrieben habe:
  
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_YU14H.jpg]]
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_YU14H.jpg]]
  
Da das Auslesen der Pixeldaten direkt aus einem Bitmap eine wirklich sehr,
+
Da das Auslesen der Pixeldaten direkt aus einem Bitmap mithilfe eines
 +
TBitmap eine wirklich sehr,
 
sehr langsame Angelegenheit ist, sollten wir die Daten zuerst in einem
 
sehr langsame Angelegenheit ist, sollten wir die Daten zuerst in einem
 
wesentlich schnelleren Delphi Array zwischenspeichern. Die Größe des Arrays
 
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
 
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
+
eigentlich genügen, allein schon, da höhere Auflösungen mehr Rechenzeit
beanspruchen. Wir sollten auch überlegen, auf welche Größe wir unsere Karte
+
beanspruchen - hey, das Tutorial ist schon ein wenig älter!
 +
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
 
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
+
der Umgebung später erhöhen, ohne dabei die Größe der Map zu verändern,  
gleich mit zu verändern, ist dies durchaus sinnvoll). Fügen wir also unserem
+
ist das sinnvoll). Fügen wir also unserem
 
Code folgende Zeilen hinzu:
 
Code folgende Zeilen hinzu:
  
Zeile 243: Zeile 250:
 
   end; (*LoadHeightMap*)
 
   end; (*LoadHeightMap*)
 
</pascal>
 
</pascal>
 +
 +
Benutzt man die Scanline Funktion von TBitmap, ein bischen
 +
Pointerarithmetik und ein fest vorgegebenes Pixelformat im Bitmap,
 +
dann kann man das noch billiger haben.
  
 
Ich denke, das meiste erklärt sich bereits von selbst. Die Zeile
 
Ich denke, das meiste erklärt sich bereits von selbst. Die Zeile
Zeile 253: Zeile 264:
 
Multiplizieren wir den erhaltenen Wert mit 255, landen wir im gewünschten
 
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
 
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,
+
aber ein Byte brauchen, schneiden wir den nichtganzzahligen Anteil einfach ab.
was die Funktion Trunc erledigt. Beim Zugriff auf das Pixel mittels
+
Beim Zugriff auf das Pixel machen wirs wieder genauso. Da hat sichs dann wieder
Bmp.Canvas.Pixles[...] wird ähnlich verfahren.
+
gerächt, daß MapDaten eine andere Auflösung hat, als das Bitmap. Das merken
 +
wir uns dann gleich mal für später geschriebene Heightmap-Renderer, gell?
  
 
===Rendern der Heightmap===
 
===Rendern der Heightmap===
  
Wie bereits erwähnt, wird die Heightmap mit Dreiecken gerendert (Vierecke
+
Ach ja, gezeichnet werden will der Kram ja jetzt auch noch. Wie war das?
die zwar naheliegend wären, sind eher unpraktisch, aber das wird sicher erst
+
Dreiecke wollten wir nehmen? Die Höhen können wir ja jetzt aus MapDaten
in fortgeschritteneren Tutorials ersichtlich). Wir werden immer vier
+
nehmen, sonst hätte sich der Aufwandt da oben mal echt nicht gelohnt.
aneinanderliegende Punkte aus den frisch erzeugten MapDaten entnehmen und
 
daraus zwei Dreiecke zaubern.
 
  
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_triangle-grid.gif]]
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_triangle-grid.gif]]
  
 
+
Grid abgehen, Dreiecke zeichnen, nächster Gridpunkt..., danach siehts
Es dürfte also kein größeres Problem vor uns liegen. Grid abgehen, Dreiecke
+
jedenfalls aus.
zeichnen, nächster Gridpunkt...
 
  
 
<pascal>
 
<pascal>
Zeile 316: Zeile 325:
 
</pascal>
 
</pascal>
  
Was passiert genau? Wie bereits gesagt, werden die Gridpunkte der Reihe nach
+
Was passiert genau? Die Gridpunkte werden der Reihe nach
 
abgegangen. An jedem werden die Orte der vier anliegenden Vertices berechnet
 
abgegangen. An jedem werden die Orte der vier anliegenden Vertices berechnet
und schließlich die zwei Dreiecke gezeichnet. Als Parameter werden der
+
und schließlich mit PaintTriangle zwei Dreiecke gezeichnet, welches zuerst die
Funktion PaintTriangle die drei für das zu zeichnende Dreieck relevanten
+
Flächennormale fürs [[Tutorial_Lektion_8|Licht]] berechnet und dann die Koordinaten an
Vertices übergeben. PaintTriangle selbst berechnet zuerst die
+
OpenGl weiterreicht. Und nicht vergessen, die [[Skybox]] verträgt das Licht nicht ganz
Flächennormale, damit auch eine halbwegs realistische Darstellung des Lichts
+
so gut, weil deren Licht ist ja bereits durch die Texturen bestimmt.
zusammen kommt. Um das Licht zu aktivieren, habe ich die
+
Was wir noch bedenken müssen ist, daß nach dem Aufruf von PaintSkyBox der [[Tiefenpuffer]] geleert
Initialisierungsfunktion SetupGl etwas erweitern müssen. Zu beachten ist,
+
werden muss. Alternativ könnten wir vor dem Zeichnen der [[Skybox]] auch mit [[glDepthMask]] das Schreiben
dass im OnIdle-Event das Licht mittels glDisable(GL_LIGHTING) vor dem Aufruf
+
in den Tiefenpuffer unterbinden und danach wieder anschalten.
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:
+
Der Lohn der Müh::
  
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_ergebnis.jpg]]
 
[[Bild:Tutorial_Heightmaps-Aussenlandschaften_mit_System_ergebnis.jpg]]
Zeile 338: Zeile 341:
 
==Workshop==
 
==Workshop==
  
Um euer Hirn noch ein wenig zu belasten, habe ich hier noch ein paar Ideen
+
Um euer Hirn noch ein wenig zu belasten, habe ich noch eine Idee zur Erweiterung des kleinen Programms:
zur Erweiterung des kleinen Programms:
+
Die Landschaft kann mit einer Textur überzogen werden, dann ist sie nicht ganz so langweilig
 
+
grau. Details? [[Tutorial_Terrain2|Hier]]!
* 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.
 
  
 +
Und jetzt? Kann ich mich nur noch verabschieden...
 
Euer
 
Euer
  
: '''Nico Michaelis''' aka DelphiC
+
: '''Nico Michaelis''' aka Delphic
 
 
  
 
{{TUTORIAL_NAVIGATION|-|[[Tutorial_Terrain2]]}}
 
{{TUTORIAL_NAVIGATION|-|[[Tutorial_Terrain2]]}}
  
 
[[Kategorie:Tutorial|Terrain1]]
 
[[Kategorie:Tutorial|Terrain1]]

Version vom 4. April 2008, 17:06 Uhr

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 mithilfe einer Skybox der Szene etwas mehr Atmosphäre verleiht.

Vorarbeit

Für dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke verwendet (OpenGL12.pas). Dazu eine für 3DNow! optimierte Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt (alle Dateien liegen den Sources bei). Zum Laden der Textur kommt noch die GlAux Library zum Einsatz, was ihr besser nicht nachmacht, weils schrecklich alt ist und lang nicht mehr gepflegt wurde. Als moderne Alternative wäre da z.B. Glbitmap zu nennen. Gerendert wird im OnIdle-Event der Anwendung, aber schaut bei Fragen doch einfach selbst hinein.

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 hop hop, raus an die Frische Luft! Wenns geht Laptop gleich mitnehmen, an der frischen Luft machts gleich dreimal soviel Spaß!). Nun, was seht ihr? Ich sehe blauen Himmel, Wolken,... Nix unbekanntes jedenfalls. Das wollen wir doch gleich mal in OpenGl nachbauen... Wir nehmen einen Würfel (die schon angekündigte Skybox), den wir von innen anschauen und bekleben den mithübschen Bildchen, die unseren Himmel darstellen.

Zum erzeugen entsprechender Bilder können wir uns einer Kamera oder einiger kleiner Softwaretools bedienen. Ich mache letzteres und verweise mal auf die Programme Terragenund Bryce. Was wir brauchen sind sechs Bilder. Eines von oben, eines von unten und je eines für die 4 Himmelsrichtungen. Macht zusammen, ganz nach Adam Riese, 6. Ich habe mir die Arbeit bereits gemacht, ihr müsst euch also nicht abrackern und könnt für erste Gehversuche meine Bilder nehmen,. An der Texturauflösung solltet ihr nicht zu sehr sparen, immerhin ist die Skybox statisch und man hat sie doch recht häufig vor der Nase. Also ruhig klotzen statt kleckern, damits keine klötzchen gibt. Ich habe ürsprünglich eine Auflösung von 512x512 gewählt, wobei da ruhig mit der Zeit gegangen werden darf und mit jeder Grafikkartengeneration ist schließlich mehr Platz im Speicher. Trotzdem kann der Einsatz von Texturkompression nicht schaden.

Auf dem Einkaufszettelchen steht also:

  • ein unbelichteter Würfel.
  • Sechs geladene Texturen.

Um das Verständnis meines Codes ein bischen zu erhöhen, gibts ein wenig von dem Framework, in das wir unsere Skybox einpflegen wollen

SkyBoxTexturen : Array[0..5] of TGlUInt;
...
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*)

Wenn OpenGL noch nicht initialisiert wurde, wird abgebrochen. Ansonsten werden die Puffer geleert und die Einheitsmatrix geladen, die Kamera gedreht, die Skybox gemalt, auf Fehler geprüft und die Leistung gemessen.

Und wie zeichnen wir jetzt die Skybox? Zuerst sollten wir festlegen, wie die [[Textur|Texturen] in unserem Array abgelegt werden. Und weil ich hier der Capo bin entscheide ich unumstürzlich: 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 bescheiden, da in der Demo die nahe Clipping Plane eine Entfernung von 1 zum Betrachter hat und mir ist es im echten Leben noch nie passiert, dass der Himmel abgeschnitten erscheint. Warum also sollte ich das dann in einem OpenGl_Programm provozieren? Zum Zeichnen definieren wir uns ein Array, welches die 4 Vertices jeder Wand des Würfels beschreibt.

  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

Was erzählt jetzt der für einen Mist? Das ist kein anständiger Würfel, der kann doch wohl nicht die Ecken alle so unsystematisch wählen. Oder doch? Schau mer nochmal genauer hin: Die etwas abgearteten Werte sind alle nahe an 2. Sie sind nötig, da OpenGl sich ein klein wenig "verrechnet", was bei genauen Werten Streifen erzeugen kann. Um dies zu umgehen, werden die vier Himmelrichtungen etwas näher an den Betrachter gezogen und gleichzeitig etwas gestreckt. Jetzt 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*)

Und jetzt hat unsere Arraysammelwut doch noch etwas gutes: Das Zeichnen geht in einer for-Schleife ganz einfach von der Hand

    glBegin(GL_QUADS);
    for Side := 0 to 5 do
    begin
      //Textur aktivieren
      glBindTexture(GL_TEXTURE_2D, SkyBoxTexturen[Side]);
      //Ecken und Texturkoordinaten übergeben
      for Vertex := 3 downto 0 do
      begin
        glTexCoord2fv(@TexturePos[Vertex][0]);
        glVertex3fv(@QuadPosition[Side][Vertex][0])
      end;
    end
    glEnd()

Und voilà. Schon fertig

Tutorial Heightmaps-Aussenlandschaften mit System skyboxdemo.jpg

Das System der Heightmaps

Das ist jetzt alles schön und toll, aber durch eine 3D-Landschaft kann man wandeln. In Unserer Skybox dürfen wir uns nur drehen, wie langweilig. Drum wollen wir uns um eine etwas "tiefergehende", weniger flache Landschaft bemühen.

Heightmaps sind Graustufenkarten einer Landschaft, topologische Karten sozusagen. Je weißer ein Pixel ist, desto höher ist der Punkt in der Landschaft. Die Punkte wollen wir mit Dreiecken verbinden, die wir schließlich zeichnen wollen.

Die Sache mit dem Bitmap

Also her mit so einer Karte! Die könnten wir mit einem Generator wie Perlin Noise erzeugen oder, für die ganz wilden, mit einem Bildbearbeitungsprogramm selbst zeichnen. Ich packe dagegen eine Heightmap einer echten Landschaft aus, welches ich irgendwo im Netz aufgetrieben habe:

Tutorial Heightmaps-Aussenlandschaften mit System YU14H.jpg

Da das Auslesen der Pixeldaten direkt aus einem Bitmap mithilfe eines TBitmap 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 - hey, das Tutorial ist schon ein wenig älter! 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 erhöhen, ohne dabei die Größe der Map zu verändern, ist das 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*)

Benutzt man die Scanline Funktion von TBitmap, ein bischen Pointerarithmetik und ein fest vorgegebenes Pixelformat im Bitmap, dann kann man das noch billiger haben.

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, schneiden wir den nichtganzzahligen Anteil einfach ab. Beim Zugriff auf das Pixel machen wirs wieder genauso. Da hat sichs dann wieder gerächt, daß MapDaten eine andere Auflösung hat, als das Bitmap. Das merken wir uns dann gleich mal für später geschriebene Heightmap-Renderer, gell?

Rendern der Heightmap

Ach ja, gezeichnet werden will der Kram ja jetzt auch noch. Wie war das? Dreiecke wollten wir nehmen? Die Höhen können wir ja jetzt aus MapDaten nehmen, sonst hätte sich der Aufwandt da oben mal echt nicht gelohnt.

Tutorial Heightmaps-Aussenlandschaften mit System triangle-grid.gif

Grid abgehen, Dreiecke zeichnen, nächster Gridpunkt..., danach siehts jedenfalls aus.

  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? Die Gridpunkte werden der Reihe nach abgegangen. An jedem werden die Orte der vier anliegenden Vertices berechnet und schließlich mit PaintTriangle zwei Dreiecke gezeichnet, welches zuerst die Flächennormale fürs Licht berechnet und dann die Koordinaten an OpenGl weiterreicht. Und nicht vergessen, die Skybox verträgt das Licht nicht ganz so gut, weil deren Licht ist ja bereits durch die Texturen bestimmt. Was wir noch bedenken müssen ist, daß nach dem Aufruf von PaintSkyBox der Tiefenpuffer geleert werden muss. Alternativ könnten wir vor dem Zeichnen der Skybox auch mit glDepthMask das Schreiben in den Tiefenpuffer unterbinden und danach wieder anschalten.

Der Lohn der Müh::

Tutorial Heightmaps-Aussenlandschaften mit System ergebnis.jpg

Workshop

Um euer Hirn noch ein wenig zu belasten, habe ich noch eine Idee zur Erweiterung des kleinen Programms: Die Landschaft kann mit einer Textur überzogen werden, dann ist sie nicht ganz so langweilig grau. Details? Hier!

Und jetzt? Kann ich mich nur noch verabschieden... Euer

Nico Michaelis aka Delphic

Vorhergehendes Tutorial:
-
Nächstes Tutorial:
Tutorial_Terrain2

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.