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.
Checking the stable version of the documentation...
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:
O buffer de profundidade não pode ser acessado
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:
Adicionar o QuadMesh como um filho da câmera, de modo que a câmera esteja sempre apontada para ele
Definir a propriedade de geometria
extra_cull_margino 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.