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.

Pós-processamento avançado

Introdução

Este tutorial descreve um método avançado para pós-processamento no Godot. Em particular, ele explicará como escrever um shader de pós-processamento que usa o buffer de profundidade (depth buffer). Você já deve estar familiarizado com pós-processamento em geral e, em particular, com os métodos descritos no custom post-processing tutorial.

Quad em tela cheia

Uma maneira de criar efeitos de pós-processamento personalizados é usando uma viewport. No entanto, existem duas desvantagens principais ao usar uma Viewport:

  1. O buffer de profundidade não pode ser acessado

  2. O efeito do shader de pós-processamento não é visível no editor

Para contornar a limitação do uso do buffer de profundidade, use um MeshInstance3D com uma primitiva QuadMesh. Isso nos permite usar um shader e acessar a textura de profundidade da cena. Em seguida, use um vertex shader para fazer com que o quad cubra a tela o tempo todo, de modo que o efeito de pós-processamento seja aplicado constantemente, inclusive no editor.

Primeiro, crie um novo MeshInstance3D e defina sua malha (mesh) para um QuadMesh. Isso cria um quad centrado na posição (0, 0, 0) com largura e altura de 1. Defina a largura e a altura para 2 e ative a opção Flip Faces. No momento, o quad ocupa uma posição no espaço do mundo na origem. No entanto, queremos que ele se mova com a câmera para que sempre cubra a tela inteira. Para fazer isso, vamos ignorar as transformações de coordenadas que traduzem as posições dos vértices pelos diferentes espaços de coordenadas e tratar os vértices como se já estivessem no espaço de clipe (clip space).

O vertex shader espera que as coordenadas sejam geradas no espaço de clipe, que são coordenadas que variam de -1 na parte esquerda e inferior da tela até 1 na parte superior e direita da tela. É por isso que o QuadMesh precisa ter altura e largura de 2. O Godot cuida da transformação do espaço do modelo para o espaço de visualização e depois para o espaço de clipe nos bastidores, então precisamos anular os efeitos das transformações do Godot. Fazemos isso definindo a variável integrada POSITION para a posição desejada. A POSITION ignora as transformações integradas e define a posição do vértice diretamente no espaço de clipe.

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

Em versões do Godot anteriores à 4.3, este código recomendava usar POSITION = vec4(VERTEX, 1.0);, o que assumia implicitamente que o plano próximo (near plane) do espaço de clipe estava em 0.0. Esse código agora está incorreto e não funcionará nas versões 4.3+, pois agora usamos um buffer de profundidade com "z invertido" (reversed-z), onde o plano próximo fica em 1.0.

Mesmo com esse vertex shader, o quad continua desaparecendo. Isso se deve ao frustum culling, que é feito na CPU. O frustum culling usa a matriz da câmera e as AABBs das malhas para determinar se a malha estará visível antes de passá-la para a GPU. A CPU não tem conhecimento do que estamos fazendo com los vértices, então ela assume que as coordenadas especificadas se referem a posições no mundo, não a posições no espaço de clipe, o que faz com que o Godot descarte (cull) o quad quando olhamos para longe do centro da cena. Para evitar que o quad seja descartado, existem algumas opções:

  1. Adicionar o QuadMesh como um filho da câmera, de modo que a câmera esteja sempre apontada para ele

  2. Definir a propriedade de geometria extra_cull_margin o maior possível no QuadMesh

A segunda opção garante que o quad seja visível no editor, enquanto a primeira opção garante que ele ainda estará visível mesmo se a câmera se mover para fora da margem de descarte. Você também pode usar ambas as opções.

Textura de profundidade

Para ler a partir da textura de profundidade, primeiro precisamos criar um uniform de textura associado ao buffer de profundidade usando hint_depth_texture.

uniform sampler2D depth_texture : hint_depth_texture;

Uma vez definida, a textura de profundidade pode ser lida com a função texture().

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

Nota

Semelhante ao acesso à textura da tela, o acesso à textura de profundidade só é possível ao ler a partir da viewport atual. A textura de profundidade não pode ser acessada a partir de outra viewport na qual você tenha renderizado.

