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

Apprendre OpenGL moderne

Quatrième partie : OpenGL avancé


précédentsommaire

XI. Anticrénelage

À un certain moment de notre aventure dans le monde du rendu, vous avez dû rencontrer des bords irréguliers sur vos modèles. La raison de ces bords escarpés est liée au fonctionnement du rasterizer transformant les données des sommets en fragments dans la scène. Voici un exemple d’une bordure escarpée qui peut être vue rien qu’avec un cube :

Containeur avec un aliasing visible

Même s’il n’est pas visible immédiatement, si vous regardez en détail les bordures du cube, vous pouvez voir un motif irrégulier. Si nous zoomons, nous pourrions voir ceci :

Containeur ayant de l'aliasing zoomé

Ce n’est pas vraiment ce que nous voulons dans notre version finale. Cet effet où l’on voit les pixels sur les bordures est appelé effet d’escalier (aliasing). Il y a quelques techniques justement nommées techniques de diminution de l’effet d’escalier ou d’anticrénelage (antialiasing) permettant de réduire cet effet et de produire des bordures plus douces.

Premièrement, nous avons une technique nommée super sample anti-aliasing (SSAA) qui utilise temporairement une résolution supérieure pour afficher la scène (super-échantillonnage), puis, lors de l’affichage dans le tampon d’image, la résolution est réduite pour revenir à la normale. Cette augmentation de résolution permet d’éviter l’effet d’escalier sur les bordures. Même si cela offre une solution au problème, la contrepartie en performance est énorme, car la carte graphique doit afficher beaucoup plus de fragments que d’habitude. Cette technique a toutefois eu un petit moment de succès.

Cette technique a donné naissance à des techniques modernes comme l’anticrénelage multi-échantillonné (multisample anti-aliasing, MSAA) qui emprunte certains concepts du SSAA, tout en offrant une approche plus efficace. Dans ce tutoriel, nous allons grandement parler de cette technique qui est incluse à OpenGL.

XI-A. Multiéchantillonnage

Pour comprendre ce qu’est le multiéchantillonnage et comment il fonctionne dans la réduction de l’effet d’escalier, nous devons d’abord nous plonger dans le fonctionnement du rasterizer d’OpenGL.

Le rasterizer est la combinaison d’algorithmes et de processus qui sont effectués entre le traitement de vos sommets et le fragment shader. Le rasterizer prend tous les sommets appartenant à une primitive et les transforme en un ensemble de fragments. Les coordonnées des sommets peuvent théoriquement être n’importe lesquelles, mais ce n’est pas le cas pour les fragments, car ils sont liés à la résolution de la fenêtre. Il n’y aura donc jamais de correspondance parfaite entre les coordonnées des sommets et les fragments. Le rasterizer doit donc déterminer à quel fragment/coordonnées correspond un sommet.

Image d'un triangle rasterizé en OpenGL

Ici, nous voyons une grille de pixels à l’écran, où le centre de chaque pixel contient un point d’échantillonnage qui est utilisé pour déterminer si le pixel est dans le triangle ou non. Les points en rouges sont ceux dans le triangle et un fragment sera généré pour ce pixel. Même si certaines parties du triangle couvre tel ou tel pixel, si le point d’échantillonnage n’est pas dans le triangle, il n’y aura pas de fragment shader exécuté pour ce pixel.

Vous pouvez certainement commencer à comprendre l’origine de l’effet d’escalier. Vous obtenez un triangle comme ceci sur votre écran :

Triangle rempli correspondant au résultat de la raterization en OpenGL

Comme le nombre de pixels à l’écran est limité, certains seront affichés en bordure du triangle, alors que d’autres non. Le résultat est que nous obtenons des primitives avec des bordures non douces, provoquant l’effet d’escalier vu précédemment.

Avec le multi-échantillonnage, au lieu d’avoir un seul point d’échantillonnage pour déterminer si le pixel est dans le triangle, nous utilisons plusieurs points (devinez d’où vient le nom). Au lieu d’un seul point au centre de chaque pixel, nous allons placer quatre suréchantillons suivant un certain motif et les utiliser pour déterminer si le pixel est dans le triangle. Cela signifie que la taille du tampon de couleurs est augmentée du nombre de suréchantillons que nous utilisons par pixel.

Sur-échantillonage (multisampling) en OpenGL

