Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
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.
In the inspector tab, set the MeshInstance3D's Mesh property to a new
PlaneMesh resource, by clicking on <empty>
and
choosing New PlaneMesh. Then expand the resource by clicking on the image of
a plane that appears.
This adds a plane to our scene.
Then, in the viewport, click in the upper left corner on the Perspective button. In the menu that appears, select Display Wireframe.
So können Sie die Dreiecke sehen, aus denen die Ebene besteht.

Now set Subdivide Width and Subdivide Depth of the PlaneMesh to 32
.

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.

PrimitiveMeshes, like PlaneMesh, only have one surface, so instead of an array of materials there is only one. Set the Material to a new ShaderMaterial, then expand the material by clicking on the sphere that appears.
Bemerkung
Materials that inherit from the Material resource, such as StandardMaterial3D and ParticleProcessMaterial, can be converted to a ShaderMaterial and their existing properties will be converted to an accompanying text shader. To do so, right-click on the material in the FileSystem dock and choose Convert to ShaderMaterial. You can also do so by right-clicking on any property holding a reference to the material in the inspector.
Now set the material's Shader to a new Shader by clicking <empty>
and
select New Shader.... Leave the default settings, give your shader a name,
and click Create.
Click on the shader in the inspector, and the shader editor should now pop up. You are ready to begin writing your first Spatial shader!
Shader-Magie

The new shader is already generated with a shader_type
variable, the
vertex()
function, and the fragment()
function. The first thing Godot
shaders need is a declaration of what type of shader they are. In this case the
shader_type
is set to spatial
because this is a spatial shader.
shader_type spatial;
The vertex()
function determines where the vertices of your MeshInstance3D
appear in the final scene. We will be using it to offset the height of each vertex
and make our flat plane appear like a little terrain.
With nothing in the vertex()
function, Godot will use its default vertex
shader. We can start to make changes by adding a single line:
void vertex() {
VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}
Wenn Sie diese Zeile hinzufügen, sollten Sie ein Bild wie das folgende erhalten.

Okay, let's unpack this. The y
value of the VERTEX
is being increased.
And we are passing the x
and z
components of the VERTEX
as arguments
to cos() and sin(); that gives
us a wave-like appearance across the x
and z
axes.
What we want to achieve is the look of little hills; after all. cos()
and
sin()
already look kind of like hills. We do so by scaling the inputs to the
cos()
and sin()
functions.
void vertex() {
VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}

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;
This will allow you to send a noise texture to the shader. Now look in the inspector under your material. You should see a section called Shader Parameters. If you open it up, you'll see a parameter called "Noise".
Set this Noise parameter to a new NoiseTexture2D. Then in your NoiseTexture2D, set its Noise property to a new FastNoiseLite. The FastNoiseLite class is used by the NoiseTexture2D to generate a heightmap.
Sobald Sie es eingerichtet haben und so aussehen sollten.

Now, access the noise texture using the texture()
function:
void vertex() {
float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;
}
texture() takes a texture as the first argument and
a vec2
for the position on the texture as the second argument. We use the
x
and z
channels of VERTEX
to determine where on the texture to look
up.
Since the PlaneMesh coordinates are within the [-1.0, 1.0]
range (for a size
of 2.0
), while the texture coordinates are within [0.0, 1.0]
, to remap
the coordinates we divide by the size of the PlaneMesh by 2.0
and add
0.5
.
texture()
returns a vec4
of the r, g, b, a
channels at the position.
Since the noise texture is grayscale, all of the values are the same, so we can
use any one of the channels as the height. In this case we'll use the r
, or
x
channel.
Bemerkung
xyzw
is the same as rgba
in GLSL, so instead of texture().x
above, we could use texture().r
. See the OpenGL documentation for more
details.
Mit diesem Code können Sie sehen, wie die Textur zufällig aussehende Hügel erzeugt.

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 variables allow you to pass data
from the game into the shader. They are
very useful for controlling shader effects. Uniforms can be almost any datatype
that can be used in the shader. To use a uniform, you declare it in your
Shader using the keyword uniform
.
Lassen Sie uns ein Uniform herstellen, das die Höhe des Geländes verändert.
uniform float height_scale = 0.5;
Godot lets you initialize a uniform with a value; here, height_scale
is set
to 0.5
. You can set uniforms from GDScript by calling the function
set_shader_parameter()
on the material corresponding to the shader. The value passed from GDScript
takes precedence over the value used to initialize it in the shader.
# 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.
Remember that the string passed into set_shader_parameter()
must match the name
of the uniform variable in the shader. You can use the
uniform variable anywhere inside your shader. Here, we will
use it to set the height value instead of arbitrarily multiplying by 0.5
.
VERTEX.y += height * height_scale;
Nun sieht es viel besser aus.

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
First, turn wireframe off. To do so, open the Perspective menu in the upper-left of the viewport again, and select Display Normal. Additionally in the 3D scene toolbar, turn off preview sunlight.

Beachten Sie, dass die Farbe des Meshes flach wird. Das liegt daran, dass die Beleuchtung darauf flach ist. Fügen wir ein Licht hinzu!
First, we will add an OmniLight3D to the scene, and drag it up so it is above the terrain.

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;
Set this second uniform texture to another NoiseTexture2D with another FastNoiseLite. But this time, check As Normal Map.

When we have normals that correspond to a specific vertex we set NORMAL
, but
if you have a normalmap that comes from a texture, set the normal using
NORMAL_MAP
in the fragment()
function. This way Godot will handle
wrapping the texture around the mesh automatically.
Lastly, in order to ensure that we are reading from the same places on the noise
texture and the normalmap texture, we are going to pass the VERTEX.xz
position from the vertex()
function to the fragment()
function. We do
that using a varying.
Above the vertex()
define a varying vec2
called tex_position
. And
inside the vertex()
function assign VERTEX.xz
to tex_position
.
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;
}
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.

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

Full code
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.