Shaders de lecture d'écran

Introduction

Très souvent, on souhaite faire un shader qui lit à partir du même écran que celui sur lequel il écrit. Les API 3D, telles que OpenGL ou DirectX, rendent la tâche très difficile en raison de limitations matérielles internes. Les GPU sont extrêmement parallèles, de sorte que la lecture et l'écriture causent toutes sortes de problèmes de cache et de cohérence. Par conséquent, même le matériel le plus moderne ne le supporte pas correctement.

La solution consiste à faire une copie de l'écran, ou d'une partie de l'écran, dans une mémoire tampon, puis à la lire pendant le dessin. Godot fournit quelques outils qui rendent ce processus facile !

SCREEN_TEXTURE texture intégrée

Godot Langue de shading a une texture spéciale, SCREEN_TEXTURE (et DEPTH_TEXTURE pour la profondeur, dans le cas de la 3D). Il prend comme argument l'UV de l'écran et renvoie un vec3 RGB avec la couleur. Une varying spéciale intégrée : SCREEN_UV peut être utilisé pour obtenir l'UV du fragment actuel. Il en résulte ce simple shader de fragment de canvas_item :

void fragment() {
    COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
}

résulte en un objet invisible, car il ne fait que montrer ce qui se cache derrière.

La raison pour laquelle textureLod doit être utilisé est que, lorsque Godot copie une partie de l'écran, il effectue également un flou gaussien séparable efficace sur ses mipmaps.

Cela permet non seulement de lire depuis l'écran, mais aussi de lire depuis celui-ci avec différentes quantités de flou sans frais.

Note

Les Mipmaps ne sont pas générées dans le GLES2 en raison de mauvaise performance et de la compatibilité avec les anciens appareils.

Exemple SCREEN_TEXTURE

SCREEN_TEXTURE peut être utilisé pour beaucoup de choses. Il existe une démo spéciale pour Screen Space Shaders, que vous pouvez télécharger pour voir et apprendre. Un exemple est un simple shader pour ajuster la luminosité, le contraste et la saturation :

shader_type canvas_item;

uniform float brightness = 1.0;
uniform float contrast = 1.0;
uniform float saturation = 1.0;

void fragment() {
    vec3 c = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0).rgb;

    c.rgb = mix(vec3(0.0), c.rgb, brightness);
    c.rgb = mix(vec3(0.5), c.rgb, contrast);
    c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);

    COLOR.rgb = c;
}

Dans les coulisses

Bien que cela semble magique, ce n'est pas le cas. En 2D, la fonction SCREEN_TEXTURE intégrée, lorsqu'elle est trouvée pour la première fois dans un nœud sur le point d'être dessiné, effectue une copie de tout l'écran vers une mémoire tampon. Les nœuds suivants qui l'utilisent dans les shaders ne verront pas l'écran copié pour eux, car cela finit par être inefficace. En 3D, l'écran est copié après la passe de géométrie opaque, mais avant la passe de géométrie transparente, de sorte que les objets transparents ne seront pas capturés dans la SCREEN_TEXTURE.

Par conséquent, en 2D si les shaders qui utilisent SCREEN_TEXTURE se chevauchent, le second n'utilisera pas le résultat du premier, ce qui entraînera des visuels inattendus :

../../_images/texscreen_demo1.png

Dans l'image ci-dessus, la deuxième sphère (en haut à droite) utilise la même source pour SCREEN_TEXTURE que la première en dessous, donc la première "disparaît", ou n'est pas visible.

En 2D, cela peut être corrigé via le nœud BackBufferCopy, qui peut être instancié entre les deux sphères. BackBufferCopy peut fonctionner en spécifiant soit une région de l'écran, soit l'écran entier :

../../_images/texscreen_bbc.png

Avec une copie correcte du back-buffer, les deux sphères se mélangent correctement :

../../_images/texscreen_demo2.png

En 3D, il y a moins de flexibilité pour résoudre ce problème particulier car la SCREEN_TEXTURE n'est capturée qu'une seule fois. Faites attention lorsque vous utilisez la fonction SCREEN_TEXTURE en 3D, car elle ne capture pas les objets transparents et peut capturer certains objets opaques qui se trouvent devant l'objet.

Vous pouvez reproduire la logique du back-buffer en 3D en créant un Viewport avec une caméra dans la même position que votre objet, puis en utilisant la texture Viewport au lieu de SCREEN_TEXTURE.

Logique du back-buffer

Donc, pour être plus clair, voici comment fonctionne la logique de copie du backbuffer dans Godot :

  • Si un nœud utilise la SCREEN_TEXTURE, l'écran entier est copié dans le back buffer avant de dessiner ce nœud. Cela ne se produit que la première fois ; les nœuds suivants ne déclenchent pas cela.
  • Si un nœud BackBufferCopy a été traité avant la situation décrite au point ci-dessus (même si SCREEN_TEXTURE n'a pas été utilisé), le comportement décrit au point ci-dessus ne se produit pas. En d'autres termes, la copie automatique de l'écran entier ne se produit que si SCREEN_TEXTURE est utilisé dans un nœud pour la première fois et qu'aucun nœud BackBufferCopy (non désactivé) n'a été trouvé auparavant dans l'ordre de l'arbre.
  • BackBufferCopy peut copier soit l'écran entier, soit une région. S'il est réglé sur une seule région (et non sur l'ensemble de l'écran) et que votre shader utilise des pixels qui ne se trouvent pas dans la région copiée, le résultat de cette lecture est indéfini (très probablement les déchets des images précédentes). En d'autres termes, il est possible d'utiliser BackBufferCopy pour recopier une région de l'écran et d'utiliser ensuite SCREEN_TEXTURE sur une autre région. Évitez ce comportement !

DEPTH_TEXTURE

Pour les shaders 3D, il est également possible d'accéder au buffer de profondeur de l'écran. Pour cela, on utilise la DEPTH_TEXTURE intégrée. Cette texture n'est pas linéaire, elle doit être convertie via la matrice de projection inverse.

Le code suivant récupère la position 3D sous le pixel dessiné :

void fragment() {
    float depth = textureLod(DEPTH_TEXTURE, SCREEN_UV, 0.0).r;
    vec4 upos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
    vec3 pixel_position = upos.xyz / upos.w;
}