Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

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.

In the previous post-processing tutorial, we rendered the scene to a Viewport and then rendered the Viewport in a SubViewportContainer to the main scene. One limitation of this method is that we could not access the depth buffer because the depth buffer is only available in shaders and Viewports do not maintain depth information.

Quadrant plein écran

Dans le tutoriel custom post-processing tutorial, nous avons couvert comment utiliser un Viewport pour faire des effets de post-traitement personnalisés. L'utilisation d'un Viewport présente deux inconvénients majeurs :

  1. Le tampon de profondeur n'est pas accessible

  2. 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.

Le shader de sommet s'attend à ce que les coordonnées soient affichées dans l'espace de clipping, qui sont des coordonnées allant de -1 en bas et à gauche de l'écran à 1 en haut et à droite de l'écran. C'est pourquoi la QuadMesh doit avoir une hauteur et une largeur de 2. Godot s'occupe de la transformation d'espace de modèle en espace de vue pour couper l'espace en coulisses, nous devons donc annuler les effets des transformations de Godot. Nous le faisons en plaçant la POSITION intégrée à la position souhaitée. La POSITION contourne les transformations intégrées et fixe directement la position du sommet.

shader_type spatial;

void vertex() {
  POSITION = vec4(VERTEX, 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 :

  1. Ajouter le QuadMesh comme enfant de la caméra, de sorte que la caméra regarde toujours vers lui

  2. Définissez la propriété extra_cull_margin aussi 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 : source_color, 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.

The values returned by depth_texture are between 0.0 and 1.0 and are nonlinear. When displaying depth directly from the depth_texture, everything will look almost white unless it is very close. This is because the depth buffer stores objects closer to the camera using more bits than those further, so most of the detail in depth buffer is found close to the camera. 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.

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.

Note

This tutorial assumes the use of the Vulkan renderer, which uses NDCs with a Z-range of [0.0, 1.0]. In contrast, OpenGL uses NDCs with a Z-range of [-1.0, 1.0].

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

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, il nous faut mettre``view.z`` négative.

The world position can be constructed from the depth buffer using the following code. Note that the INV_VIEW_MATRIX is needed to transform the position from view space into world space, so it needs to be passed to the fragment shader with a varying.

varying mat4 CAMERA;

void vertex() {
  CAMERA = INV_VIEW_MATRIX;
}

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

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.

Set the Mesh in the MeshInstance3D to an ArrayMesh. An ArrayMesh is a tool that allows you to easily construct a Mesh from Arrays for vertices, normals, colors, etc.

Now, attach a script to the MeshInstance3D and use the following code:

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(-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)

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.

The one drawback to using an ArrayMesh over using a QuadMesh is that the ArrayMesh is not visible in the editor because the triangle is not constructed until the scene is run. To get around that, construct a single triangle Mesh in a modeling program and use that in the MeshInstance3D instead.