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
En este tutorial vamos a crear un post effect de color grading a pantalla completa. El método básico de renderizado usado se puede aplicar ampliamente a varios tipos de post effects como blur, trails, glow, ajustes de color, etc.
Se asume que sabes moverte por el editor Defold y que tienes una comprensión básica de shaders GL y del rendering pipeline de Defold. Si necesitas leer sobre estos temas, consulta nuestro manual de Shader y el manual de Render.
Con el render script predeterminado, cada componente visual (sprite, tilemap, efecto de partículas, GUI, etc.) se renderiza directamente al frame buffer de la tarjeta gráfica. El hardware hace entonces que los gráficos aparezcan en la pantalla. El dibujo real de los pixels de un componente lo realiza un shader program GL. Defold incluye un shader program predeterminado para cada tipo de componente, que dibuja los datos de pixel en la pantalla sin modificarlos. Normalmente, este es el comportamiento que quieres—tus imágenes deberían aparecer en pantalla tal como fueron concebidas originalmente.
Puedes reemplazar el shader program de un componente por uno que modifique los datos de pixel, o que cree colores de pixel completamente nuevos de forma programática. El tutorial Shadertoy te enseña cómo hacerlo.
Ahora supongamos que quieres renderizar todo tu juego en blanco y negro. Una solución posible es modificar el shader program individual para cada tipo de componente de modo que cada shader desature los colores de pixel. Actualmente, Defold incluye 6 materiales integrados y 6 pares de programas de vertex y fragment shader, así que requerirá bastante trabajo. Además, cualquier cambio posterior o agregado de efectos debe hacerse en cada shader program.
Un enfoque mucho más flexible es hacer el renderizado en dos pasos separados:

Con este método, podemos leer los datos visuales resultantes y modificarlos antes de que lleguen a la pantalla. Al agregar shader programs al paso 2 anterior, podemos lograr fácilmente efectos de pantalla completa. Veamos cómo configurar esto en Defold.
Necesitamos modificar el render script integrado y agregar la nueva funcionalidad de renderizado. El render script predeterminado es un buen punto de partida, así que empieza copiándolo:
Abre grade.render y define su propiedad Script como “/main/grade.render_script”.

Abre game.project y define Render como “/main/grade.render”.

Ahora el juego está configurado para ejecutarse con un nuevo render pipeline que podemos modificar. Para probar que el motor usa nuestra copia del render script, ejecuta el juego, luego haz una modificación en el render script que dé un resultado visual y recarga el script. Por ejemplo, puedes deshabilitar el dibujo de tiles y sprites, luego presionar ⌘ + R para hacer hot-reload del render script “roto” en el juego en ejecución:
...
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()
...
Si los sprites y tiles desaparecen con esta prueba simple, sabes que el juego ejecuta tu render script. Si todo funciona como se espera, puedes deshacer el cambio al render script.
Ahora, modifiquemos el render script para que dibuje al render target fuera de pantalla en lugar del frame buffer. Primero necesitamos crear el 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_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", 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
Ahora solo necesitamos envolver el código de renderizado original con render.set_render_target() así:
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() dibujará en los buffers de nuestro render target fuera de pantalla.update() se deja tal cual, salvo el viewport, que se define a la resolución del render target.Eso es todo lo que necesitamos hacer. Si ejecutas el juego ahora, dibujará todo al render target. Pero como ahora no dibujamos nada al frame-buffer, solo veremos una pantalla negra.
Para dibujar los pixels del buffer de color del render target en la pantalla, necesitamos configurar algo que podamos texturizar con los datos de pixel. Para ese propósito vamos a usar un modelo 3D plano y cuadrático.
main.collection y crea un nuevo objeto de juego llamado “grade”.grade”.quad.gltf que se encuentra en builtins/assets/meshes.Deja el objeto de juego sin escalar en el origen. Más tarde, cuando rendericemos el quad, lo proyectaremos para que llene toda la pantalla. Pero primero necesitamos un material y shader programs para el quad:
grade.material haciendo click derecho en main en la vista Asset y seleccionando New ▸ Material.grade.vp y un fragment shader program llamado grade.fp haciendo click derecho en main en la vista Asset y seleccionando New ▸ Vertex program y New ▸ Fragment program.view_proj” de tipo CONSTANT_TYPE_VIEWPROJ. Esta es la matriz de vista y proyección usada en el vertex program para los vértices del quad.original”. Se usará para muestrear pixels desde el buffer de color del render target fuera de pantalla.Agrega un Tag llamado “grade”. Crearemos un nuevo render predicate en el render script que coincida con este tag para dibujar el quad.

