Up to date

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

Ihr erster 3D-Shader

Sie haben sich entschlossen, Ihren eigenen Spatial-Shader zu schreiben. Vielleicht haben Sie im Internet einen coolen Trick gesehen, der mit Shadern gemacht wurde, oder Sie haben festgestellt, dass das StandardMaterial3D Ihren Bedürfnissen nicht ganz gerecht wird. In jedem Fall haben Sie beschlossen, Ihr eigenes Material zu schreiben, und jetzt müssen Sie herausfinden, wo Sie anfangen sollen.

Dieses Tutorial erklärt, wie Spatial-Shader erstellt werden und gehen mehr ins Detail als CanvasItem.

Spatial-Shader verfügen über mehr Built-in-Funktionen als CanvasItem-Shader. Bei räumlichen Shadern wird erwartet, dass Godot bereits die Funktionalität für allgemeine Anwendungsfälle besitzt und der Benutzer im Shader lediglich die richtigen Parameter festlegen muss. Dies gilt insbesondere für einen PBR (Physical Based Rendering)-Workflow.

Dies ist ein zweiteiliges Tutorial. In diesem ersten Teil werden wir Terrain mit Vertex-Displacement aus einer Heightmap in der Vertex-Funktion erstellen. Im zweiten Teil werden wir die Konzepte aus diesem Tutorial übernehmen und benutzerdefinierte Materialien in einem Fragment-Shader einrichten, indem wir einen Meerwasser-Shader schreiben.

Bemerkung

Dieses Tutorial setzt einige grundlegende Shader-Kenntnisse wie Typen (vec2, float, sampler2D) und Funktionen voraus. Wenn Sie mit diesen Konzepten nicht vertraut sind, ist es am besten, sich sanfte Einführung über The Book of Shaders zu verschaffen, bevor Sie dieses Tutorial beenden.

Wo kann ich mein Material zuordnen?

In 3D werden Objekte mit Meshes gezeichnet. Meshes sind ein Ressourcentyp, der Geometrie (die Form des Objekts) und Materialien (die Farbe und wie das Objekt auf Licht reagiert) in Einheiten speichert, die "Oberflächen" genannt werden. Ein Mesh kann mehrere Oberflächen haben oder auch nur eine. Normalerweise importiert man ein Mesh aus einem anderen Programm (z.B. Blender). Aber Godot hat auch ein paar PrimitiveMeshes, mit denen man einer Szene grundlegende Geometrie hinzufügen kann, ohne Meshes zu importieren.

Es gibt mehrere Node-Typen, die Sie zum Zeichnen eines Meshs verwenden können. Der wichtigste ist MeshInstance3D, aber man kann auch GPUParticles3D, MultiMeshes (mit einem MultiMeshInstance3D), oder andere verwenden.

Normalerweise ist ein Material mit einer bestimmten Oberfläche in einem Mesh verbunden, aber einige Nodes, wie MeshInstance3D, ermöglichen es Ihnen, das Material für eine bestimmte Oberfläche oder für alle Oberflächen zu überschreiben.

Wenn Sie ein Material auf der Oberfläche oder dem Mesh selbst festlegen, wird dieses Material von allen MeshInstance3Ds, die dieses Mesh verwenden, übernommen. Wenn Sie jedoch dasselbe Mesh in mehreren Mesh-Instanzen wiederverwenden möchten, aber unterschiedliche Materialien für jede Instanz haben, sollten Sie das Material auf der MeshInstance3D festlegen.

Für dieses Tutorial werden wir unser Material auf das Mesh selbst setzen, anstatt die Fähigkeit von MeshInstance3D zu nutzen, Materialien zu überschreiben.

Einrichtung

Fügen Sie einen neuen MeshInstance3D-Node zu Ihrer Szene hinzu.

Klicken Sie im Inspektor-Tab neben "Mesh" auf "[leer]" und wählen Sie "Neu: PlaneMesh". Dann klicken Sie auf das Bild einer Ebene, die erscheint.

Dies fügt ein PlaneMesh zu unserer Szene hinzu.

