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

Apprendre OpenGL moderne

Quatrième partie : OpenGL avancé


précédentsommairesuivant

III. Mélange des couleurs (blending)

Le mélange des couleurs (blending) est une technique classique pour implémenter la transparence entre objets dans OpenGL. La transparence est le fait d’avoir une couleur non unie sur l’intégralité (ou une partie) d’un objet. À la place, l’objet sera teinté avec les couleurs des objets se plaçant derrière lui. Une fenêtre teintée est un objet transparent : le verre a une couleur propre, mais la couleur finale contient la couleur de tous les objets derrière la fenêtre. Cela explique l’origine du nom, sachant que nous mélangeons plusieurs couleurs (de différents objets) vers une unique couleur. La transparence nous permet de voir à travers les objets.

Image d'une fenêtre complètement transparente et d'une fenêtre teintée

Les objets transparents peuvent être complètement transparents (ils laissent passer toutes les couleurs) ou partiellement transparents (ils laissent les couleurs passer tout en donnant sa propre couleur). Le niveau de transparence d’un objet est défini par sa valeur alpha qui est la quatrième composante d’un vecteur de couleur. Jusqu’à présent, nous avons toujours donné une valeur de 1,0 à cette quatrième composante, pour avoir des objets opaques. Une valeur alpha de 0,0 rend un objet complètement transparent. Une valeur de 0,5 indique que la couleur de l’objet doit prendre 50 % de sa propre couleur et 50 % de l’objet derrière.

Les textures que nous avons utilisées jusqu’à présent ne contenait que trois composantes par texel : rouge, vert et bleu, mais certaines textures peuvent intégrer un canal alpha avec une valeur alpha en plus pour chaque texel. La valeur alpha indique précisément quelles parties de la texture doit être transparente. Par exemple, la texture suivante a une valeur alpha de 0,25 sur les parties représentant le verre (normalement, la texture devrait être complètement rouge, mais comme elle est 75 % transparente, elle montre le fond du site, et semble avoir perdu du rouge) une valeur de 0,0 dans ses coins.

Texture d'une fenêtre avec transparence

Nous allons bientôt ajouter cette fenêtre à la scène, mais parlons d’abord d’une technique plus simple pour les textures qui sont soit totalement transparentes, soit totalement opaques.

III-A. Rejeter des fragments

Certaines images n’ont pas de transparence partielle, elles représentent quelque chose ou rien suivant la couleur de la texture. Prenons l’exemple de l’herbe : pour créer de l’herbe avec peu d’effort, vous copiez une texture d’herbe sur un rectangle 2D et placez ce rectangle dans la scène. Toutefois, l’herbe n’est pas vraiment rectangulaire donc vous devez afficher certaines parties de la texture tout en ignorant le reste.

La texture suivante suit ce concept : elle est soit complètement opaque (une valeur alpha de 1,0), soit totalement transparente (une valeur alpha de 0,0). Il n’y a jamais une valeur intermédiaire. Vous pouvez voir que, lorsqu’il n’y a pas d’herbe, l’image affiche le fond du site à la place :

Une texture d'herbe avec de la transparence

Ainsi, lorsque vous ajoutez de la végétation à votre scène, vous ne souhaitez pas voir le rectangle de l’image, mais juste l’herbe et voir à travers le reste de l’image. Nous souhaitons rejeter les fragments complètement transparents et ne pas les stocker dans le tampon d’image. Avant d’implémenter cette technique, nous devons apprendre à charger une texture transparente.

Pour charger une texture avec des valeurs alpha, il n’y a pas grand-chose à modifier. stb_image charge automatiquement le canal alpha lorsqu’il est présent, mais nous devons indiquer à OpenGL que notre texture utilise un canal alpha :

 
Sélectionnez
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

Aussi, assurez-vous que vous récupérez bien les quatre composantes de votre texture dans le fragment shader et non juste RGB :

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

Maintenant que vous savez comment charger des textures transparentes, il est temps d’ajouter quelques brins d’herbe dans notre scène du test de profondeur.

