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
Neste tutorial, vamos criar um efeito de pós-processamento de color grading em tela cheia. O método básico de renderização usado é amplamente aplicável a vários tipos de pós-efeitos, como blur, trails, glow, ajustes de cor e assim por diante.
Assumimos que você sabe se orientar no editor Defold e que tem entendimento básico de shaders GL e do pipeline de renderização do Defold. Se precisar estudar esses assuntos, confira nosso manual de Shader e o manual de Render.
Com o script de renderização padrão, cada componente visual (sprite, tilemap, efeito de partículas, GUI etc.) é renderizado diretamente no frame buffer da placa gráfica. O hardware então faz os gráficos aparecerem na tela. O desenho real dos pixels de um componente é feito por um shader program GL. O Defold vem com um shader program padrão para cada tipo de componente, que desenha os dados de pixel na tela sem alterações. Normalmente, esse é o comportamento desejado—suas imagens devem aparecer na tela como foram originalmente concebidas.
Você pode substituir o shader program de um componente por outro que modifica os dados de pixel ou cria cores de pixel totalmente novas de forma programática. O tutorial Shadertoy ensina como fazer isso.
Agora imagine que você quer renderizar todo o seu jogo em preto e branco. Uma solução possível é modificar o shader program individual para cada tipo de componente, para que cada shader dessature as cores dos pixels. Atualmente, o Defold vem com 6 materiais integrados e 6 pares de programas de vertex e fragment shader, então isso daria uma boa quantidade de trabalho. Além disso, quaisquer alterações ou adições de efeito posteriores teriam que ser feitas em cada shader program.
Uma abordagem muito mais flexível é fazer a renderização em duas etapas separadas:

Com esse método, conseguimos ler os dados visuais resultantes e modificá-los antes que cheguem à tela. Ao adicionar shader programs à etapa 2 acima, podemos obter efeitos de tela cheia com facilidade. Vamos ver como configurar isso no Defold.
Precisamos modificar o script de renderização integrado e adicionar a nova funcionalidade de renderização. O script de renderização padrão é um bom ponto de partida, então comece copiando-o:
Abra grade.render e defina sua propriedade Script como “/main/grade.render_script”.

Abra game.project e defina Render como “/main/grade.render”.

