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.

Il tuo secondo shader 3D

Da un punto di vista generale, quel che fa Godot è fornire all'utente una serie di parametri si possono impostare facoltativamente (AO, SSS_Strength, RIM, ecc.). Questi parametri corrispondono a diversi effetti complessi (Ambient Occlusion, SubSurface Scattering, Rim Lighting, ecc.). Se non vi si scrivono, il codice viene scartato prima di essere compilato, e così lo shader evita l'impatto della funzionalità aggiuntiva. Questo rende molto più facile per gli utenti avere ombreggiatura complessa e PBR-correct, senza dover scrivere shader complessi. Naturalmente, Godot consente anche di ignorare tutti questi parametri e di scrivere uno shader completamente personalizzato.

Per un elenco completo di questi parametri, consulta il documento di riferimento spatial shader.

Una differenza tra la funzione vertex e la funzione fragment è che la funzione vertex è eseguita per ogni vertice e imposta proprietà quali VERTEX (posizione) e NORMAL, mentre lo shader fragment è eseguito per ogni pixel e, cosa più importante, imposta il colore ALBEDO del MeshInstance3D.

La tua prima funzione fragment spatial

Come accennato nella parte precedente di questo tutorial, l'uso standard della funzione "fragment" in Godot è quello di impostare diverse proprietà dei materiali e lasciare che Godot si occupi del resto. Per offrire ancora più flessibilità, Godot offre anche le cosiddette modalità di rendering. Le modalità di rendering si impostano in cima allo shader, direttamente sotto shader_type, e specificano il tipo di funzionalità che si desidera assegnare agli aspetti integrati dello shader.

Ad esempio, se non vuoi che le luci influenzino un oggetto, imposta la modalità di rendering su unshaded:

render_mode unshaded;

È anche possibile impilare più modalità di rendering assieme. Ad esempio, se si desidera utilizzare l'ombreggiatura toon invece della più realistica ombreggiatura PBR, imposta la modalità diffusa e quella speculare su toon:

render_mode diffuse_toon, specular_toon;

Questo modello di funzionalità integrata consente di scrivere shader personalizzati complessi cambiando solo pochi parametri.

Per un elenco completo delle modalità di rendering, consulta il Riferimento di shader Spatial.

In questa parte del tutorial, spiegheremo come prendere il terreno irregolare della parte precedente e trasformarlo in un oceano.

Cominciamo impostando il colore dell'acqua. Lo facciamo impostando ALBEDO.

ALBEDO è un vec3 che contiene il colore dell'oggetto.

Impostiamolo su una bella tonalità di blu.

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

Lo abbiamo impostato su una tonalità di blu molto scura perché la maggioranza del blu dell'acqua proverrà dai riflessi del cielo.

Il modello PBR utilizzato da Godot si basa su due parametri principali: METALLIC e ROUGHNESS.

ROUGHNESS specifica quanto è liscia/ruvida la superficie di un materiale. Una ROUGHNESS bassa farà apparire il materiale come una plastica lucida, mentre una rugosità elevata farà apparire il materiale più uniforme nel colore.

METALLIC specifica quanto l'oggetto sia simile a un metallo. È preferibile impostarlo vicino a 0 o 1. Pensa che METALLIC modifichi l'equilibrio tra la riflessione e il colore ALBEDO. Un valore METALLIC alto ignora quasi completamente ALBEDO e appare come uno specchio del cielo. Mentre un valore METALLIC basso ha una rappresentazione più equilibrata del colore del cielo e del colore ALBEDO.

ROUGHNESS aumenta da 0 a 1 da sinistra a destra, mentre METALLIC aumenta da 0 a 1 dall'alto verso il basso.

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

Nota

METALLIC dovrebbe essere vicino a 0 o 1 per una corretta ombreggiatura PBR. Impostalo solo tra questi due valori per fondere tra materiali.

L'acqua non è un metallo, quindi imposteremo la sua proprietà METALLIC su 0.0. L'acqua è anche altamente riflettente, quindi imposteremo anche la sua proprietà ROUGHNESS su un valore piuttosto basso.

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

Ora abbiamo una superficie liscia dall'aspetto plastico. È il momento di pensare ad alcune proprietà specifiche dell'acqua che vogliamo emulare. Ce ne sono due principali che trasformeranno questa strana superficie plastica in un'acqua piacevolmente stilizzata. La prima sono i riflessi speculari, ovvero quei punti luminosi che vedi dove il sole riflette direttamente nei tuoi occhi. La seconda è la riflettanza di Fresnel, ovvero la proprietà degli oggetti di diventare più riflettenti ad angolazioni ridotte. È il motivo per cui puoi vedere nell'acqua da sotto di te, ma più lontano riflette il cielo.

Per aumentare i riflessi speculari, faremo due cose. Innanzitutto, cambieremo la modalità di rendering da speculare a toon, perché la modalità di rendering toon ha riflessi speculari più larghi.

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

Secondo, aggiungeremo l'illuminazione del bordo. L'illuminazione del bordo aumenta l'effetto della luce negli angoli di osservazione. Di solito è utilizzata per emulare il modo in cui la luce attraversa il tessuto sui bordi di un oggetto, ma qui la useremo per ottenere un piacevole effetto acquatico.

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