Du côté gauche de l’image, vous pouvez voir ce que nous aurions eu pour déterminer si le pixel est dans le triangle. Ce pixel n’exécutera pas de fragment shader (et restera donc blanc), car son point d’échantillonnage n’est pas couvert par le triangle. Du côté droit de l’image, chaque pixel est suréchantillonné, c’est-à-dire, contient quatre points d’échantillonnage. Ici, nous pouvons voir que seuls deux des points d’échantillonnage sont dans le triangle.

Le nombre de points d’échantillonnage peut être celui que vous souhaitez afin d’obtenir une meilleure précision.

C’est ici que le suréchantillonnage devient intéressant. Nous avons déterminé que deux suréchantillons sont dans le triangle. Il reste à déterminer la couleur du pixel. Notre hypothèse initiale est que nous exécutons le fragment shader pour chaque suréchantillon couvert, puis nous calculons la moyenne des couleurs des suréchantillons du pixel. Dans ce cas, nous exécutons le fragment shader deux fois et interpolons les données des sommets pour chaque suréchantillon et stockons le résultat. Heureusement, ce n’est pas ainsi que cela fonctionne, car cela voudrait dire que nous exécutons beaucoup plus de fragment shader que sans le suréchantillonnage, ce qui réduirait les performances.

En réalité, le MSAA fait que le fragment shader n’est exécuté qu’une seule fois par pixel (pour chaque primitive), peu importe le nombre de suréchantillons couverts par le triangle. Le fragment shader est exécuté avec les données des sommets interpolés au centre du pixel et la couleur obtenue est stockée dans chaque suréchantillon couvert. Une fois que les suréchantillons du tampon de couleurs sont remplis pour toutes les primitives que nous avons dessinées, une moyenne des couleurs est effectuée pour chaque pixel. Comme deux des quatre échantillons sont couverts par le triangle, la couleur du pixel est une moyenne entre la couleur du triangle et la couleur stockée dans les deux points d’échantillonnage (dans ce cas, une couleur claire), ce qui donne une couleur bleutée claire.

Le tampon de couleurs résultant où toutes les bordures des primitives sont plus douces. Voyons voir comment le résultat du suréchantillonage sur le triangle précédant :

Triangle rastérizé avec sur-échantillonnage en OpenGL

Chaque pixel contient quatre suréchantillons (les échantillons non utiles sont cachés) : les suréchantillons qui se trouvent dans le triangle sont bleus, ceux qui sont hors du triangle sont gris. À l’intérieur du triangle, tous les pixels exécutent le fragment shader une seule fois et le résultat sera stocké dans les quatre suréchantillons. Sur les bordures du triangle, le résultat du fragment shader ne sera stocké que dans certains suréchantillons. Suivant le nombre de suréchantillons à l’intérieur du triangle, la couleur du pixel sera déterminée grâce à la couleur du triangle et à la couleur des échantillons stockés.

Grossièrement, plus il y a de points suréchantillonnés dans le triangle, plus la couleur du pixel sera proche de celle du triangle. Si nous remplissons les pixels avec la couleur correspondante, nous obtiendrons l’image suivante :

Triangle rasterizé avec le sur-échantillonnage dans OpenGL

Pour chaque pixel, moins il y a de suréchantillons dans le triangle, moins le pixel aura la couleur du triangle. Les bordures du triangle sont entourées par des pixels de couleurs plus clairs, rendant ainsi les bords plus doux.

Non seulement les couleurs sont affectées par le suréchantillonnage, mais aussi par la profondeur et le test de pochoir. Pour le test de profondeur, celle-ci est interpolée pour chaque suréchantillon avant d’exécuter le test. Pour le test de pochoir, chaque valeur est stockée pour chaque suréchantillon et non pour chaque pixel. Cela veut donc dire que la taille de ces tampons est augmentée pour contenir les suréchantillons.

Nous avons donc vu un aperçu du fonctionnement du suréchantillonnage comme technique de suppression de l’effet d’escalier. La logique du rasterizer est plus compliquée que ce nous avons vu, mais vous devriez être capable de comprendre la logique.

XI-B. MSAA dans OpenGL

Si vous souhaitez utiliser le MSAA en OpenGL, vous devez utiliser un tampon de couleurs qui peut stocker plus d’une couleur par pixel (car le suréchantillonnage nécessite de stocker une couleur par point d’échantillonnage). Nous avons donc besoin d’un nouveau type de tampon qui peut stocker un certain nombre de suréchantillons : le tampon de suréchantillonnage (multisample buffer).