Abre main.collection, selecciona el componente model en el objeto de juego “grade” y define su propiedad Material como “/main/grade.material”.

El vertex shader program se puede dejar como se crea desde la plantilla base:
// grade.vp
uniform mediump mat4 view_proj;
// las posiciones están en espacio del mundo
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;
}
En el fragment shader program, en lugar de definir gl_FragColor directamente al valor de color muestreado, hagamos una manipulación de color simple. Hacemos esto principalmente para asegurarnos de que todo funcione como se espera hasta ahora:
// grade.fp
varying mediump vec4 position;
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D original;
void main()
{
vec4 color = texture2D(original, var_texcoord0.xy);
// Desatura el color muestreado desde la textura original
float grey = color.r * 0.3 + color.g * 0.59 + color.b * 0.11;
gl_FragColor = vec4(grey, grey, grey, 1.0);
}
Ahora tenemos el modelo quad en su lugar con su material y shaders. Solo tenemos que dibujarlo al frame buffer de la pantalla.
Necesitamos agregar un render predicate al render script para poder dibujar el modelo quad. Abre grade.render_script y edita la función 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.Después de que el buffer de color del render target se haya llenado en update(), configuramos una vista y una proyección que hacen que el modelo quad llene toda la pantalla. Luego usamos el buffer de color del render target como textura del 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() afecta al render target, no al frame buffer de la pantalla.grade.material, así que el fragment shader muestreará desde el render target.grade.material, que define ese tag—por lo tanto, el quad se dibujará.Ahora ejecutemos el juego y veamos el resultado:

Los colores se expresan como valores de tres componentes donde cada componente dicta la cantidad de rojo, verde o azul de la que consta un color. El espectro completo de color desde negro, pasando por rojo, verde, azul, amarillo y rosa hasta blanco puede encajar en una forma de cubo:

Cualquier color que se pueda mostrar en pantalla puede encontrarse en este cubo de color. La idea básica del color grading es usar un cubo de color así, pero con colores alterados, como una lookup table 3D.
Para cada pixel:
Podemos hacer esto en nuestro fragment shader:

Open GL ES 2.0 no soporta texturas 3D, así que necesitamos encontrar otra forma de representar el cubo de color 3D. Una forma común de hacerlo es cortar el cubo a lo largo del eje Z (azul) y poner cada corte lado a lado en una cuadrícula bidimensional. Cada uno de los 16 cortes contiene una cuadrícula de 16⨉16 pixels. Almacenamos esto en una textura que podemos leer desde el fragment shader con un sampler:

La textura resultante contiene 16 celdas (una por cada intensidad de color azul) y dentro de cada celda 16 colores rojos a lo largo del eje X y 16 colores verdes a lo largo del eje Y. La textura representa todo el espacio de color RGB de 16 millones de colores en solo 4096 colores—apenas 4 bits de profundidad de color. Según la mayoría de los estándares esto es malo, pero gracias a una funcionalidad del hardware gráfico GL podemos recuperar una precisión de color muy alta. Veamos cómo.
Buscar un color consiste en comprobar el componente azul y averiguar qué celda elegir para los valores rojo y verde. La fórmula para encontrar la celda con el conjunto rojo-verde correcto es simple:
cell = \left \lfloor{B \times (N - 1)} \right \rfloor
Aquí B es el valor del componente azul entre 0 y 1 y N es el número total de celdas. En nuestro caso, el número de celda estará en el rango 0–15, donde la celda 0 contiene todos los colores con el componente azul en 0 y la celda 15 todos los colores con el componente azul en 1.
Por ejemplo, el valor RGB (0.63, 0.83, 0.4) se encuentra en la celda que contiene todos los colores con un valor azul de 0.4, que es la celda número 6. Sabiendo eso, la búsqueda de las coordenadas de textura finales según los valores verde y rojo es directa:

Ten en cuenta que necesitamos tratar los valores rojo y verde (0, 0) como si estuvieran en el centro del pixel inferior izquierdo y los valores (1.0, 1.0) como si estuvieran en el centro del pixel superior derecho.
La razón por la que leemos empezando en el centro del pixel inferior izquierdo y hasta el centro del pixel superior derecho es que no queremos que ningún pixel fuera de la celda actual afecte el valor muestreado. Consulta más abajo sobre filtering.
Al muestrear en estas coordenadas específicas de la textura, vemos que terminamos justo entre 4 pixels. Entonces, ¿qué valor de color nos dirá GL que tiene ese punto?