Klicken Sie dann im Viewport in der oberen linken Ecke auf den Button "Perspective". Ein Menü wird angezeigt. In der Mitte des Menüs finden Sie Optionen für die Darstellung der Szene. Wählen Sie "Drahtgitter anzeigen".

So können Sie die Dreiecke sehen, aus denen die Ebene besteht.

../../../_images/plane.png

Setzen Sie nun Unterteilungsbreite und Unterteilungstiefe des PlaneMesh auf 32.

../../../_images/plane-sub-set.webp

Sie können sehen, dass es jetzt viel mehr Dreiecke in der MeshInstance3D gibt. Dadurch haben wir mehr Vertices, mit denen wir arbeiten können, und können somit mehr Details hinzufügen.

../../../_images/plane-sub.png

PrimitiveMeshes, wie PlaneMesh, haben nur eine Oberfläche, so dass es anstelle eines Arrays von Materialien nur eines gibt. Klicken Sie neben "Material", wo "[leer]" steht, und wählen Sie "Neu: ShaderMaterial". Klicken Sie dann auf die Kugel, die erscheint.

Klicken Sie nun neben "Shader" auf die Stelle, wo "[leer]" steht, und wählen Sie "Neuer Shader".

Der Shader-Editor sollte nun erscheinen und alles ist bereit um Ihren ersten Spatial-Shader zu schreiben!

Shader-Magie

../../../_images/shader-editor.webp

Der neue Shader wird bereits mit einer shader_type-Variablen und der fragment()-Funktion erzeugt. Das erste, was Godot-Shader brauchen, ist eine Deklaration, welcher Typ von Shader sie sind. In diesem Fall ist der shader_type auf spatial gesetzt, da dies ein räumlicher Shader ist.

shader_type spatial;

Für den Moment ignorieren Sie die fragment()-Funktion und definieren die vertex() Funktion. Die vertex()-Funktion bestimmt, wo die Vertices Ihrer MeshInstance3D in der endgültigen Szene erscheinen. Wir werden sie benutzen, um die Höhe jedes Vertex zu verschieben und unsere flache Ebene wie ein kleines Terrain erscheinen zu lassen.

Wir definieren den Vertex-Shader wie folgt:

void vertex() {

}

Wenn die Funktion vertex() nichts enthält, wird Godot seinen Default-Vertex-Shader verwenden. Wir können leicht damit beginnen, Änderungen vorzunehmen, indem wir eine einzige Zeile hinzufügen:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

Wenn Sie diese Zeile hinzufügen, sollten Sie ein Bild wie das folgende erhalten.

../../../_images/cos.png

Okay, packen wir das mal aus. Der y-Wert des VERTEX wird erhöht. Und wir geben die x- und z-Komponenten der VERTEX als Argumente an cos und sin weiter; das gibt uns ein wellenförmiges Aussehen über die x- und z-Achsen.

Was wir schließlich erreichen wollen, ist das Aussehen von kleinen Hügeln. cos und sin sehen ja schon irgendwie wie Hügel aus. Wir tun dies, indem wir die Eingaben für die Funktionen cos und sin skalieren.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

Das sieht besser aus, aber es ist immer noch zu stachelig und repetitiv, wir sollten es etwas interessanter gestalten.

Rausch-Höhenkarte

Rauschen ist ein sehr beliebtes Mittel, um das Aussehen eines Geländes vorzutäuschen. Stellen Sie es sich ähnlich wie die Kosinusfunktion vor, bei der Sie sich wiederholende Hügel haben, mit dem Unterschied, dass bei Rauschen jeder Hügel eine andere Höhe hat.

Godot stellt die Ressource NoiseTexture2D zur Verfügung, um eine Geräuschtextur zu erzeugen, auf die von einem Shader aus zugegriffen werden kann.

Um auf eine Textur in einem Shader zuzugreifen, fügen Sie den folgenden Code am Anfang Ihres Shaders ein, außerhalb der vertex() Funktion.

uniform sampler2D noise;

Dies ermöglicht es Ihnen, eine Rauschtextur an den Shader zu senden. Schauen Sie nun im Inspektor unter Ihrem Material nach. Sie sollten einen Abschnitt namens "Shader-Params" sehen. Wenn Sie diesen öffnen, sehen Sie einen Abschnitt namens "Rauschen".

