Post procesado personalizado

Introducción

Godot proporciona muchos efectos de post-procesamiento fuera del paquete, incluyendo Bloom, DOF, y SSAO. A veces quieres escribir tu propio efecto personalizado. Así es como puedes hacerlo.

Los efectos de post-procesamiento son shaders aplicados a un fotograma después de que Godot lo haya renderizado. Primero quieres renderizar tu escena en un Viewport, luego renderizar el Viewport dentro de un ViewportTexture y mostrarlo en la pantalla.

La forma más fácil de implementar un shader personalizado de post-procesamiento es usar la capacidad incorporada de Godot para leer la textura de la pantalla. Si no estás familiarizado con esto, deberías leer primero el Screen Reading Shaders Tutorial.

Nota

En el momento de escribir este artículo, Godot no soporta la renderización a múltiples buffers al mismo tiempo. Su shader de post-procesamiento no tendrá acceso a los normales u otros pases de renderización. Sólo tiene acceso al fotograma renderizado.

Post procesado de paso simple

Necesitarás un Viewport para renderizar tu escena, y una escena para renderizar tu Viewport en la pantalla. Puedes usar un ViewportContainer para mostrar tu Viewport en toda la pantalla o dentro de otro nodo Control.

Nota

Renderizar usando un Viewport te da control sobre cómo se renderiza la escena, incluyendo la velocidad de fotogramas, y puedes usar el ViewportContainer para renderizar objetos 3D en una escena 2D.

Para esta demostración, usaremos un Node2D con un ViewportContainer y finalmente un Viewport. Tu pestaña Scene debería verse así:

../../_images/post_hierarchy1.png

Dentro del Viewport, puedes tener lo que quieras. Esto contendrá tu escena principal. Para este tutorial, usaremos un campo de cajas aleatorias:

../../_images/post_boxes.png

Añade un nuevo ShaderMaterial al ViewportContainer y asígnale un nuevo recurso shader. Puedes acceder a tu Viewport renderizado con el uniform incorporado TEXTURE.

Nota

Puedes elegir no usar un ViewportContainer, pero si lo haces, necesitarás crear tu propio uniforme en el shader y pasar la textura Viewport manualmente, así:

// Inside the Shader.
uniform sampler2D ViewportTexture;

Y puedes pasar la textura al shader desde el GDScript así:

# In GDScript.
func _ready():
  $Sprite.material.set_shader_param("ViewportTexture", $Viewport.get_texture())

Copia el siguiente código en tu shader. El código anterior es un filtro de detección de bordes de un solo paso, un filtro sobel.

shader_type canvas_item;

void fragment() {
    vec3 col = -8.0 * texture(TEXTURE, UV).xyz;
    col += texture(TEXTURE, UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz;
    col += texture(TEXTURE, UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz;
    col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz;
    col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz;
    col += texture(TEXTURE, UV + SCREEN_PIXEL_SIZE.xy).xyz;
    col += texture(TEXTURE, UV - SCREEN_PIXEL_SIZE.xy).xyz;
    col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y)).xyz;
    col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, -SCREEN_PIXEL_SIZE.y)).xyz;
    COLOR.xyz = col;
}

Nota

El filtro Sobel lee los píxeles en una cuadrícula de 9x9 alrededor del píxel actual y los suma, usando el peso. Lo que lo hace interesante es que asigna pesos a cada píxel; +1 para cada uno de los ocho alrededor del centro y -8 para el píxel del centro. La elección de los pesos se llama "kernel". Puedes usar diferentes kernels para crear filtros de detección de bordes, contornos y todo tipo de efectos.

../../_images/post_outline.png

Post procesado de pasos múltiples

Algunos efectos de post-procesamiento como el difuminado son intensivos en recursos. Sin embargo, si los descompones en múltiples pasadas, puedes hacer que se ejecuten mucho más rápido. En un material de múltiples pasadas, cada pasada toma el resultado de la pasada anterior como entrada y lo procesa.

Para hacer un shader de post-procesamiento multi-paso, apilas los nodos Viewport. En el ejemplo de arriba, has llevado el contenido de un objeto Viewport a la raíz Viewport, a través de un nodo ViewportContainer. Puedes hacer lo mismo para un shader multi-pass renderizando el contenido de un Viewport en otro y luego renderizando el último Viewport en el Viewport raíz.

La escena final debería verse similar a esta:

../../_images/post_hierarchy2.png

Godot hará primero el nodo inferior Viewport. Así que si el orden de los pases es importante para tus shaders, asegúrate de asignar el shader que quieres aplicar primero al ViewportContainer más bajo del árbol.

Nota

También puedes renderizar tus Viewports por separado sin anidarlos así. Solo necesitas usar dos Viewports y renderizarlos uno tras otro.

Aparte de la estructura de nodos, los pasos son los mismos que en el shader de post-procesamiento de una sola pasada.

Como ejemplo, podrías escribir un efecto de desenfoque gaussiano a pantalla completa adjuntando las siguientes piezas de código a cada uno de los ViewportContainers. El orden en que se aplican los shaders no importa:

shader_type canvas_item;

// Blurs the screen in the X-direction.
void fragment() {
    vec3 col = texture(TEXTURE, UV).xyz * 0.16;
    col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
    col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
    col += texture(TEXTURE, UV + vec2(2.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
    col += texture(TEXTURE, UV + vec2(2.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
    col += texture(TEXTURE, UV + vec2(3.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
    col += texture(TEXTURE, UV + vec2(3.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
    col += texture(TEXTURE, UV + vec2(4.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
    col += texture(TEXTURE, UV + vec2(4.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
    COLOR.xyz = col;
}
shader_type canvas_item;

// Blurs the screen in the Y-direction.
void fragment() {
    vec3 col = texture(TEXTURE, UV).xyz * 0.16;
    col += texture(TEXTURE, UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
    col += texture(TEXTURE, UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
    col += texture(TEXTURE, UV + vec2(0.0, 2.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
    col += texture(TEXTURE, UV + vec2(0.0, 2.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
    col += texture(TEXTURE, UV + vec2(0.0, 3.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
    col += texture(TEXTURE, UV + vec2(0.0, 3.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
    col += texture(TEXTURE, UV + vec2(0.0, 4.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
    col += texture(TEXTURE, UV + vec2(0.0, 4.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
    COLOR.xyz = col;
}

Usando el código anterior, deberías terminar con un efecto de desenfoque en pantalla completa como el de abajo.

../../_images/post_blur.png

Para más información sobre cómo funcionan los nodos Viewport, lee Viewports Tutorial.