IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre OpenGL moderne

Quatrième partie : OpenGL avancé


précédentsommairesuivant

V. Tampon d’image (frame buffers)

Jusqu’à présent, nous avons vu plusieurs composantes d’un tampon d’image : un tampon pour les couleurs, un autre pour la profondeur et finalement un dernier pour enlever certains fragments suivant une condition définie par l’utilisateur. La combinaison de ces tampons s’appelle un tampon d’image (frame buffer) et est stocké quelque part en mémoire. OpenGL nous donne la flexibilité de définir nos propres tampons d’image et donc de définir nos propres tampons de couleur, de profondeur et de pochoir.

Les opérations de rendu que nous avons effectuées jusqu’à présent ont été réalisées sur le tampon de rendu attaché au tampon d’image par défaut. Le tampon d’image par défaut est créé et configuré lors de la création de votre fenêtre (GLFW le fait pour nous). En créant votre propre tampon d’image, vous pouvez obtenir un nouvel endroit où faire le rendu.

L’utilité des tampons d’image peut ne pas être immédiat, mais le rendu de la scène dans un second tampon d’image permet de créer des miroirs ou d’effectuer de super effets de post-traitement (post-process). Premièrement, nous allons voir comment ils fonctionnent ; nous allons ensuite les utiliser pour implémenter ces super effets.

V-A. Créer un tampon d’image

Comme pour tout autre objet OpenGL, nous devons créer un objet tampon d’image (FBO) en utilisant la fonction glGenFramebuffers() :

 
Sélectionnez
unsigned int fbo;
glGenFramebuffers(1, &fbo);

Cela a déjà été vu une dizaine de fois et la mise en place d’un FBO est similaire à la mise en place des autres objets OpenGL. Premièrement, on crée l’objet, on le lie en tant que FBO actif, on effectue des opérations et on le délie. Pour lier un FBO, on utilise :

 
Sélectionnez
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

En liant la cible GL_FRAMEBUFFER, toutes les opérations de lecture et d’écriture sur le tampon d’image seront effectuées sur le tampon d’image lié. Il est possible de lier un tampon d’image, pour la lecture, différent de celui pour l’écriture en utilisant GL_READ_FRAMEBUFFER et GL_DRAW_FRAMEBUFFER. Le tampon d’image lié à GL_READ_FRAMEBUFFER est utilisé pour les opérations de lecture telles que glReadPixels() et le tampon d’image lié à GL_DRAW_FRAMEBUFFER est utilisé comme destination pour le rendu, le nettoyage et toutes autres opérations d’écriture. La plupart du temps, vous n’avez pas besoin de différencier les opérations et vous utilisez donc GL_FRAMEBUFFER.

Malheureusement, nous ne pouvons pas utiliser notre tampon d’image pour le moment, car il n’est pas complet. Un tampon d’image est complet lorsque ces prérequis sont satisfaits :

  • nous devons attacher au moins un tampon (couleur, profondeur ou stencil) ;
  • il doit y avoir au moins une attache pour les couleurs ;
  • toutes les attaches doivent être complètes (réservé en mémoire) ;
  • chaque tampon doit avoir le même nombre d’échantillons.

Ne vous inquiétez pas, vous ne devez pas savoir ce que sont les échantillons. Nous verrons cela dans un prochain tutoriel.

À partir des prérequis, cela devient évident que nous devons créer une sorte d’attache pour le tampon d’image et l’attacher à celle-ci. Après que nous ayons rempli tous les prérequis, nous pouvons vérifier que nous avons réellement réussi à compléter le tampon d’image en appelant la fonction glCheckFrameBufferStatus() avec GL_FRAMEBUFFER. La fonction vérifie le tampon d’image actuellement lié et retourne une de ces valeurs. Si c’est GL_FRAMEBUFFER_COMPLETE, alors nous l’avons bien configuré :

 
Sélectionnez
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
  // faire la danse de la victoire