Nous créons un vecteur où nous ajoutons plusieurs glm::vec3 pour indiquer la position des brins d’herbe :

 
Sélectionnez
vector<glm::vec3> vegetation;
vegetation.push_back(glm::vec3(-1.5f,  0.0f, -0.48f));
vegetation.push_back(glm::vec3( 1.5f,  0.0f,  0.51f));
vegetation.push_back(glm::vec3( 0.0f,  0.0f,  0.7f));
vegetation.push_back(glm::vec3(-0.3f,  0.0f, -2.3f));
vegetation.push_back(glm::vec3( 0.5f,  0.0f, -0.6f));

Chaque brin d’herbe est affiché grâce à un simple rectangle avec la texture d’herbe. Ce n’est pas une représentation 3D parfaite de l’herbe, mais c’est bien plus performant que d’afficher des modèles complexes. Avec quelques astuces, notamment en ajoutant plusieurs brins d’herbe à une même position et leur donnant une rotation différente, vous pouvez obtenir de bons résultats.

Comme la texture d’herbe est appliquée à un objet rectangulaire, nous devons créer un autre VAO, remplir le VBO et définir les attributs de sommets. Après avoir dessiné le sol et les deux cubes, nous dessinons les brins :

 
Sélectionnez
glBindVertexArray(vegetationVAO);
glBindTexture(GL_TEXTURE_2D, grassTexture);  
for(unsigned int i = 0; i < vegetation.size(); i++) 
{
    model = glm::mat4();
    model = glm::translate(model, vegetation[i]);                
    shader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

Si vous exécutez l’application, vous devriez obtenir ceci :

En ne supprimant pas les parties transparentes de la texture, vous obtenez des artefacts étranges sur OpenGL

Le résultat est ainsi, car OpenGL ne sait pas quoi faire avec les valeurs alpha, ni même qu’il doit les ignorer. Nous devons le faire nous-même. Heureusement, c’est très facile avec les shaders. Le langage GLSL fournit la commande discard qui, une fois appelée, provoque l’abandon du fragment  : par conséquent, il n’aura aucun impact sur le tampon de couleurs. Nous pouvons vérifier dans le fragment shader si le fragment a une valeur alpha inférieure à un seuil donné et, grâce à cette commande, nous pouvons l’ignorer :

 
Sélectionnez
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    vec4 texColor = texture(texture1, TexCoords);
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

Nous vérifions si la couleur de la texture échantillonnée a une valeur alpha inférieure à 0,1 et, dans ce cas, nous ignorons le fragment. Le fragment shader nous permet de n’afficher que les fragments qui sont opaques. Voici la scène corrigée :

Brin d'herbes affichées avec un fragment shader ignorant les pixels transparents en OpenGL

Remarquez que, lors de l’échantillonnage des bordures de la texture, OpenGL interpole les valeurs de la bordure avec la prochaine répétition de la texture (car nous avons utilisé le paramètre GL_REPEAT). Habituellement, c’est le comportement voulu, mais lorsque nous utilisons des valeurs transparentes, le haut de la texture est interpolé avec le bas de la texture, qui, lui, est opaque. Cela forme une bordure semi-transparente autour du rectangle. Pour éviter cela, définissez le wrapping à GL_CLAMP_TO_EDGE lorsque vous utilisez des textures avec transparence :

 
Sélectionnez
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

Vous pouvez consulter le code source ici.

III-B. Mélange de couleurs

L’abandon des fragments est une bonne approche, mais elle ne permet pas d’afficher des images semi-transparentes ; nous ne pouvons que dessiner le fragment ou l’abandonner. Pour afficher des images avec différents niveaux de transparence, nous devons utiliser le mélange de couleurs. Comme pour la plupart des fonctionnalités d’OpenGL, nous pouvons activer le mélange de couleurs en activant GL_BLEND :

 
Sélectionnez
glEnable(GL_BLEND);

Maintenant que nous avons activé le mélange de couleurs, nous devons indiquer à OpenGL comment l’utiliser.

Dans OpenGL, le mélange se fait avec l’équation suivante :

kitxmlcodelatexdvp\bar{C}_{result} = \bar{\color{green}C}_{source} * \color{green}F_{source} + \bar{\color{red}C}_{destination} * \color{red}F_{destination}finkitxmlcodelatexdvp
  • kitxmlcodeinlinelatexdvp\bar{\color{green}C}_{source}finkitxmlcodeinlinelatexdvp : la couleur source. C’est la couleur provenant de la texture ;
  • kitxmlcodeinlinelatexdvp\bar{\color{red}C}_{destination}finkitxmlcodeinlinelatexdvp : la couleur de destination. C’est la couleur qui est actuellement dans le tampon de couleurs ;
  • kitxmlcodeinlinelatexdvp\color{green}F_{source}finkitxmlcodeinlinelatexdvp : le facteur de la source. Définit l’impact de la couleur source ;
  • kitxmlcodeinlinelatexdvp\color{red}F_{destination}finkitxmlcodeinlinelatexdvp : le facteur de destination. Définit l’impact de la couleur destination.

Après l’exécution du fragment shader et la réussite de tous les tests, l’équation de mélange est appliquée pour déterminer la couleur de sortie à partir de la couleur actuellement dans le tampon de couleurs (soit, la couleur du fragment précédemment sauvegardée). Les couleurs de la source et de la destination sont définies automatiquement par OpenGL, mais nous pouvons imposer le facteur de source et de destination. Voici un exemple simple :

Deux carrés ayant une valeur alpha inférieur à 1

Nous avons deux carrés et nous souhaitons superposer le vert sur le rouge. Le carré rouge sera la couleur de destination (et devra donc être la première couleur dans le tampon de couleurs). Ensuite, nous allons dessiner le carré vert au-dessus.

La question qui se pose est : quelle sont les valeurs des facteurs ? D’abord, nous souhaitons multiplier le carré vert avec sa valeur alpha, nous définissons donc kitxmlcodeinlinelatexdvpF_{src}finkitxmlcodeinlinelatexdvp à la valeur alpha de la couleur source, soit 0.6. Ensuite, il est logique que le carré de destination ait une valeur alpha du reste de la transparence. Si le carré vert donne 60 % de la couleur finale, alors le carré rouge fournit les 40 % restant, soit 1.0 – 0.6. Ainsi, nous avons défini kitxmlcodeinlinelatexdvpF_{destination}finkitxmlcodeinlinelatexdvp à la valeur alpha de la couleur source soustraite de 1. L’équation devient :

kitxmlcodelatexdvp\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \\ \color{green}{1.0} \\ \color{blue}{0.0} \\ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \\ \color{green}{0.0} \\ \color{blue}{0.0} \\ \color{purple}{1.0} \end{pmatrix} * \color{red}{(1 - 0.6)}finkitxmlcodelatexdvp

Le résultat donne une couleur 60 % verte et 40 % rouge :

Deux carrés où l'un d'entre eux à une valeur alpha inférieure à 1

La couleur résultante est alors stockée dans le tampon de couleurs, remplaçant l’ancienne.

Bien, mais comment faire cela avec OpenGL ? OpenGL fournit la fonction glBlendFunc() spécialement pour ça.

La fonction glBlendFunc(GLenum sfactor, Glenum dfactor) attend deux paramètres pour définir les facteurs source et destination. OpenGL définit quelques options dont nous décrirons ci-dessous les plus courantes. Notez que la constante kitxmlcodeinlinelatexdvp\bar{\color{blue}C}_{constant}finkitxmlcodeinlinelatexdvp peut être définie avec la fonction glBlendColor().

Option

Valeur

GL_ZERO

Le facteur est égal à 0.

GL_ONE

Le facteur est égal à 1.

GL_SRC_COLOR

Le facteur est égal à la couleur source kitxmlcodeinlinelatexdvp\bar{\color{green}C}_{source}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_SRC_COLOR

Le facteur est égal à 1 moins la couleur source kitxmlcodeinlinelatexdvp1 - \bar{\color{green}C}_{source}finkitxmlcodeinlinelatexdvp.

GL_DST_COLOR

Le facteur est égal à la couleur de destination kitxmlcodeinlinelatexdvp\bar{\color{red}C}_{destination}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_DST_COLOR

Le facteur est égal à 1 moins la couleur de destination kitxmlcodeinlinelatexdvp1 - \bar{\color{red}C}_{destination}finkitxmlcodeinlinelatexdvp.

GL_SRC_ALPHA

Le facteur est égal à la composante alpha de la couleur source kitxmlcodeinlinelatexdvp\bar{\color{green}C}_{source}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_SRC_ALPHA

Le facteur est égal à kitxmlcodeinlinelatexdvp1 - alphafinkitxmlcodeinlinelatexdvp de la couleur source kitxmlcodeinlinelatexdvp\bar{\color{green}C}_{source}finkitxmlcodeinlinelatexdvp.

GL_DST_ALPHA

Le facteur est égal à la composante alpha de la couleur de destination kitxmlcodeinlinelatexdvp\bar{\color{red}C}_{destination}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_DST_ALPHA

Le facteur est égal à kitxmlcodeinlinelatexdvp1 - alphafinkitxmlcodeinlinelatexdvp de la couleur de destination kitxmlcodeinlinelatexdvp\bar{\color{red}C}_{destination}finkitxmlcodeinlinelatexdvp.

GL_CONSTANT_COLOR

Le facteur est égal à la couleur constante kitxmlcodeinlinelatexdvp\bar{\color{blue}C}_{constant}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_CONSTANT_COLOR

Le facteur est égal à 1 moins la couleur constante kitxmlcodeinlinelatexdvp\bar{\color{blue}C}_{constant}finkitxmlcodeinlinelatexdvp.

GL_CONSTANT_ALPHA

Le facteur est égal à la composante alpha de la couleur constante kitxmlcodeinlinelatexdvp\bar{\color{blue}C}_{constant}finkitxmlcodeinlinelatexdvp.

GL_ONE_MINUS_CONSTANT_ALPHA

Le facteur est égal à kitxmlcodeinlinelatexdvp1 - alphafinkitxmlcodeinlinelatexdvp de la couleur constante kitxmlcodeinlinelatexdvp\bar{\color{blue}C}_{constant}finkitxmlcodeinlinelatexdvp.

Pour obtenir le même résultat que dans l’exemple précédent, nous devons prendre la composante kitxmlcodeinlinelatexdvpalphafinkitxmlcodeinlinelatexdvp de la couleur source comme source et kitxmlcodeinlinelatexdvp1 - alphafinkitxmlcodeinlinelatexdvp pour le facteur destination. Cela correspond à l’appel suivant :

 
Sélectionnez
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Il est aussi possible de définir des options différentes pour les canaux RGB et alpha avec la fonction glBlendFuncSeparate() :

 
Sélectionnez
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

Cette fonction définit les composantes RGB aux mêmes valeurs que précédemment, mais fait en sorte que seule la couleur alpha résultante soit influencée par la valeur alpha source.

OpenGL nous offre encore plus de flexibilité en nous permettant de changer l’opérateur utilisé dans l’équation mêlant la source à la destination. Jusqu’à présent, la source et la destination sont ajoutées, mais nous pouvons les soustraire si nous le souhaitons. Cela est grâce à la fonction glBlendEquation(GLenum mode) qui nous permet trois options :

  • GL_FUNC_ADD : l’opération par défaut, additionne les deux composantes : kitxmlcodeinlinelatexdvp\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}finkitxmlcodeinlinelatexdvp ;
  • GL_FUNC_SUBTRACT : soustrait les deux composantes entre elles : kitxmlcodeinlinelatexdvp\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}finkitxmlcodeinlinelatexdvp ;
  • GL_FUNC_REVERSE_SUBTRACT : soustrait les deux composantes, mais dans l’autre sens : kitxmlcodeinlinelatexdvp\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}finkitxmlcodeinlinelatexdvp.