Os valores retornados por depth_texture estão entre 1.0 e 0.0 (correspondendo ao plano próximo e distante, respectivamente, devido ao uso de um buffer de profundidade com "z invertido") e não são lineares. Ao exibir a profundidade diretamente a partir da depth_texture, tudo parecerá quase preto, a menos que esteja muito perto, devido a essa não-linearidade. Para fazer com que o valor de profundidade se alinhe com as coordenadas do mundo ou do modelo, precisamos linearizar o valor. Quando aplicamos a matriz de projeção à posição do vértice, o valor z torna-se não-linear; portanto, para linearizá-lo, nós o multiplicamos pelo inverso da matriz de projeção, que no Godot está acessível através da variável INV_PROJECTION_MATRIX.

Primeiramente, pegue as coordenadas do espaço de tela e transforme-as em coordenadas normalizadas de dispositivo (NDC). As NDCs variam de -1.0 a 1.0 nas direções x e y e de 0.0 a 1.0 na direção z ao usar o backend Vulkan. Reconstrua as NDCs usando SCREEN_UV para os eixos x e y, e o valor de profundidade para z.

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

Nota

Este tutorial pressupõe o uso dos renderizadores Forward+ ou Mobile, que usam NDCs do Vulkan com um intervalo Z de [0.0, 1.0]. Em contrapartida, o renderizador Compatibility usa NDCs do OpenGL com um intervalo Z de [-1.0, 1.0]. Para o renderizador Compatibility, substitua o cálculo de NDC por este:

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

Você também pode usar as diretivas integradas CURRENT_RENDERER e RENDERER_COMPATIBILITY para um shader que funcionará em todos os renderizadores:

#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

Converta NDC para o espaço de visualização (view space) multiplicando a NDC por INV_PROJECTION_MATRIX. Lembre-se de que o espaço de visualização fornece posições relativas à câmera, de modo que o valor z nos dará a distância até o ponto.

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

Como a câmera está voltada para a direção negativa de z, a posição terá um valor z negativo. Para obter um valor de profundidade utilizável, temos que inverter o sinal de view.z.

A posição no mundo pode ser reconstruída a partir do buffer de profundidade usando o seguinte código, utilizando a INV_VIEW_MATRIX para transformar a posição do espaço de visualização para o espaço do mundo.

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

Exemplo de shader

Assim que adicionamos uma linha para enviar a saída para o ALBEDO, temos um shader completo que se parece com isto. Este shader permite visualizar a profundidade linear ou as coordenadas do espaço do mundo, dependendo de qual linha estiver comentada.

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;
}

Uma otimização

Você pode se beneficiar ao usar um único triângulo grande em vez de usar um quad em tela cheia. O motivo para isso é explicado aqui. No entanto, o benefício é bastante pequeno e só traz vantagens ao executar fragment shaders especialmente complexos.

Defina o Mesh no MeshInstance3D para um ArrayMesh. Um ArrayMesh é uma ferramenta que permite construir facilmente uma malha a partir de Arrays para vértices, normais, cores, etc.

Agora, anexe um script ao MeshInstance3D e use o seguinte código:

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

O triângulo é especificado em coordenadas normalizadas de dispositivo (NDC). Lembre-se de que as NDCs vão de -1.0 a 1.0 tanto na direção x quanto na y. Isso faz com que a tela tenha 2 unidades de largura e 2 unidades de altura. Para cobrir a tela inteira com um único triângulo, use um triângulo que tenha 4 unidades de largura e 4 unidades de altura, o dobro de sua altura e largura.

Atribua o mesmo vertex shader de cima e tudo deverá parecer exatamente igual.

A única desvantagem de usar um ArrayMesh em vez de um QuadMesh é que o ArrayMesh não é visível no editor, porque o triângulo não é construído até que a cena seja executada. Para contornar isso, construa uma malha de um único triângulo em um programa de modelagem e use-a no MeshInstance3D.