Tutorial Terrain2: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
K (Das Spiel von Licht und Schatten)
K (->Delphic)
 
(8 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt)
Zeile 5: Zeile 5:
 
Hi,  
 
Hi,  
  
dies ist ein weiteres Tutorial zu Heightmaps. Ich verrate euch heute, wie man Heightmaps ein paar sehr interessante Texturen verpasst. Der erste Teil besteht darin, eine Procedural Texture zu erstellen, die eine Kombination aus der Heightmap und Bodentexturen ist, z.B. werden die hochsten Punkte der Landschaft mit Schnee bedeckt sein, niedrigere mit Stein, Gras und zum Schluss Wasser.  
+
dies ist ein weiteres Tutorial zu Heightmaps. Ich verrate euch heute, wie man Heightmaps ein paar interessante Texturen verpasst. Der erste Teil besteht darin, eine Procedural Texture zu erstellen, die eine Kombination aus der Heightmap und einer Wahl von Bodentexturen ist, z.B. werden die höchsten Punkte der Landschaft mit Schnee bedeckt sein, niedrigere mit Stein, Gras und zum Schluss Wasser.  
  
Der zweite Teil besteht dann darin, eine Lightmap zu Rendern. Sie enthalt die Lichtintensitat der einfallenden Sonne auf der Karte.  
+
Der zweite Teil besteht darin, eine Lightmap zu Rendern. Sie enthält die Lichtintensitat der einfallenden Sonne auf der Karte.  
Damit ihr wisst, wofur die Muhen dieser Aktion sind, hier einmal ein Bild der kombinierten Procedural Texture und Lightmap im Programm des letzten Tutorials(Um die Anzeige von Bodentexturen erweitert):  
+
Damit ihr wisst, wofür die ganze Müh erst einmal ein kleiner Teaser:  
 
 
[[Bild:Tutorial_Terrain2_result.jpg]]
 
 
{{Hinweis|Besonders zu beachten: Die Landschaft wirft Schatten auf sich selbst}}
 
  
 +
[[Bild:Tutorial_Terrain2_result.jpg|center|framed|Besonders zu beachten: Die Landschaft wirft Schatten auf sich selbst]]
  
 
==Der Trick der Procedural Textures==
 
==Der Trick der Procedural Textures==
  
Wollen wir uns zuerst einmal den Procedural Textures widmen. Die Idee dahinter ist folgende:  
+
Wollen wir uns zuerst den Procedural Textures widmen. Die Idee:  
  
Wir haben eine Heightmap. Wir wollen uber diese eine Textur ziehen. Je nach Hohe soll eine andere Grundtextur gewahlt werden, z.B. Hohenstufe 0 Unterwassertextur. Stufe 60 Sand, Stufe 100 Gras, 170 Stein und 255 Schnee. Daneben wollen wir erreichen, dass die Texturen fliesend ineinander ubergehen. Dies erreichen wir, indem wir immer genau zwischen zwei Stufen die Farbwerte linear interpolieren. Die Grundtexturen werden gekachelt, damit sie nicht so gros sein mussen, wie die Heightmap selbst.
+
Wir haben eine Heightmap. Wir wollen über diese eine Textur ziehen. Je nach Hohe soll eine andere Grundtextur gewählt werden, z.B. Hohenstufe 0 Unterwassertextur. Stufe 60 Sand, Stufe 100 Gras, 170 Stein und 255 Schnee. Daneben wollen wir erreichen, dass die Texturen ineinander übergehe, indem wir immer genau zwischen zwei Stufen die Farbwerte linear interpolieren. Die Grundtexturen werden gekachelt, damit sie nicht so hoch aufgelöst werden müssen, wie die Heightmap selbst.
  
Ich habe folgende Texturen verwendet:
+
Ich habe folgende Texturen verwandt:
 
<div align="center">
 
<div align="center">
 
{| {{Prettytable_B1}}
 
{| {{Prettytable_B1}}
Zeile 37: Zeile 34:
 
|}</div>
 
|}</div>
  
