II. Test de pochoir (stencil test)▲
Le test de pochoir (stencil test) est réalisé juste après l’exécution du fragment shader. Tout comme le test de profondeur, il permet d’ignorer des fragments. Les fragments non ignorés subissent ensuite le test de profondeur qui peut, à son tour, exclure encore plus de fragments. Le test de pochoir repose sur le contenu d’un autre tampon, appelé le tampon de pochoir (stencil buffer) que nous pouvons mettre à jour au cours du rendu pour obtenir des effets intéressants.
Un tampon de pochoir contient (généralement) 8 bits par valeur, ce qui permet un total de 256 valeurs différentes pour chaque pixel. Nous pouvons définir ces valeurs comme bon nous semble, puis abandonner les fragments suivant ces valeurs.
Chaque bibliothèque de fenêtrage doit définir un tampon de pochoir. GLFW le fait automatiquement et vous n’avez pas besoin de lui dire d’en créer un, mais d’autres bibliothèques peuvent ne pas le créer par défaut. Vérifiez cet aspect dans la documentation de la bibliothèque que vous utilisez.
Voici un simple exemple de tampon de pochoir :
Le tampon de pochoir est d’abord initialisé avec des zéros, puis certaines valeurs du tampon sont passées à 1. Les fragments de la scène ne sont affichés que si la valeur de pochoir est à 1.
Les opérations sur le tampon de pochoir nous permettent de définir les valeurs du tampon lors du rendu des fragments. En changeant le contenu du tampon de pochoir au cours du rendu, nous écrivons dans celui-ci. Au cours du rendu d’une image (ou pour la prochaine image), nous pouvons lire ces valeurs pour ignorer certains fragments. Lors de l’utilisation des tampons de pochoir, vous pouvez faire ce que vous voulez, mais l’application suit souvent ce déroulement :
- activation en écriture du tampon de pochoir ;
- rendu des objets mettant à jour le tampon de pochoir ;
- désactivation en écriture du tampon de pochoir ;
- rendu des (autres) objets, dont certains fragments seront ignorés selon le contenu du tampon de pochoir.
En utilisant le tampon de pochoir, nous pouvons donc ignorer des fragments selon les fragments d’autres objets affichés dans la scène.
Vous pouvez activer le test de pochoir avec GL_STENCIL_TEST. À partir de ce moment, tous les appels de rendu vont influencer le tampon de pochoir :
glEnable
(
GL_STENCIL_TEST);
Remarquez que vous devez aussi nettoyer le tampon de pochoir avant chaque itération, tout comme pour le tampon de couleurs et celui de profondeur :
glClear
(
GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT);
Aussi, tout comme pour la fonction glDepthMask() pour le test de profondeur, il y a une fonction équivalente pour le tampon de pochoir. La fonction glStencilMask() permet de définir un masque bit à bit utilisé. Ce masque est utilisé pour réaliser un test logique ET avec la valeur de pochoir qui va être inscrite dans le tampon. Par défaut, le masque est complètement à 1, c’est-à-dire qu’il n’affecte pas la sortie, mais si nous définissions un masque 0x00, toutes les valeurs du tampon de pochoir finiront par être des 0. C’est équivalent à la désactivation du test de profondeur avec glDepthMask(GL_FALSE) :
glStencilMask
(
0xFF
); // chaque bit est écrit dans le tampon de pochoir tel quel
glStencilMask
(
0x00
); // chaque bit devient un 0 dans le tampon de pochoir (désactivation de l’écriture)
Dans la plupart des cas, vous ne définirez le masque qu’à 0x00 ou 0xFF, mais il est bon de savoir qu’il est possible de définir ce que l’on veut.
II-A. Fonctions de test du pochoir▲
Tout comme pour le test de profondeur, vous pouvez configurer la fonction de test pour le pochoir et ainsi déterminer la manière de mettre à jour le tampon. Pour cela, il y a deux fonctions : glStencilFunc et glStencilOp.
La fonction glStencilFunc(GLenum func, Glint ref, Gluint mask) a trois paramètres :
- func : la fonction de test. Elle est utilisée pour comparer la valeur stockée dans le tampon avec la valeur ref. Les options sont : GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, GL_EQUAL, GL_NOTEQUAL et GL_ALWAYS. Elles sont équivalentes aux fonctions pour le test de profondeur ;
- ref : indique la valeur de référence du test de pochoir, dont le contenu est comparé à cette valeur ;
- mask : indique un masque utilisé dans un ET logique avec la valeur de référence et la valeur du tampon avant le test. Par défaut, le masque est complètement à 1.
Donc, dans le cas d’un exemple simple, nous aurions :
glStencilFunc
(
GL_EQUAL, 1
, 0xFF
)
Ce code indique à OpenGL que, pour n’importe quelle valeur de pochoir du fragment égale (GL_EQUAL) à la valeur de référence 1, le fragment passe le test et est dessiné. Sinon, il est ignoré.
Cependant, glStencilFunc() ne décrit que ce qu’OpenGL doit faire avec le contenu du tampon de pochoir, mais n’indique pas comment mettre à jour le tampon. C’est pour cela qu’il y a glStencilOp().
La fonction glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass) contient trois options pour lesquelles nous pouvons indiquer l’action à prendre :
- sfail : l’action à faire lorsque le test échoue ;
- dpfail : l’action à prendre lorsque le test de pochoir réussit, mais pas le test de profondeur ;
- dppass : l’action à effectuer lorsque les tests de pochoir et de profondeur réussissent.
Pour chacune des options, vous pouvez indiquer les actions suivantes :
GL_KEEP |
La valeur actuelle est gardée. |
GL_ZERO |
La valeur de pochoir est mise à 0. |
GL_REPLACE |
La valeur de pochoir est remplacée par la valeur de référence indiquée avec glStencilValue(). |
GL_INCR |
La valeur de pochoir est incrémentée de 1, si elle est inférieure à la valeur maximale. |
GL_INCR_WRAP |
Idem à GL_INCR, mais la valeur est remise à 0 si le maximum est dépassé. |
GL_DECR |
La valeur de pochoir est décrémentée de 1 si elle est supérieure à la valeur minimale. |
GL_DECR_WRAP |
Idem à GL_DECR, mais la valeur est remise à la valeur maximale lorsque 0 est dépassé. |
GL_INVERT |
La valeur est inversée binairement. |
Par défaut, la fonction glStencilOp() est définie avec les options (GL_KEEP, GL_KEEP, GL_KEEP). Quoiqu’il arrive, le tampon garde ses valeurs. Le comportement par défaut ne met pas à jour le tampon de pochoir. Si vous voulez écrire dans le tampon, vous devez spécifier au moins une action différente pour l’un de ces cas.
Par conséquent, en utilisant les fonctions glStencilFunc() et glStencilOp(), nous pouvons spécifier précisément quand et comment le tampon de pochoir doit être mis à jour et nous pouvons aussi indiquer lorsque le test réussit ou non, c’est-à-dire, quand le fragment doit être oublié ou non.
II-B. Contour d’un objet▲
Il est difficile de comprendre le fonctionnement du test de pochoir rien qu’en lisant les sections précédentes. Voyons comment l’utiliser pour réaliser un effet mettant en avant le contour des objets.
Pour chaque objet (ici, seulement un), nous allons créer une petite bordure colorée autour des objets (combinés). Cet effet est particulièrement utile dans les jeux de stratégie, lorsque vous devez montrer les éléments sélectionnés par l’utilisateur. La procédure pour le réaliser est la suivante :
- définir la fonction de pochoir à GL_ALWAYS avant d’afficher les objets qui auront des bordures. Le tampon de pochoir sera rempli de 1 lors de l’affichage des objets ;
- afficher les objets ;
- désactiver l’écriture dans le tampon de pochoir et le test de profondeur ;
- agrandir légèrement les objets ;
- utiliser un autre fragment shader qui n’affiche que la couleur de bordure ;
- afficher les objets une nouvelle fois, mais seulement si la valeur de pochoir est différente de 1 ;
- réactiver l’écriture dans le tampon de pochoir et le test de profondeur.
Ce processus remplit le tampon de pochoir de 1 pour les fragments où les objets sont dessinés : lorsque nous souhaitons dessiner les bordures, nous dessinons une version plus grande des objets. Ainsi, lorsque le test de pochoir réussit, la version agrandie est affichée, ce qui correspond au contour de l’objet. Nous ignorons tous les fragments de l’objet plus grand qui sont compris dans l’objet original grâce au tampon de pochoir.
Nous allons d’abord créer un fragment shader basique qui affiche la couleur du contour. Nous définissons simplement une couleur en dur dans le shader :
void
main
(
)
{
FragColor =
vec4
(
0
.04
, 0
.28
, 0
.26
, 1
.0
);
}
Nous allons ajouter une bordure aux deux conteneurs, mais pas au sol. Nous allons donc d’abord dessiner le sol, puis les deux conteneurs (en écrivant dans le tampon de pochoir), finalement une version plus grande des conteneurs (en ignorant les fragments qui recouvrent les conteneurs précédemment affichés).
Nous devons d’abord activer le test de pochoir et définir les actions à prendre lorsque le test réussit ou échoue :
glEnable
(
GL_STENCIL_TEST);
glStencilOp
(
GL_KEEP, GL_KEEP, GL_REPLACE);
Si l’un des tests échoue, nous ne faisons rien. Nous gardons la couleur du tampon de pochoir. Si les deux tests (profondeur et pochoir) réussissent, on remplace la valeur de pochoir par la valeur de référence (1) définie avec glStencilFunc().
Nous nettoyons le tampon de pochoir avec des 0 et les conteneurs remplissent le tampon de 1 :
glStencilFunc
(
GL_ALWAYS, 1
, 0xFF
); // tous les fragments doivent mettre à jour le tampon de pochoir
glStencilMask
(
0xFF
); // active l’écriture dans le tampon de pochoir
normalShader.use
(
);
DrawTwoContainers
(
);
En utilisant la fonction GL_ALWAYS, nous nous assurons que tous les fragments des conteneurs vont mettre à jour le tampon de pochoir à 1. Comme les fragments réussissent toujours le test de pochoir, le tampon est mis à jour avec la valeur de référence à chaque fois que nous les dessinons.
Maintenant que le tampon de pochoir est à jour avec des 1, là où sont les conteneurs, nous pouvons dessiner la version agrandie des conteneurs, mais en désactivant l’écriture dans le tampon de pochoir :
glStencilFunc
(
GL_NOTEQUAL, 1
, 0xFF
);
glStencilMask
(
0x00
); // désactive l’écriture dans le tampon de pochoir
glDisable
(
GL_DEPTH_TEST);
shaderSingleColor.use
(
);
DrawTwoScaledUpContainers
(
);
Nous définissons la fonction de test à GL_NOTEQUAL pour s’assurer que nous ne dessinons que les parties du conteneur où le tampon de pochoir n’est pas à 1, donc que les parties qui ne recouvrent pas les conteneurs affichés précédemment. Notez que nous désactivons aussi le test de profondeur, afin que les bordures ne soient pas recouverte par le sol.
Aussi, assurez-vous que vous réactivez le test de profondeur une fois les opérations de rendu finies.
La routine d’affichage des contours de la scène doit ressembler à ceci :
glEnable
(
GL_DEPTH_TEST);
glStencilOp
(
GL_KEEP, GL_KEEP, GL_REPLACE);
glClear
(
GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT);
glStencilMask
(
0x00
); // ne pas mettre à jour le tampon de pochoir lors du dessin du sol
normalShader.use
(
);
DrawFloor
(
)
glStencilFunc
(
GL_ALWAYS, 1
, 0xFF
);
glStencilMask
(
0xFF
);
DrawTwoContainers
(
);
glStencilFunc
(
GL_NOTEQUAL, 1
, 0xFF
);
glStencilMask
(
0x00
);
glDisable
(
GL_DEPTH_TEST);
shaderSingleColor.use
(
);
DrawTwoScaledUpContainers
(
);
glStencilMask
(
0xFF
);
glEnable
(
GL_DEPTH_TEST);
Si vous avez compris le fonctionnement du test de pochoir, ce fragment de code ne devrait pas être compliqué. Sinon, essayez de relire les sections précédentes afin de mieux comprendre ce que chaque fonction fait, maintenant que vous connaissez la mise en pratique.
Le résultat de cet algorithme, appliqué à la scène du tutoriel du test de profondeur ressemble à ceci :
Le code source complet de ce programme de contour est disponible ici.
Vous pouvez voir que les bordures se rejoignent entre les deux conteneurs. Généralement, c’est l’effet voulu (imaginez-vous sélectionner une dizaine d’unités dans un jeu de stratégie). Si vous souhaitez une bordure complète pour chaque objet, vous devez nettoyer le tampon de pochoir pour chaque objet et être astucieux avec le tampon de profondeur.
L’algorithme de contour que vous venez de voir est utilisé dans de nombreux jeux pour mettre en avant les objets sélectionnés (par exemple, dans les jeux de stratégie) et un tel algorithme peut être implémenté dans une classe. Vous pouvez simplement définir un booléen dans la classe modèle pour dessiner l’objet avec ou sans bordures. Si vous souhaitez être créatif, vous pouvez même donner un aspect plus naturel aux bordures avec un post-traitement comme un flou gaussien.
Les tests de pochoir ont plusieurs utilités, autres qu’afficher le contour d’un objet, telles qu’afficher une texture dans un miroir afin que la texture corresponde à la forme du miroir ou afficher des ombres en temps réel avec une technique appelée « shadow volumes ». Les tampons de pochoir nous offrent un outil sympa de plus pour notre boîte à outils OpenGL.
II-C. 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.