Shaders de lectura de pantalla

Introducción

A menudo se desea crear un shader que lea desde la misma pantalla en la que está escribiendo. Las API 3D, como OpenGL o DirectX, hacen esto muy difícil debido a las limitaciones internas del hardware. Las GPU son extremadamente paralelas, por lo que leer y escribir causa problemas de caché y coherencia. Como resultado, ni siquiera el hardware más moderno admite esto correctamente.

La solución alternativa es hacer una copia de la pantalla, o una parte de la pantalla, en un búfer de respaldo y luego leer de él mientras dibuja. Godot proporciona algunas herramientas que facilitan este proceso.

SCREEN_TEXTURE textura incorporada

Godot Lenguaje de shading tiene una textura especial, SCREEN_TEXTURE (y DEPTH_TEXTURE para la profundidad, en el caso de 3D). Toma como argumento el UV de la pantalla y devuelve un vec3 RGB con el color. Una variación especial incorporada: SCREEN_UV puede ser usado para obtener el UV del fragmento actual. Como resultado, este simple shader de fragmentos de canvas_item:

void fragment() {
    COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
}

resulta en un objeto invisible, porque sólo muestra lo que hay detrás.

La razón por la que debe usarse textureLod es porque, cuando Godot copia un trozo de la pantalla, también hace un eficiente desenfoque gaussiano separable en sus mipmaps.

Esto permite no sólo leer de la pantalla, sino leer de ella con diferentes cantidades de borrosidad sin costo alguno.

Nota

Los Mipmaps no se generan en GLES2 debido al pobre rendimiento y la compatibilidad con los dispositivos más antiguos.

ejemplo de SCREEN_TEXTURE

SCREEN_TEXTURE puede ser usada para muchas cosas. Hay una demo especial para Screen Spade Shader, que puedes descargar para ver y aprender. Un ejemplo es un simple shader para ajustar el brillo, el contraste y la saturación:

shader_type canvas_item;

uniform float brightness = 1.0;
uniform float contrast = 1.0;
uniform float saturation = 1.0;

void fragment() {
    vec3 c = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0).rgb;

    c.rgb = mix(vec3(0.0), c.rgb, brightness);
    c.rgb = mix(vec3(0.5), c.rgb, contrast);
    c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);

    COLOR.rgb = c;
}

Entre bastidores

Aunque esto parece mágico, no lo es. En 2D, la SCREEN_TEXTURE incorporada, cuando se encuentra por primera vez en un nodo que está a punto de ser dibujado, hace una copia a pantalla completa a un búfer trasero. Los nodos subsiguientes que lo usan en los shaders no tendrán la pantalla copiada para ellos, porque esto termina siendo ineficiente. En 3D, la pantalla se copia después de la pasada de geometría opaca, pero antes de la pasada de geometría transparente, por lo que los objetos transparentes no serán capturados en la SCREEN_TEXTURE.

Como resultado, en 2D, si los shaders que usan SCREEN_TEXTURE se superponen, el segundo no usará el resultado del primero, resultando en visuales inesperados:

../../_images/texscreen_demo1.png

En la imagen de arriba, la segunda esfera (arriba a la derecha) está usando la misma fuente de SCREEN_TEXTURE que la primera de abajo, por lo que la primera "desaparece", o no es visible.

En 2D, esto puede ser corregido a través del nodo BackBufferCopy, que puede ser instanciado entre ambas esferas. BackBufferCopy puede funcionar especificando una región de la pantalla o la pantalla completa:

../../_images/texscreen_bbc.png

Con la copia correcta de la memoria intermedia, las dos esferas se mezclan correctamente:

../../_images/texscreen_demo2.png

En 3D, hay menos flexibilidad para resolver este problema en particular porque la SCREEN_TEXTURE sólo se captura una vez. Tenga cuidado al usar "SCREEN_TEXTURE" en 3D, ya que no capturará objetos transparentes y puede capturar algunos objetos opacos que están en frente del objeto.

Puedes reproducir la lógica del búfer trasero en 3D creando un Viewport con una cámara en la misma posición que tu objeto, y luego usar la textura Viewport's en lugar de SCREEN_TEXTURE.

Lógica del Back-buffer

Así que, para dejarlo más claro, así es como funciona la lógica de copia del backbuffer en Godot:

  • Si un nodo utiliza la SCREEN_TEXTURE, la pantalla entera se copia en el buffer de atrás antes de dibujar ese nodo. Esto sólo ocurre la primera vez; los nodos subsiguientes no lo activan.

  • Si un nodo BackBufferCopy fue procesado antes de la situación en el punto anterior (incluso si no se usó SCREEN_TEXTURE), el comportamiento descrito en el punto anterior no ocurre. En otras palabras, el copiado automático de toda la pantalla sólo ocurre si SCREEN_TEXTURE se usa en un nodo por primera vez y no se encontró antes un nodo BackBufferCopy (no deshabilitado) en el orden del árbol.

  • BackBufferCopy puede copiar la pantalla completa o una región. Si se configura sólo una región (no toda la pantalla) y el shader utiliza píxeles que no están en la región copiada, el resultado de esa lectura es indefinido (lo más probable es que sea basura de fotogramas). En otras palabras, es posible usar BackBufferCopy para copiar una región de la pantalla y luego usar "SCREEN_TEXTURE" en una región diferente. ¡Evita este comportamiento!

DEPTH_TEXTURE

Para los shaders 3D, también es posible acceder al búfer de profundidad de la pantalla. Para esto, se utiliza el valor incorporado DEPTH_TEXTURE. Esta textura no es lineal; debe ser convertida a través de la matriz de proyección inversa.

El siguiente código recupera la posición 3D debajo del píxel que se está dibujando:

void fragment() {
    float depth = textureLod(DEPTH_TEXTURE, SCREEN_UV, 0.0).r;
    vec4 upos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
    vec3 pixel_position = upos.xyz / upos.w;
}