This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
In questo tutorial creeremo un effetto di post-elaborazione per la correzione cromatica (color grading) a schermo intero. Il metodo di rendering di base utilizzato si presta a molti tipi di effetti di post-elaborazione, come sfocature, scie, bagliori, regolazioni del colore e così via.
Si presuppone che tu sappia orientarti nell’editor Defold e che abbia una conoscenza di base degli shader GL e della pipeline di rendering di Defold. Se hai bisogno di approfondire questi argomenti, consulta il nostro manuale sugli shader e il manuale sul rendering.
Con lo script di rendering predefinito, ogni componente visivo (sprite, tilemap, effetto particellare, GUI ecc.) viene renderizzato direttamente nel frame buffer della scheda grafica. L’hardware fa quindi apparire la grafica sullo schermo. Il disegno effettivo dei pixel di un componente viene eseguito da un programma shader GL. Defold include un programma shader predefinito per ogni tipo di componente, che disegna i dati dei pixel sullo schermo senza modificarli. Normalmente è il comportamento desiderato: le immagini devono apparire sullo schermo come sono state concepite in origine.
Puoi sostituire il programma shader di un componente con uno che modifichi i dati dei pixel o crei colori dei pixel completamente nuovi tramite codice. Il tutorial su Shadertoy ti insegna come fare.
Supponiamo ora che tu voglia renderizzare l’intero gioco in bianco e nero. Una possibile soluzione consiste nel modificare il programma shader di ogni tipo di componente, in modo che ciascuno shader desaturi i colori dei pixel. Attualmente Defold include 6 materiali integrati e 6 coppie di programmi vertex shader e fragment shader, quindi questo richiederà una discreta quantità di lavoro. Inoltre, ogni modifica successiva o aggiunta di effetti dovrà essere applicata a ciascun programma shader.
Un approccio molto più flessibile consiste nell’eseguire il rendering in due passaggi separati:

Con questo metodo possiamo leggere i dati visivi risultanti e modificarli prima che raggiungano lo schermo. Aggiungendo programmi shader al passaggio 2, possiamo ottenere facilmente effetti a schermo intero. Vediamo come configurare tutto questo in Defold.
Dobbiamo modificare lo script di rendering integrato e aggiungere la nuova funzionalità di rendering. Lo script di rendering predefinito è un buon punto di partenza, quindi inizia copiandolo:
Apri grade.render e imposta la proprietà Script su “/main/grade.render_script”.

Apri game.project e imposta Render su “/main/grade.render”.

