Post procesamiento avanzado

Introducción

Este tutorial describe un método avanzado para el post-procesamiento en Godot. En particular, explicará cómo escribir un shader de post-procesamiento que utilice el buffer de profundidad. Ya deberías estar familiarizado con el post-procesamiento en general y, en particular, con los métodos descritos en el tutorial de post-procesamiento personalizado.

En el anterior tutorial de post-procesamiento, renderizamos la escena a Viewport y luego renderizamos el Viewport en un ViewportContainer a la escena principal. Una limitación de este método es que no podemos acceder al buffer de profundidad porque el buffer de profundidad sólo está disponible en los shaders espaciales y los Viewports no mantienen la información de profundidad.

Quad de pantalla completa

En el tutorial custom post-processing, cubrimos cómo usar un Viewport para hacer efectos de post-procesamiento personalizados. Hay dos inconvenientes principales de usar un Viewport:

  1. No se puede acceder al buffer de profundidad

  2. El efecto del shader de post-procesamiento no es visible en el editor

Para evitar la limitación del uso del buffer de profundidad, usa un MeshInstance con un QuadMesh primitivo. Esto nos permite usar un shader espacial y acceder a la textura de profundidad de la escena. A continuación, utilizar un vertex shader para hacer que el quad cubra la pantalla en todo momento para que el efecto de post-procesamiento se aplique en todo momento, incluso en el editor.

Primero, crear una nueva MeshInstance y establecer su malla en una QuadMesh. Esto crea un quad centrado en la posición (0, 0, 0) con una anchura y altura de 1. Poner el ancho y la altura en 2. Ahora mismo, el quah ocupa una posición en el espacio mundial en el origen; sin embargo, queremos que se mueva con la cámara para que siempre cubra toda la pantalla. Para ello, pasaremos por alto las transformaciones de coordenadas que traducen las posiciones de los vértices a través de los espacios de coordenadas de diferencia y trataremos los vértices como si ya estuvieran en el espacio recortado.

El sombreador de vértices espera que se emitan coordenadas en el espacio de los clips, que son coordenadas que van desde 1 a la izquierda y la parte inferior de la pantalla hasta 1 en la parte superior y la derecha de la pantalla. Por eso la malla cuádruple necesita tener una altura y un ancho de 2. Godot maneja la transformación de modelo a espacio de vista para recortar el espacio detrás de las escenas, por lo que necesitamos anular los efectos de las transformaciones de Godot. Lo hacemos poniendo la POSITION incorporada en nuestra posición deseada. La POSITION evita las transformaciones incorporadas y establece la posición del vértice directamente.

shader_type spatial;

void vertex() {
  POSITION = vec4(VERTEX, 1.0);
}

Incluso con este shader de vértices, el quad sigue desapareciendo. Esto se debe a la recolección de frustum, que se hace en la CPU. El Frustum culling utiliza la matriz de la cámara y el AABB de las mallas para determinar si la malla será visible antes de pasarla a la GPU. La CPU no tiene conocimiento de lo que estamos haciendo con los vértices, por lo que asume que las coordenadas especificadas se refieren a las posiciones del mundo, no a las posiciones del espacio de recorte, lo que resulta en la selección de Godot en el quad cuando nos alejamos del centro de la escena. Para evitar que el quad sea eliminado, hay algunas opciones:

  1. Añade la QuadMesh como un hijo a la cámara, para que la cámara siempre esté apuntando a ella

  2. Establecer la propiedad de Geometría "extra_cull_margin" tan grande como sea posible en el QuadMesh

La segunda opción asegura que el quad sea visible en el editor, mientras que la primera garantiza que seguirá siendo visible incluso si la cámara se mueve fuera del margen de selección. También puedes usar ambas opciones.

Depth texture

Para leer de la textura de profundidad, realice una búsqueda de textura usando texture() y la variable uniforme DEPTH_TEXTURE.

float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;

Nota

De manera similar al acceso a la textura de la pantalla, el acceso a la textura de profundidad sólo es posible cuando se lee desde el puerto de visualización actual. No se puede acceder a la textura de profundidad desde otra ventana de visualización en la que se haya renderizado.

