Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Advanced post-processing

Introduzione

This tutorial describes an advanced method for post-processing in Godot. In particular, it will explain how to write a post-processing shader that uses the depth buffer. You should already be familiar with post-processing generally and, in particular, with the methods outlined in the custom post-processing tutorial.

Full screen quad

One way to make custom post-processing effects is by using a viewport. However, there are two main drawbacks of using a Viewport:

  1. The depth buffer cannot be accessed

  2. The effect of the post-processing shader is not visible in the editor

To get around the limitation on using the depth buffer, use a MeshInstance3D with a QuadMesh primitive. This allows us to use a shader and to access the depth texture of the scene. Next, use a vertex shader to make the quad cover the screen at all times so that the post-processing effect will be applied at all times, including in the editor.

First, create a new MeshInstance3D and set its mesh to a QuadMesh. This creates a quad centered at position (0, 0, 0) with a width and height of 1. Set the width and height to 2 and enable Flip Faces. Right now, the quad occupies a position in world space at the origin. However, we want it to move with the camera so that it always covers the entire screen. To do this, we will bypass the coordinate transforms that translate the vertex positions through the difference coordinate spaces and treat the vertices as if they were already in clip space.

The vertex shader expects coordinates to be output in clip space, which are coordinates ranging from -1 at the left and bottom of the screen to 1 at the top and right of the screen. This is why the QuadMesh needs to have height and width of 2. Godot handles the transform from model to view space to clip space behind the scenes, so we need to nullify the effects of Godot's transformations. We do this by setting the POSITION built-in to our desired position. POSITION bypasses the built-in transformations and sets the vertex position in clip space directly.

shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;

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

Nota

In versions of Godot earlier than 4.3, this code recommended using POSITION = vec4(VERTEX, 1.0); which implicitly assumed the clip-space near plane was at 0.0. That code is now incorrect and will not work in versions 4.3+ as we use a "reversed-z" depth buffer now where the near plane is at 1.0.

Even with this vertex shader, the quad keeps disappearing. This is due to frustum culling, which is done on the CPU. Frustum culling uses the camera matrix and the AABBs of Meshes to determine if the Mesh will be visible before passing it to the GPU. The CPU has no knowledge of what we are doing with the vertices, so it assumes the coordinates specified refer to world positions, not clip space positions, which results in Godot culling the quad when we turn away from the center of the scene. In order to keep the quad from being culled, there are a few options:

  1. Add the QuadMesh as a child to the camera, so the camera is always pointed at it

  2. Set the Geometry property extra_cull_margin as large as possible in the QuadMesh

The second option ensures that the quad is visible in the editor, while the first option guarantees that it will still be visible even if the camera moves outside the cull margin. You can also use both options.

Texture di profondità

To read from the depth texture, we first need to create a texture uniform set to the depth buffer by using hint_depth_texture.

uniform sampler2D depth_texture : hint_depth_texture;

Once defined, the depth texture can be read with the texture() function.

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

Nota

Similar to accessing the screen texture, accessing the depth texture is only possible when reading from the current viewport. The depth texture cannot be accessed from another viewport to which you have rendered.

The values returned by depth_texture are between 1.0 and 0.0 (corresponding to the near and far plane, respectively, because of using a "reverse-z" depth buffer) and are nonlinear. When displaying depth directly from the depth_texture, everything will look almost black unless it is very close due to that nonlinearity. In order to make the depth value align with world or model coordinates, we need to linearize the value. When we apply the projection matrix to the vertex position, the z value is made nonlinear, so to linearize it, we multiply it by the inverse of the projection matrix, which in Godot, is accessible with the variable INV_PROJECTION_MATRIX.

Innanzitutto, prendi le coordinate dello spazio dello schermo e trasformale in coordinate normalizzate del dispositivo (NDC). Le NDC vanno da -1.0 a 1.0 nelle direzioni x e y e da 0.0 a 1.0 nella direzione z quando si utilizza il backend Vulkan. Ricostruisci le NDC usando SCREEN_UV per gli assi x e y e il valore di profondità per z.

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

Nota

Questo tutorial presuppone l'uso dei renderer Forward+ o Mobile, che entrambi utilizzano NDC di Vulkan con un intervallo Z di [0.0, 1.0]. In confronto, il renderer Compatibilità utilizza NDC OpenGL con un intervallo Z di [-1.0, 1.0]. Per il renderer Compatibilità, sostituisci il calcolo per le NDC con il seguente:

vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;

È inoltre possibile utilizzare le definizioni integrate CURRENT_RENDERER e RENDERER_COMPATIBILITY per uno shader che funzionerà in tutti i renderer:

#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
#endif

Convertiamo le NDC nello spazio visivo moltiplicandolo per INV_PROJECTION_MATRIX. Ricorda che lo spazio visivo fornisce le posizioni relative alla telecamera, quindi il valore z ci darà la distanza dal punto.

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

Poiché la telecamera è orientata verso la direzione negativa dell'asse z, la posizione avrà un valore z negativo. Per ottenere un valore di profondità utilizzabile, dobbiamo negare view.z.

La posizione mondiale si può derivare dal buffer di profondità tramite il seguente codice, usando la INV_VIEW_MATRIX per trasformare la posizione dallo spazio visivo allo spazio mondiale.

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

Shader di esempio

Una volta aggiunta una riga per l'output su ALBEDO, otteniamo uno shader completo che apparirà più o meno così. Questo shader permette di visualizzare la profondità lineare o le coordinate dello spazio mondiale, a seconda di quale riga viene commentata.

shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;

uniform sampler2D depth_texture : hint_depth_texture;

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

void fragment() {
  float depth = texture(depth_texture, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;

  vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;

  // Visualize linear depth
  ALBEDO.rgb = vec3(fract(linear_depth));

  // Visualize world coordinates
  //ALBEDO.rgb = fract(world_position).xyz;
}

Un'ottimizzazione

È possibile usufruire da un singolo, grande triangolo anziché di un quadrilatero che copre l'intero schermo. Il motivo è spiegato qui. Tuttavia, il vantaggio è piuttosto piccolo e solamente utile quando si eseguono shader di frammenti particolarmente complessi.

Imposta la mesh nel MeshInstance3D su un ArrayMesh. Un ArrayMesh è uno strumento che consente di costruire facilmente una mesh a partire da array per vertici, normali, colori, ecc.

Ora, allega uno script alla MeshInstance3D e usa il seguente codice:

extends MeshInstance3D

func _ready():
  # Create a single triangle out of vertices:
  var verts = PackedVector3Array()
  verts.append(Vector3(-1.0, -1.0, 0.0))
  verts.append(Vector3(3.0, -1.0, 0.0))
  verts.append(Vector3(-1.0, 3.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

Il triangolo è specificato in coordinate normalizzate del dispositivo. Ricordiamo che le coordinate NDC vanno da -1.0 a 1.0 in entrambe le direzioni x e y. Questo rende lo schermo largo 2 unità e alto 2 unità. Per coprire l'intero schermo con un singolo triangolo, utilizza un triangolo largo 4 unità e alto 4 unità, il doppio della sua altezza e larghezza.

Assegna lo stesso shader di vertici qui sopra e tutto dovrebbe avere esattamente lo stesso aspetto.

L'unico svantaggio di usare un ArrayMesh rispetto a un QuadMesh è che l'ArrayMesh non è visibile nell'editor perché il triangolo non viene costruito finché la scena non viene eseguita. Per aggirare questo problema, è possibile costruire una singola mesh triangolare in un programma di modellazione e utilizzarla nel MeshInstance3D al posto dell'ArrayMesh.