Ora il gioco è configurato per essere eseguito con una nuova pipeline di rendering che possiamo modificare. Per verificare che il motore utilizzi la nostra copia dello script di rendering, avvia il gioco, apporta allo script una modifica che produca un risultato visibile, quindi ricarica lo script. Per esempio, puoi disattivare il disegno di tile e sprite, poi premere ⌘ + R per eseguire l’hot reload dello script di rendering “alterato” nel gioco in esecuzione:
...
render.set_projection(vmath.matrix4_orthographic(0, render.get_width(), 0, render.get_height(), -1, 1))
-- render.draw(self.tile_pred) -- <1>
render.draw(self.particle_pred)
render.draw_debug3d()
...
Se gli sprite e i tile scompaiono durante questo semplice test, hai la conferma che il gioco esegue il tuo script di rendering. Se tutto funziona come previsto, puoi annullare la modifica allo script.
Ora modifichiamo lo script di rendering in modo che disegni nella destinazione di rendering fuori schermo anziché nel frame buffer. Per prima cosa dobbiamo creare la destinazione di rendering:
function init(self)
self.tile_pred = render.predicate({"tile"})
self.gui_pred = render.predicate({"gui"})
self.text_pred = render.predicate({"text"})
self.particle_pred = render.predicate({"particle"})
self.clear_color = vmath.vector4(0, 0, 0, 1)
self.clear_color.x = sys.get_config_number("render.clear_color_red", 0)
self.clear_color.y = sys.get_config_number("render.clear_color_green", 0)
self.clear_color.z = sys.get_config_number("render.clear_color_blue", 0)
self.clear_color.w = sys.get_config_number("render.clear_color_alpha", 1)
self.view = vmath.matrix4()
local color_params = { format = graphics.TEXTURE_FORMAT_RGBA,
width = render.get_width(),
height = render.get_height() } -- <1>
local target_params = {[graphics.BUFFER_TYPE_COLOR0_BIT] = color_params }
self.target = render.render_target("original", target_params) -- <2>
end
Ora dobbiamo soltanto racchiudere il codice di rendering originale tra chiamate a render.set_render_target(), in questo modo:
function update(self)
render.set_render_target(self.target) -- <1>
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear({[graphics.BUFFER_TYPE_COLOR0_BIT] = self.clear_color, [graphics.BUFFER_TYPE_DEPTH_BIT] = 1, [graphics.BUFFER_TYPE_STENCIL_BIT] = 0})
render.set_viewport(0, 0, render.get_width(), render.get_height()) -- <2>
render.set_view(self.view)
...
render.set_render_target(render.RENDER_TARGET_DEFAULT) -- <3>
end
render.draw() disegnerà nei buffer della nostra destinazione di rendering fuori schermo.update() rimane invariato, a parte il viewport, che viene impostato sulla risoluzione della destinazione di rendering.Non serve altro. Se esegui il gioco ora, disegnerà tutto nella destinazione di rendering. Ma poiché non disegniamo più nulla nel frame buffer, vedremo soltanto uno schermo nero.
Per disegnare sullo schermo i pixel del buffer dei colori della destinazione di rendering, dobbiamo predisporre un elemento a cui applicare i dati dei pixel come texture. A questo scopo useremo un modello 3D piatto e quadrato.
main.collection e crea un nuovo oggetto di gioco chiamato “grade”.grade”.quad.gltf che si trova in builtins/assets/meshes.Lascia l’oggetto di gioco all’origine, senza modificarne la scala. In seguito, quando renderizzeremo il quadrilatero, lo proietteremo in modo che riempia l’intero schermo. Prima, però, ci servono un materiale e dei programmi shader per il quadrilatero:
grade.material facendo clic con il pulsante destro su main nella vista Asset e selezionando New ▸ Material.grade.vp e un programma fragment shader chiamato grade.fp facendo clic con il pulsante destro su main nella vista Asset e selezionando New ▸ Vertex program e New ▸ Fragment program.view_proj” di tipo CONSTANT_TYPE_VIEWPROJ. È la matrice di vista e proiezione utilizzata nel programma vertex shader per i vertici del quadrilatero.original”. Servirà a campionare i pixel dal buffer dei colori della destinazione di rendering fuori schermo.Aggiungi un Tag chiamato “grade”. Creeremo nello script di rendering un nuovo predicato di rendering che corrisponda a questo tag per disegnare il quadrilatero.

Apri main.collection, seleziona il componente modello nell’oggetto di gioco “grade” e imposta la sua proprietà Material su “/main/grade.material”.