La plupart des systèmes de fenêtrage sont capables de fournir un tampon de suréchantillonnage à la place du tampon de couleurs par défaut. GLFW nous donne aussi cette possibilité et nous devons juste l’indiquer à GLFW, ainsi que le nombre d’échantillons à la place du tampon de couleurs, grâce à la fonction glfwWindowHint() avant de créer une fenêtre :

 
Sélectionnez
glfwWindowHint(GLFW_SAMPLES, 4);

Lorsque nous appelons la fonction glfwCreateWindow(), la fenêtre de rendu est créée, mais cette fois avec un tampon de couleurs contenant quatre suréchantillons par coordonnée à l’écran. GLFW crée aussi un tampon de profondeur et de pochoir avec quatre suréchantillons par pixel. Cela veut dire que la taille des tampons est augmentée d’un facteur quatre.

Maintenant que nous avons demandé des tampons suréchantillonnés à GLFW, nous devons activer le suréchantillonnage en appelant la fonction glEnable() avec GL_MULTISAMPLE. Avec la plupart des pilotes, le suréchantillonnage est activé par défaut, c’est donc un peu redondant, mais c’est toujours mieux de l’activer manuellement. C’est tout pour l’activation du suréchantillonnage.

 
Sélectionnez
glEnable(GL_MULTISAMPLE);

Une fois que le tampon d’image par défaut est attaché à des tampons suréchantillonnés, il ne nous reste plus qu’à activer le suréchantillonnage en appelant glEnable() et c’est bon. Comme les algorithmes de suréchantillonnage sont implémentés dans le rasterizer de votre pilote OpenGL, il n’y a rien que nous puissions faire. Si nous affichons maintenant le cube vert, nous verrions des bordures plus douces :

Image d'un cube sur-échantillonné en OpenGL

Évidemment, ce conteneur a des bords plus doux et il en sera de même avec les autres objets de votre scène. Vous pouvez trouver le code de cet exemple ici.

XI-C. MSAA hors écran

Comme c’est GLFW qui crée les tampons suréchantillonnés, l’activation du MSAA est plutôt facile. Si nous souhaitons utiliser notre propre tampon d’image, pour un quelconque rendu hors écran, nous devons générer le tampon suréchantillonné nous-même.

Il y a deux façons de créer des tampons suréchantillonnés à attacher au tampon d’image : les textures et les tampons de rendu, comme ce que nous avons vu dans le tutoriel sur les tampons d’image.

XI-C-1. Attache d’une texture suréchantillonnée

Pour créer une texture qui supporte le stockage de plusieurs points d’échantillonnage, nous utilisons la fonction glTexImage2DMultisample() avec la cible GL_TEXTURE_2D_MULTISAMPLE :

 
Sélectionnez
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

Le deuxième argument définit maintenant le nombre d’échantillons que nous souhaitons pour la texture. Si le dernier argument est GL_TRUE, l’image utilisera les mêmes échantillons et le même nombre de suréchantillons pour chaque texel.

Pour attacher une texture suréchantillonnée à un tampon d’image, nous utilisons glFramebufferTexture2D() mais avec le type de texture GL_TEXTURE_2D_MULTISAMPLE :

 
Sélectionnez
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);

Le tampon d’image est maintenant lié à un tampon de couleurs suréchantillonné correspondant à une texture.

XI-C-2. Tampon de rendu suréchantillonné

Comme pour les textures, la création des tampons de rendu suréchantillonnés n’est pas très difficile. C’est même plutôt simple, sachant que nous devons juste appeler la fonction glRenderbufferStorageMultisample() au lieu de glRenderbufferStorage() lorsque nous spécifions le tampon de rendu (actuellement lié) :

 
Sélectionnez
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);

La seule chose qui a changé ici est le paramètre supplémentaire après la cible du tampon de rendu, qui indique le nombre d’échantillons (ici, 4).

XI-C-3. Rendu dans un tampon d’image suréchantillonné

Le rendu dans un tampon d’image suréchantillonné s’effectue automatiquement. Chaque fois que nous dessinons quelque chose alors que ce tampon d’image est lié, le rasterizer tiendra compte des opérations de suréchantillonnage. Nous avons donc un tampon de couleurs suréchantillonné et/ou un tampon de profondeur et de pochoir. Toutefois, le tampon suréchantillonné est un peu spécial, car nous ne pouvons pas l’utiliser dans toutes les opérations, par exemple pour de l’échantillonnage dans un shader.

Une image suréchantillonnée contient plus d’informations qu’une image classique et nous devons donc en réduire ou changer sa résolution. Le changement de résolution d’une image suréchantillonnée se fait généralement avec la fonction glBlitFramebuffer(). Cette dernière permet de copier une région d’un tampon d’image vers une autre tout en résolvant le suréchantillonnage.

