Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Ihr zweiter 3D-Shader

Auf einer hohen Betrachtungsebene gibt Godot dem Benutzer eine Reihe von Parametern, die optional eingestellt werden können (AO, SSS_Strength, RIM, etc.). Diese Parameter entsprechen verschiedenen komplexen Effekten (Ambient Occlusion, SubSurface Scattering, Randbeleuchtung, etc.). Wenn sie nicht beschrieben werden, wird der Code verworfen, bevor er kompiliert wird, so dass dem Shader die Kosten für die zusätzliche Funktion nicht entstehen. Dies macht es den Benutzern leicht, komplexe PBR-korrekte Schattierungen zu erzeugen, ohne komplexe Shader zu schreiben. Natürlich können Sie mit Godot auch alle diese Parameter ignorieren und einen vollständig benutzedefinierten Shader schreiben.

Eine vollständige Liste dieser Parameter finden Sie im Referenzdokument Spatial Shader.

Ein Unterschied zwischen der Vertex-Funktion und einer Fragment-Funktion ist, dass die Vertex-Funktion pro Vertex läuft und Eigenschaften wie VERTEX (Position) und NORMAL setzt, während der Fragment-Shader pro Pixel läuft und, am wichtigsten, die ALBEDO-Farbe des MeshInstance3D setzt.

Ihre erste Spatial Fragment-Funktion

Wie im vorangegangenen Teil dieses Tutorials erwähnt, besteht die Standardanwendung der Fragment-Funktion in Godot darin, verschiedene Material Properties einzurichten und Godot den Rest erledigen zu lassen. Um noch mehr Flexibilität zu bieten, stellt Godot auch so genannte Rendermodi zur Verfügung. Rendermodi werden oben im Shader, direkt unter shader_type, eingestellt und geben an, welche Art von Funktionalität die eingebauten Aspekte des Shaders haben sollen.

Wenn Sie z.B. nicht wollen, daß Lichter auf ein Objekt einwirken, stellen Sie den Rendermodus auf unshaded:

render_mode unshaded;

Sie können auch mehrere Rendermodi kombinieren. Wenn Sie z.B. Toon-Schattierung anstelle der realistischeren PBR-Schattierung verwenden möchten, stellen Sie den Diffusmodus und den Specular-Modus auf Toon ein:

render_mode diffuse_toon, specular_toon;

Dieses Modell von Built-in-Funktionen ermöglicht es Ihnen, komplexe benutzerdefinierte Shader zu schreiben, indem Sie nur einige wenige Parameter ändern.

Eine vollständige Liste der Rendermodi finden Sie unter Spatial shader reference.

In diesem Teil des Tutorials gehen wir durch, wie man das unebene Gelände aus dem vorherigen Teil in einen Ozean verwandelt.

Lassen Sie uns zuerst die Farbe des Wassers einstellen, was über die Einstellung ALBEDO geschieht.

ALBEDO ist ein vec3, das die Farbe des Objekts enthält.

Lassen Sie uns es auf einen schönen Blauton einstellen.