Les opérations de rendu suivantes seront effectuées sur les attaches du tampon d’image lié et comme il n’est pas le tampon d’image par défaut, les commandes de rendu n’auront pas d’impact sur ce que vous verrez à l’écran. Le fait de faire un rendu qui n’est pas affiché à l’écran s’appelle un rendu hors écran (off-screen rendering). Pour s’assurer que toutes les opérations de rendu auront un impact visuel dans la fenêtre, il faut lier le tampon d’image par défaut, qui a l’identifiant 0 :

 
Sélectionnez
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Lorsque vous avez terminé avec vos opérations sur le tampon d’image, n’oubliez pas de supprimer l’objet :

 
Sélectionnez
glDeleteFramebuffers(1, &fbo);

Avant de vérifier la complétude du tampon d’image, nous devons attacher un ou plusieurs tampons au tampon d’image. Une attache est un emplacement mémoire qui peut agir comme un tampon pour le tampon d’image, telle une image. Lors de la création d’un attachement, nous avons deux options : des textures ou des tampons de rendu (render buffer).

V-A-1. Attache de type texture

Lorsque vous attachez une texture à un tampon d’image, toutes les commandes de rendu écriront dans la texture comme si c’était un tampon de couleurs, de profondeur ou de pochoir. L’avantage d’utiliser les textures est que le résultat de toutes les opérations de rendu seront stockées dans une image que nous pouvons réutiliser dans nos shaders.

Créer une texture pour un tampon d’image est globalement identique à la création d’une texture classique :

 
Sélectionnez
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
  
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Les seules différences sont que nous définissons la dimension à celle de la taille de la fenêtre (même si ce n’est pas une nécessité) et nous passons NULL pour les données de la texture. Ainsi, nous ne faisons qu’allouer la mémoire. La définition de la texture s’effectuera lors du rendu dans le tampon d’image. Aussi, nous ne nous soucions pas de la méthode de dépassement (wrapping) ni du mipmapping, sachant que nous n’en avons pas besoin dans la plupart des cas.

Si vous souhaitez afficher l’intégralité de votre écran dans une texture plus petite ou plus grande, vous devez appeler glViewport() (avant le rendu dans votre tampon d’image) avec les nouvelles dimensions de votre texture, sinon, seule une partie de la texture ou de l’écran sera dessinée dans la texture.

Maintenant que vous avez créé une texture, il ne reste plus qu’à l’attacher au tampon d’image :

 
Sélectionnez
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

La fonction glFrameBufferTexture2D() possède les paramètres suivants :

  • target : le type de tampon d’image que nous utilisons (rendu, lecture ou les deux) ;
  • attachment : le type d’attache que nous allons utiliser. Actuellement, nous attachons un tampon de couleurs. Notez que le 0 à la fin suggère que nous pouvons attacher plus d’un tampon de couleurs. Nous explorerons ceci dans un prochain tutoriel ;
  • textarget : le type de texture que nous souhaitons attacher ;
  • texture : la texture à attacher ;
  • level : le niveau de mipmap. Nous le gardons à 0.

En plus de l’attache pour les couleurs, nous pouvons attacher une texture pour la profondeur ou le pochoir au tampon d’image. Pour attacher une texture pour la profondeur, nous devons spécifier le type GL_DEPTH_ATTACHMENT. Notez que le format de la texture et le format interne (internalformat) de la texture doivent devenir GL_DEPTH_COMPONENT pour correspondre au format de stockage du tampon de profondeur. Pour attacher un tampon de pochoir, vous devez utiliser GL_STENCIL_ATTACHMENT comme deuxième argument et spécifier GL_STENCIL_INDEX comme format de texture.

Il est aussi possible d’attacher un tampon de profondeur et de pochoir à une unique texture. Chaque valeur sur 32 bits de la texture consistera en deux informations : 24 bits pour la profondeur et 8 bits pour le pochoir. Pour attacher une texture de ce type, nous devons utiliser le type GL_DEPTH_STENCIL_ATTACHMENT et configurer les formats de la texture pour contenir les deux valeurs. En voici un exemple :

 
Sélectionnez
glTexImage2D(
  GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
  GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);

V-A-2. Attache de type tampon de rendu

