====== Terrain Shader Tutorial ====== ==== Height-map Erklärung ==== Zu Beginn, was ist eine Height-map? {{https://de.wikipedia.org/wiki/Höhenfeld|Höhenfeld}} Eine Height-map ist ein Schwarzweiß Bild welches Höhen darstellen soll. Schwarz ist dabei der tiefste Punkt, und weiß der am höchsten ist. Jedes Pixel repräsentiert dabei einen Höhenwert zwischen "0.0"(schwarz, keine Höhe) und "1.0"(weiß, höchste Höhe) ==== Hight-map Erstellung ==== Wie wird eine Height-map erstellt? Dazu gibt es viele Wege. Der einfachste ist im Internet nach Height-map oder Terrain-map zu suchen. Ein weiteres Online-Tool ist Terrain-Party (https://terrain.party/), wo auf einer Karte der Welt, einfach ein kleiner Bereich ausgewählt wird, und mit Klicken auf Download dieser Bereich als Height-map heruntergeladen werden kann. Wir wollen aber die Height-map von selbst erstellen, nur um zu zeigen, dass es keine Hexerei ist. Dabei soll eine Height-map für ein Tal entstehen. Dazu wird ein Grafikprogramm benötigt. Das kann Gimp, paint3d, Photoshop, Affinity-Designer, oder ein Programm deiner Wahl sein. ==== Hight-map mit Paint.net ==== Um es einfach zu halten, verwende ich für dieses Tutorial das Programm "Paint.net" (https://www.getpaint.net/) Es ist ein einfaches Tool mit welchem man schnell zwischendurch Bilder anpassen und konvertieren kann. Da es einfach ist, und auch DDS-Dateien lesen und schreiben kann, empfehle ich dieses Tool jedem Indie-Entwickler der mit 2D Grafiken (Texturen) zu tun hat. Nach dem Starten vom Programm "Paint.net", mit ''-> Datei -> Neu'' Eine neue Datei anlegen {{:godot:workflow:d3:terrain:terrain_01.png|}} Unter den Einstellungen für die Pixelgröße bei ''-> "Breite" -> 1024 -> "Höhe" -> 1024'' eingeben, und mit "OK" bestätigen. {{:godot:workflow:d3:terrain:terrain_02.png|}} Um spätere Fehler zu vermeiden, bitte kontrollieren das die Vordergrundfarbe "schwarz", und die Hintergrundfarbe auf "weiß" eingestellt ist. -------------- Als nächstes das Tool "Farbverlauf" wählen. {{:godot:workflow:d3:terrain:terrain_04.png|}} Damit markieren wir den Bereich, wo unser Tal (oder Canyon) verlaufen soll. Je nachdem wie wir diesen Bereich erstellen, kann der Verlauf angepasst werden. Dazu ein paar Beispiele. {{:godot:workflow:d3:terrain:terrain_05.jpg?340|}} {{:godot:workflow:d3:terrain:terrain_06.jpg?340|}} Damit keine unschönen Stufen im Terrain wegen den Kanten im Verlauf entstehen, sollte der Verlauf weichgezeichnet werden. Dazu unter ''-> Effekte -> Unschärfe und Weichzeichner -> Graußscher Weichzeichner'' auswählen und so weit wie möglich (abhängig von der Auflösung des Bildes) eingestellt werden. Und mit "OK" bestätigen. {{:godot:workflow:d3:terrain:terrain_08.png|}} --------------- Nachdem wir den Bereich für unser Tal festgelegt haben, kümmern wir uns um die Hügel/ Berge um das Tal herum. Dazu erstellen wir eine neue Ebene. Das geht unter ''-> Ebenen -> Neue Ebene hinzufügen'' oder mit dem Symbol unten links in der Ebenen Anzeige. {{:godot:workflow:d3:terrain:terrain_11.png?nolink|}} Danach sollten wir sicherstellen das die neue Ebene "Ebene 2" ausgewählt ist. Und weisen dieser Ebene mit ''-> Effekte -> Rendern -> Wolken'' einen neuen Effekt zu. {{:godot:workflow:d3:terrain:terrain_12.png|}} Bei den Einstellungen für den Effekt "Wolken" sollte die "Skalierung" möglichst hoch, und die "Grobheit" verringert werden. Je nach Geschmack und gewünschtem Endeffekt kann jeder natürlich die Parameter nach seinen Vorlieben anpassen. Es sollte aber bedacht werden das hier nur die groben Höhenunterschiede festgelegt werden. Kleine Farbänderungen können schon einen enormen Effekt auf das Endergebnis haben. Das ganze mit "OK" bestätigen. {{:godot:workflow:d3:terrain:terrain_13.jpg?600|}} -------------- Jetzt kommt der Trick, der das Tal für unsere Height-map erstellt. Mit ausgewählter "Ebene 2" öffnen wir die Ebenen-Eigenschaften unter ''-> Ebenen -> Ebenen-Eigenschaften'' oder mit dem Symbol rechts unten in der Ebenen-Ansicht. Unter dem "Blend-Modus" stellen wir den "Modus" "Differenz" ein, und schon haben wir unser Tal. {{:godot:workflow:d3:terrain:terrain_15.jpg?600|}} Der Modus "Differenz" errechnet den Unterschied einzelner Pixel der beiden Ebenen, und stellt das Ergebnis dar. Und weil in der einen Ebene ein Farbverlauf von schwarz bis weiß, und in der anderen Ebene Wolken von schwarz bis weiß sind erhalten wir diesen Effekt. Einziger Nachteil ist, dass schwarz jetzt nicht immer ganz schwarz und weiß nicht immer ganz weiß ist. Das kann in unserem Beispiel vernachlässigt werden, oder wenn jemand möchte mit Kontrast und Helligkeit, bzw. in den Kurvenwerkzeug angepasst werden. Bei höherem Kontrast wird das Tal tiefer und die Berge höher. Aber Vorsicht! kleine Änderungen können oft große Auswirkungen haben. --------------- Als nächstes fixieren wir unseren "Blend"-Modus in dem wir die Ebenen vereinen. Das geschieht unter dem Menü ''-> Ebenen -> Ebene nach unten zusammenführen'' oder mit Klick auf das mittlere Symbol in der Ebenenansicht. Wer jetzt gerne, so wie ich, ein bißchen mehr Abwechslung in seinem Terrain haben möchte, kann das mit dem Effekt "Dellen" machen. Unter ''-> Effekte -> Verzerren -> Dellen'' auswählen Mit den Schiebereglern ein wenig experimentieren um den gewünschten Effekt zu erreichen. Es fügt dem Terrain etwas "Erosion" hinzu, welches meist nur mit sehr teuren Terrain-Programmen möglich ist. Und dann wieder mit "OK" übernehmen. Als Tipp: zuerst die "Brechung" verringern, so dass keine scharfen Kanten entstehen. Auch hier gilt: nicht zu stark verändern, außer es ist ein extremes Ergebnis gewünscht. Ich empfehle das fertige Bild unter dem "PNG" Format zu speichern. {{:godot:workflow:d3:terrain:terrain_16.jpg?300|}} ----------- == QuickTipp: == Texturen so lange diese noch bearbeitet werden verlustfrei mit "TGA" oder "TIFF" speichern, damit keine Bild-Informationen durch Komprimierung verloren gehen. Die fertigen Texturen die nur "Graustufen" enthalten (z.B.: "Roughness", "Metallic", "BumpmaP") immer als "PNG" speichern. Fertige Texturen mit Farbwerten (z.B.: "Albedo" oder "Normal") als "JPG" speichern. Außer diese haben einen Alpha-Kanal, dann auch als "PNG" speichern. ---------- ===== Vom Hight-map zum Mesh ===== Mit Hilfe einer Hight-map ein Mesh zu einem Terrain verformen {{:godot:workflow:d3:terrain:terrain_17.png}} Dafür verwenden wir ein einfaches Shader-Script. Was ist nun ein Shader? (https://de.wikipedia.org/wiki/Shader) Ein Shader ist ein kleines Programm das in der Grafikkarte ausgeführt wird. Er dient dazu, die Geometrie eines Meshes(3d-Objekt) oder das Aussehen (Farbe,Pixel 2D-Textur) vor der Anzeige am Bildschirm zu verändern. Jede Spiele-Engine auch die Godot-Engine, und alle Grafikprogramme verwenden Shader. Deshalb sind Shader eine der wichtigsten Bestandteile in der digitalen Visualisierung. Der Vorteil von Shader ist, dass moderne Grafikkarten durch ihre Prozessor-Architektur so stark optimiert sind, das Shader-Programme extrem schnell ausgeführt werden können. So wie für jedes andere Programm benötigen Shader auch Programmiersprachen in denen sie geschrieben werden. Damit nicht direkt in Maschinensprache geschrieben werden muss, stellen die Grafikkarten sogenannte "Schnittstellen" zur Verfügung. Die bekanntesten sind "DirectX" und "OpenGL". * "DirectX" verwendet "HLSL" ([[https://de.wikipedia.org/wiki/High_Level_Shading_Language]]) als Programmiersprache. * "OpenGL" verwendet "GLSL" ([[https://de.wikipedia.org/wiki/OpenGL_Shading_Language]]) als Programmiersprache. Beide Programmiersprachen haben eine Syntax der ähnlich der Programmiersprache "C" ist. Die Godot-Engine verwendet eine eigene an "GLSL" angelehnte und vereinfachte Programmiersprache. Diese werden wir auch hier in diesem Tutorial nutzen. Grundsätzlich gibt es mehrere Funktionen(Methoden), die jeweils für bestimmte Aufgaben verwendet werden können. Eine die "Vertex"-Punkte berechnet, eine für "Primitive", eine für "Fragmente", eine weitere die einzelne Pixel (Licht) berechnet. Wir werden hier nur die für "Vertex" und eventuell "Fragmente" verwenden. Auch bekannt unter "Vertex-shader" und "Fragment-shader" Wie funktioniert das? Jedes 3D-Objekt besteht aus Polygonen (https://de.wikipedia.org/wiki/Polygon). Die End-/Eck-Punkte werden als "Vertex"-Punkte bezeichnet. {{:godot:workflow:d3:terrain:terrain_18.png?400|}} In unserem Fall wollen wir die "Vertex"-Punkte einer Fläche (Plane) so verändern das daraus ein Terrain entsteht. Dazu lesen wir aus der Height-Map-Textur den passenden Höhenwert von einem Pixel, und verändern damit die Position eines "Vertex"-Punktes. Dabei spielt die Anzahl der vorhandenen "Vertex"-Punkte eine entscheidende Rolle. Je mehr Punkte vorhanden sind, desto besser sieht am Ende auch das Ergebnis aus. Wie auch am folgenden Bild erkennbar. {{:godot:workflow:d3:terrain:terrain_19.png?400|}} Des Weiteren kommt uns zugute, dass standardmäßig die Positionen von den "Vertex"-Punkten auf der UV-Map, und die von der Pixel-Position in der Height-Map(UV), immer relativ zur Größe angegeben werden. Das heißt der Ausgangspunkt liegt immer bei 0, und der letzte liegt immer bei 1. Zum Beispiel liegt der Mittelpunkt immer bei 0,5. Es kann auch als Prozentwert angesehen werden, 0% (0,0) bis 100% (1,0) ist der gesamte Bereich, dann liegt die Hälfte bei 50% (0,5). Deshalb wird in unserem Fall keine Positionsumrechnung benötigt. Zum weiteren Verständnis müssen wir uns noch mit dem Koordinaten-Systemen auseinandersetzen. Im 3D Bereich gibt es 3 Achsen, "x", "y", "z". Wobei "x" die horizontale Achse (Breite) darstellt, "y" die vertikale Achse (Höhe), und "z" die Entfernungsachse (Tiefe). {{:godot:workflow:d3:terrain:terrain_20.png?200|}} Bei Texturen gibt es 2 Achsen, "u" und "v" Wobei "u" die horizontale Achse (Breite) und "v" die vertikale Achse (Höhe) darstellt. Daher kommt auch die Bezeichnung von der UV-Map, welche die Positionen der "Vertex"-Punkte auf der Textur enthalten. {{:godot:workflow:d3:terrain:terrain_21.png?200|}} Und zu guter Letzt rufen wir uns noch in Erinnerung was ein "Pixel" ist. Ein Pixel ist ein Farbpunkt auf einer Textur, der an der Position "uv" liegt, und 4 Werte hat. Es sind die anteiligen Werte (Kanäle) für "rot", "grün", "blau" und "alpha", abgekürzt "r", "g", "b", "a" oder alle zusammen "rgba". Wobei jeder dieser werte von 0 bis 1 enthalten kann und somit 0 für keine Farbe und 1 für 100% Farbe steht. Ein Wert von "r:1", "g:0", "b:0" und "a:1" stellt somit die Farbe "rot" dar. Bei einem Bild in "Graustufen", wie bei unserer Height-Map haben "r"(rot), "g"(grün) und "b"(blau) immer den selben Wert, (sonst wäre es nicht "grau", "eintönig", "alle gleich") und wir können uns zum Auslesen des Wertes für die Höhe einfach einen von den dreien aussuchen. Ich nehme den Farbwert vom Kanal Grün(g), da auch die "y"-Achse auch grün ist und für Höhe steht. Aber wie gesagt es funktioniert auch mit "r"(rot) und "b"(blau). Fassen wir zusammen: - lesen der Vertex Position "x" und "z" von unserer Fläche (3D)(Plane) position = VERTEX.xz; - lesen von einen Farbwert (für die Höhe) von unserer Textur an der selben Position wie unser "Vertex"-Punkt. hoehe = textur( position ).g; - Verschieben des "Vertex"-Punktes auf die Höhe (y) VERTEX.y = hoehe; oder vereinfacht: VERTEX.y = textur( VERTEX.xz ).g; // vertex.höhe(y) = in der Height-map(textur) ( an der Position Vertex.breite(x) und Vertex.tiefe(z) ) der Wert vom Pixel.grün(g) Ganz einfach, oder? Genug der Theorie und schreiten wir zum praktischen Teil. ==== Neues Godot Projekt ==== * Beginnen wir mit einem neue Godot Projekt, und stellen sicher das wir einen neue Szene in 3D offen haben. * In der Szene eine neue "root"-Node vom Typ "Spatial" hinzufügen. * Unter "Spatial" eine neue Node "Meshinstance" für unsere Fläche "Plane" hinzufügen. * bei der "Meshinstance" im Inspektor bei Mesh ein "Neues PlaneMesh" hinzufügen. Jetzt haben wir unsere Basis Mesh. {{:godot:workflow:d3:terrain:terrain_22.png?600|}} Wie zuvor bereits erwähnt, je mehr "Vertex"-Punkte wir haben, desto besser wird das Ergebnis, sollten wir nun unser Mesh etwas vergrößern und unterteilen. * Bei der "Meshinstance" auf das Bild der "Plane" klicken. * Unter "PlaneMesh" die Größe (Size) auf "100, 100" einstellen und "Subdivide Width" und "Subdivide Depth" auch jeweils auf "100" stellen. Jetzt haben wir genug Punkte zum Bearbeiten. {{:godot:workflow:d3:terrain:terrain_23.png?600|}} Als nächstes legen wir unseren Shader an. Zwischendurch die Szene speichern nicht vergessen. * Mit ausgewählter "MeshInstance" unter "Material" "Neues ShaderMaterial" auswählen * Auf das Material-Symbol klicken zum bearbeiten * Bei "Shader" "neuer Shader" auswählen * Und wieder auf "" klicken {{:godot:workflow:d3:terrain:terrain_24.png?600|}} Jetzt sollte unter der 3D Ansicht ein Fenster im Editor aufgegangen sein und eine Fehlermeldung die besagt, das kein "Shader Typ" angegeben ist. {{:godot:workflow:d3:terrain:terrain_25.png?400|}} Godot erwartet von uns, das wir angeben welche Art von Shader wir programmieren wollen. ==== Das Skript ==== Es gibt 3 Shader Typen * "spatial" -> für 3D Rendering * "canvas_item" -> für 2D Rendering * "particles" -> für das Particle System [[http://docs.godotengine.org/en/3.0/tutorials/shading/shading_language.html]] In unserem Fall wird es ein 3D Shader, und deshalb verwenden wir den Typ "spatial". In der 1. Zeile unseres Skriptes steht: shader_type spatial; Jetzt können wir noch angeben wie der Shader gerendert werden soll. Eine Liste der Render-Typen gibt es unter [[http://docs.godotengine.org/en/3.0/tutorials/shading/shading_language.html#id1]] Wir entscheiden uns für den Standard-Typ "diffuse_lambert", und schreiben in die zweite Zeile unseres Skriptes: render_mode diffuse_lambert; Wie oben bereits erklärt gibt es mehrere Methoden für den Shader. Wir benötigen einen der "Vertex"-Punkte verändert. Diese Methode(Funktion) heißt "vertex". Da diese Methode keinen Wert zurückliefert, schreiben wir davor "void" als Typ für die Methode. void vertex() { } Für den nächsten Schritt benötigen wir unsere "Height-Map"-Textur. Dazu ziehen wir die Datei vom Filesystem in die Godot Datei Übersicht. {{:godot:workflow:d3:terrain:terrain_27.png?400|}} Um diese Datei im Shader nutzen zu können, benötigen wir in unserem Skript eine Variable vom Typ "sampler2D". "sampler2D" ist der Standard-Typ für Texturen im Shader. Das Attribut "uniform" sagt dem Shader, das der Wert für die Variable von der Benutzeroberfläche (Inspektor), als Eigenschaft gelesen werden soll. "uniform" kann für alle Variablen verwendet werden, die wir von außerhalb des Shaders im Inspektor konfigurieren möchten. Bei "sampler2D" Variablen können wir mit einem Doppelpunkt ":" getrennt noch eine sogenannte "hint" Eigenschaft angeben. Diese bestimmt den Wert der Textur, wenn keine ausgewählt wurde. Ansonsten würde im Shader vor der Auswahl einer Textur immer ein Fehler bei der Berechnung passieren. Eine Auflistung der "hint" Werte gibt es unter: [[http://docs.godotengine.org/en/3.0/tutorials/shading/shading_language.html#uniforms]] Wir nehmen "hint_black", so das alle Positionen(Pixel) standardmäßig schwarz(black)(wert: 0.0) sind. Der Befehl sieht dann folgendermaßen aus. uniform sampler2D heightmap : hint_black; "heightmap" ist der Name der Variable den ich mir ausgesucht habe. Diese könnte auch "buxtehude" heißen. Für jeden "Vertex"-Punkt unserer "plane" wird die Methode "vertex()" einmal aufgerufen. Innerhalb unserer Methode "vertex()" existiert ein Objekt mit dem Namen "VERTEX". Dieses repräsentiert den aktuellen "Vertex"-Punkt. Es stehen auch einige andere Objekte und Werte innerhalb der "vertex()" Methode zur Verfügung. [[http://docs.godotengine.org/en/3.0/tutorials/shading/shading_language.html#vertex-built-ins]] Um einen Farbwert aus unserer "Height-Map" zu lesen verwenden wir die Funktion "texture()". Diese erwartet eine Textur und eine Position als Parameter, und liefert einen RGBa-Wert zurück. Jetzt können wir die Formel, die wir zuvor ermittelt haben in unsere "vertex()" Funktion schreiben. VERTEX.y = texture(heightmap, UV).g; Momentan sehen wir auch keine Änderung in der 3D-Ansicht. Wir müssen noch unsere Textur zuweisen. * dazu im Inspektor mit "<" einen Schritt zurück gehen. * jetzt sehen wir unter "Shader Param" unsere mit "uniform" angelegte Variable als Parameter. * das Bild der Heightmap können wir durch Ziehen oder Auswählen der "Heightmap" Parameter zuweisen. {{:godot:workflow:d3:terrain:terrain_30.png?400|}} In der 3D-Ansicht sollten wir jetzt eine kleine Veränderung unserer Fläche (plane) wahrnehmen. Da in unserer Textur die Werte für die Höhe nur zwischen 0.0 und 1.0 liegen, also sehr gering sind, sehen wir kaum eine Veränderung. Um dem entgegen zu wirken, können wir den Wert mit einem beliebigen Faktor multiplizieren. In unserem Fall einfach " * 10 " hinter unsere Formel im Shader schreiben, und sehen was passiert. VERTEX.y = texture(heightmap, UV).g * 10.0; {{:godot:workflow:d3:terrain:terrain_31.png?400|}} Jetzt sind die Änderungen deutlicher zu sehen. Um jetzt den Wert des Faktors auch einstellbar zu machen, erstellen wir unter der vorigen "uniform" Variable eine neue Variable für den Faktor. uniform float faktor = 10; Und in unserer Methode "vertex()" ändern wir " * 10 " in " * faktor" VERTEX.y = texture(heightmap, UV).g * faktor; (bild_20) Somit können wir jetzt mit dem Faktor die Höhe unseres Terrains beeinflussen. {{:godot:workflow:d3:terrain:terrain_33.png?400|}} In unserer 3D-Ansicht können wir noch keine Details erkennen. Das liegt daran, das wir unserer Fläche(plane) noch keine Farbe zugewiesen haben. Der Einfachheit halber legen wir einfach unsere "Height-map"-Textur als Farbe über unser Terrain. Mit der Methode "fragment()" im Shader können die Farbeinstellungen angepasst werden. Diese Methode wird für jedes Fragment (Polygon Pixel) unserer Fläche aufgerufen. Innerhalb der "fragment()" Methode haben wir Zugriff auf einige Objekte und Werte, die wir lesen (in) und auch verändern/schreiben (out) können. [[http://docs.godotengine.org/en/3.0/tutorials/shading/shading_language.html#fragment-built-ins]] Wir wollen einfach die Farbe für die Ansicht ändern und verwenden somit "ALBEDO" Eigenschaft. "ALBEDO" erwartet einen "rgb"-Wert für die Anzeige. Diesen lesen wir wie zuvor bei der "vertex()" Methode mit Hilfe der "texture()"-Funktion aus. void fragment() { ALBEDO = texture(heightmap, UV).rgb; } {{:godot:workflow:d3:terrain:terrain_34.png?400|}} Hier nochmal der gesamte Code: shader_type spatial; render_mode diffuse_lambert; // hier wird eine Textur vom Userinterface abgefragt // "hint_black" ist der Standard wenn keine Textur angegeben, dann soll alles schwarz (Höhe = 0.0) sein uniform sampler2D heightmap : hint_black; // Faktor für die Höhe einstellbar uniform float faktor = 10.0; // Diese Funktion wird automatisch für jeden "Vertex"-Punkt aufgerufen void vertex() { // Vertext Höhe (y) wird von der Textur "heightmap" vom wert "grün"(g) an der UV Position gelesen VERTEX.y = texture(heightmap, UV).g * faktor; } void fragment() { // hier werden die Farbwerte von der Heightmap Textur (rgb) auf das Mesh übertragen ALBEDO = texture(heightmap, UV).rgb; } Einige Punkte bleiben noch offen, wie z.B.: weitere Farben/ Texturen Einstell-Möglichkeiten, oder das hinzufügen eines Collisions-Meshes damit man am Terrain auch herum laufen kann.