Agora o jogo está configurado para rodar com um novo pipeline de renderização que podemos modificar. Para testar se nossa cópia do script de renderização é usada pela engine, execute o jogo, faça uma modificação no script de renderização que gere um resultado visual e então recarregue o script. Por exemplo, você pode desabilitar o desenho de tiles e sprites e então pressionar ⌘ + R para fazer hot reload do script de renderização “quebrado” no jogo em execução:
...
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 os sprites e tiles desaparecerem com esse teste simples, você sabe que o jogo está usando seu script de renderização. Se tudo funcionar como esperado, pode desfazer a alteração no script de renderização.
Agora vamos modificar o script de renderização para que ele desenhe no render target fora da tela em vez do frame buffer. Primeiro precisamos criar o render target:
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, 0)
self.clear_color.x = sys.get_config("render.clear_color_red", 0)
self.clear_color.y = sys.get_config("render.clear_color_green", 0)
self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)
self.view = vmath.matrix4()
local color_params = { format = render.FORMAT_RGBA,
width = render.get_width(),
height = render.get_height() } -- <1>
local target_params = {[render.BUFFER_COLOR_BIT] = color_params }
self.target = render.render_target("original", target_params) -- <2>
end
Agora só precisamos envolver o código de renderização original com render.set_render_target() assim:
function update(self)
render.set_render_target(self.target) -- <1>
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear({[render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_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() desenhará nos buffers do nosso render target fora da tela.update() é mantido como está, exceto pelo viewport, que é definido para a resolução do render target.Isso é tudo de que precisamos. Se você executar o jogo agora, ele desenhará tudo no render target. Mas, como agora não desenhamos nada no frame-buffer, veremos apenas uma tela preta.
Para desenhar os pixels do color buffer do render target na tela, precisamos configurar algo que possamos texturizar com esses dados de pixel. Para isso, usaremos um modelo 3D plano e quadrado.
main.collection e crie um novo objeto de jogo chamado “grade”.grade”.quad.gltf encontrado em builtins/assets/meshes.Deixe o objeto de jogo sem escala e na origem. Mais tarde, ao renderizar o quad, vamos projetá-lo para que preencha a tela inteira. Mas primeiro precisamos de um material e de shader programs para o quad:
grade.material clicando com o botão direito em main na visualização Asset e selecionando New ▸ Material.grade.vp e um programa de fragment shader chamado grade.fp clicando com o botão direito em main na visualização Asset e selecionando New ▸ Vertex program e New ▸ Fragment program.view_proj” do tipo CONSTANT_TYPE_VIEWPROJ. Essa é a matriz de view e projeção usada no vertex program para os vértices do quad.original”. Ele será usado para amostrar pixels do color buffer do render target fora da tela.Adicione uma Tag chamada “grade”. Criaremos um novo render predicate no script de renderização que corresponda a essa tag para desenhar o quad.

Abra main.collection, selecione o componente de modelo no objeto de jogo “grade” e defina sua propriedade Material como “/main/grade.material”.

O programa de vertex shader pode ser deixado como foi criado a partir do template 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;
}
No programa de fragment shader, em vez de definir gl_FragColor diretamente para o valor de cor amostrado, vamos realizar uma manipulação de cor simples. Fazemos isso principalmente para garantir que tudo funcione como esperado até aqui:
// grade.fp
varying mediump vec4 position;
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D original;
void main()
{
vec4 color = texture2D(original, var_texcoord0.xy);
// Dessatura a cor amostrada da textura original
float grey = color.r * 0.3 + color.g * 0.59 + color.b * 0.11;
gl_FragColor = vec4(grey, grey, grey, 1.0);
}
Agora temos o modelo quad no lugar com seu material e shaders. Só precisamos desenhá-lo no frame buffer da tela.
Precisamos adicionar um render predicate ao script de renderização para podermos desenhar o modelo quad. Abra grade.render_script e edite a função 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.Depois que o color buffer do render target for preenchido em update(), configuramos uma view e uma projeção que fazem o modelo quad preencher a tela inteira. Então usamos o color buffer do render target como textura do quad:
function update(self)
render.set_render_target(self.target)
...
render.set_render_target(render.RENDER_TARGET_DEFAULT)
render.clear({[render.BUFFER_COLOR_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, render.BUFFER_COLOR_BIT) -- <4>
render.draw(self.grade_pred) -- <5>
render.disable_texture(0, self.target) -- <6>
end
render.clear() afeta o render target, não o frame buffer da tela.grade.material, então o fragment shader amostrará a partir do render target.grade.material, que define essa tag—assim, o quad será desenhado.Agora vamos executar o jogo e ver o resultado:

Cores são expressas como valores de três componentes, em que cada componente determina a quantidade de vermelho, verde ou azul de uma cor. O espectro completo de cores, do preto, passando por vermelho, verde, azul, amarelo e rosa até o branco, pode caber em uma forma de cubo:

Qualquer cor que pode ser exibida na tela pode ser encontrada nesse cubo de cores. A ideia básica do color grading é usar esse cubo de cores, mas com cores alteradas, como uma lookup table 3D.
Para cada pixel:
Podemos fazer isso em nosso fragment shader:

Open GL ES 2.0 não suporta texturas 3D, então precisamos encontrar outra forma de representar o cubo de cores 3D. Uma forma comum de fazer isso é fatiar o cubo ao longo do eixo Z (azul) e colocar cada fatia lado a lado em uma grade bidimensional. Cada uma das 16 fatias contém uma grade de 16⨉16 pixels. Armazenamos isso em uma textura que podemos ler no fragment shader com um sampler:

A textura resultante contém 16 células (uma para cada intensidade de cor azul) e, dentro de cada célula, 16 cores vermelhas ao longo do eixo X e 16 cores verdes ao longo do eixo Y. A textura representa todo o espaço de cores RGB de 16 milhões de cores em apenas 4096 cores—meros 4 bits de profundidade de cor. Pela maioria dos padrões, isso é ruim, mas graças a um recurso do hardware gráfico GL podemos recuperar uma precisão de cor muito alta. Vamos ver como.
Procurar uma cor é uma questão de verificar o componente azul e descobrir de qual célula pegar os valores vermelho e verde. A fórmula para encontrar a célula com o conjunto correto de cores vermelho-verde é simples:
cell = \left \lfloor{B \times (N - 1)} \right \rfloor
Aqui, B é o valor do componente azul entre 0 e 1, e N é o número total de células. No nosso caso, o número da célula ficará no intervalo 0–15, em que a célula 0 contém todas as cores com o componente azul em 0 e a célula 15 contém todas as cores com o componente azul em 1.
Por exemplo, o valor RGB (0.63, 0.83, 0.4) é encontrado na célula que contém todas as cores com valor azul de 0.4, que é a célula número 6. Sabendo disso, a busca das coordenadas finais de textura com base nos valores verde e vermelho é direta:

Observe que precisamos tratar os valores vermelho e verde (0, 0) como estando no centro do pixel inferior esquerdo, e os valores (1.0, 1.0) como estando no centro do pixel superior direito.
O motivo de lermos começando no centro do pixel inferior esquerdo e indo até o centro do pixel superior direito é que não queremos que pixels fora da célula atual afetem o valor amostrado. Veja abaixo sobre filtragem.
Ao amostrar nessas coordenadas específicas na textura, vemos que acabamos exatamente entre 4 pixels. Então que valor de cor o GL nos dirá que esse ponto tem?

A resposta depende de como especificamos a filtering do sampler no material.
Se a filtragem do sampler for NEAREST, o GL retornará o valor de cor do pixel mais próximo (valor de posição arredondado para baixo). No caso acima, o GL retornará o valor de cor na posição (0.60, 0.80). Para nossa textura de lookup de 4 bits, isso significa que quantizaremos os valores de cor em apenas 4096 cores no total.
Se a filtragem do sampler for LINEAR, o GL retornará o valor de cor interpolado. O GL misturará uma cor com base na distância até os pixels ao redor da posição de amostra. No caso acima, o GL retornará uma cor composta por 25% de cada um dos 4 pixels ao redor do ponto de amostra.
Assim, ao usar filtragem linear, eliminamos a quantização de cores e obtemos precisão de cor muito boa a partir de uma lookup table relativamente pequena.
Vamos implementar a busca na textura no fragment shader:
grade.material.lut” (de lookup table).Defina a propriedade Filter min como FILTER_MODE_MIN_LINEAR e a propriedade Filter mag como FILTER_MODE_MAG_LINEAR.

Baixe a textura de lookup table a seguir (lut16.png) e adicione-a ao seu projeto.

Abra main.collection e defina a propriedade de textura lut para a textura de lookup baixada.

Por fim, abra grade.fp para adicionarmos suporte a busca de cor:
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) da textura original (o color buffer do render target fora da tela).px.px.Atualmente, a textura de lookup table apenas retorna os mesmos valores de cor que procuramos. Isso significa que o jogo deve ser renderizado com sua coloração original:

Até aqui, parece que fizemos tudo certo, mas há um problema escondido sob a superfície. Veja o que acontece quando adicionamos um sprite com uma textura de teste em gradiente:

O gradiente azul mostra um banding bem feio. Por quê?
O problema do banding no canal azul é que o GL não consegue realizar nenhuma interpolação do canal azul ao ler a cor da textura. Pré-selecionamos uma célula específica para ler com base no valor da cor azul, e pronto. Por exemplo, se o canal azul contiver um valor em qualquer ponto do intervalo 0.400–0.466, o valor não importa—sempre amostraremos a cor final da célula número 6, onde o canal azul está definido como 0.400.
Para obter melhor resolução no canal azul, podemos implementar a interpolação nós mesmos. Se o valor azul estiver entre os valores de duas células adjacentes, podemos amostrar de ambas e então misturar as cores. Por exemplo, se o valor azul for 0.420, devemos amostrar da célula número 6 e da célula número 7, e então misturar as cores.
Então, devemos ler de duas células:
cell_{low} = \left \lfloor{B \times (N - 1)} \right \rfloor
e:
cell_{high} = \left \lceil{B \times (N - 1)} \right \rceil
Depois amostramos valores de cor de cada uma dessas células e interpolamos as cores linearmente, de acordo com a fórmula:
color = color_{low} \times (1 - C_{frac}) + color_{high} \times C_{frac}
Aqui color~low~ é a cor amostrada da célula menor (mais à esquerda) e color~high~ é a cor amostrada da célula maior (mais à direita). A função GLSL mix() realiza essa interpolação linear para nós.
O valor C~frac~ acima é a parte fracionária do valor do canal azul escalado para o intervalo de cores 0–15:
C_{frac} = B \times (N - 1) - \left \lfloor{B \times (N - 1)} \right \rfloor
Novamente, há uma função GLSL que nos dá a parte fracionária de um valor. Ela se chama frac(). A implementação final no fragment shader (grade.fp) é bem direta:
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, que é o valor da cor azul escalado.Executar o jogo novamente com a textura de teste agora produz resultados muito melhores. O banding no canal azul desapareceu:

Certo, foi muito trabalho para desenhar algo que parece exatamente igual ao mundo original do jogo. Mas essa configuração nos permite fazer algo muito interessante. Aguente firme!

lut16.png).lut16.png usada no seu projeto Defold pela versão com ajustes de cor.
Pronto!