La fonction glBlitFramebuffer() transfère une région source indicée par quatre coordonnées écran vers une région cible aussi définie par quatre coordonnées écran. Rappelez-vous du tutoriel sur les tampons d’image, si nous lions GL_FRAMEBUFFER comme cible de tampon, cela lie aussi les cibles du tampon d’image en lecteur et en écriture. Nous pouvons aussi lier ces cibles individuellement, en liant les tampons d’image aux cibles GL_READ_FRAMEBUFFER et GL_DRAW_FRAMEBUFFER respectivement. La fonction glBlitFramebuffer() lit ces deux cibles pour déterminer qui est la source et qui est la destination. Nous pouvons ensuite transférer le tampon d’image suréchantillonné vers l’écran en copiant l’image vers le tampon d’image par défaut :

 
Sélectionnez
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);

Si ensuite nous affichions l’application, nous obtiendrions la même sortie que sans le tampon d’image : un cube vert rendu avec du MSAA et ayant des bords moins escarpés.

Image du cube sur-échantillonné en OpenGL

Vous pouvez trouver le code source ici.

Que faire si nous voulons utiliser la texture provenant du tampon d’image suréchantillonné afin de faire des choses comme du post-traitement ? Nous ne pouvons pas utiliser directement la texture suréchantillonnée dans le fragment shader. Ce que nous pouvons faire, par contre, c’est copier le tampon suréchantillonné dans un autre FBO avec une texture non suréchantillonnée attachée. Ensuite, nous pourrions utiliser le tampon de couleurs attaché pour le post-traitement. Cela veut dire que nous devrions générer un nouveau FBO qui agit uniquement comme un tampon d’image intermédiaire pour changer la résolution du tampon suréchantillonné. Ce processus ressemble à cela en pseudo-code :

 
Sélectionnez
unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
// créer ensuite un autre FBO avec une texture comme attache de couleurs
...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
...
while(!glfwWindowShouldClose(window))
{
    ...
    
    glBindFramebuffer(msFBO);
    ClearFrameBuffer();
    DrawScene();
    // maintenant, change la résolution du tampon suréchantillonné et place le résultat dans le FBO intermédiaire
    glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
    glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
    // maintenant, la scène est stockée dans une image 2D : on peut l’utiliser dans le post-traitement
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    ClearFramebuffer();
    glBindTexture(GL_TEXTURE_2D, screenTexture);
    DrawPostProcessingQuad();  
  
    ... 
}

Nous pouvons maintenant implémenter les effets de post-traitement du tutoriel des tampons d’image pour créer des effets intéressants avec une texture de la scène avec (pratiquement) plus de bordure escarpées. Avec le filtre de noyau de flou, cela donne :

Image d'un post-traitement sur une scène dessinée avec du MSAA et OpenGL

Comme la texture est une texture normale avec juste un point échantillonné, certains filtres de post-traitement comme celui de la détection de bordure produisent à nouveau des bordures escarpées. Pour contrer cela, vous pouvez flouter la texture après ou créer votre propre algorithme pour supprimer les effets d’escalier.

Vous pouvez voir que, lorsque vous voulez combiner le suréchantillonnage avec un rendu hors-écran, vous devez prendre en compte plus de choses. Toutes ces opérations sont nécessaires, car cela augmente grandement la qualité de la scène. Notez que l’activation du suréchantillonnage réduit la performance de votre application suivant le nombre d’échantillons utilisés. Au moment de l’écriture de ce tutoriel, l’utilisation de quatre échantillons pour le MSAA est courant.

XI-D. Algorithme personnalisé de suppression de l’effet d’escalier

Il est aussi possible de passer la texture suréchantillonnée au shader sans faire de traitement. Le GLSL nous donne l’option d’échantillonner une texture par suréchantillon afin de créer nos propres algorithmes de suppression de l’effet d’escalier, ce qui est courant dans les grandes applications graphiques.

Pour récupérer la couleur par suréchantillon, vous devez définir l’échantillonneur comme un sampler2DMS à la place du classique sampler2D :

 
Sélectionnez
uniform sampler2DMS screenTextureMS;

Avec la fonction texelFetch(), il est possible de récupérer la couleur par échantillon :

 
Sélectionnez
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 4e suréchantillon

Nous n’entrerons pas dans les détails de la création d’une technique de suppression d’effet d’escalier, mais donnons simplement des pointeurs sur la manière d’implémenter une fonctionnalité comme celle-ci.

XI-E. 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édentsommaire

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.