Klicken Sie daneben, wo "[leer]" steht, und wählen Sie "Neu: NoiseTexture2D". Dann klicken Sie in Ihrem NoiseTexture2D auf die Stelle, an der "Rauschen" steht, und wählen Sie "Neu: FastNoiseLite".

Bemerkung

FastNoiseLite wird von der NoiseTexture2D verwendet, um eine Höhenkarte zu erzeugen.

Sobald Sie es eingerichtet haben und so aussehen sollten.

../../../_images/noise-set.webp

Greifen Sie nun mit der Funktion texture() auf die Rauschtextur zu. Die Funktion texture() nimmt eine Textur als erstes Argument und ein vec2 für die Position auf der Textur als zweites Argument. Wir benutzen die x und z-Kanäle von VERTEX, um zu bestimmen, wo auf der Textur nachgeschaut werden soll. Beachten Sie, dass die Koordinaten des PlaneMesh im Bereich [-1,1] liegen (bei einer Größe von 2), während die Koordinaten der Textur im Bereich [0,1] liegen, so dass wir zur Normalisierung die Größe des PlaneMesh durch 2,0 teilen und 0,5 addieren. Textur() gibt eine Vec4 der r, g, b, a Kanäle an der Position zurück. Da die Rauschtextur Graustufen hat, sind alle Werte gleich, so dass wir jeden der Kanäle als Höhe verwenden können. In diesem Fall verwenden wir den r- oder x-Kanal.

void vertex() {
  float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
  VERTEX.y += height;
}

Anmerkung: xyzw ist dasselbe wie rgba in GLSL, also könnten wir statt texture().x oben texture().r verwenden. Siehe die OpenGL Dokumentation für weitere Details.

Mit diesem Code können Sie sehen, wie die Textur zufällig aussehende Hügel erzeugt.

../../../_images/noise.png

Im Moment ist es zu stachelig, wir wollen die Hügel ein wenig abmildern. Um das zu tun, werden wir ein Uniform verwenden. Sie haben bereits oben ein Uniform verwendet um die Rausch-Textur zu übergeben, jetzt lassen Sie uns lernen, wie sie funktionieren.

Uniforms

Uniform-Variablen ermöglichen es Ihnen, Daten aus dem Spiel an den Shader zu übergeben. Sie sind sehr nützlich für die Steuerung von Shader-Effekten. Uniforms können fast jeder Datentyp sein, der im Shader verwendet werden kann. Um ein Uniform zu verwenden, deklariert man es in seinem Shader mit dem Schlüsselwort uniform.

Lassen Sie uns ein Uniform herstellen, das die Höhe des Geländes verändert.

uniform float height_scale = 0.5;

Godot erlaubt es Ihnen, ein Uniform mit einem Wert zu initialisieren; hier ist height_scale auf 0.5 gesetzt. Sie können Uniforms von GDScript aus setzen, indem Sie die Funktion set_shader_parameter() auf dem Material, das dem Shader entspricht, aufrufen. Der von GDScript übergebene Wert hat Vorrang vor dem Wert, mit dem er im Shader initialisiert wurde.

# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)

Bemerkung

Das Ändern von Uniforms in Spatial-basierten Nodes unterscheidet sich von CanvasItem-basierten Nodes. Hier setzen wir das Material innerhalb der PlaneMesh-Ressource. In anderen Mesh-Ressourcen müssen Sie möglicherweise zuerst auf das Material zugreifen, indem Sie surface_get_material() aufrufen. In der MeshInstance3D würde man auf das Material mit get_surface_material() oder material_override zugreifen.

Denken Sie daran, dass der String, der in set_shader_parameter() übergeben wird, mit dem Namen der Uniform-Variable im Shader übereinstimmen muss. Sie können die Uniform-Variable überall in Ihrem Shader verwenden. Hier werden wir sie benutzen, um den Höhenwert zu setzen, anstatt willkürlich mit 0.5 zu multiplizieren.

VERTEX.y += height * height_scale;

Nun sieht es viel besser aus.

../../../_images/noise-low.png