Les tampons de rendu ont été ajoutés dans OpenGL peu après les textures. À l’origine, seules les textures pouvaient être utilisées. Tout comme pour une texture, un tampon de rendu est un tampon, c’est-à-dire, un tableau d’octets, d’entiers, de pixels ou autre. Un tampon de rendu possède l’avantage d’utiliser le format natif d’OpenGL pour le stockage des données. Celui-ci est optimisé pour le rendu hors écran dans un tampon d’image.

Les tampons de rendu stockent les données de rendu directement dans leur tampon sans effectuer de conversion spécifique au format de la texture et sont donc plus rapide. Toutefois, les tampons de rendu ne sont généralement accessible qu’en écriture. Vous ne pouvez donc pas les lire dans un shader (contrairement aux textures). Il est possible de les lire avec glReadPixels(), qui retourne une zone provenant du tampon d’image actuellement lié, mais il n’existe pas de méthode pour obtenir les pixels de l’attache directement.

Comme leurs données sont déjà au format natif, ils sont plutôt rapides à l’écriture et lors de la copie dans d’autres tampons. Les opérations telles que le changement des tampons est donc rapide lors de l’utilisation d’un tampon de rendu. La fonction glfwSwapBuffers() que nous avons utilisée à la fin de chaque itération peut aussi être implémentée avec les objets de rendu : nous écrivons simplement dans le tampon de rendu, puis l’échangeons avec un autre à la fin. Les tampons de rendu sont parfaits pour ce type d’opération.

La création d’un tampon de rendu ressemble à la création d’un tampon d’image :

 
Sélectionnez
unsigned int rbo;
glGenRenderbuffers(1, &rbo);

De même, nous voulons lier le tampon de rendu afin que les opérations suivantes l’affectent :

 
Sélectionnez
glBindRenderbuffer(GL_RENDERBUFFER, rbo);

Comme les tampons de rendu sont généralement des tampons en écriture seule, nous les utilisons souvent comme attaches pour la profondeur et le pochoir. Habituellement, nous n’avons pas besoin de lire les valeurs de profondeur et de pochoir, juste de les tester lors du rendu. Lorsqu’il n’y a pas besoin d’échantillonner les tampons, les tampons de rendu sont préférables, car plus optimisés.

Créer un tampon de rendu pour la profondeur et le pochoir s’effectue ainsi :

 
Sélectionnez
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

Créer un tampon de rendu est similaire à la création des textures, la différence étant que l’objet est conçu pour être utilisé comme une image et non pour des données génériques. Ici, nous avons choisi le format interne GL_DEPTH24_STENCIL8, permettant de contenir les 24 bits de profondeur et les 8 bits de pochoir.

La dernière chose à faire est d’attacher le tampon de rendu :

 
Sélectionnez
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

Les tampons de rendu peuvent apporter des optimisations à vos tampons d’image, mais il est important de comprendre quand utiliser un tampon de rendu et quand utiliser une texture. La règle générale est que, si vous n’avez pas besoin d’échantillonner un tampon, alors il est préférable d’utiliser un tampon de rendu. Si vous devez échantillonner des données d’un tampon, telles des couleurs ou les valeurs de profondeur, vous devez utiliser une texture. Du côté des performances, l’impact n’est pas énorme.

V-B. Rendu dans une texture

Maintenant que nous savons comment (en quelque sorte) les tampons d’image fonctionnent, nous pouvons les utiliser. Nous allons afficher la scène dans une texture de couleur attachée à notre tampon d’image et dessiner cette texture dans un rectangle couvrant l’intégralité de l’écran. Le résultat visuel sera exactement identique à celui sans tampon d’image, mais affiché sur un simple rectangle. Pourquoi est-ce donc utile ? Nous allons voir cela dans la prochaine section.

Premièrement, nous devons créer le tampon d’image et le lier :

 
Sélectionnez
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

Ensuite, nous créons une texture que nous attachons comme tampon de couleurs au tampon d’image. Nous définissons les dimensions de la texture aux dimensions de la fenêtre et nous gardons ses données non initialisées :

 
Sélectionnez
// génère la texture
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);