void fragment() {
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/albedo.png

Wir stellen ihn auf einen sehr dunklen Blauton ein, weil das meiste Blau des Wassers von den Reflexionen des Himmels herrührt.

Das von Godot verwendete PBR-Modell basiert auf zwei Hauptparametern: METALLIC und ROUGHNESS.

ROUGHNESS gibt an, wie glatt/rauh die Oberfläche eines Materials ist. Eine niedrige ROUGHNESS lässt ein Material wie ein glänzendes Plastik erscheinen, während eine hohe Rauheit das Material in der Farbe kräftiger erscheinen lässt.

Der Parameter METALLIC gibt an, wie sehr das Objekt einem Metall ähnelt. Es ist besser, ihn nahe an 0 oder 1 einzustellen. Betrachten Sie METALLIC als ein Gleichgewicht zwischen der Reflexion und der Farbe ALBEDO. Ein hoher METALLIC-Wert ignoriert ALBEDO fast völlig und sieht wie ein Spiegel des Himmels aus. Bei einem niedrigen METALLIC werden die Farben des Himmels und des ALBEDO gleichmäßiger dargestellt.

Die ROUGHNESS steigt von 0 auf 1 von links nach rechts, während die METALLIC von 0 auf 1 von oben nach unten steigt.

../../../_images/PBR.png

Bemerkung

Der Wert METALLIC sollte nahe bei 0 oder 1 liegen, um eine korrekte PBR-Schattierung zu erreichen. Setzen Sie ihn nur dazwischen, um zwischen den Materialien zu überblenden.

Da Wasser kein Metall ist, setzen wir seine Property METALLIC auf 0.0. Wasser ist außerdem stark reflektierend, daher setzen wir seine Property ROUGHNESS ebenfalls auf einen recht niedrigen Wert.

void fragment() {
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/plastic.png

Jetzt haben wir eine glatte, wie Plastik aussehende Oberfläche. Es ist Zeit, über einige besondere Eigenschaften von Wasser nachzudenken, die wir emulieren möchten. Es gibt zwei Hauptmerkmale, die dies von einer seltsamen Plastikoberfläche zu schön stilisiertem Wasser führen. Das erste sind Spiegelreflexionen. Spiegelreflexionen sind die hellen Flecken, von denen aus Sie die Sonne direkt in Ihr Auge reflektieren. Das zweite ist das Fresnel-Reflektanz. Die Fresnel-Reflektanz ist die Eigenschaft von Objekten, in flachen Winkeln reflektierender zu werden. Es ist der Grund, warum Sie unter sich ins Wasser hineinsehen können, es aber weiter entfernt den Himmel reflektiert.

Um die Spielgelreflexionen zu erhöhen, werden wir zwei Dinge tun. Erstens werden wir den Rendermodus für Spiegelungen auf "Toon" umstellen, da der Rendermodus "Toon" größere Spiegelreflexe aufweist.

render_mode specular_toon;
../../../_images/specular-toon.png

Zweitens werden wir eine Randbeleuchtung hinzufügen. Die Randbeleuchtung verstärkt die Wirkung des Lichts bei Streiflichtern. Normalerweise wird es verwendet, um die Art und Weise zu emulieren, wie das Licht durch den Stoff an den Kanten eines Objekts hindurchgeht, aber wir werden es hier verwenden, um einen schönen wässrigen Effekt zu erzielen.

void fragment() {
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/rim.png

Um die Fresnel-Reflektanz hinzuzufügen, werden wir einen Fresnel-Term in unserem Fragment-Shader berechnen. Hier werden wir aus Performance-Gründen keinen echten Fresnel-Term verwenden. Stattdessen werden wir ihn mit dem Sklarprodukt der Vektoren NORMAL und VIEW approximieren. Der NORMAL-Vektor zeigt von der Oberfläche des Meshs weg, während der VIEW-Vektor die Richtung zwischen Ihrem Auge und diesem Punkt auf der Oberfläche ist. Das Skalarprodukt zwischen diesen beiden Vektoren ist ein praktisches Mittel, um festzustellen, ob Sie die Oberfläche frontal oder in einem Streifwinkel betrachten.

float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

Und mischen Sie es sowohl in ROUGHNESS als auch in ALBEDO. Dies ist der Vorteil von ShaderMaterials gegenüber StandardMaterial3Ds. Mit StandardMaterial3D konnten wir diese Propertys mit einer Textur oder einer pauschalen Zahl einstellen. Aber mit Shadern können wir sie basierend auf jeder mathematischen Funktion, die wir uns ausdenken können, einstellen.

void fragment() {
  float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01 * (1.0 - fresnel);
  ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}
../../../_images/fresnel.png

Und nun, mit nur 5 Zeilen Code, können Sie komplex aussehendes Wasser haben. Jetzt, wo wir die Beleuchtung haben, sieht das Wasser zu hell aus. Wir sollten es abdunkeln. Das geht einfach, indem wir die Werte der vec3, die wir in ALBEDO übergeben, verringern. Setzen wir sie auf vec3(0.01, 0.03, 0.05).

../../../_images/dark-water.png

Animieren mit TIME

Kehren wir zur Vertex-Funktion zurück, so können wir die Wellen mit Hilfe der Built-in-Variable TIME animieren.

TIME ist eine Built-in-Variable, auf die über die Vertex- und Fragmentfunktionen zugegriffen werden kann.

Im vorigen Tutorial haben wir die Höhe durch Auslesen einer Höhenkarte berechnet. In diesem Tutorial werden wir das Gleiche tun. Fügen Sie den Code der Höhenkarte in eine Funktion namens height() ein.

float height(vec2 position) {
  return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}

Um TIME in der Funktion height() verwenden zu können, müssen wir sie übergeben.

float height(vec2 position, float time) {
}

Und stellen Sie sicher, dass sie innerhalb der Vertexfunktion korrekt übergeben wird.

void vertex() {
  vec2 pos = VERTEX.xz;
  float k = height(pos, TIME);
  VERTEX.y = k;
}

Anstatt eine Normal Map zur Berechnung der Normalen zu verwenden, werden wir sie manuell in der Funktion vertex() berechnen. Um dies zu tun, verwenden Sie die folgende Code-Zeile.

NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

Wir müssen NORMAL manuell berechnen, weil wir im nächsten Abschnitt Mathematik benutzen werden, um komplex aussehende Wellen zu erzeugen.

Jetzt machen wir die Funktion height() ein wenig komplizierter, indem wir position um den Kosinus von TIME verschieben.

float height(vec2 position, float time) {
  vec2 offset = 0.01 * cos(position + time);
  return texture(noise, (position / 10.0) - offset).x;
}

Das Ergebnis sind Wellen, die sich langsam, aber nicht sehr natürlich bewegen. Im nächsten Abschnitt wird die Verwendung von Shadern zur Erzeugung komplexerer Effekte, in diesem Fall realistischer Wellen, durch Hinzufügen einiger weiterer mathematischer Funktionen näher erläutert.

Fortgeschrittene Effekte: Wellen

Was Shader so mächtig macht, ist die Tatsache, dass man mit Hilfe von Mathematik komplexe Effekte erzielen kann. Um dies zu veranschaulichen, werden wir unsere Wellen weiterentwickeln, indem wir die Funktion height() modifizieren und eine neue Funktion namens wave() einführen.

wave() hat einen Parameter position, der mit dem in height() identisch ist.

Wir werden wave() mehrfach in height() aufrufen, um das Aussehen von Wellen zu simulieren.

float wave(vec2 position){
  position += texture(noise, position / 10.0).x * 2.0 - 1.0;
  vec2 wv = 1.0 - abs(sin(position));
  return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}

Das sieht zunächst kompliziert aus. Gehen wir es also Zeile für Zeile durch.

position += texture(noise, position / 10.0).x * 2.0 - 1.0;

Versetzen Sie die Position um die noise-Textur. Dadurch werden die Wellen gekrümmt, so dass sie keine geraden Linien sind, die vollständig am Raster ausgerichtet sind.

vec2 wv = 1.0 - abs(sin(position));

Definieren Sie eine wellenartige Funktion mit Hilfe von sin() und Position. Normalerweise sind sin() Wellen sehr rund. Wir benutzen abs(), um ihnen eine scharfe Kante zu geben und sie auf den Bereich 0-1 zu beschränken. Und dann subtrahieren wir sie von 1.0, um die Spitze nach oben zu setzen.

return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

Multiplizieren Sie die Welle in x-Richtung mit der Welle in y-Richtung und erhöhen Sie sie um eine Potenz, um die Spitzen zu schärfen. Dann subtrahieren Sie das Ergebnis von 1,0, so dass die Grate zu Spitzen werden, und erhöhen Sie das Ergebnis um eine Potenz, um die Grate zu schärfen.

Wir können jetzt den Inhalt unserer height() Funktion durch wave() ersetzen.

float height(vec2 position, float time) {
  float h = wave(position);
  return h;
}

Damit erhalten Sie:

../../../_images/wave1.png

Die Form der Sinuswelle ist zu offensichtlich. Lassen Sie uns also die Wellen ein wenig ausbreiten. Wir tun dies, indem wir position skalieren.

float height(vec2 position, float time) {
  float h = wave(position * 0.4);
  return h;
}

Nun sieht es viel besser aus.

../../../_images/wave2.png

Noch besser geht es, wenn wir mehrere Wellen mit unterschiedlichen Frequenzen und Amplituden übereinander legen. Das bedeutet, dass wir die Position für jede einzelne Welle skalieren, um sie dünner oder breiter zu machen (Frequenz). Und wir werden den Output der Welle multiplizieren, um sie kürzer oder höher zu machen (Amplitude).

Hier ist ein Beispiel dafür, wie Sie die vier Wellen schichten können, um schönere Wellen zu erzielen.

float height(vec2 position, float time) {
  float d = wave((position + time) * 0.4) * 0.3;
  d += wave((position - time) * 0.3) * 0.3;
  d += wave((position + time) * 0.5) * 0.2;
  d += wave((position - time) * 0.6) * 0.2;
  return d;
}

Beachten Sie, dass wir die Zeit zu zwei addieren und von den anderen beiden subtrahieren. Dadurch bewegen sich die Wellen in verschiedene Richtungen und erzeugen einen komplexen Effekt. Beachten Sie auch, dass die Amplituden (die Zahl, mit der das Ergebnis multipliziert wird) sich alle zu 1,0 addieren. Dadurch bleibt die Welle im Bereich von 0-1.

Mit diesem Code sollten Sie komplexer aussehende Wellen erhalten, und alles, was Sie tun mussten, war, ein wenig Mathematik hinzuzufügen!

../../../_images/wave3.png

Weitere Informationen zu Spatial-Shadern finden Sie in den Dokumenten Shader-Sprache und Spatial-Shader. Weitere fortgeschrittene Anleitungen finden Sie im Shading-Abschnitt und den 3D-Abschnitten.