Il tuo primo shader 3D
Hai deciso di cominciare a scrivere il tuo shader Spatial personalizzato. Forse hai trovato online un trucco interessante realizzato con gli shader, oppure hai scoperto che StandardMaterial3D non soddisfa del tutto i tuoi bisogni. In ogni caso, hai deciso di scriverne uno tuo e ora devi capire da dove cominciare.
Questo tutorial spiegherà come scrivere uno shader Spatial e coprirà più argomenti rispetto al tutorial CanvasItem.
Spatial shaders have more built-in functionality than CanvasItem shaders. The expectation with spatial shaders is that Godot has already provided the functionality for common use cases and all the user needs to do in the shader is set the proper parameters. This is especially true for a PBR (physically based rendering) workflow.
This is a two-part tutorial. In this first part we will create terrain using vertex displacement from a heightmap in the vertex function. In the second part we will take the concepts from this tutorial and set up custom materials in a fragment shader by writing an ocean water shader.
Nota
This tutorial assumes some basic shader knowledge such as types
(vec2, float, sampler2D), and functions. If you are
uncomfortable with these concepts it is best to get a gentle
introduction from The Book of Shaders before completing this tutorial.
Dove assegnare il mio materiale
In 3D, gli oggetti vengono disegnati utilizzando Mesh. Le mesh sono un tipo di risorsa che memorizza la geometria (la forma dell'oggetto) e i materiali (il colore e la reazione dell'oggetto alla luce) in unità chiamate "superfici". Una mesh può avere più superfici o solo una. In genere, si importa una mesh da un altro programma (ad esempio Blender). Ma Godot offre anche alcune PrimitiveMeshes che consentono di aggiungere geometria di base a una scena senza importare mesh.
Esistono diversi tipi di nodi che puoi utilizzare per disegnare una mesh. Quello principale è MeshInstance3D, ma puoi anche usare GPUParticles3D, MultiMesh (con MultiMeshInstance3D) o altri.
In genere, un materiale è associato a una determinata superficie in una mesh, ma alcuni nodi, come MeshInstance3D, consentono di sovrascrivere il materiale per una superficie specifica o per tutte le superfici.
Se imposti un materiale sulla superficie o sulla mesh stessa, tutte le MeshInstance3D che condividono quella mesh condivideranno quel materiale. Tuttavia, se desideri riutilizzare la stessa mesh su più istanze, ma con materiali diversi per ogni istanza, è necessario impostare il materiale sulla MeshInstance3D.
In questo tutorial imposteremo il nostro materiale sulla mesh stessa anziché sfruttare la capacità di MeshInstance3D di sovrascrivere i materiali.
Impostare
Aggiungi un nuovo nodo MeshInstance3D alla tua scena.
Nella scheda dell'ispettore, imposta la proprietà Mesh di MeshInstance3D su una nuova risorsa PlaneMesh, cliccando su <vuoto> e scegliendo Nuovo PlaneMesh. Poi espandi la risorsa cliccando sull'immagine di un piano che appare.
Questo aggiunge un piano alla nostra scena.
Quindi, nella viewport, fai clic sul pulsante Prospettiva nell'angolo in alto a sinistra. Nel menu che appare, seleziona Visualizza wireframe.
Questo ti permetterà di vedere i triangoli che compongono il piano.
Now set Subdivide Width and Subdivide Depth of the PlaneMesh to 32.
Come puoi vedere, ora ci sono molti più triangoli in MeshInstance3D. Questo ci darà più vertici con cui lavorare e ci permetterà di aggiungere più dettagli.
PrimitiveMeshes, come PlaneMesh, hanno una sola superficie, quindi invece di un array di materiali ce n'è solo uno. Imposta Material su un nuovo ShaderMaterial, quindi espandi il materiale cliccando sulla sfera che appare.
Nota
I materiali che ereditano dalla risorsa Material, come StandardMaterial3D e ParticleProcessMaterial, si possono convertire in un ShaderMaterial e le loro proprietà esistenti saranno convertite in uno shader di testo corrispondente. Per farlo, fai clic destro sul materiale nel pannello FileSystem e scegli Converti in ShaderMaterial. Puoi anche fare clic destro su qualsiasi proprietà che contenga un riferimento al materiale nell'ispettore.
Ora imposta lo Shader del materiale su un nuovo Shader cliccando su <vuoto> e selezionando Nuovo Shader.... Lascia le impostazioni predefinite, assegna un nome allo shader e clicca su Crea.
Fai clic sullo shader nell'ispettore, e dovrebbe apparire l'editor degli shader. Sei pronto per cominciare a scrivere il tuo primo shader Spatial!
Shader magic
Il nuovo shader è già generato con la variabile shader_type, la funzione vertex() e la funzione fragment(). La prima cosa di cui hanno bisogno gli shader di Godot è una dichiarazione del tipo di shader. In questo caso, shader_type è impostato su spatial perché è uno shader spaziale.
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);
}
Aggiungendo questa riga dovresti ottenere un'immagine come quella qui sotto.
Ok, analizziamo. Il valore y di VERTEX viene aumentato. E stiamo passando le componenti x e z di VERTEX come argomenti a cos() e sin(); questo ci dà un aspetto ondulato lungo gli assi x e z.
Quel che vogliamo ottenere è l'aspetto di piccole colline; dopotutto, cos() e sin() hanno già un aspetto simile a delle colline. Lo facciamo scalando gli input delle funzioni cos() e sin().
void vertex() {
VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
Sembra meglio, ma è ancora troppo spinoso e ripetitivo. Rendiamolo un po' più interessante.
Noise heightmap
Noise is a very popular tool for faking the look of terrain. Think of it as similar to the cosine function where you have repeating hills except, with noise, each hill has a different height.
Godot fornisce la risorsa NoiseTexture2D per generare una texture di noise a cui è possibile accedere da uno shader.
Per accedere a una texture in uno shader, aggiungi il seguente codice in cima allo shader, fuori dalla funzione vertex().
uniform sampler2D noise;
Questo ti permetterà di inviare una texture di noise allo shader. Ora guarda nell'ispettore sotto il tuo materiale. Dovresti vedere una sezione chiamata Parametri dello shader. Se la apri, vedrai un parametro chiamato "Noise".
Imposta questo parametro Noise su un nuovo NoiseTexture2D. Quindi, in NoiseTexture2D, imposta la sua proprietà Noise su un nuovo FastNoiseLite. La classe FastNoiseLite è utilizzata da NoiseTexture2D per generare una heightmap.
Una volta che l'hai impostato e dovrebbe apparire così.
Ora, accedi alla texture di noise attraverso la funzione texture():
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.
Poiché le coordinate del PlaneMesh rientrano nell'intervallo [-1.0, 1.0] (per una dimensione di 2.0), mentre le coordinate della texture rientrano in [0.0, 1.0], per rimappare le coordinate dividiamo per la dimensione del PlaneMesh per 2.0 e aggiungiamo 0.5.
texture() restituisce un vec4 dei canali r, g, b, a nella posizione. Poiché la texture di noise è in scala di grigi, tutti i valori sono identici, quindi possiamo usare uno qualsiasi dei canali come altezza. In questo caso useremo il canale r o x.
Nota
xyzw è uguale a rgba in GLSL, quindi invece di texture().x, potremmo usare texture().r. Consulta la documentazione di OpenGL per più dettagli.
Utilizzando questo codice puoi vedere come la texture crea delle colline dall'aspetto casuale.
Al momento è troppo spigoloso, vogliamo ammorbidire un pochetto le colline. Per farlo, useremo un'uniforme. Hai già usato un'uniforme prima per passare la texture di noise, ora impariamo come funzionano.
Uniformi
Le variabili uniformi permettono di passare dati dal gioco allo shader. Sono molto utili per controllare gli effetti dello shader. Le uniformi possono essere praticamente di qualsiasi tipo di dato utilizzabile nello shader. Per usare un'uniforme, la si dichiara nel proprio Shader usando la parola chiave uniform.
Creiamo un'uniforme che cambi l'altezza del terreno.
uniform float height_scale = 0.5;
Godot consente di inizializzare un'uniforme con un valore; in questo caso, height_scale è impostato su 0.5. È possibile impostare le uniformi da GDScript chiamando la funzione set_shader_parameter() sul materiale corrispondente allo shader. Il valore passato da GDScript ha la precedenza sul valore utilizzato per inizializzarlo nello shader.
# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)
Nota
La modifica delle uniformi nei nodi basati su Spatial è diversa da quella nei nodi basati su CanvasItem. Qui, impostiamo il materiale all'interno della risorsa PlaneMesh. In altre risorse mesh potrebbe essere necessario prima accedere al materiale chiamando surface_get_material(). In MeshInstance3D, invece, si accede al materiale tramite get_surface_material() o material_override.
Ricorda che la stringa passata a set_shader_parameter() deve corrispondere al nome della variabile uniforme nello shader. Puoi usare la variabile uniforme ovunque all'interno dello shader. Qui, la useremo per impostare il valore dell'altezza invece di moltiplicarla arbitrariamente per 0.5.
VERTEX.y += height * height_scale;
Now it looks much better.
Utilizzando le uniformi, possiamo persino modificare il valore a ogni frame per animare l'altezza del terreno. In combinazione con Tweens, ciò può essere particolarmente utile per le animazioni.
Interagire con la luce
Per prima cosa, disattiva il wireframe. Per farlo, apri di nuovo il menu Prospettiva in alto a sinistra della viewport e seleziona Visualizza predefinita. Inoltre, nella barra degli strumenti della scena 3D, disattiva l'anteprima della luce solare.
Nota come il colore della mesh diventa piatto. Questo perché l'illuminazione è piatta. Aggiungiamo una luce!
Per prima cosa aggiungeremo un OmniLight3D alla scena e lo trascineremo verso l'alto in modo che si trovi sopra il terreno.
Puoi vedere la luce che influenza il terreno, ma sembra strano. Il problema è che la luce influenza il terreno come se fosse un piano piatto. Questo perché lo shader di luce utilizza le normali della Mesh per calcolare la luce.
The normals are stored in the Mesh, but we are changing the shape of the Mesh in the shader, so the normals are no longer correct. To fix this, we can recalculate the normals in the shader or use a normal texture that corresponds to our noise. Godot makes both easy for us.
È possibile calcolare manualmente la nuova normale nella funzione vertex e poi impostare NORMAL. Con NORMAL impostato, Godot eseguirà tutti i complessi calcoli di illuminazione per noi. Parleremo di questo metodo nella prossima parte di questo tutorial, per ora leggeremo le normali da una texture.
Ci affideremo nuovamente alla NoiseTexture per calcolare le normali al posto nostro. Lo faremo passando una seconda texture di noise.
uniform sampler2D normalmap;
Set this second uniform texture to another NoiseTexture2D with another FastNoiseLite. But this time, check As Normal Map.
Quando abbiamo normali che corrispondono a un vertice specifico, impostiamo NORMAL, ma se abbiamo una mappa di normali che deriva da una texture, impostiamo la normale usando NORMAL_MAP nella funzione fragment(). In questo modo Godot gestirà automaticamente l'avvolgimento della texture attorno alla mesh.
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.
Sopra vertex() definisci un varying vec2 chiamato tex_position. E dentro la funzione vertex() assegna VERTEX.xz a 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;
}
Ora possiamo accedere a tex_position dalla funzione fragment().
void fragment() {
NORMAL_MAP = texture(normalmap, tex_position).xyz;
}
Con le normali a posto, la luce ora reagisce dinamicamente all'altezza della mesh.
Possiamo anche trascinare la luce e l'illuminazione si aggiornerà automaticamente.
Codice completo
Here is the full code for this tutorial. You can see it is not very long as Godot handles most of the difficult stuff for you.
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;
}
That is everything for this part. Hopefully, you now understand the basics of vertex shaders in Godot. In the next part of this tutorial we will write a fragment function to accompany this vertex function and we will cover a more advanced technique to turn this terrain into an ocean of moving waves.