La respuesta depende de cómo hayamos especificado el filtering del sampler en el material.
Si el filtering del sampler es NEAREST, GL devolverá el valor de color del pixel más cercano (valor de posición redondeado hacia abajo). En el caso anterior, GL devolverá el valor de color en la posición (0.60, 0.80). Para nuestra textura de lookup de 4 bits, significa que cuantizaremos los valores de color en solo 4096 colores en total.
Si el filtering del sampler es LINEAR, GL devolverá el valor de color interpolado. GL mezclará un color según la distancia a los pixels alrededor de la posición de muestreo. En el caso anterior, GL devolverá un color que es 25% de cada uno de los 4 pixels alrededor del punto de muestreo.
Al usar filtering lineal eliminamos así la cuantización de color y obtenemos muy buena precisión de color a partir de una lookup table bastante pequeña.
Implementemos la búsqueda de textura en el fragment shader:
grade.material.lut” (por lookup table).Define la propiedad Filter min como FILTER_MODE_MIN_LINEAR y la propiedad Filter mag como FILTER_MODE_MAG_LINEAR.

Descarga la siguiente textura de lookup table (lut16.png) y agrégala a tu proyecto.

Abre main.collection y define la propiedad de textura lut con la textura de lookup descargada.

Finalmente, abre grade.fp para que podamos agregar soporte para la búsqueda de color:
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) desde la textura original (el buffer de color del render target fuera de pantalla).px.px.Actualmente, la textura de lookup table solo devuelve los mismos valores de color que buscamos. Esto significa que el juego debería renderizarse con su coloración original:

Hasta ahora parece que hicimos todo bien, pero hay un problema oculto bajo la superficie. Mira qué ocurre cuando agregamos un sprite con una textura de prueba en gradiente:

El gradiente azul muestra unas bandas realmente feas. ¿Por qué ocurre eso?
El problema con las bandas en el canal azul es que GL no puede realizar ninguna interpolación del canal azul al leer el color desde la textura. Preseleccionamos una celda particular para leer según el valor de color azul, y eso es todo. Por ejemplo, si el canal azul contiene un valor en cualquier punto del rango 0.400–0.466, el valor no importa—siempre muestrearemos el color final desde la celda número 6, donde el canal azul está definido en 0.400.
Para obtener mejor resolución en el canal azul, podemos implementar nosotros mismos la interpolación. Si el valor azul está entre el valor de dos celdas adyacentes, podemos muestrear desde ambas celdas y luego mezclar los colores. Por ejemplo, si el valor azul es 0.420, deberíamos muestrear desde la celda número 6 y desde la celda número 7 y luego mezclar los colores.
Entonces, deberíamos leer desde dos celdas:
cell_{low} = \left \lfloor{B \times (N - 1)} \right \rfloor
y:
cell_{high} = \left \lceil{B \times (N - 1)} \right \rceil
Luego muestreamos valores de color desde cada una de estas celdas e interpolamos los colores linealmente, según la fórmula:
color = color_{low} \times (1 - C_{frac}) + color_{high} \times C_{frac}
Aquí color~low~ es el color muestreado desde la celda inferior (más a la izquierda) y color~high~ es el color muestreado desde la celda superior (más a la derecha). La función GLSL mix() realiza esta interpolación lineal por nosotros.
El valor C~frac~ anterior es la parte fraccionaria del valor del canal azul escalado al rango de color 0–15:
C_{frac} = B \times (N - 1) - \left \lfloor{B \times (N - 1)} \right \rfloor
De nuevo, hay una función GLSL que nos da la parte fraccionaria de un valor. Se llama frac(). La implementación final en el fragment shader (grade.fp) es bastante directa:
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 es el valor de color azul escalado.Ejecutar el juego otra vez con la textura de prueba ahora produce resultados mucho mejores. Las bandas en el canal azul desaparecieron:

Bien, fue mucho trabajo para dibujar algo que se ve exactamente como el mundo de juego original. Pero esta configuración nos permite hacer algo realmente genial. ¡Espera!

lut16.png).lut16.png usada en tu proyecto Defold con la ajustada en color.
¡Qué alegría!