Tutorial Terrain1: Unterschied zwischen den Versionen
BenBE (Diskussion | Beiträge) K (Rechtschreibung korrigiert) |
K (Bischen -> Bisschen) |
||
(9 dazwischenliegende Versionen von 7 Benutzern werden nicht angezeigt) | |||
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 | + | [[Skybox]] der Szene etwas mehr |
+ | Atmosphäre verleiht. | ||
==Vorarbeit== | ==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 | |
− | dieses kleine Demo habe ich die OpenGl Bibliothek 1.2 von Mike Lischke | ||
− | verwendet (OpenGL12.pas). Dazu | ||
Geometry.pas eingebunden, die aus der GlScene Komponentensammlung stammt | 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 | + | 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_loader|Glbitmap]] zu nennen. | |
− | Gerendert wird im OnIdle-Event der Anwendung, aber schaut | + | Gerendert wird im OnIdle-Event der Anwendung, aber schaut bei Fragen |
− | einfach | + | doch einfach selbst hinein. |
− | |||
− | |||
− | |||
− | |||
− | |||
===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 | + | erblicken, dann hop hop, raus an die Frische Luft! Wenns geht Laptop gleich |
− | Nun, was seht ihr? Ich sehe blauen Himmel, Wolken,... | + | 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 | wir brauchen sind sechs Bilder. Eines von oben, eines von unten und je eines | ||
− | für die 4 Himmelsrichtungen. Macht | + | 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 |
− | + | 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 | |
− | ist der Einsatz von | + | 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 [[Textur#Kompressionen|Texturkompression]] | ||
+ | nicht schaden. | ||
− | + | Auf dem Einkaufszettelchen steht also: | |
− | + | * ein unbelichteter Würfel. | |
− | + | * Sechs geladene [[Textur|Texturen]]. | |
− | <pascal> | + | Um das Verständnis meines Codes ein bisschen zu erhöhen, gibts ein wenig |
+ | von dem Framework, in das wir unsere [[Skybox]] einpflegen wollen | ||
+ | <source lang="pascal"> | ||
SkyBoxTexturen : Array[0..5] of TGlUInt; | SkyBoxTexturen : Array[0..5] of TGlUInt; | ||
− | + | ... | |
− | |||
− | |||
− | |||
− | |||
procedure TSkyBoxForm.ApplicationEventsIdle(Sender: TObject; | procedure TSkyBoxForm.ApplicationEventsIdle(Sender: TObject; | ||
var Done: Boolean); | var Done: Boolean); | ||
Zeile 95: | Zeile 96: | ||
SwapBuffers(Canvas.Handle) | SwapBuffers(Canvas.Handle) | ||
end; (*ApplicationEventsIdle*) | end; (*ApplicationEventsIdle*) | ||
− | </ | + | </source> |
− | + | 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 | + | geladen, die [[Tutorial_Kamera1|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 Texturen in unserem | + | die [[Textur|Texturen]] in unserem Array abgelegt werden. Und weil ich hier |
− | + | 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 |
− | + | [[Textur|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. | ||
− | <pascal> | + | <source lang="pascal"> |
procedure PaintSkyBox; | procedure PaintSkyBox; | ||
const | const | ||
Zeile 126: | Zeile 129: | ||
((-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)), //Oberseite | ||
((2,-2,-2),(2,-2,2),(-2,-2,2),(-2,-2,-2))); //Unterseite | ((2,-2,-2),(2,-2,2),(-2,-2,2),(-2,-2,-2))); //Unterseite | ||
− | </ | + | </source> |
− | + | 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 | |
− | da | + | 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 | näher an den Betrachter gezogen und gleichzeitig etwas gestreckt. Jetzt | ||
− | + | noch Texturkoordinaten pro Vertex und einige andere Variablen | |
definieren: | definieren: | ||
− | <pascal> | + | <source lang="pascal"> |
TexturePos : Array[0..3] of Array[0..1] of Single = ((1,1),(1,0),(0, 0),(0, 1)); | TexturePos : Array[0..3] of Array[0..1] of Single = ((1,1),(1,0),(0, 0),(0, 1)); | ||
var | var | ||
Zeile 143: | Zeile 148: | ||
... | ... | ||
end; (*PaintSkyBox*) | end; (*PaintSkyBox*) | ||
− | </ | + | </source> |
− | + | Und jetzt hat unsere Arraysammelwut doch noch etwas Gutes: Das Zeichnen | |
− | + | geht in einer for-Schleife ganz einfach von der Hand. | |
− | |||
− | |||
− | <pascal> | + | <source lang="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]); | ||
− | + | //Ecken und Texturkoordinaten ü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; | ||
− | |||
end | end | ||
− | </ | + | glEnd() |
+ | </source> | ||
− | + | 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== | ||
− | + | 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. Darum | |
− | ein Pixel ist, desto höher ist der Punkt in der Landschaft. | + | 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=== | ===Die Sache mit dem Bitmap=== | ||
− | + | Also her mit so einer Karte! Die könnten wir mit einem Generator wie | |
− | mit einem | + | [[Perlin Noise]] erzeugen oder, für die ganz wilden, mit einem |
− | + | Bildbearbeitungsprogramm selbst zeichnen. | |
− | im Netz | + | 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 | + | 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 | + | 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: | Code folgende Zeilen hinzu: | ||
− | <pascal> | + | <source lang="pascal"> |
... | ... | ||
MapSize = 1024; | MapSize = 1024; | ||
Zeile 213: | Zeile 220: | ||
MapDaten : TMapDaten; | MapDaten : TMapDaten; | ||
... | ... | ||
− | </ | + | </source> |
Bevor wir anfangen können zu rendern, müssen wir die Daten aus dem Bitmap | 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 | + | 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 | ||
MapDaten ablegt: | MapDaten ablegt: | ||
− | <pascal> | + | <source lang="pascal"> |
procedure LoadHeightMap; | procedure LoadHeightMap; | ||
var | var | ||
Zeile 242: | Zeile 249: | ||
Bmp.Free | Bmp.Free | ||
end; (*LoadHeightMap*) | end; (*LoadHeightMap*) | ||
− | </ | + | </source> |
+ | |||
+ | Benutzt man die Scanline Funktion von TBitmap, ein bisschen | ||
+ | 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, | + | 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, dass 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 | + | 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. | |
− | |||
− | |||
[[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 | ||
+ | jedenfalls aus. | ||
− | + | <source lang="pascal"> | |
− | |||
− | |||
− | <pascal> | ||
procedure PaintHeightMap; | procedure PaintHeightMap; | ||
const | const | ||
Zeile 314: | Zeile 323: | ||
end | end | ||
end; (*PaintHeightMap*) | end; (*PaintHeightMap*) | ||
− | </ | + | </source> |
− | Was passiert genau? | + | 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 | + | und schließlich mit PaintTriangle zwei Dreiecke gezeichnet, welches zuerst die |
− | + | Flächennormale fürs [[Tutorial_Lektion_8|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, dass nach dem Aufruf von PaintSkyBox der [[Tiefenpuffer]] geleert | |
− | + | werden muss. Alternativ könnten wir vor dem Zeichnen der [[Skybox]] auch mit [[glDepthMask]] das Schreiben | |
− | dass | + | in den Tiefenpuffer unterbinden und danach wieder anschalten. |
− | von PaintSkyBox | ||
− | |||
− | Tiefenpuffer | ||
− | |||
− | |||
− | |||
− | Der Lohn der | + | 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 | + | 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]]! | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
+ | Und jetzt? Kann ich mich nur noch verabschieden... | ||
Euer | Euer | ||
− | + | Delphic | |
+ | == Dateien == | ||
+ | * {{ArchivLink|file=tut_terrain1_src_vcl|text=Beispiel-Quelltext (Delphi)}} | ||
− | {{TUTORIAL_NAVIGATION|-| | + | {{TUTORIAL_NAVIGATION|-|[[Tutorial_Terrain2]]}} |
[[Kategorie:Tutorial|Terrain1]] | [[Kategorie:Tutorial|Terrain1]] |
Aktuelle Version vom 21. März 2012, 15:01 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 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 bisschen 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 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
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. Darum 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:
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 bisschen 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, dass 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.
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, dass 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::
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
Delphic
Dateien
|
||
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. |