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...
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.
Gli shader Spatial offrono più funzionalità integrate rispetto agli shader CanvasItem. Ci si aspetta che Godot abbia già fornito le funzionalità per i casi d'uso più comuni e che l'utente debba semplicemente impostare i parametri appropriati nello shader. Questo è particolarmente il caso per un flusso di lavoro PBR (rendering basato sulla fisica).
Questo tutorial è diviso in due parti. Nella prima parte creeremo un terreno usando uno scostamento di vertici a partire da una heightmap nella funzione vertex. Nella seconda parte prenderemo i concetti di questo tutorial e configureremo materiali personalizzati in uno shader di frammenti, scrivendo uno shader per l'acqua di un oceano.
Nota
Questo tutorial presuppone una conoscenza di base degli shader, come i tipi (vec2, float, sampler2D) e le funzioni. Se non sei pratico di questi concetti, ti consigliamo di leggere una breve introduzione da The Book of Shaders prima di completare il 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.
Configurazione
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.
Ora imposta Subdivide Width e Subdivide Depth del PlaneMesh su 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!
Magia di shader
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;
La funzione vertex() determina dove appaiono i vertici del MeshInstance3D nella scena finale. La useremo per compensare l'altezza di ogni vertice e far apparire il nostro piano come un piccolo terreno.
Con nulla nella funzione vertex(), Godot userà il proprio shader di vertici predefinito. Possiamo cominciare ad apportare modifiche aggiungendo una singola riga:
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.
Heightmap con noise
Il noise è un mezzo molto popolare per simulare l'aspetto del terreno. È simile alla funzione del coseno, che crea colline che si ripetono, solo che, con il noise, ogni collina ha un'altezza diversa.
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;
Ora sembra molto meglio.
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;
Imposta questa seconda texture uniforme su un altro NoiseTexture2D con un altro FastNoiseLite. Ma questa volta, seleziona Come mappa di normali.
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.
Infine, per assicurarci di leggere dagli stessi punti della texture di noise e della texture della mappa di normali, passeremo la posizione VERTEX.xz dalla funzione vertex() alla funzione fragment(). Lo faremo attraverso un 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
Ecco il codice completo per questo tutorial. Come puoi vedere, non è molto lungo, dato che Godot si occupa di gran parte delle cose difficili per te.
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;
}
Questo è tutto per questa parte. Spero che abbiate compreso le basi degli shader di vertici in Godot. Nella prossima parte di questo tutorial scriveremo una funzione fragment da abbinare a questa funzione vertex e tratteremo una tecnica più avanzata per trasformare questo terreno in un oceano di onde in movimento.