Généralement, glBlendEquation() n’est pas appelée, car l’option GL_FUNC_ADD correspond à la majorité des cas. Mais si vous voulez tout faire pour ne pas être dans le courant, alors n’importe quelle autre option vous conviendra.

III-C. Affichage de textures semi-transparentes

Maintenant que nous savons comment OpenGL gère la transparence, nous pouvons ajouter plusieurs fenêtres semi-transparentes. Nous allons utiliser la même scène qu’au début du tutoriel, mais au lieu de la texture d’herbe, nous utilisons la texture de la fenêtre transparente.

Premièrement, lors de l’initialisation, nous activons la fusion des couleurs et définissons l’équation de fusion :

 
Sélectionnez
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Comme nous activons la transparence, il n’y a plus besoin d’ignorer des fragments. Le fragment shader correspond à sa version originale :

 
Sélectionnez
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

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

Cette fois (en fait, chaque fois qu’OpenGL affiche un fragment), la couleur du fragment est mélangée à la couleur du fragment présente dans le tampon de couleurs suivant sa valeur alpha. Comme le verre de la texture de la fenêtre est semi-transparent, nous pouvons voir le reste de la scène en regardant à travers.

Une scène OpenGL avec transparence, mais avec l'ordre d'affichage incorrect

Si vous regardez de plus près, vous allez voir que cela ne va pas. Les parties transparentes de la première fenêtre cachent les fenêtres derrière. Que se passe t-il ?