// attache la texture au tampon d’image
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

Nous souhaitons aussi qu’OpenGL fasse ses tests de profondeur (et, si vous le voulez, les tests de pochoir), nous devons donc ajouter un tampon de profondeur (et de pochoir) au tampon d’image. Comme nous n’échantillonnons que le tampon des couleurs, nous pouvons créer un tampon de rendu pour cela. Vous rappelez-vous que c’est un bon choix lorsque vous n’échantillonnez pas ces tampons ?

Créer un tampon de rendu n’est pas très difficile. La seule chose à se rappeler est que nous le créons pour avoir un tampon de profondeur et de pochoir. Nous définissons son format interne à GL_DEPTH24_STENCIL8 pour avoir assez de précision pour nos besoins.

 
Sélectionnez
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

Une fois que nous avons alloué assez de mémoire pour le tampon de rendu, nous pouvons le délier.

Ensuite, avant de pouvoir compléter le tampon d’image, nous devons attacher le tampon de rendu au tampon d’image :

 
Sélectionnez
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

Puis, par précaution, nous allons vérifier si le tampon d’image est complet, sinon nous affichons un message d’erreur.

 
Sélectionnez
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Finalement, assurez-vous de délier le tampon d’image afin de ne pas faire de rendu dedans par accident.

Maintenant que le tampon d’image est complet, il ne nous reste plus qu’à faire le rendu dans ce tampon d’image au lieu de celui par défaut, en le liant comme le tampon d’image. Toutes les commandes de rendu qui suivent vont influer sur le tampon d’image lié. Toutes les opérations sur la profondeur et le pochoir seront effectués sur les tampons de profondeur et de pochoir attachés au tampon d’image (si disponible). Si vous n’avez pas attaché de tampon de profondeur, les tests de profondeur ne fonctionnerons pas, car il n’y a pas de tampon de profondeur dans le tampon d’image.

Donc, pour dessiner la scène dans une texture, nous devons suivre les étapes suivantes :

  • dessiner la scène comme d’habitude avec le nouveau tampon d’image lié ;
  • lier le tampon d’image par défaut ;
  • dessiner un rectangle qui couvre l’écran et utiliser le tampon de couleurs du nouveau tampon d’image comme texture.

Nous allons dessiner la même scène que celle du tutoriel sur le test de profondeur, mais, cette fois, avec la texture du conteneur.

Pour dessiner le rectangle, nous allons créer un nouvel ensemble de shaders. Nous n’allons pas inclure de transformations de matrice, car nous allons juste fournir les coordonnées de sommets dans l’espace de coordonnées de l’écran. Ainsi, nous pouvons directement les utiliser comme sortie du vertex shader. Le vertex shader sera donc :

 
Sélectionnez
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    TexCoords = aTexCoords;
}

Rien de bien compliqué. Le fragment shader sera encore plus simple, il nous suffit juste d’échantillonner la texture :

 
Sélectionnez
#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D screenTexture;

void main()
{ 
    FragColor = texture(screenTexture, TexCoords);
}

Il ne reste plus qu’à créer et configurer le VAO pour le rectangle. Une itération de rendu avec le tampon d’image aura la structure suivante :

 
Sélectionnez
// première passe
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // nous n’utilisons pas de tampon de profondeur
glEnable(GL_DEPTH_TEST);
DrawScene();    
  
// deuxième passe
glBindFramebuffer(GL_FRAMEBUFFER, 0); // retour au framebuffer par défaut
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);
  
screenShader.use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);

Il y a quelques détails à prendre en compte. Premièrement, comme chaque tampon d’image possède ses propres tampons, nous souhaitons nettoyer ces tampons avec les options appropriées de la fonction glClear(). Deuxièmement, lorsque nous dessinons le rectangle, nous désactivons le test de profondeur, car nous n’en avons pas besoin pour un simple rectangle. Nous avons effectué les tests de profondeur lors du rendu classique de la scène.