Mit Uniforms können wir den Wert sogar bei jedem Frame ändern, um die Höhe des Geländes zu animieren. In Kombination mit Tweens kann dies besonders für Animationen nützlich sein.

Mit Licht interagieren

Schalten Sie zunächst die Drahtgitterdarstellung aus. Klicken Sie dazu erneut oben links im Ansichtsfenster, wo "Perspektive" steht, und wählen Sie "Normalen anzeigen".

../../../_images/normal.png

Beachten Sie, dass die Farbe des Meshes flach wird. Das liegt daran, dass die Beleuchtung darauf flach ist. Fügen wir ein Licht hinzu!

Zunächst fügen wir der Szene ein OmniLight3D hinzu.

../../../_images/light.png

Sie können sehen, wie das Licht auf das Gelände wirkt, aber es sieht seltsam aus. Das Problem ist, dass das Licht auf das Terrain einwirkt, als ob es eine flache Ebene wäre. Das liegt daran, dass der Licht-Shader die Normalen aus dem Mesh zur Berechnung des Lichts verwendet.

Die Normalen sind im Mesh gespeichert, aber wir ändern die Form des Mesh im Shader, so dass die Normalen nicht mehr korrekt sind. Um dies zu beheben, können wir die Normalen im Shader neu berechnen oder eine Normalentextur verwenden, die unserem Rauschen entspricht. Godot macht beides für uns einfach.

Sie können die neue Normale manuell in der Vertex-Funktion berechnen und dann einfach NORMAL setzen. Wenn NORMAL gesetzt ist, wird Godot alle schwierigen Beleuchtungsberechnungen für uns erledigen. Wir werden diese Methode im nächsten Teil dieses Tutorials behandeln, für jetzt werden wir Normalen von einer Textur lesen.

Stattdessen werden wir uns wieder auf die NoiseTexture verlassen, um die Normalen für uns zu berechnen. Wir tun dies, indem wir eine zweite Rauschtextur übergeben.

uniform sampler2D normalmap;

Setzen Sie diese zweite Uniform-Textur auf eine weitere NoiseTexture2D mit einer weiteren FastNoiseLite. Aber dieses Mal aktivieren Sie Als Normal Map.

../../../_images/normal-set.webp

Da es sich hier um eine Normal Map und nicht um eine Per-Vertex-Normale handelt, werden wir sie in der Funktion fragment() zuweisen. Die fragment()-Funktion wird im nächsten Teil des Tutorials genauer erklärt.

void fragment() {
}

Wenn wir Normalen haben, die einem bestimmten Vertex entsprechen, setzen wir NORMAL, aber wenn Sie eine Norma Map haben, die von einer Textur kommt, setzen Sie die Normalen mit NORMAL_MAP. Auf diese Weise wird Godot das umschließen der Textur um das Mesh automatisch handhaben.

Um sicherzustellen, daß wir von den gleichen Stellen in der Rausch-Textur und der Normal Map-Textur lesen, übergeben wir die Position VERTEX.xz von der Funktion vertex() an die Funktion fragment(). Wir machen das mit Varyings.

Oberhalb von vertex() definieren Sie einen vec2 namens tex_position. Und innerhalb der vertex()-Funktion weisen Sie VERTEX.xz der tex_position zu.

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

Und jetzt können wir auf tex_position von der fragment() Funktion aus zugreifen.

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Mit den Normalen reagiert das Licht nun dynamisch auf die Höhe des Meshs.

../../../_images/normalmap.png

Wir können das Licht sogar herumziehen und die Beleuchtung wird automatisch aktualisiert.

../../../_images/normalmap2.png

Hier ist der vollständige Code für dieses Tutorial. Wie Sie sehen können, ist er nicht sehr lang, da Godot die meisten schwierigen Dinge für Sie erledigt.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Das ist alles für diesen Teil. Hoffentlich verstehen Sie jetzt die Grundlagen von Vertex-Shadern in Godot. Im nächsten Teil dieses Tutorials werden wir eine Fragment-Funktion schreiben, um diese Vertex-Funktion zu begleiten, und wir werden eine fortgeschrittenere Technik behandeln, um dieses Terrain in ein Meer von sich bewegenden Wellen zu verwandeln.