La raison est que le test de profondeur en conjonction avec la transparence ne font pas bon ménage. Lorsque vous écrivez dans le tampon de profondeur, le test de profondeur ne prend pas en compte la transparence du fragment, donc les parties transparentes sont écrasées dans le tampon de profondeur. Le résultat est que le rectangle complet de la fenêtre passe dans le test de profondeur, peu importe sa transparence. Même si la partie transparente fait que la fenêtre derrière soit visible, le test de profondeur ignore cela.

Par conséquent, nous ne pouvons pas simplement afficher les fenêtres comme nous le voulons et espérer que le tampon de profondeur règle les problèmes pour nous ; c’est aussi là que l’implémentation de la transparence devient un peu moins évidente. Pour s’assurer que les fenêtres à l’arrière de la scène s’affichent, nous devons dessiner les fenêtres en partant de la fin. Cela veut dire que nous devons trier les fenêtres de la plus lointaine à la plus proche selon la position de la caméra.

Avec les objets complètement transparents, comme l’herbe, nous avons l’option de simplement ignorer les fragments transparents, ce qui nous évite des complications (pas de problème avec la profondeur).

III-C-1. Ne pas casser l’ordre

Pour que la transparence fonctionne avec plusieurs objets, nous devons dessiner les objets les plus éloignés avant ceux qui sont plus proches. Les objets normaux (non transparents) peuvent être dessinés comme à l’habitude, ils ne doivent donc pas être triés. Nous devons nous assurer qu’ils sont affichés avant les objets transparents (triés). Lors du dessin d’une scène avec des objets transparents et non transparents, l’algorithme d’affichage est le suivant :

  • dessiner les objets opaques ;
  • trier les objets transparents ;
  • dessiner les objets transparents suivant le tri.

