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.

Dans le précédent tutoriel de post-traitement, nous avons rendu la scène dans un Viewport et ensuite rendu le Viewport dans un ViewportContainer dans la scène principale. L'une des limites de cette méthode est que nous ne pouvons pas accéder au tampon de profondeur car celui-ci n'est disponible que dans les shaders spatiaux et les Viewports ne conservent pas d'informations sur la profondeur.

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

Pour contourner la limitation d'utilisation du tampon de profondeur, utilisez une MeshInstance avec une QuadMesh primitive. Cela nous permet d'utiliser un shader spatial et d'accéder à la texture de profondeur de la scène. Ensuite, utilisez un shader de sommet pour que le quad couvre l'écran en permanence afin que l'effet de post-traitement soit appliqué à tout moment, y compris dans l'éditeur.

Tout d'abord, créez une nouvelle MeshInstance et réglez son maillage sur une QuadMesh. Cela crée un quad centré sur la position (0, 0, 0) avec une largeur et une hauteur de 1. Réglez la largeur et la hauteur sur 2. Actuellement, le quad occupe une position dans l'espace monde à l'origine, mais nous voulons qu'il se déplace avec la caméra pour qu'il couvre toujours l'ensemble de l'écran. Pour ce faire, nous contournerons les transformées de coordonnées qui traduisent les positions des sommets à travers les différents espaces de coordonnées et traiterons les sommets comme s'ils étaient déjà dans l'espace de clipping.

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

Pour lire depuis la texture de profondeur, faites une recherche de texture en utilisant texture() et la variable uniform DEPTH_TEXTURE.

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 retournées par DEPTH_TEXTURE sont comprises entre 0 et 1 et ne sont pas linéaire. Lorsque l'on affiche directement la profondeur depuis DEPTH_TEXTURE, tout ce qui n'est pas très proche semble presque blanc. Cela est du au fait que le tampon de profondeur utilise plus de bits pour stocker les objets près de la caméra que pour ceux plus éloignés, de fait, la plupart des détails du tampon de profondeur se trouvent à proximité de la caméra. 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 n'est pas linéaire, 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.

Tout d'abord, prenons les coordonnée de l'espace écran et transformons les en coordonnées normalisées pour l'appareil (NDC). Les NDC vont de -1 à 1, comme les bornes de l'espace des coordonnées. Reconstruisez le NDC en utilisant SCREEN_UV pour les axes x et y, et la valeur de profondeur pour z.

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

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.

Les positions dans l'espace global peuvent être construites à partir du tampon de profondeur en utilisant le code suivant. Notez que CAMERA_MATRIX est nécessaire pour transposer la position de l'espace visuel à l'espace global, elle doit être passée au fragment shader par un varying.

varying mat4 CAMERA;

void vertex() {
  CAMERA = CAMERA_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.

Réglez le Mesh dans la MeshInstance 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 à MeshInstance et utilisez le code suivant :

extends MeshInstance

func _ready():
  # Create a single triangle out of vertices:
  var verts = PoolVector3Array()
  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

Le triangle est spécifié en coordonnées normalisées de l'appareil. Rappelons que les NDC vont de -1 à 1 dans les directions x et y. Ce qui donne une taille d'écran de 2 unités en largeur et 2 unités en hauteur. Afin de couvrir l'entièreté de l'écran avec un seul triangle, utilisez un triangle de 4 unités de largeur et 4 unités de hauteur, soit le double de la hauteur et de la largeur de l'écran.

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 MeshInstance à la place du ArrayMesh.