Il y a plusieurs étapes qui peuvent être en défaut : si vous n’avez pas de résultat, essayez de déboguer là où cela est possible en relisant les sections appropriées du tutoriel. Si tout fonctionne, vous devriez obtenir :

Une image d'une scène OpenGL 3D affichée dans une texture grâce aux framebuffers

À gauche vous avez exactement le résultat du tutoriel sur le test de profondeur, mais cette fois affiché dans un rectangle. Si nous dessinons la scène en mode fil de fer, il devient évident que ce n’est qu’un simple rectangle qui est affiché sur le tampon d’image par défaut.

Vous pouvez trouver le code source de cet exemple ici.

Quelle pouvait bien être l’utilité de cela ? Eh bien, maintenant, nous avons un accès libre à tous les pixels de la scène à partir d’une texture : nous pouvons créer des effets intéressants à l’aide du fragment shader. La combinaison de ces effets se nomment des effets de post-traitement.

V-C. Post-traitement

Maintenant que la scène est dessinée dans une texture, nous pouvons créer des effets intéressants en manipulant les données de la texture. Dans cette section, nous allons voir comment créer les effets les plus populaires et vous pourrez créer les vôtres avec un peu d’imagination.

Commençons par l’effet le plus simple.

V-C-1. Inversion

Comme nous avons accès aux couleurs du rendu, il devient simple d’inverser les couleurs dans le fragment shader. Nous prenons donc la couleur de la texture et nous la soustrayons à 1.0 :

 
Sélectionnez
void main()
{
    FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}

Même si l’inversion est un effet très simple, il permet d’obtenir des résultats amusant :

Image avec post traitement d'inversion des couleurs de la scène 3D OpenGL rendu dans un framebuffer

La scène entière a maintenant ses couleurs inversées avec juste une ligne de code dans le fragment shader. Super, n’est-ce pas ?

V-C-2. Niveau de gris

Un autre effet intéressant est de supprimer les couleurs à part le blanc, le gris et le noir afin d’avoir une image en nuances de gris. Une façon simple pour ce faire est de prendre chaque composante de la couleur et d’en faire la moyenne :

 
Sélectionnez
void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
    FragColor = vec4(average, average, average, 1.0);
}

Cela donne de bons résultats, mais l’œil humain est plus sensible aux teintes vertes et moins aux bleues. Pour obtenir un effet plus proche de la physique, nous devons ajouter des poids aux canaux de couleur :

 
Sélectionnez
void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
    FragColor = vec4(average, average, average, 1.0);
}
Image avec post-traitement d'une scène 3D OpenGL en nuance de gris

Vous pouvez ne pas voir la différence immédiatement, mais, avec des scènes plus complexes, l’ajout des poids permet d’avoir un résultat plus réaliste.

V-C-3. Effet de noyau

Un autre avantage d’effectuer un post-traitement sur une texture est que vous pouvez échantillonner les valeurs de couleurs des autres parties de la texture. Par exemple, nous pouvons obtenir les valeurs autour de la coordonnées de texture actuelle. Nous pouvons ainsi créer des effets intéressants en les combinant.

Un noyau (ou matrice de convolution) est une simple matrice de valeurs centrées sur le pixel actuel et qui permet de multiplier les pixels avec leur valeur correspondante dans le noyau puis d’en faire la somme afin d’obtenir une seule valeur finale. En résumé, en ajoutant un petit décalage aux coordonnées de texture du pixel actuel et ce dans toutes les directions, nous pouvons obtenir les valeurs de texture des alentours et les combiner grâce au noyau. Voici un exemple d’un noyau :

kitxmlcodelatexdvp\begin{bmatrix}2 & 2 & 2 \\ 2 & -15 & 2 \\ 2 & 2 & 2 \end{bmatrix}finkitxmlcodelatexdvp

Ce noyau prend les huit pixels alentour et les multiplie par 2 et le pixel courant par -15. Ce noyau permet donc d’ajouter un poids aux pixels à proximité et contre-balance le résultat en multipliant le pixel actuel par un poids négatif.