Une façon de trier les objets transparents est d’utiliser la distance entre l’objet et le spectateur. Cela peut se faire en prenant la distance entre le vecteur position de la caméra et le vecteur position de l’objet. Ensuite, nous stockons la distance avec son vecteur position dans une table de hachage (std::map). La std::map trie automatiquement les objets suivant leur clé : une fois que nous avons ajouté toutes les positions avec leur distance comme clé, ils sont automatiquement triés suivant la distance. Cela donne le code suivant :

 
Sélectionnez
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
    float distance = glm::length(camera.Position - windows[i]);
    sorted[distance] = windows[i];
}

Le résultat est un conteneur d’objets triés qui stocke les positions de chaque fenêtre suivant leur distance, du plus proche au plus éloigné.

Ensuite, au moment du rendu, nous prenons chaque valeur de la table de hachage dans l’ordre inverse (du plus loin au plus proche) et nous dessinons la fenêtre correspondante :

 
Sélectionnez
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it) 
{
    model = glm::mat4();
    model = glm::translate(model, it->second);
    shader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

Nous utilisons un itérateur inversé de la std::map pour parcourir les éléments et placer le rectangle des fenêtres à leur position. Cette approche simple permet de corriger le problème précédent et d’obtenir la scène suivante :

Image d'une scène OpenGL avec transparence. Les objets sont triés suivant leur distance

Vous pouvez trouver le code source complet ici.

Même si le tri des objets selon leur distance fonctionne correctement pour certains scénarios, il ne prend pas en compte les transformations (rotations, redimensionnement) ni les objets ayant une forme plus complexe.

Le tri des objets dans la scène est compliqué (et coûteux en termes de performance) et dépend grandement du type de scène que vous avez. L’affichage d’une scène avec des objets solides et transparents n’est pas aussi simple. Il existe des techniques avancées comme la transparence indépendante de l’ordre d’affichage (order independent transparency), mais ceux-ci dépassent le cadre de ce tutoriel. Pour le moment, vous devez continuer ainsi, mais si vous êtes prudent et que vous connaissez les limitations, vous pouvez déjà obtenir des implémentations décentes de la transparence.

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