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...
Post-traitement avancé
Introduction
Ce tutoriel décrit une méthode avancée de post-traitement dans Godot. Il expliquera notamment comment écrire un shader de post-traitement qui utilise le tampon de profondeur. Vous devriez déjà être familier avec le post-traitement en général et, en particulier, avec les méthodes décrites dans le tutoriel custom post-processing tutorial.
Quadrant plein écran
Un moyen de faire des effets de post-traitement personnalisés est d'utiliser un Viewport. L'utilisation d'un Viewport présente deux inconvénients majeurs :
Le tampon de profondeur n'est pas accessible
L'effet du shader de post-traitement n'est pas visible dans l'éditeur
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);
}
Note
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.
Même avec ce shader de sommet, le quad continue de disparaître. Cela est dû au frustum culling, qui se fait sur le CPU. Le Frustum culling utilise la matrice de la caméra et les AABB des mailles pour déterminer si la maille sera visible avant de la passer au GPU. Le CPU ne sait pas ce que nous faisons avec les sommets, il suppose donc que les coordonnées spécifiées se réfèrent à des positions mondiales, et non à des positions d'espace de clipping, ce qui a pour conséquence que Godot élimine le quadrant lorsque nous nous éloignons du centre de la scène. Afin d'éviter que le quad ne soit éliminé, il existe plusieurs options :
Ajouter le QuadMesh comme enfant de la caméra, de sorte que la caméra regarde toujours vers lui
Définissez la propriété
extra_cull_marginaussi large que possible dans le QuadMesh
La deuxième option garantie que le quad soit visible dans l'éditeur, alors que la première option assure qu'il reste visible même si la caméra se déplace en dehors de l'espace d'affichage. Vous pouvez également utiliser les deux options.
Texture de profondeur
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;
Note
Semblable à l'accès à la texture d'écran, accéder à la texture de profondeur n'est possible qu'en lisant à partir du viewport courant. La texture de profondeur n'est pas accessible à partir d'un autre viewport dans lequel vous avez fait un rendu.
Les valeurs renvoyées par depth_texture sont comprises entre 1.0 et 0.0 (correspondant au plan proche (near plane) et plan éloigné (far plane), respectivement, à cause de l'utilisation d'un tampon de profondeur à "z-inversé") et ne sont pas linéaires. Lorsque l'on affiche directement la profondeur depuis la depth_texture, tout va être presque noir sauf si c'est très proche à cause de la non-linéarité. Pour que la valeur de profondeur soit cohérente avec les coordonnées du monde ou du modèle, il nous faut linéariser la valeur. Lorsque l'on applique la matrice de projection à la position du sommet, la valeur z est rendue non-linéaire, alors pour la linéariser, il faut la multiplier par la matrice de projection inverse qui, dans Godot, est accessible par la variable INV_PROJECTION_MATRIX.
Firstly, take the screen space coordinates and transform them into normalized device
coordinates (NDC). NDC run -1.0 to 1.0 in x and y directions and
from 0.0 to 1.0 in the z direction when using the Vulkan backend.
Reconstruct the NDC using SCREEN_UV for the x and y axis, and
the depth value for z.
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
}
Note
This tutorial assumes the use of the Forward+ or Mobile renderers, which both
use Vulkan NDCs with a Z-range of [0.0, 1.0]. In contrast, the Compatibility
renderer uses OpenGL NDCs with a Z-range of [-1.0, 1.0]. For the Compatibility
renderer, replace the NDC calculation with this instead:
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
You can also use the CURRENT_RENDERER and RENDERER_COMPATIBILITY
built-in defines for a shader that will work in all renderers:
#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
Convertissez les NDC en coordonnées de l'espace visuel les NDC en multipliant par INV_PROJECTION_MATRIX. Rappelez-vous que l'espace visuel donne les positions relatives à la caméra, la valeur de z nous renseigne donc sur la distance au point.
void fragment() {
...
vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
view.xyz /= view.w;
float linear_depth = -view.z;
}
Comme la caméra est orientée dans la direction z négative, la position aura une valeur z négative. Pour obtenir une valeur de profondeur utilisable, nous devons rendre view.z négative.
The world position can be constructed from the depth buffer using the following code, using the
INV_VIEW_MATRIX to transform the position from view space into world space.
void fragment() {
...
vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 world_position = world.xyz / world.w;
}
Shader d'exemple
Once we add a line to output to ALBEDO, we have a complete shader that looks something like this.
This shader lets you visualize the linear depth or world space coordinates, depending on which
line is commented out.
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;
}
Une optimisation
Vous pouvez tirer profit de l'utilisation d'un seul grand triangle plutôt que d'un grand quadrilatère de la taille de l'écran. Vous trouverez une explication à cette adresse. Cependant, le bénéfice est assez faible et seulement utile lors de l'utilisation de fragments shaders particulièrement complexes.
Réglez le Mesh dans la MeshInstance3D sur un ArrayMesh. Un ArrayMesh est un outil qui vous permet de construire facilement un Mesh à partir de Arrays pour les sommets, les normales, les couleurs, etc.
Maintenant, attachez un script à MeshInstance3D et utilisez le code suivant :
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)
Note
The triangle is specified in normalized device coordinates.
Recall, NDC run from -1.0 to 1.0 in both the x and y
directions. This makes the screen 2 units wide and 2 units
tall. In order to cover the entire screen with a single triangle, use
a triangle that is 4 units wide and 4 units tall, double its
height and width.
Assignez le même vertex shader qu'au-dessus et tout devrait avoir exactement la même apparence.
Le seul inconvénient de l'utilisation d'un ArrayMesh par rapport à celle d'un QuadMesh est que le ArrayMesh n'est pas visible dans l'éditeur car le triangle n'est pas construit avant que la scène ne soit exécutée. Pour contourner ce problème, construisez un Mesh triangulaire dans un programme de modélisation et utilisez-le dans le MeshInstance3D à la place du ArrayMesh.