Per aggiungere la riflettanza di Fresnel, calcoleremo un termine di Fresnel nel nostro shader di frammenti. Qui, non useremo un termine di Fresnel reale per motivi di prestazioni. Invece, lo approssimeremo tramite il prodotto scalare dei vettori NORMAL e VIEW. Il vettore NORMAL punta lontano dalla superficie della mesh, mentre il vettore VIEW è la direzione tra l'occhio e quel punto sulla superficie. Il prodotto scalare tra i due è un modo pratico per capire se si sta guardando la superficie frontalmente o di traverso.

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

E fondilo sia in ROUGHNESS sia in ALBEDO. Questo è il vantaggio degli ShaderMaterial rispetto agli StandardMaterial3D. Con StandardMaterial3D, potevamo impostare queste proprietà con una texture o con un numero fisso. Ma con gli shader possiamo impostarle in base a qualsiasi funzione matematica ci venga in mente.

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

E ora, con solamente 5 righe di codice, puoi ottenere un'acqua dall'aspetto complesso. Ora che abbiamo l'illuminazione, quest'acqua sembra troppo chiara. Oscuriamola. Questo si fa facilmente diminuendo i valori di vec3 che passiamo in ALBEDO. Impostiamoli a vec3(0.01, 0.03, 0.05).

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

Animare con TIME

Tornando alla funzione vertex, possiamo animare le onde utilizzando la variabile integrata TIME.

TIME è una variabile integrata che è accessibile dalle funzioni vertex e fragment.

Nell'ultimo tutorial abbiamo calcolato l'altezza leggendola da una heightmap. In questo tutorial faremo lo stesso. Inserisci il codice della heightmap in una funzione chiamata height().

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

Per poter utilizzare TIME nella funzione height(), dobbiamo passarlo.

float height(vec2 position, float time) {
}

E assicurati di passarlo correttamente dentro la funzione vertex.

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

Invece di usare una mappa normale per calcolare le normali, le calcoleremo manualmente nella funzione vertex(). Per farlo, utilizza la seguente riga di codice.

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

Dobbiamo calcolare NORMAL manualmente perché nella prossima sezione useremo la matematica per creare onde dall'aspetto complesso.

Adesso renderemo la funzione height() un po' più complicata, compensando position con il coseno di TIME.

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

Questo risulta in onde che si muovono lentamente, ma non in modo molto naturale. La prossima sezione approfondirà l'uso degli shader per creare effetti più complessi, in questo caso onde realistiche, aggiungendo qualche funzione matematica in più.

Effetti avanzati: onde

Ciò che rende gli shader così potenti è la possibilità di ottenere effetti complessi grazie alla matematica. Per illustrarlo, passeremo le nostre onde al prossimo livello modificando la funzione height() e introducendo una nuova funzione chiamata wave().

wave() ha un solo parametro, position, che è lo stesso che è in height().

Chiameremo wave() più volte in height() per simulare l'aspetto delle onde.

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);
}

Sembra complicato a prima vista. Pertanto, analizziamolo riga per riga.

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

Compensa la posizione con la texture noise. Questo curverà le onde, affinché non siano linee rette completamente allineate con la griglia.

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

Definiamo una funzione di tipo onda usando sin() e position. Normalmente le onde da sin() sono molto rotonde. Usiamo abs() per dare loro una cresta netta e limitarle all'intervallo 0-1. Quindi le sottraiamo da 1.0 per mettere il picco in alto.

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

Moltiplica l'onda direzionale x per l'onda direzionale y ed elevalo a una potenza per accentuare i picchi. Quindi sottrai questo risultato da 1.0 in modo che le creste diventino picchi e elevalo a una potenza per accentuare i picchi.

Ora possiamo sostituire il contenuto della nostra funzione height() con wave().

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

Utilizzando questo, ottieni:

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

La forma dell'onda sinusoidale è troppo evidente. Quindi, distribuiamo un po' le onde. Lo facciamo ridimensionando position.

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

Ora sembra molto meglio.

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

Possiamo fare ancora meglio se sovrapponiamo più onde una sopra l'altra a frequenze e ampiezze diverse. Ciò significa che ridimensioneremo la posizione di ciascuna per renderle più sottili o più larghe (frequenza). E moltiplicheremo l'output dell'onda per renderle più corte o più alte (ampiezza).

Ecco un esempio di come potresti sovrapporre le quattro onde per ottenere onde più gradevoli.

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;
}

Noda che sommiamo il tempo a due e lo sottraiamo dalle altre due. Questo fa in modo che le onde si muovano in direzioni diverse, creando un effetto complesso. Nota inoltre che le ampiezze (il numero per cui è moltiplicato il risultato) sommate danno tutte 1.0. Questo mantiene l'onda nell'intervallo 0-1.

Con questo codice dovresti ottenere onde dall'aspetto più complesso, e tutto ciò che hai dovuto fare è stato aggiungere un po' di matematica!

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

Per maggiori informazioni sugli shader Spatial, leggi la documentazione Shading Language e la documentazione Shader Spatial. Consulta anche i tutorial più avanzati nella sezione Shading e nelle sezioni 3D.