Los valores devueltos por DEPTH_TEXTURE están entre 0 y 1 y son nolineales. Cuando se muestra la profundidad directamente desde la DEPTH_TEXTURE, todo se verá casi blanco a menos que esté muy cerca. Esto se debe a que la memoria intermedia de profundidad almacena los objetos más cercanos a la cámara usando más bits que los que están más lejos, por lo que la mayor parte de los detalles de la memoria intermedia de profundidad se encuentran cerca de la cámara. Para hacer que el valor de profundidad se alinee con las coordenadas del mundo o del modelo, necesitamos linealizar el valor. Cuando aplicamos la matriz de proyección a la posición del vértice, el valor z se hace no lineal, así que para linealizarlo, lo multiplicamos por el inverso de la matriz de proyección, que en Godot, es accesible con la variable INV_PROJECTION_MATRIX.

En primer lugar, tomar las coordenadas del espacio de la pantalla y transformarlas en coordenadas normalizadas del dispositivo (NDC). Las NDC van de -1 a 1, similares a las coordenadas del espacio de recorte. Reconstruye el NDC usando SCREEN_UV para el eje x y y, y el valor de profundidad para z.

void fragment() {
  float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
}

En primer lugar, tomar las coordenadas del espacio de la pantalla y transformarlas en coordenadas normalizadas del dispositivo (NDC). Las NDC van de -1 a 1, similares a las coordenadas del espacio de recorte. Reconstruye el NDC usando SCREEN_UV para el eje x y y, y el valor de profundidad para z.

void fragment() {
  ...
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;
}

Debido a que la cámara está orientada hacia la dirección z negativa, la posición tendrá un valor z negativo. Para obtener un valor de profundidad utilizable, tenemos que negar view.z.

La posición mundial puede construirse a partir del buffer de profundidad usando el siguiente código. Tenga en cuenta que la CAMERA_MATRIX es necesaria para transformar la posición del espacio visual en el espacio mundial, por lo que debe ser pasada al shader de fragmentos con una variación.

varying mat4 CAMERA;

void vertex() {
  CAMERA = CAMERA_MATRIX;
}

void fragment() {
  ...
  vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;
}

Una optimización

Puedes beneficiarte de usar un solo triángulo grande en lugar de usar un cuadrángulo de pantalla completa. La razón de esto se explica aquí. Sin embargo, el beneficio es bastante pequeño y sólo beneficioso cuando se ejecutan shaders de fragmentos especialmente complejos.

Coloca una malla dentro de MeshInstance ArrayMesh. Un ArrayMesh es una herramienta que permite construir fácilmente un Array de Mallas para vértices, normales, colores, etc.

Ahora anexa un script a la MeshInstance y usa el siguiente código:

extends MeshInstance

func _ready():
  # Create a single triangle out of vertices:
  var verts = PoolVector3Array()
  verts.append(Vector3(-1.0, -1.0, 0.0))
  verts.append(Vector3(-1.0, 3.0, 0.0))
  verts.append(Vector3(3.0, -1.0, 0.0))

  # Create an array of arrays.
  # This could contain normals, colors, UVs, etc.
  var mesh_array = []
  mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
  mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array

  # Create mesh from mesh_array:
  mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)

Nota

El triángulo se especifica en las coordenadas normalizadas del dispositivo. Recuerda, NDC va de -1 a 1 en ambas direcciones, x y y. Esto hace que la pantalla tenga 2 unidades de ancho y 2 unidades de alto. Para cubrir toda la pantalla con un solo triángulo, usa un triángulo de 4 unidades de ancho y 4 unidades de alto, el doble de su altura y anchura.

Asigne el mismo shader de vértice desde arriba y todo debería verse exactamente igual.

El único inconveniente de usar un ArrayMesh en vez de usar un QuadMesh es que el ArrayMesh no es visible en el editor porque el triángulo no se construye hasta que se ejecuta la escena. Para evitarlo, construye una malla de un solo triángulo en un programa de modelado y úsala en MeshInstance.