{{Hinweis|Wenn ihr selbst Texturen erstellen wollt, zeichnet einfach drauf los, und lasst dann einen der Kachel Filter in Image Ready(Photoshop) daruber laufen. Ich habe den Kaleidoskop-Kachel Filter benutzt. Jedes Andere Prog das aus bestehenden Bildern Kacheln machen kann, fuhrt naturlich auch zum Erfolg. Allen die mehr von Texturen machen verstehen, bleibt es naturlich selbst uberlassen, dies mit Hand zu tun aber darin bin ich einfach nicht gut genug. Eine andere Moglichkeit besteht darin, auf einem Torus zu zeichen(das Prog dazu werdet ihr wohl selbst schreiben mussen :-().}}
+
{{Hinweis|Wenn ihr selbst Texturen erstellen wollt, zeichnet oder fotografiert wild drauf los, und lasst einen Kachel Filter in einem Bildbearbeitungsprogramm drüberlaufen. Von Hand Kacheln erzeugen geht natürlich auch, bedarf aber deutlich mehr Aufwandt.
  
Bevor wir unsere Textur rendern konnen, mussen wir uns eine Umgebung schaffen. Wir brauchen eine geladene Heightmap und die Grundtexturen:  
+
Bevor wir unsere Textur rendern können, müssen wir uns eine Umgebung schaffen. Wir brauchen eine geladene Heightmap und die Grundtexturen:  
  
<pascal>
+
<source lang="pascal">
 
type
 
type
 
   TProcTexStage = record
 
   TProcTexStage = record
Zeile 55: Zeile 52:
 
     ProcTexs : Array of TProcTexStage;
 
     ProcTexs : Array of TProcTexStage;
 
   ...
 
   ...
</pascal>
+
</source>
 +
 
 +
Der Typ '''TProcTexStage''' beinhaltet je eine Grundtextur, geladen in der '''Image''' Komponente. Die jeweilige Höhenstufe ist in '''Stage''' abgelegt. Die restlichen Objekte sind nur für die GUI gedacht und haben keine weitere Bedeutung für den Algorithmus selbst.
  
Der Typ '''TProcTexStage''' beinhaltet je eine Grundtextur, geladen in der '''Image''' Komponente. Die jeweilige Hohenstufe ist in '''Stage''' abgelegt. Die restlichen Objekte sind nur fur das Utility das ich zum erstellen von Texturen geschrieben habe gedacht und haben keine weitere Bedeutung fur den Algorithmus selbst.
 
 
Die Variable '''ProcTexs''' enhalt mehrere '''TProcTexStages'''. Sie sind bereits nach Stufen sortiert, beginnend mit der kleinsten, wobei gilt: Die niedrigste Stufe ist immer 0, die Hochste immer 255 (wird spater wichtig). Die Mindestzahl der Elemente ist demnach 2. Mehr ist kein Problem, unterschiedliche '''TProcTexStage''' sollten aber niemals die selbe Stufe haben.  
 
Die Variable '''ProcTexs''' enhalt mehrere '''TProcTexStages'''. Sie sind bereits nach Stufen sortiert, beginnend mit der kleinsten, wobei gilt: Die niedrigste Stufe ist immer 0, die Hochste immer 255 (wird spater wichtig). Die Mindestzahl der Elemente ist demnach 2. Mehr ist kein Problem, unterschiedliche '''TProcTexStage''' sollten aber niemals die selbe Stufe haben.  
  
Sobal diese Kriterien erfullt sind, konnen wir beginnen jedem Pixel der '''ProceduralTex''' einen neuen Farbwert zuzuweisen. Das Grundgerust ist wie folgt:
+
Sobal diese Annahmen erfüllt sind, können wir beginnen, jedem Pixel der '''ProceduralTex''' einen neuen Farbwert zuzuweisen. Das Grundgerüst ist wie folgt:
  
<pascal>
+
<source lang="pascal">
 
...
 
...
 
var
 
var
Zeile 84: Zeile 82:
 
     end
 
     end
 
end;
 
end;
</pascal>
+
</source>
  
Da wir die Stufen nicht auf bestimmte Werte festgelegt haben (z.B. alle 64 Hohenwerte eine Stufe) mussen wir zunachst das Intervall finden, in welchem die aktuelle Stufe liegt. Die jeweils niedrigere Textur wird in einer variable '''Lower''' abgelegt, die hohere in '''Upper'''. Daneben benotigen wir noch einen Satz weiterer Variablen:  
+
Da wir die Stufen nicht auf bestimmte Werte festgelegt haben (z.B. alle 64 Hohenwerte eine Stufe) müssen wir zunächst das Intervall finden, in welchem die aktuelle Stufe liegt. Die jeweils niedrigere Textur wird in einer Variable '''Lower''' abgelegt, die höhere in '''Upper'''. Daneben benötigen wir noch einen Satz weiterer Variablen:  
  
<pascal>
+
<source lang="pascal">
 
var
 
var
 
   ...
 
   ...
Zeile 105: Zeile 103:
 
       Lower := 0;
 
       Lower := 0;
 
       //Intervall suchen, das für diese Höhenstufe interessant ist
 
       //Intervall suchen, das für diese Höhenstufe interessant ist
 +
      // Schonmal was von binärer Suche gehört?
 
       for I := 1 to Length(ProcTexs)-1 do
 
       for I := 1 to Length(ProcTexs)-1 do
 
         if HeightMapHeight <= ProcTexs[I].Stage then
 
         if HeightMapHeight <= ProcTexs[I].Stage then
Zeile 118: Zeile 117:
 
       Prozent := LowerDifference / LevelDifference;
 
       Prozent := LowerDifference / LevelDifference;
 
       ...
 
       ...
</pascal>
+
</source>
  
Der verwendete Typ '''TRGB''' ist ein '''Array[0..2] of Byte'''. Wenn ein '''TColor''' Wert als Ausgangsmaterial dient, wird dieser mithile folgender Funktion in ein '''TRGB''' umgewandelt:
+
Der verwendete Typ '''TRGB''' ist ein '''Array[0..2] of Byte'''. Wenn ein '''TColor''' Wert als Ausgangsmaterial dient, wird dieser mit in ein '''TRGB''' umgewandelt:
  
<pascal>
+
<source lang="pascal">
 
   procedure MakeRGB(Color : TColor; var RGB : TRGB);
 
   procedure MakeRGB(Color : TColor; var RGB : TRGB);
 
   begin
 
   begin
 
     Move(Color, RGB[0], 3)
 
     Move(Color, RGB[0], 3)
 
   end;
 
   end;
</pascal>
+
</source>
  
Damit haben wir's fast geschafft. Das einzige was noch fehlt ist die lineare Interpolation zwischen den Farbwerten der '''Upper''' und '''Lower''' Textur. Dies ist aber an sich nicht weiter schwierig. Man denke an den Mathematikunterricht zuruck und erinnert sich an folgende gleichung: '''y= m * x + t''', wobei '''m''' die Steigung und '''t''' der y-Achsenabschnitt ist:
+
Damit haben wir's fast geschafft. Das einzige was noch fehlt ist die lineare Interpolation zwischen den Farbwerten der '''Upper''' und '''Lower''' Textur. Dies ist aber an sich nicht weiter schwierig. Man denke an den Mathematikunterricht zurück und erinnert sich an folgende gleichung: '''y= m * x + t''', wobei '''m''' die Steigung und '''t''' der y-Achsenabschnitt ist:
  
<pascal>
+
<source lang="pascal">
 
       ...
 
       ...
 
       Prozent := LowerDifference / LevelDifference;
 
       Prozent := LowerDifference / LevelDifference;
Zeile 150: Zeile 149:
 
                                       FinalRGB[1],
 
                                       FinalRGB[1],
 
                                       FinalRGB[2])
 
                                       FinalRGB[2])
</pascal>
+
</source>
  
 
Zum Einsatz kam folgende Heightmap:  
 
Zum Einsatz kam folgende Heightmap:  
Zeile 156: Zeile 155:
 
[[Bild:Tutorial_Terrain2_heightmap.jpg|center]]
 
[[Bild:Tutorial_Terrain2_heightmap.jpg|center]]
 
   
 
   
Jede andere funktioniert naturlich auch, wie z.B. die, aus meinem letzten Tutorial. Der Algorithmus liefert zusammen mit den Oben gezeigten Grundtexturen dieses Bild:  
+
Jede andere funktioniert natürlich auch, wie z.B. die, aus meinem letzten Tutorial. Der Algorithmus liefert zusammen mit den Oben gezeigten Grundtexturen dieses Bild:  
  
 
[[Bild:Tutorial_Terrain2_proctex.jpg|center]]
 
[[Bild:Tutorial_Terrain2_proctex.jpg|center]]
Zeile 162: Zeile 161:
 
==Das Spiel von Licht und Schatten==
 
==Das Spiel von Licht und Schatten==
  
Wir konnten damit jetzt unsere Welt uberziehen. Was dann aber noch fehlt ist Beleuchtung und Schatten. Wir wollen hier Ambient Light und Diffuse Light zum Einsatz kommen lassen. Die Typische Formel zum Berechnen ist:
+
Wir könnten damit jetzt unsere Welt überziehen. Was dann aber noch fehlt ist Licht und Schatten. Wir wollen Ambient Light und Diffuse Light zum Einsatz kommen lassen. Die Typische Formel zum Berechnen ist:
  
'''Intensity=Ambient + Diffuse * (L ?N)'''
+
'''Intensity=Ambient + Diffuse * (L N)'''
  
'''L''' ist ein Vektor zur Lichtquelle, '''N''' die Flachennormale. '''Ambient''' und '''Diffuse''' die Starken der jeweiligen Lichttypen. Wir wollen wieder einmal alle Punkte der Heightmap durchgehen und unser Ergebnis in '''LightMaptex''' ablegen. Wenn ein Punkt nicht im Schatten eines anderen liegt, wird sein Farbwert mithilfe obiger Formel berechnet, ansonsten bekommt der Punkt den Farbwert des Ambient Light zugewiesen.  
+
'''L''' ist ein Vektor zur Lichtquelle, '''N''' die Flachennormale. '''Ambient''' und '''Diffuse''' die Stärken der jeweiligen Lichttypen. Wir wollen abermals alle Punkte der Heightmap durchgehen und unser Ergebnis in '''LightMaptex''' ablegen. Wenn ein Punkt nicht im Schatten eines anderen liegt, wird sein Farbwert mithilfe obiger Formel berechnet, ansonsten bekommt der Punkt den Farbwert des Ambient Light zugewiesen.  
  
 
===Die Normal Map===
 
===Die Normal Map===
  
Bevor wir loslegen konnen, mussen wir eine Normal Map berechnen, deren Inhalt alle Normalen der Heightmap sind. Diese Komprimieren wir gleich auf Bytes, damit sie nicht Unmengen an Platz beansprucht. Leider kann man die Normalen nur schlecht fur einzelne Punkte direkt berechnen, wir mussen uns also Flachen zur Hilfe nehmen. Damit die Sache etwas runder wird, betrachten wir aber nicht nur eine Flache, sondern gleich alle anliegenden und berechnen daraus einen Normalendurchschnitt.
+
Bevor wir loslegen, legen wir eine Normal Map an, deren Inhalt alle Normalen der Heightmap ist. Diese Komprimieren wir gleich auf Bytes, damit sie nicht Unmengen an Platz beansprucht. Leider kann man die Normalen nur schlecht für einzelne Punkte direkt berechnen, nehmen also Flächen zur Hilfe. Damit die Sache rund wird, betrachten wir aber nicht nur eine Fläche, sondern gleich alle anliegenden und berechnen daraus einen Normalendurchschnitt.
  
<pascal>
+
<source lang="pascal">
 
...
 
...
 
type
 
type
Zeile 204: Zeile 203:
 
     RaytracingCheckBox.Checked)
 
     RaytracingCheckBox.Checked)
 
end;
 
end;
</pascal>
+
</source>
  
 
Dies stellt erst einmal ein Grundgerust dar. Statt einer einzelnen Beschreibung der einzelnen UI Komponenten folgendes Bild:  
 
Dies stellt erst einmal ein Grundgerust dar. Statt einer einzelnen Beschreibung der einzelnen UI Komponenten folgendes Bild:  
Zeile 210: Zeile 209:
 
[[Bild:Tutorial_Terrain2_lightedit.jpg|center]]
 
[[Bild:Tutorial_Terrain2_lightedit.jpg|center]]
 
   
 
   
Nun also zur Funktion '''CreateNormalMap'''. Wie gesagt berechnen wir den Durchschnitt der Anliegenden Normalen. Folgendes Schaubild soll die verwendeten Flachen verdeutlichen, wobei '''v0''' das derzeit betrachtete Vertex ist, alle anderen '''vxs''' anliegende:
+
Nun also zur Funktion '''CreateNormalMap'''. Wie gesagt berechnen wir den Durchschnitt der Anliegenden Normalen. Folgendes Schaubild soll die verwendeten Flächen verdeutlichen, wobei '''v0''' das derzeit betrachtete Vertex ist, alle anderen '''vxs''' anliegende:
  
 
             v8
 
             v8
       v7?--?--•v1
+
       v7•----•v1
 
         |8 /|1 /|
 
         |8 /|1 /|
 
         | / | / |
 
         | / | / |
 
         |/ 7|/ 2|
 
         |/ 7|/ 2|
       v6?-v0---•v2
+
       v6•-v0•--•v2
 
         |6 /|3 /|
 
         |6 /|3 /|
 
         | / | / |
 
         | / | / |
 
         |/ 5|/ 4|
 
         |/ 5|/ 4|
       v5?--?--•v3
+
       v5•----•v3
 
             v4
 
             v4
  
Mit dem Vektor Kreuzprodukt werden die Normalen fur die 8 Flachen berechnet, die dann zusammengezahlt und normalisiert werden, um den Durchschnitt zu erhalten, der in der Normal Map abgelegt wird:
+
Mit dem Vektor Kreuzprodukt werden die Normalen für die 8 Flachen berechnet, zusammengezahlt und normiert:
  
<pascal>
+
<source lang="pascal">
 
   procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
 
   procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
 
                               var NormalMap : TNormalMap);
 
                               var NormalMap : TNormalMap);
Zeile 274: Zeile 273:
 
       end
 
       end
 
   end;
 
   end;
</pascal>
+
</source>
  
  
Zeile 281: Zeile 280:
 
Jetzt sind wir soweit. Wir konnen jetzt mit Hilfe unserer Formel fur jeden Punkt die Beleuchtung berechnen. Wie immer: Alle Punkte durchgehen, berechnen:  
 
Jetzt sind wir soweit. Wir konnen jetzt mit Hilfe unserer Formel fur jeden Punkt die Beleuchtung berechnen. Wie immer: Alle Punkte durchgehen, berechnen:  
  
<pascal>
+
<source lang="pascal">
 
   procedure CreateLightMap(HM : TBitMap; HeightMapScale : Single;
 
   procedure CreateLightMap(HM : TBitMap; HeightMapScale : Single;
 
     NormalMap : TNormalMap; Sun : TVertex; LightMap : TBitMap;
 
     NormalMap : TNormalMap; Sun : TVertex; LightMap : TBitMap;
Zeile 352: Zeile 351:
 
       end
 
       end
 
   end;
 
   end;
</pascal>
+
</source>
  
Argh.... Wieder so ein Monster an Code! Naja, kampfen wir uns der Reihe mach durch... In '''HeightMapData''' wird die Heightmap geladen, um schnelleren zugriff auf sie zu haben (zur Erinnerung an das 1. Tutorial: GDI Befehle sind arsch langsam). Die entstandenen Daten werden von '''RayIntersected''' verwendet um schneller ausfuhrbar zu sein (glaubt mir, hier ist das wirklich notig!). '''RayIntersected''' selbst gibt bisher immer False zuruck (wir werden das spater noch andern), was bedeutet, auf den derzeit betrachteten Punkt wird kein Schatten geworfen.  
+
Argh.... Wieder so ein Monster an Code! Naja, kämpfen wir uns der Reihe mach durch... In '''HeightMapData''' wird die Heightmap geladen, um schnelleren Zugriff auf sie zu haben (zur Erinnerung an das 1. Tutorial: GDI Befehle sind arsch langsam). Die entstandenen Daten werden von '''RayIntersected''' verwendet um schneller ausführbar zu sein (glaubt mir, hier ist das wirklich nötig!). '''RayIntersected''' selbst gibt bisher immer False züruck (wir werden das spater noch ändern), was bedeutet, auf den derzeit betrachteten Punkt wird kein Schatten geworfen.  
  
Bevor ich weitermache erlautere ich kurz das Prinzip von Ray Tracern (z.B.: [http://www.povray.org POV-Ray]). Ein Ray ist ein Strahl, der von einem bestimmten Punkt (Origin) losgeschickt, in eine bestimmte Richtung (Direction). Das Tracing, also das abgehen des Rays wird die Funktion '''RayIntersected''' fur uns machen.  
+
Bevor ich weitermache erläutere ich kurz das Prinzip von Ray Tracern (z.B.: [http://www.povray.org POV-Ray]). Ein Ray ist ein Strahl, der von einem bestimmten Punkt (Origin) losgeschickt, in eine bestimmte Richtung (Direction). Das Tracing, also das abgehen des Rays wird die Funktion '''RayIntersected''' für uns übernehmen.  
  
Die Richtung des Rays ist fur alle Punkte gleich, da die Sonne zur Erde einen so grosen Abstand hat, sodass das Licht parallel einfallt. Wird ein Ray geschnitten, wird dem Punkt Origin der Wert des Ambient Light zugewiesen, ansonsten wird mit der vorhin vorgestellten Formel die starke des einfallenden Lichtes berechnet.
+
Die Richtung des Rays ist fur alle Punkte gleich, da die Sonne zur Erde einen so großen Abstand hat, dass Licht parallel einfällt. Wird ein Ray geschnitten, wird dem Punkt Origin der Wert des Ambient Light zugewiesen, ansonsten wird mit der vorhin vorgestellten Formel die stärke des einfallenden Lichtes berechnet.  
 
 
Dies ist noch nichts besonderes, denn OpenGL berechnet das Licht auf ahnliche Weise. Was noch fehlt sind die Schatten. Wollen wir also ein wenig uber '''RayIntersected''' nachdenken...
 
  
 +
Dies ist noch nichts besonderes, denn OpenGL berechnet das Licht auf ähnliche Weise. Was noch fehlt sind die Schatten. Wollen wir also ein wenig über '''RayIntersected''' nachdenken...
  
 
===Schattenspiele===
 
===Schattenspiele===
  
Wir haben also Ray. Der Einfachheit halber, gehen wir diesen immer ein Stucken weiter, schauen ob die Hohe der Heightmap groser ist als der Ray oder nicht, solange bis wir am Ende der Karte angelangt sind. Klingt simpel? Ist es auch.
+
Wir haben also den Ray. Der Einfachheit halber, gehen wir diesen immer ein Stücken weiter, schauen ob die Höhe der Heightmap gröser ist als der Ray oder nicht, solange bis wir am Ende der Karte angelangt sind. Klingt simpel? Ist es.
  
<pascal>
+
<source lang="pascal">
 
     function RayIntersected(Ray : TRay; HM : TBitMap;
 
     function RayIntersected(Ray : TRay; HM : TBitMap;
 
                                 HeightMapScale : Single):Boolean;
 
                                 HeightMapScale : Single):Boolean;
Zeile 390: Zeile 388:
 
       end
 
       end
 
     end;
 
     end;
</pascal>
+
    (*Achtung: In diesem Codestück ist eine kleine Performancebremse eingebaut.
 +
      Wer sie findet kommt deutlich schneller an die ersehnte Textur.
 +
      Es handelt sich übrigens um eine fehlende Bedingung im while. *)
 +
</source>
  
Das entlanglaufen des Rays ist nich unbedingt Ideal gelost, aber es tut seinen Zweck. Starten wir einmal unseren nun fertigen Lightmap Generator und betrachten das Ergebnis:  
+
Das entlanglaufen des Rays ist nich unbedingt Ideal gelöst, aber es erfüllt seinen Zweck. Starten wir einmal unseren nun fertigen Lightmap Generator und betrachten das Ergebnis:  
  
 
[[Bild:Tutorial_Terrain2_lightmap.jpg|center]]
 
[[Bild:Tutorial_Terrain2_lightmap.jpg|center]]
 
   
 
   
Diese kann man dann mit der Procedural Texture verknupfen... Je Heller ein Punkt in der Lightmap ist, desto mehr wird von den Farben der Procedural Texture genommen:
+
Diese kann man dann mit der Procedural Texture verknupfen... Je Heller ein Punkt in der Lightmap, desto mehr wird von den Farben der Procedural Texture genommen:
  
 
[[Bild:Tutorial_Terrain2_combined.jpg|center]]
 
[[Bild:Tutorial_Terrain2_combined.jpg|center]]
  
 
==Nachwort==
 
==Nachwort==
Aha, du hast also durchgehalten *g*. Ich hoffe jetzt bald viele schone Landschaften mit Schatten zu sehen. Bin jetzt etwas geschafft vom vielen Tippen.
+
Aha, du hast also durchgehalten. Ich hoffe jetzt bald viele schone Landschaften mit Schatten zu sehen. Bin jetzt etwas geschafft vom vielen Tippen.
  
 
...have a lot of fun!
 
...have a lot of fun!
  
'''Nico Michaelis''' aka DelphiC
+
DelphiC
 +
 
 +
== Dateien ==
 +
* {{ArchivLink|file=tut_terrain2_src_vcl‎|text=Beispiel-Quelltext}}
 +
* {{ArchivLink|file=tut_terrain2_exe‎|text=Beispiel-Programm}}
  
  

Aktuelle Version vom 23. Juli 2009, 17:10 Uhr

Heightmap Texturen

Vorwort

Hi,

dies ist ein weiteres Tutorial zu Heightmaps. Ich verrate euch heute, wie man Heightmaps ein paar interessante Texturen verpasst. Der erste Teil besteht darin, eine Procedural Texture zu erstellen, die eine Kombination aus der Heightmap und einer Wahl von Bodentexturen ist, z.B. werden die höchsten Punkte der Landschaft mit Schnee bedeckt sein, niedrigere mit Stein, Gras und zum Schluss Wasser.

Der zweite Teil besteht darin, eine Lightmap zu Rendern. Sie enthält die Lichtintensitat der einfallenden Sonne auf der Karte. Damit ihr wisst, wofür die ganze Müh erst einmal ein kleiner Teaser:

Besonders zu beachten: Die Landschaft wirft Schatten auf sich selbst

Der Trick der Procedural Textures

Wollen wir uns zuerst den Procedural Textures widmen. Die Idee:

Wir haben eine Heightmap. Wir wollen über diese eine Textur ziehen. Je nach Hohe soll eine andere Grundtextur gewählt werden, z.B. Hohenstufe 0 Unterwassertextur. Stufe 60 Sand, Stufe 100 Gras, 170 Stein und 255 Schnee. Daneben wollen wir erreichen, dass die Texturen ineinander übergehe, indem wir immer genau zwischen zwei Stufen die Farbwerte linear interpolieren. Die Grundtexturen werden gekachelt, damit sie nicht so hoch aufgelöst werden müssen, wie die Heightmap selbst.

Ich habe folgende Texturen verwandt:

Tutorial Terrain2 underwater.jpg Tutorial Terrain2 sand.jpg Tutorial Terrain2 grass.jpg Tutorial Terrain2 rock.jpg Tutorial Terrain2 snow.jpg
Stufe: 0 Stufe: 60 Stufe: 100 Stufe: 170 Stufe: 255

{{Hinweis|Wenn ihr selbst Texturen erstellen wollt, zeichnet oder fotografiert wild drauf los, und lasst einen Kachel Filter in einem Bildbearbeitungsprogramm drüberlaufen. Von Hand Kacheln erzeugen geht natürlich auch, bedarf aber deutlich mehr Aufwandt.

Bevor wir unsere Textur rendern können, müssen wir uns eine Umgebung schaffen. Wir brauchen eine geladene Heightmap und die Grundtexturen:

type
  TProcTexStage = record
                    Image : TImage;
                    ScrollBox : TScrollBox;
                    Stage : Byte;
                    StageLabel : TLabel;
                  end;
  ...
    HeightMap : TBitMap;

    ProceduralTex : TBitMap;
    ProcTexs : Array of TProcTexStage;
  ...

Der Typ TProcTexStage beinhaltet je eine Grundtextur, geladen in der Image Komponente. Die jeweilige Höhenstufe ist in Stage abgelegt. Die restlichen Objekte sind nur für die GUI gedacht und haben keine weitere Bedeutung für den Algorithmus selbst.

Die Variable ProcTexs enhalt mehrere TProcTexStages. Sie sind bereits nach Stufen sortiert, beginnend mit der kleinsten, wobei gilt: Die niedrigste Stufe ist immer 0, die Hochste immer 255 (wird spater wichtig). Die Mindestzahl der Elemente ist demnach 2. Mehr ist kein Problem, unterschiedliche TProcTexStage sollten aber niemals die selbe Stufe haben.

Sobal diese Annahmen erfüllt sind, können wir beginnen, jedem Pixel der ProceduralTex einen neuen Farbwert zuzuweisen. Das Grundgerüst ist wie folgt:

...
var
  X,Y, HeightMapHeight : LongInt;
  ...
begin
  //Texturen sind bereits sortiert...
  if Length(ProcTexs) < 2 then
    Exit;
  //Größe anpassen
  ProceduralTex.Width := HeightMap.Width;
  ProceduralTex.Height := HeightMap.Height;

  for X := 0 to HeightMap.Width - 1 do
    for Y := 0 to HeightMap.Height - 1 do
    begin
      HeightMapHeight :=
         Trunc((HeightMap.Canvas.Pixels[X,Y]/clWhite)*255);
      //Farbwert zuweisen
      ...
    end
end;

Da wir die Stufen nicht auf bestimmte Werte festgelegt haben (z.B. alle 64 Hohenwerte eine Stufe) müssen wir zunächst das Intervall finden, in welchem die aktuelle Stufe liegt. Die jeweils niedrigere Textur wird in einer Variable Lower abgelegt, die höhere in Upper. Daneben benötigen wir noch einen Satz weiterer Variablen:

var
  ...
  I : Integer;
  Upper, Lower : LongInt;
  LevelDifference : LongInt; //Levelunterschied zw. Upper u. Lower
  LowerDifference : LongInt; //Abstand HeightmapHeight - Lower
  LowerRGB, UpperRGB : TRGB; //RGB werte von Upper Lower
  Prozent : Single;          //Prozent der eingesetzten Farbwerte
  FinalRGB : TRGB;           //End RGB Wert

      ...
      HeightMapHeight := Trunc(
             (HeightMap.Canvas.Pixels[X,Y]/clWhite)*255);
      Upper := 0;
      Lower := 0;
      //Intervall suchen, das für diese Höhenstufe interessant ist
      // Schonmal was von binärer Suche gehört? 
      for I := 1 to Length(ProcTexs)-1 do
        if HeightMapHeight <= ProcTexs[I].Stage then
        begin
          Upper := I;
          Lower := I -1;
          Break
        end;
      Assert(Upper <> 0);
      LevelDifference := ProcTexs[Upper].Stage - ProcTexs[Lower].Stage;
      LowerDifference := HeightMapHeight - ProcTexs[Lower].Stage;
      Assert(LevelDifference <> 0);
      Prozent := LowerDifference / LevelDifference;
      ...

Der verwendete Typ TRGB ist ein Array[0..2] of Byte. Wenn ein TColor Wert als Ausgangsmaterial dient, wird dieser mit in ein TRGB umgewandelt:

  procedure MakeRGB(Color : TColor; var RGB : TRGB);
  begin
    Move(Color, RGB[0], 3)
  end;

Damit haben wir's fast geschafft. Das einzige was noch fehlt ist die lineare Interpolation zwischen den Farbwerten der Upper und Lower Textur. Dies ist aber an sich nicht weiter schwierig. Man denke an den Mathematikunterricht zurück und erinnert sich an folgende gleichung: y= m * x + t, wobei m die Steigung und t der y-Achsenabschnitt ist:

      ...
      Prozent := LowerDifference / LevelDifference;

      MakeRGB(ProcTexs[Lower].Image.Canvas.
           Pixels[X mod ProcTexs[Lower].Image.Width,
           Y mod ProcTexs[Lower].Image.Height],LowerRGB);
      MakeRGB(ProcTexs[Upper].Image.Canvas.
           Pixels[X mod ProcTexs[Upper].Image.Width,
           Y mod ProcTexs[Upper].Image.Height],UpperRGB);

      for I := 0 to 2 do
        FinalRGB[I] := Trunc(
                    (UpperRGB[I] - LowerRGB[I])*Prozent +
                                 LowerRGB[I]);
        //    y      =       (            m            )*   x    +      t
      ProceduralTex.Canvas.Pixels[X,Y] := RGB(FinalRGB[0],
                                       FinalRGB[1],
                                       FinalRGB[2])

Zum Einsatz kam folgende Heightmap:

Tutorial Terrain2 heightmap.jpg

Jede andere funktioniert natürlich auch, wie z.B. die, aus meinem letzten Tutorial. Der Algorithmus liefert zusammen mit den Oben gezeigten Grundtexturen dieses Bild:

Tutorial Terrain2 proctex.jpg

Das Spiel von Licht und Schatten

Wir könnten damit jetzt unsere Welt überziehen. Was dann aber noch fehlt ist Licht und Schatten. Wir wollen Ambient Light und Diffuse Light zum Einsatz kommen lassen. Die Typische Formel zum Berechnen ist:

Intensity=Ambient + Diffuse * (L • N)

L ist ein Vektor zur Lichtquelle, N die Flachennormale. Ambient und Diffuse die Stärken der jeweiligen Lichttypen. Wir wollen abermals alle Punkte der Heightmap durchgehen und unser Ergebnis in LightMaptex ablegen. Wenn ein Punkt nicht im Schatten eines anderen liegt, wird sein Farbwert mithilfe obiger Formel berechnet, ansonsten bekommt der Punkt den Farbwert des Ambient Light zugewiesen.

Die Normal Map

Bevor wir loslegen, legen wir eine Normal Map an, deren Inhalt alle Normalen der Heightmap ist. Diese Komprimieren wir gleich auf Bytes, damit sie nicht Unmengen an Platz beansprucht. Leider kann man die Normalen nur schlecht für einzelne Punkte direkt berechnen, nehmen also Flächen zur Hilfe. Damit die Sache rund wird, betrachten wir aber nicht nur eine Fläche, sondern gleich alle anliegenden und berechnen daraus einen Normalendurchschnitt.

...
type
  TNormalMap = Array of Array[0..2] of ShortInt;
...

  procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
                               var NormalMap : TNormalMap);
  begin
    ...
  end;
...
var
  NormalMap : TNormalMap;
  HeightMapScale : Single;
  NormalMap : TNormalMap;

begin
  Sun[0] := Cos(LightRotation.Position*Pi/180);
  Sun[1] := (LightHeight.Max - LightHeight.Position)/100;
  Sun[2] := Sin(LightRotation.Position*Pi/180);
  HeightMapScale := HeightScaleEdit.Value;

  LightMapTex.Width := HeightMap.Width;
  LightMapTex.Height := HeightMap.Height;

  CreateNormalMap(HeightMap, HeightMapScale, NormalMap);

  CreateLightMap(HeightMap, HeightMapScale, NormalMap, Sun, LightMapTex,
    AmbientLight.Position / 100, DiffuseLight.Position/100,
    RaytracingCheckBox.Checked)
end;

Dies stellt erst einmal ein Grundgerust dar. Statt einer einzelnen Beschreibung der einzelnen UI Komponenten folgendes Bild:

Tutorial Terrain2 lightedit.jpg

Nun also zur Funktion CreateNormalMap. Wie gesagt berechnen wir den Durchschnitt der Anliegenden Normalen. Folgendes Schaubild soll die verwendeten Flächen verdeutlichen, wobei v0 das derzeit betrachtete Vertex ist, alle anderen vxs anliegende:

           v8
     v7•--•--•v1
       |8 /|1 /|
       | / | / |
       |/ 7|/ 2|
     v6•-v0•--•v2
       |6 /|3 /|
       | / | / |
       |/ 5|/ 4|
     v5•--•--•v3
           v4

Mit dem Vektor Kreuzprodukt werden die Normalen für die 8 Flachen berechnet, zusammengezahlt und normiert:

  procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
                               var NormalMap : TNormalMap);
  const
    VertexPoss : Array[0..8] of Array[0..1] of ShortInt =
      ((0,0),(1,-1),(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1));
    Faces : Array[0..7] of Array[0..2] of Byte =
      ((0,1,8),(0,2,1),(0,4,2),(4,3,2),(0,5,4),(0,6,5),(0,8,6),(8,7,6));
  var
    X,Z : LongInt;
    I : LongInt;

    Vertieces : Array[0..8] of TVertex;
    Normals   : Array[0..7] of TVertex;
  begin
    SetLength(NormalMap, HM.Width*HM.Height);
    FillChar(Vertieces[0,0], SizeOf(Vertieces), 0);
    for Z := 0 to HM.Height - 1 do
      for X := 0 to HM.Width - 1 do
      begin
        //Vertieces holen
        for I := 0 to 8 do
        begin
          Vertieces[I][0] := X + VertexPoss[I][0]*VertexPosMultiply;
          Vertieces[I][2] := Z + VertexPoss[I][1]*VertexPosMultiply;
          Vertieces[I][1] := HM.Canvas.Pixels[X + VertexPoss[I][0], Z +
                          VertexPoss[I][1]]
            /clWhite*255*HeightScale
        end;
        //Normalen holen
        for I := 0 to 7 do
        begin
          Normals[I] := VectorCrossProduct(
            VectorNormalize(VectorSubtract(Vertieces[Faces[I][0]],
               Vertieces[Faces[I][1]])),
            VectorNormalize(VectorSubtract(Vertieces[Faces[I][1]],
               Vertieces[Faces[I][2]])));
          Normals[I] := VectorNormalize(Normals[I])
        end;
        //Den "Durchschnittsvektor"...
        for I := 1 to 7 do
          Normals[0] := VectorAdd(Normals[0], Normals[I]);
        Normals[0] := VectorScale(VectorNormalize(Normals[0]),127.0);
        NormalMap[Z*(HM.Height) + X][0] := Trunc(Normals[0][0]);
        NormalMap[Z*(HM.Height) + X][1] := Trunc(Normals[0][1]);
        NormalMap[Z*(HM.Height) + X][2] := Trunc(Normals[0][2])
      end
  end;


Es werde Licht

Jetzt sind wir soweit. Wir konnen jetzt mit Hilfe unserer Formel fur jeden Punkt die Beleuchtung berechnen. Wie immer: Alle Punkte durchgehen, berechnen:

  procedure CreateLightMap(HM : TBitMap; HeightMapScale : Single;
    NormalMap : TNormalMap; Sun : TVertex; LightMap : TBitMap;
    LightAmbient, LightDiffuse : Single; Raytracing : Boolean);

  type
    TRay = record
             Origin, Direction : TVertex
           end;

  var
    HeightMapData : Array of Array of byte;

    procedure PrecacheHeightmap;
    var
      X,Z : LongInt;
    begin
      //Läd die Heightmap in ein Delphi Array
      SetLength(HeightMapData, HM.Width);
      for X := 0 to Length(HeightMapData) -1 do
        SetLength(HeightMapData[X], HM.Height);

      for Z := 0 to HM.Height - 1 do
        for X := 0 to HM.Width - 1 do
          HeightMapData[X,Z] := Trunc(HM.Canvas.Pixels[X,Z]/clWhite * 255)
    end;

    function RayIntersected(Ray : TRay; HM : TBitMap;
                                HeightMapScale : Single):Boolean;
    begin
      Result := False;
    end;

  var
    X,Z : LongInt;
    Ray : TRay;
    n : TVertex;
    f : Single;
  begin
    if Raytracing then PrecacheHeightmap;

    //Wir haben paraleles Licht, also bleibt der Richtungsvektor des
    //Rays immer gleich
    Ray.Direction := VectorNormalize(Sun);


    for Z := 0 to HM.Height - 1 do
      for X := 0 to HM.Width - 1 do
      begin
        Ray.Origin[0] := X;
        Ray.Origin[1] := HM.Canvas.Pixels[X,Z]/clWhite*255*HeightMapScale;
        Ray.Origin[2] := Z;
        if RayTracing and RayIntersected(Ray, HM, HeightMapScale) then
          f := LightAmbient
        else begin
          //Komprimierte Normale zurückverwandeln
          n[0] := NormalMap[Z*(HM.Height) + X][0];
          n[1] := NormalMap[Z*(HM.Height) + X][1];
          n[2] := NormalMap[Z*(HM.Height) + X][2];

          f := LightAmbient+LightDiffuse*(VectorDotProduct(
          VectorNormalize(n),VectorNormalize(Ray.Direction)));
          //Zuviel licht?
          if f > 1.0 then f := 1.0;
          if f < 0.0 then f := 0.0
        end;
        LightMap.Canvas.Pixels[X,Z] := RGB(Trunc(f*255),
                                       Trunc(f*255),
                                       Trunc(f*255))
      end
  end;

Argh.... Wieder so ein Monster an Code! Naja, kämpfen wir uns der Reihe mach durch... In HeightMapData wird die Heightmap geladen, um schnelleren Zugriff auf sie zu haben (zur Erinnerung an das 1. Tutorial: GDI Befehle sind arsch langsam). Die entstandenen Daten werden von RayIntersected verwendet um schneller ausführbar zu sein (glaubt mir, hier ist das wirklich nötig!). RayIntersected selbst gibt bisher immer False züruck (wir werden das spater noch ändern), was bedeutet, auf den derzeit betrachteten Punkt wird kein Schatten geworfen.

Bevor ich weitermache erläutere ich kurz das Prinzip von Ray Tracern (z.B.: POV-Ray). Ein Ray ist ein Strahl, der von einem bestimmten Punkt (Origin) losgeschickt, in eine bestimmte Richtung (Direction). Das Tracing, also das abgehen des Rays wird die Funktion RayIntersected für uns übernehmen.

Die Richtung des Rays ist fur alle Punkte gleich, da die Sonne zur Erde einen so großen Abstand hat, dass Licht parallel einfällt. Wird ein Ray geschnitten, wird dem Punkt Origin der Wert des Ambient Light zugewiesen, ansonsten wird mit der vorhin vorgestellten Formel die stärke des einfallenden Lichtes berechnet.

Dies ist noch nichts besonderes, denn OpenGL berechnet das Licht auf ähnliche Weise. Was noch fehlt sind die Schatten. Wollen wir also ein wenig über RayIntersected nachdenken...

Schattenspiele

Wir haben also den Ray. Der Einfachheit halber, gehen wir diesen immer ein Stücken weiter, schauen ob die Höhe der Heightmap gröser ist als der Ray oder nicht, solange bis wir am Ende der Karte angelangt sind. Klingt simpel? Ist es.

    function RayIntersected(Ray : TRay; HM : TBitMap;
                                HeightMapScale : Single):Boolean;
    var
       Position : TVertex;
       Vor : TVertex;
    begin
      Result := False;
      Position := Ray.Origin;
      Vor := VectorNormalize(Ray.Direction);
      Position := VectorAdd(Position, Vor);
      while (Position[0] > 0) and (Position[0] < HM.Width - 1) and
        (Position[2] > 0) and (Position[2] < HM.Height - 1) do
      begin
        if HeightMapData[Trunc(Position[0]), Trunc(Position[2])]*
          HeightMapScale >= Position[1] then
        begin
          Result := True;
          Break
        end;
        Position := VectorAdd(Position, Vor)
      end
    end;
    (*Achtung: In diesem Codestück ist eine kleine Performancebremse eingebaut. 
      Wer sie findet kommt deutlich schneller an die ersehnte Textur. 
      Es handelt sich übrigens um eine fehlende Bedingung im while. *)

Das entlanglaufen des Rays ist nich unbedingt Ideal gelöst, aber es erfüllt seinen Zweck. Starten wir einmal unseren nun fertigen Lightmap Generator und betrachten das Ergebnis:

Tutorial Terrain2 lightmap.jpg

Diese kann man dann mit der Procedural Texture verknupfen... Je Heller ein Punkt in der Lightmap, desto mehr wird von den Farben der Procedural Texture genommen:

Tutorial Terrain2 combined.jpg

Nachwort

Aha, du hast also durchgehalten. Ich hoffe jetzt bald viele schone Landschaften mit Schatten zu sehen. Bin jetzt etwas geschafft vom vielen Tippen.

...have a lot of fun!

DelphiC

Dateien



Vorhergehendes Tutorial:
Tutorial_Terrain1
Nächstes Tutorial:
Tutorial_Terrain3

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