Puoi lasciare il programma vertex shader così come viene creato dal modello di base:
// grade.vp
uniform mediump mat4 view_proj;
// positions are in world space
attribute mediump vec4 position;
attribute mediump vec2 texcoord0;
varying mediump vec2 var_texcoord0;
void main()
{
gl_Position = view_proj * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
}
Nel programma fragment shader, invece di impostare direttamente gl_FragColor sul valore del colore campionato, eseguiamo una semplice manipolazione del colore. Lo facciamo soprattutto per assicurarci che fin qui tutto funzioni come previsto:
// grade.fp
varying mediump vec4 position;
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D original;
void main()
{
vec4 color = texture2D(original, var_texcoord0.xy);
// Desaturate the color sampled from the original texture
float grey = color.r * 0.3 + color.g * 0.59 + color.b * 0.11;
gl_FragColor = vec4(grey, grey, grey, 1.0);
}
Ora abbiamo predisposto il modello del quadrilatero con il suo materiale e i suoi shader. Non ci resta che disegnarlo nel frame buffer dello schermo.
Dobbiamo aggiungere un predicato di rendering allo script di rendering per poter disegnare il modello del quadrilatero. Apri grade.render_script e modifica la funzione init():
function init(self)
self.tile_pred = render.predicate({"tile"})
self.gui_pred = render.predicate({"gui"})
self.text_pred = render.predicate({"text"})
self.particle_pred = render.predicate({"particle"})
self.grade_pred = render.predicate({"grade"}) -- <1>
...
end
grade.material.Dopo aver riempito il buffer dei colori della destinazione di rendering in update(), impostiamo una vista e una proiezione che facciano riempire l’intero schermo al modello del quadrilatero. Usiamo quindi il buffer dei colori della destinazione di rendering come texture del quadrilatero:
function update(self)
render.set_render_target(self.target)
...
render.set_render_target(render.RENDER_TARGET_DEFAULT)
render.clear({[graphics.BUFFER_TYPE_COLOR0_BIT] = self.clear_color}) -- <1>
render.set_viewport(0, 0, render.get_window_width(), render.get_window_height()) -- <2>
render.set_view(vmath.matrix4()) -- <3>
render.set_projection(vmath.matrix4())
render.enable_texture(0, self.target, graphics.BUFFER_TYPE_COLOR0_BIT) -- <4>
render.draw(self.grade_pred) -- <5>
render.disable_texture(0, self.target) -- <6>
end
render.clear() agisce sulla destinazione di rendering, non sul frame buffer dello schermo.grade.material, il sampler “original” si trova nello slot 0, quindi il fragment shader campionerà dalla destinazione di rendering.grade.material, che imposta quel tag: il quadrilatero verrà quindi disegnato.Ora eseguiamo il gioco e osserviamo il risultato:

I colori sono espressi da tre valori, ciascuno dei quali determina la quantità di rosso, verde o blu che compone un colore. L’intero spettro cromatico, dal nero al bianco passando per rosso, verde, blu, giallo e rosa, può essere racchiuso in un cubo:

Qualsiasi colore visualizzabile sullo schermo si trova in questo cubo dei colori. L’idea alla base della correzione cromatica è usare un cubo di questo tipo, ma con colori modificati, come tabella di ricerca 3D.
Per ogni pixel:
Possiamo farlo nel nostro fragment shader:

Open GL ES 2.0 non supporta le texture 3D, quindi dobbiamo trovare un altro modo per rappresentare il cubo dei colori 3D. Un metodo comune consiste nel suddividere il cubo in sezioni lungo l’asse Z (blu) e disporre le sezioni una accanto all’altra in una griglia bidimensionale. Ciascuna delle 16 sezioni contiene una griglia di 16⨉16 pixel. Memorizziamo il tutto in una texture dalla quale possiamo leggere nel fragment shader tramite un sampler:

La texture risultante contiene 16 celle (una per ogni intensità del blu) e, all’interno di ogni cella, 16 valori di rosso lungo l’asse X e 16 valori di verde lungo l’asse Y. La texture rappresenta l’intero spazio cromatico RGB di 16 milioni di colori con soli 4096 colori: appena 4 bit di profondità cromatica. È una precisione scarsa secondo la maggior parte dei criteri, ma grazie a una funzionalità dell’hardware grafico GL possiamo recuperare una precisione cromatica molto elevata. Vediamo come.
Per cercare un colore basta controllare la componente blu e determinare da quale cella prendere i valori di rosso e verde. La formula per trovare la cella con l’insieme corretto di colori rosso-verdi è semplice:
cell = \left \lfloor{B \times (N - 1)} \right \rfloor
Qui B è il valore della componente blu, compreso tra 0 e 1, e N è il numero totale di celle. Nel nostro caso il numero della cella sarà nell’intervallo 0–15, dove la cella 0 contiene tutti i colori con la componente blu a 0 e la cella 15 tutti i colori con la componente blu a 1.
Per esempio, il valore RGB (0.63, 0.83, 0.4) si trova nella cella che contiene tutti i colori con un valore di blu pari a 0.4, ovvero la cella numero 6. Sapendolo, è semplice determinare le coordinate finali della texture in base ai valori di verde e rosso:

Nota che dobbiamo considerare i valori di rosso e verde (0, 0) come corrispondenti al centro del pixel in basso a sinistra e i valori (1.0, 1.0) come corrispondenti al centro del pixel in alto a destra.
Leggiamo a partire dal centro del pixel in basso a sinistra fino al centro del pixel in alto a destra perché non vogliamo che i pixel esterni alla cella corrente influenzino il valore campionato. Vedi più avanti la spiegazione sul filtraggio.
Campionando a queste coordinate specifiche della texture, vediamo che ci troviamo esattamente tra 4 pixel. Quale valore di colore ci restituirà GL per quel punto?

La risposta dipende da come abbiamo specificato il filtraggio del sampler nel materiale.
Se il filtraggio del sampler è NEAREST, GL restituirà il valore del colore del pixel più vicino (con il valore della posizione arrotondato per difetto). Nel caso precedente, GL restituirà il valore del colore alla posizione (0.60, 0.80). Per la nostra texture di ricerca a 4 bit, questo significa che quantizzeremo i valori dei colori in soli 4096 colori complessivi.
Se il filtraggio del sampler è LINEAR, GL restituirà il valore del colore interpolato. GL mescolerà un colore in base alla distanza dai pixel che circondano la posizione di campionamento. Nel caso precedente, GL restituirà un colore composto per il 25% da ciascuno dei 4 pixel intorno al punto di campionamento.
Usando il filtraggio lineare eliminiamo quindi la quantizzazione dei colori e otteniamo un’ottima precisione cromatica da una tabella di ricerca piuttosto piccola.
Implementiamo la ricerca nella texture all’interno del fragment shader:
grade.material.lut” (da lookup table, tabella di ricerca).Imposta la proprietà Filter min su FILTER_MODE_MIN_LINEAR e la proprietà Filter mag su FILTER_MODE_MAG_LINEAR.

Scarica la seguente texture della tabella di ricerca (lut16.png) e aggiungila al tuo progetto.

Apri main.collection e imposta la proprietà della texture lut sulla texture di ricerca scaricata.

Infine, apri grade.fp per aggiungere il supporto alla ricerca dei colori:
varying mediump vec4 position;
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D original;
uniform lowp sampler2D lut; // <1>
#define MAXCOLOR 15.0 // <2>
#define COLORS 16.0
#define WIDTH 256.0
#define HEIGHT 16.0
void main()
{
vec4 px = texture2D(original, var_texcoord0.xy); // <3>
float cell = floor(px.b * MAXCOLOR); // <4>
float half_px_x = 0.5 / WIDTH; // <5>
float half_px_y = 0.5 / HEIGHT;
float x_offset = half_px_x + px.r / COLORS * (MAXCOLOR / COLORS);
float y_offset = half_px_y + px.g * (MAXCOLOR / COLORS); // <6>
vec2 lut_pos = vec2(cell / COLORS + x_offset, y_offset); // <7>
vec4 graded_color = texture2D(lut, lut_pos); // <8>
gl_FragColor = graded_color; // <9>
}
lut.px) dalla texture originale (il buffer dei colori della destinazione di rendering fuori schermo).px.px.Al momento, la texture della tabella di ricerca restituisce gli stessi valori di colore che cerchiamo. Questo significa che il gioco dovrebbe essere renderizzato con i suoi colori originali:

Fin qui sembra che abbiamo fatto tutto correttamente, ma c’è un problema nascosto. Guarda cosa succede quando aggiungiamo uno sprite con una texture di test contenente un gradiente:

Il gradiente blu mostra delle bande davvero sgradevoli. Perché?
Il problema delle bande nel canale blu è che GL non può eseguire alcuna interpolazione di questo canale quando legge il colore dalla texture. Selezioniamo in anticipo una specifica cella da cui leggere in base al valore del blu, e ci fermiamo lì. Per esempio, se il canale blu contiene un valore qualsiasi nell’intervallo 0.400–0.466, il valore preciso non conta: campioneremo sempre il colore finale dalla cella numero 6, dove il canale blu è impostato a 0.400.
Per ottenere una migliore risoluzione del canale blu, possiamo implementare noi stessi l’interpolazione. Se il valore del blu è compreso tra quelli di due celle adiacenti, possiamo campionare da entrambe le celle e poi mescolare i colori. Per esempio, se il valore del blu è 0.420, dovremmo campionare dalla cella numero 6 e dalla cella numero 7, poi mescolare i colori.
Dobbiamo quindi leggere da due celle:
cell_{low} = \left \lfloor{B \times (N - 1)} \right \rfloor
e:
cell_{high} = \left \lceil{B \times (N - 1)} \right \rceil
Poi campioniamo i valori dei colori da ciascuna di queste celle e interpoliamo i colori linearmente, secondo la formula:
color = color_{low} \times (1 - C_{frac}) + color_{high} \times C_{frac}
Qui color~low~ è il colore campionato dalla cella inferiore (più a sinistra) e color~high~ è il colore campionato dalla cella superiore (più a destra). La funzione GLSL mix() esegue questa interpolazione lineare per noi.
Il valore C~frac~ nella formula precedente è la parte frazionaria del valore del canale blu, riscalato nell’intervallo di colori 0–15:
C_{frac} = B \times (N - 1) - \left \lfloor{B \times (N - 1)} \right \rfloor
Anche in questo caso esiste una funzione GLSL che restituisce la parte frazionaria di un valore. Si chiama frac(). L’implementazione finale nel fragment shader (grade.fp) è piuttosto semplice:
varying mediump vec4 position;
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D original;
uniform lowp sampler2D lut;
#define MAXCOLOR 15.0
#define COLORS 16.0
#define WIDTH 256.0
#define HEIGHT 16.0
void main()
{
vec4 px = texture2D(original, var_texcoord0.xy);
float cell = px.b * MAXCOLOR;
float cell_l = floor(cell); // <1>
float cell_h = ceil(cell);
float half_px_x = 0.5 / WIDTH;
float half_px_y = 0.5 / HEIGHT;
float r_offset = half_px_x + px.r / COLORS * (MAXCOLOR / COLORS);
float g_offset = half_px_y + px.g * (MAXCOLOR / COLORS);
vec2 lut_pos_l = vec2(cell_l / COLORS + r_offset, g_offset); // <2>
vec2 lut_pos_h = vec2(cell_h / COLORS + r_offset, g_offset);
vec4 graded_color_l = texture2D(lut, lut_pos_l); // <3>
vec4 graded_color_h = texture2D(lut, lut_pos_h);
// <4>
vec4 graded_color = mix(graded_color_l, graded_color_h, fract(cell));
gl_FragColor = graded_color;
}
cell, che è il valore del blu riscalato.Eseguendo di nuovo il gioco con la texture di test, ora otteniamo risultati molto migliori. Le bande nel canale blu sono scomparse:

Bene, è stato un bel po’ di lavoro per disegnare qualcosa che ha esattamente lo stesso aspetto del mondo di gioco originale. Ma questa configurazione ci permette di fare qualcosa di davvero interessante. Preparati!

lut16.png).lut16.png utilizzata nel tuo progetto Defold con quella dai colori regolati.
Che soddisfazione!