La plupart des noyaux que vous trouverez sur Internet donne un résultat de 1, si vous additionnez tous les poids du noyau. Si cela ne donne pas 1, cela signifie que la couleur résultante sera plus lumineuse ou plus sombre que sa valeur d’origine.

Les noyaux sont très pratiques lors du post-traitement, car ils sont très faciles à utiliser et il y a beaucoup d’exemples en ligne. Nous devons légèrement adapter le fragment shader pour gérer les noyaux. Nous allons partir du principe que tous les noyaux ont une taille de 3x3 (comme la majorité) :

 
Sélectionnez
const float offset = 1.0 / 300.0;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // haut gauche
        vec2( 0.0f,    offset), // haut
        vec2( offset,  offset), // haut droit
        vec2(-offset,  0.0f),   // gauche
        vec2( 0.0f,    0.0f),   // centre
        vec2( offset,  0.0f),   // droit
        vec2(-offset, -offset), // bas gauche
        vec2( 0.0f,   -offset), // bas
        vec2( offset, -offset)  // bas droit    
    );

    float kernel[9] = float[](
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    );
    
    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];
    
    FragColor = vec4(col, 1.0);
}

Dans le fragment shader, nous créons un tableau de neuf vec2 pour le décalage de la coordonnées de texture afin d’obtenir les pixels autour du pixel actuel. Le décalage est simplement une valeur constante que vous pouvez modifier comme vous le souhaitez. Ensuite, nous définissons le noyau, qui dans ce cas permet d’exacerber chaque couleur en prenant en compte les pixels avoisinants. Finalement, nous ajoutons le décalage aux coordonnées de texture lors de l’échantillonnage, puis multiplions les valeurs de la texture avec celle du noyau et en effectuons la somme.

Ce noyau donne ce résultat :

Post-traitement d'une scène OpenGL 3D pour exarcerber les couleurs

Cela peut donner des effets intéressants, notamment si le joueur suit une aventure narcotique.

V-C-3-a. Flou

Un noyau permettant de flouter l’image est défini ainsi :

kitxmlcodelatexdvp\begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix} / 16finkitxmlcodelatexdvp

Comme la somme donne un résultat de 16, l’utilisation directe du résultat de l’application du noyau va donner une couleur trop claire et nous devons donc diviser par 16. Le noyau devient donc :

 
Sélectionnez
float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
);

En changeant les valeurs du noyau, nous changeons complètement l’effet de post-traitement. Ainsi, on obtient :

Post-traitement d'une scène OpenGL 3D pour floutter le rendu

Un tel effet crée des possibilités intéressantes. Il est possible de varier le floutage suivant le temps pour créer l’effet d’un personnage saoul ou lorsqu’il ne porte pas ses lunettes. Le floutage permet aussi d’adoucir les valeurs des couleurs, comme nous en aurons besoin dans les prochains tutoriels.

Vous pouvez voir que, avec une simple implémentation d’un noyau de convolution, il est possible d’obtenir des effets intéressants. Voyons voir un effet moins populaire pour finir.

V-C-3-b. Détection des bordures

Ci-dessous, vous pouvez trouver un noyau permettant de détecter les bordures, similaire à celui pour exacerber les couleurs :

kitxmlcodelatexdvp\begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix}finkitxmlcodelatexdvp

Le noyau met en avant les bordures et assombrit le reste, ce qui est très utile si nous voulons ne voir que les bordures d’une image.

Post-traitement d'une scène 3D OpenGL avec un filtre de détection des bordures

Ce n’est pas une surprise si ce genre de noyau est utilisé dans les outils pour manipuler les images telles que Photoshop. Comme la carte graphique permet de traiter les fragments avec un haut niveau de parallélisme, nous pouvons manipuler les images pixel par pixel en temps réel avec une grande facilité. Les outils d’édition d’images utilisent de plus en plus les cartes graphiques pour les traitements.

V-D. Remerciements

Ce tutoriel est une traduction réalisée par Alexandre Laurent dont l’original a été écrit par Joey de Vries et qui est disponible sur le site Learn OpenGL.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Joey de Vries. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.