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...
Utilizzare una SubViewport come texture
Introduzione
Questo tutorial spiegherà come utilizzare una SubViewport come texture applicabile a oggetti 3D. Per farlo, ti guiderà attraverso il processo di creazione di un pianeta procedurale come quello qui sotto:
Nota
This tutorial does not cover how to code a dynamic atmosphere like the one this planet has.
Questo tutorial presuppone che tu abbia familiarità con la configurazione di una scena basilare che includa: una Camera3D, una sorgente luminosa, una MeshInstance3D con una PrimitiveMesh e l'applicazione di uno StandardMaterial3D alla mesh. Ci concentreremo su come utilizzare la SubViewport per creare dinamicamente texture da applicare alla mesh.
In this tutorial, we'll cover the following topics:
Come utilizzare una SubViewport come texture di rendering
Mapping a texture to a sphere with equirectangular mapping
Fragment shader techniques for procedural planets
Setting a Roughness map from a Viewport Texture
Setting up the scene
Crea una nuova scena e aggiungi i seguenti nodi esattamente come mostrato di seguito.
Vai alla MeshInstance3D e rendi la mesh una SphereMesh
Preparazione della SubViewport
Clicca sul nodo SubViewport e imposta le sue dimensioni su (1024, 512). Il nodo SubViewport può in realtà avere qualsiasi dimensioni, purché la larghezza sia il doppio dell'altezza. La larghezza deve essere il doppio dell'altezza affinché l'immagine sia mappata accuratamente sulla sfera, poiché utilizzeremo la proiezione equirettangolare, ma ne parleremo più avanti.
Poi disattiva il 3D. Useremo un ColorRect per renderizzare la superficie, quindi non abbiamo bisogno nemmeno del 3D.
Seleziona il ColorRect e nell'ispettore imposta la preimpostazone delle ancore su Full Rect. Questo garantirà che il ColorRect occupi l'intera SubViewport.
Successivamente, aggiungiamo uno Shader Material al ColorRect (ColorRect > CanvasItem > Material > Material > Nuovo ShaderMaterial).
Nota
Basic familiarity with shading is recommended for this tutorial. However, even if you are new to shaders, all the code will be provided, so you should have no problem following along.
Click the dropdown menu button for the shader material and click / Edit. From here go to Shader > New Shader.
give it a name and click "Create". click the shader in the inspector to open the shader editor. Delete the default code
and add the following:
shader_type canvas_item;
void fragment() {
COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}
save the shader code, you'll see in the inspector that the above code renders a gradient like the one below.
Ora abbiamo le basi di una SubViewport su cui renderizzare e abbiamo un'immagine univoca che possiamo applicare alla sfera.
Applying the texture
Ora vai sul MeshInstance3D e aggiungi uno StandardMaterial3D. Non c'è bisogno di uno ShaderMaterial speciale (anche se sarebbe una buona idea per effetti più avanzati, come l'atmosfera nell'esempio precedente).
MeshInstance3D > GeometryInstance > Geometry > Material Override > New StandardMaterial3D
Poi clicca sul menu a tendina per StandardMaterial3D e clicca su "Modifica"
Vai alla sezione "Risorse" e seleziona la casella Local to scene. Quindi, vai alla sezione "Albedo" e clicca accanto alla proprietà "Texture" per aggiungere una texture albedo. Qui applicheremo la texture che abbiamo creato. Scegli "Nuova ViewportTexture"
Clicca sulla ViewportTexture appena creata nell'ispettore, poii clicca su "Assegna". Poi, dal menu a comparsa, seleziona la Viewport su cui abbiamo renderizzato prima.
La tua sfera dovrebbe ora essere colorata con i colori che abbiamo renderizzato nella Viewport.
Hai notato la brutta cucitura che si forma dove la texture si avvolge? Questo accade perché scegliamo un colore in base alle coordinate UV, e le coordinate UV non si avvolgono attorno alla texture. Questo è un problema classico nella proiezione di mappe 2D. Gli sviluppatori di giochi spesso hanno una mappa bidimensionale che vogliono proiettare su una sfera, ma quando si avvolge attorno, appaiono grosse cuciture. Esiste un'elegante soluzione a questo problema che illustreremo nella prossima sezione.
Creare la texture del pianeta
Quindi ora, quando renderizziamo sulla nostra SubViewport, questa appare magicamente sulla sfera. Ma c'è una brutta cucitura creata dalle coordinate della nostra texture. Quindi come possiamo ottenere un intervallo di coordinate che avvolga la sfera in maniera gradevole? Una soluzione è usare una funzione che si ripete sul dominio della nostra texture. sin e cos sono due di queste funzioni. Applichiamole alla texture e vediamo cosa succede. Sostituisci il codice esistente per il colore nello shader con il seguente:
COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
Non male. Se guardi attorno, potrai notare che la cucitura è scomparsa, ma al suo posto abbiamo una sorta di strettoia ai poli. Questa strettoia è dovuto al modo in cui Godot mappa le texture sulle sfere nel suo StandardMaterial3D. Utilizza una tecnica di proiezione chiamata proiezione equirettangolare, che trasla una mappa sferica su un piano 2D.
Nota
Se sei interessato a un po' più informazioni sulla tecnica, passeremo dalle coordinate sferiche a coordinate cartesiane. Le coordinate sferiche rappresentano la longitudine e la latitudine della sfera, mentre le coordinate cartesiane sono, a tutti gli effetti, un vettore che va dal centro della sfera a un punto.
Per ogni pixel, calcoleremo la sua posizione 3D sulla sfera. Da questa, useremo il noise 3D per determinare un valore di colore. Calcolando il noise in 3D, risolviamo il problema della strettoia ai poli. Per capire il perché, immagina il noise calcolato sulla superficie della sfera anziché sul piano 2D. Quando si calcola sulla superficie della sfera, non si incontra mai un bordo e quindi non si crea mai una giunzione o un punto stretto sul polo. Il codice seguente converte le coordinate UV in coordinate cartesiane.
float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);
unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);
E se usiamo unit come valore di output COLOR, otteniamo:
Ora che possiamo calcolare la posizione 3D della superficie della sfera, possiamo usare il noise 3D per creare il pianeta. Useremo questa funzione di noise direttamente da Shadertoy:
vec3 hash(vec3 p) {
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}
Nota
Tutti i crediti vanno all'autore, Inigo Quilez. È pubblicato sotto licenza MIT.
Ora per utilizzare noise, aggiungi quanto segue alla funzione fragment:
float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
Nota
Per evidenziare la texture, abbiamo impostato il materiale su non ombreggiato.
Ora puoi vedere che il noise avvolge senza cuciture la sfera. Anche se non assomiglia per niente al pianeta che ti era stato promesso. Quindi passiamo a qualcosa di più colorato.
Colorare il pianeta
Ora facciamo colorare il pianeta. Sebbene ci siano molti modi per farlo, per ora ci limiteremo a un gradiente tra l'acqua e la terra.
Per creare un gradiente in GLSL, usiamo la funzione mix. mix accetta due valori tra cui interpolare e un terzo argomento per scegliere di quanto interpolare; in sostanza, mescola i due valori assieme. In altre API, questa funzione è spesso detta lerp. Tuttavia, lerp è in genere riservata alla miscelazione di due valori float; mix può accettare qualsiasi valore, sia float sia vettoriale.
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
Il primo colore è il blu per l'oceano. Il secondo colore è una specie di rossastro (perché tutti i pianeti alieni hanno bisogno di terreno rosso). Infine, sono miscelati insieme da n * 0,5 + 0,5. n varia gradualmente tra -1 e 1. Quindi lo mappiamo nell'intervallo 0-1 previsto da mix. Ora puoi vedere che i colori cambiano tra blu e rosso.
È un po' più sfocato di quanto vorremmo. I pianeti solitamente hanno una separazione relativamente netta tra la terra e il mare. Per farlo, cambieremo l'ultimo termine in smoothstep(-0.1, 0.0, n). E quindi l'intera riga diventa:
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
Quello che fa smoothstep è restituire 0 se il terzo argomento è minore del primo e 1 se il terzo argomento è maggiore del secondo, e mescola gradualmente tra 0 e 1 se il terzo numero è compreso tra il primo e il secondo. Quindi, in questa riga, smoothstep restituisce 0 ogni volta che n è minore di -0.1 e restituisce 1 ogni volta che n è maggiore di 0.
Un'altra cosa per rendere il tutto un po' più simile a un pianeta. La terra non dovrebbe essere così chiazzato; rendiamo i bordi un po' più irregolari. Un trucco spesso usato negli shader per creare un terreno dall'aspetto irregolare con noise è quello di sovrapporre strati di noise a diverse frequenze. Usiamo uno strato per creare la struttura generale dei continenti. Poi un altro strato spezza un po' i bordi, e poi un altro ancora, e così via. Quello che faremo è calcolare n con quattro righe di codice shader invece di una sola. n diventa:
float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;
E ora il pianeta ha questo aspetto:
Creare un oceano
Un'ultima cosa per rendere il tutto più simile a un pianeta. L'oceano e la terra riflettono la luce diversamente. Quindi vogliamo che l'oceano brilli un po' di più della terra. Possiamo farlo passando un quarto valore al canale alpha del nostro output COLOR e usandolo come mappa di rugosità.
COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
Questa riga restituisce 0.3 per l'acqua e 1.0 per la terra. Ciò significa che la terra sarà piuttosto ruvida, mentre l'acqua sarà piuttosto liscia.
E poi, nel materiale, nella sezione "Metallic", assicurati che Metallic sia impostato su 0 e Specular su 1. Il motivo è che l'acqua riflette molto bene la luce, ma non è metallica. Questi valori non sono fisicamente accurati, ma sono sufficienti per questa dimostrazione.
Successivamente, nella sezione "Roughness", imposta la texture di rugosità su una ViewportTexture che punta alla texture del nostro pianeta SubViewport. Infine, imposta Texture Channel su Alpha. Questo indica al renderer di utilizzare il canale alpha del nostro COLOR di output come valore di Roughness.
Noterai che cambia ben poco, tranne che il pianeta non sta riflettendo più il cielo. Questo accade perché, come predefinito, quando qualcosa è renderizzato con un valore alfa, è disegnato come un oggetto trasparente sopra lo sfondo. E poiché lo sfondo predefinito della SubViewport è opaco, il canale alpha della ViewportTexture è 1, portando a disegnare la texture del pianeta con colori leggermente più sbiaditi e un valore di Roughness pari a 1 ovunque. Per correggere questo, andiamo nella SubViewport e abilitiamo la proprietà "Transparent Bg". Poiché ora stiamo renderizzando un oggetto trasparente sopra un altro, vogliamo abilitare blend_premul_alpha:
render_mode blend_premul_alpha;
Questo pre-moltiplica i colori per il valore alpha e poi li mescola correttamente. In genere, quando si mescola un colore trasparente sopra un altro, anche se lo sfondo ha un alpha di 0 (come in questo caso), appariranno strani problemi di sbavatura del colore. Impostare blend_premul_alpha li dovrebbe correggere.
Ora il pianeta dovrebbe sembrare di riflettere la luce sull'oceano ma non sulla terra. Sposta l'OmniLight3D nella scena così da poter vedere l'effetto dei riflessi sull'oceano.
Ed ecco fatto. Un pianeta generato proceduralmente utilizzando una SubViewport.