Navigation▲
Tutoriel précédent : tampons de profondeur et de stencil |
Tutoriel suivant : geometry shader |
I. Tampons de rendu▲
Dans le chapitre précédent, nous avons vu les différents types de tampons qu'OpenGL offre : le tampon de couleurs, le tampon de profondeur et le tampon de pochoir (« stencil buffer »). Ces tampons occupent la mémoire vidéo comme tout autre objet OpenGL, mais jusqu'à présent nous n'avons eu que peu de contrôle sur ceux-ci à part le fait de spécifier le format de pixel lorsque vous avez créé le contexte OpenGL. Cette combinaison de tampons est connue comme le tampon de rendu (« framebuffer ») par défaut, et comme vous l'avez vu, un tampon de rendu est une zone en mémoire qui peut être utilisée pour le rendu. Et si vous souhaitiez récupérer le résultat du rendu et faire des opérations sur celui-ci, tel que du post-processing comme dans les jeux récents ?
Dans ce chapitre nous allons voir les objets de tampons de rendu (« framebuffer objects »), qui offrent une méthode pour créer un nouveau tampon de rendu à utiliser pour l'affichage. La bonne chose sur les tampons de rendu est qu'ils vous permettent de dessiner une scène directement dans une texture, qui peut être utilisée dans des opérations de rendu. Après la découverte du fonctionnement des tampons de rendu, je vais vous montrer comment les utiliser pour effectuer du post-processing dans la scène du chapitre précédent.
I-A. Créer un nouveau tampon de rendu▲
La première chose dont vous avez besoin est d'un objet tampon de rendu pour gérer le nouveau tampon de rendu.
GLuint frameBuffer;
glGenFramebuffers
(
1
, &
frameBuffer);
Vous ne pouvez toujours pas utiliser le tampon de rendu, car il n'est pas complet. Un tampon de rendu est généralement complet si :
- au moins un tampon a été attaché (par exemple un tampon de couleurs, de profondeur ou de pochoir) ;
- il doit au moins y avoir un attachement de couleur (OpenGL 4.1 et inférieur) ;
- tous les attachements doivent être complets (par exemple, un attachement de texture doit avoir sa mémoire réservée) ;
- tous les attachements doivent avoir le même nombre de multisamples.
Vous pouvez vérifier si un tampon de rendu est complet à n'importe quel moment en appelant glCheckFramebufferStatus et voir si la fonction retourne GL_FRAMEBUFFER_COMPLETE. Regardez la référence pour toutes les autres valeurs possibles. Vous n'avez pas besoin de le faire, mais il est généralement bon de le vérifier, tout comme vous vérifiez que vos shaders ont été compilés avec succès.
Maintenant, attachons le tampon de rendu pour travailler avec.
glBindFramebuffer
(
GL_FRAMEBUFFER, frameBuffer);
Le premier paramètre indique la cible à laquelle le tampon de rendu doit être attaché. OpenGL fait la distinction entre GL_DRAW_FRAMEBUFFER et GL_READ_FRAMEBUFFER. Le tampon de rendu attaché en lecture est utilisé dans les appels à glReadPixels (doc), mais comme cette distinction est plutôt rare dans les applications classiques, vous pouvez utiliser GL_FRAMEBUFFER pour lier le tampon aux deux types d'opérations directement.
glDeleteFramebuffers
(
1
, &
frameBuffer);
N'oubliez pas de nettoyer ce que vous avez fait.
I-B. Attachements▲
Votre tampon de rendu ne peut être utilisé comme cible de rendu que si la mémoire a été allouée pour stocker les résultats. Cela s'effectue en attachant des images à chaque tampon (couleurs, profondeur, pochoir ou une combinaison de la profondeur et du pochoir). Il y a deux types d'objets pouvant fonctionner comme images : les objets textures et les objets de tampon de rendu. L'avantage du premier est qu'il peut être directement utilisé dans les shaders comme vu dans les chapitres précédents, mais les objets de tampon de rendu peuvent être mieux optimisés lorsqu'utilisés comme cible de rendu suivant votre implémentation.
I-C. Image texture▲
Nous aimerions être capable d'afficher une scène et d'utiliser le résultat du tampon de couleurs dans une autre opération de rendu, donc une texture est le choix adéquat dans notre cas. La création d'une texture pour l'utiliser comme image pour le tampon de couleurs d'un nouveau tampon de rendu est aussi simple que de créer n'importe quelle texture.
GLuint 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);
La différence entre cette texture et les textures que vous avez vues dans les chapitres précédents est l'utilisation d'une valeur NULL pour le paramètre des données. Cela est logique, car les données vont être créées dynamiquement lors des opérations de rendu. Comme c'est une image pour le tampon de couleurs, les paramètres format et internalformat sont plus restreints. Le paramètre format va être limité aux valeurs GL_RGB ou GL_RGBA et le paramètre internalformat aux formats de couleurs.
Ici, j'ai choisi le format interne par défaut : RGB, mais vous pouvez expérimenter avec des formats plus exotiques comme GL_RGB10 si vous voulez 10 bits de précision pour les couleurs. Mon application a une résolution de 800 par 600 pixels, donc j'ai créé le nouveau tampon de couleurs pour correspondre à cela. La résolution n'a pas à correspondre avec l'un des tampons de rendu par défaut, mais n'oubliez pas l'appel à la fonction glViewport si vous décidez de faire autrement.
La seule chose restante est d'attacher l'image au tampon de rendu.
glFramebufferTexture2D
(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0
);
Le deuxième paramètre implique que vous pouvez avoir plusieurs attachements de couleurs. Un fragment shader peut fournir différentes données pour chacun d'eux en liant les variables de sortie aux attachements avec la fonction glBindFragDataLocation (doc) que nous avons utilisée plus tôt. Nous allons rester à une sortie pour le moment. Le dernier paramètre indique le niveau de mipmap de l'image qui doit être attachée. Le mipmapping n'est pas utile ici, car l'image du tampon de couleurs sera dessinée à la taille d'origine pour le post-process.
I-D. Image de l'objet de tampon de rendu▲
Comme nous utilisons un tampon de profondeur et de pochoir pour afficher notre cube de toute beauté, nous allons aussi les recréer. OpenGL vous permet de combiner ceux-ci dans une image unique, donc nous allons créer un autre tampon avant de pouvoir utiliser le tampon de rendu. Bien que nous puissions le faire en créant une autre texture, il est plus efficace de stocker ces tampons dans des objets de tampon de rendu, car nous ne sommes intéressé que par la lecture du tampon de couleurs dans un shader.
GLuint rboDepthStencil;
glGenRenderbuffers
(
1
, &
rboDepthStencil);
glBindRenderbuffer
(
GL_RENDERBUFFER, rboDepthStencil);
glRenderbufferStorage
(
GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800
, 600
);
La création d'un objet de tampon de rendu est très proche de la création d'une texture. La différence est que cet objet est conçu pour être utilisé comme une image au lieu d'un tampon de données générique telle une texture. J'ai choisi le format interne GL_DEPTH24_STENCIL8 qui est prévu pour contenir les tampons de profondeur et de pochoir avec respectivement 24 bits et 8 bits.
glFramebufferRenderbuffer
(
GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthStencil
);
L'attachement est tout aussi simple. Vous pouvez détruire cet objet tout comme n'importe quel objet grâce à la fonction glDeleteRenderbuffers (doc).
I-E. Utiliser un tampon de rendu▲
La sélection d'un tampon de rendu comme cible de rendu est très simple et, en réalité, se fait avec un seul appel.
glBindFramebuffer
(
GL_FRAMEBUFFER, frameBuffer);
Après cet appel, toutes les opérations de rendu stockeront le résultat dans les attachements du nouveau tampon de rendu. Pour revenir au tampon de rendu visible par défaut à l'écran, passez 0 à la fonction.
glBindFramebuffer
(
GL_FRAMEBUFFER, 0
);
Notez que bien que le tampon de rendu par défaut sera visible sur votre écran, vous pouvez lire n'importe quel tampon de rendu actuellement lié avec un appel à glReadPixels (doc) tant qu'il n'est pas lié au GL_DRAW_FRAMEBUFFER.
II. Post-processing▲
Dans les jeux de nos jours, les effets de post-process sont tout aussi importants que la scène actuellement dessinée à l'écran et évidemment certains résultats spectaculaires peuvent être accomplis de différentes façons. Les effets de post-process dans les graphismes de temps réel sont habituellement implémentés dans le fragment shaders avec comme entrée la scène dessinée sous la forme d'une texture. Les objets de tampon de rendu nous permettent d'utiliser une texture pour contenir le tampon de couleurs, donc nous pouvons les utiliser pour préparer l'entrée de l'effet du post-process.
Pour utiliser les shaders pour créer un effet de post-process pour une scène précédemment dessinée pour une texture, il est habituel de dessiner un rectangle 2D remplissant l'écran. De cette façon la scène originale avec l'effet remplit l'écran à sa taille originelle comme s'il était dessiné directement dans le tampon de rendu par défaut.
Bien sûr vous pouvez être créatif avec les tampons de rendu et les utiliser pour faire toute sorte de choses du portail à la simulation de caméra affichant le monde plusieurs fois sous plusieurs angles sur des moniteurs de surveillance ou sur des objets de l'image finale. Ces utilisations sont plus spécifiques, mais je vais vous les laisser comme exercice.
II-A. Modifier le code▲
Malheureusement, il est un peu plus difficile de couvrir les modifications du code pas à pas dans ce cas, d'autant plus si vous vous êtes éloigné du code d'exemple. Maintenant que vous savez comment créer et lier un tampon de rendu et avec un peu de soin, vous devriez être capable de modifier le code. Voyons globalement les étapes :
- premièrement, créez le tampon de rendu et vérifiez s'il est complet. Essayez de le lier comme cible de rendu et vous allez voir que votre écran passe au noir, car la scène n'est plus dessinée dans le tampon de rendu par défaut. Essayez de changer la couleur de réinitialisation de la scène et de lire le tampon avec la fonction glReadPixels (doc) pour vérifier si la scène dessine correctement dans le nouveau tampon de rendu ;
- ensuite, essayez de créer un nouveau program shader, objet de tableau de sommets et objet de tampon de sommets pour afficher des objets 2D par opposition aux objets 3D. Il est utile de revenir au tampon de rendu par défaut pour facilement voir les résultats. Votre shader 2D ne devrait pas avoir besoin de matrices de transformation. Essayez d'afficher un rectangle devant le cube 3D ;
- finalement, essayez de dessiner la scène 3D dans le tampon de rendu créé et le rectangle dans le tampon de rendu par défaut. Essayez maintenant d'utiliser la texture du tampon de rendu dans le rectangle pour afficher la scène.
J'ai choisi d'avoir deux positions et deux coordonnées de texture pour mon rendu 2D. Mon shaders 2D ressemble à ceci :
#
version
150
in
vec2
position;
in
vec2
texcoord;
out
vec2
Texcoord;
void
main
(
)
{
Texcoord =
texcoord;
gl_Position
=
vec4
(
position, 0
.0
, 1
.0
);
}
#
version
150
in
vec2
Texcoord;
out
vec4
outColor;
uniform
sampler2D
texFramebuffer;
void
main
(
)
{
outColor =
texture
(
texFramebuffer, Texcoord);
}
Avec ce shader, la sortie de votre programme devrait être la même qu'avant. Le rendu d'une image ressemble à ceci :
// Liaison de notre tampon de rendu et dessine la scène 3D (le cube)
glBindFramebuffer
(
GL_FRAMEBUFFER, frameBuffer);
glBindVertexArray
(
vaoCube);
glEnable
(
GL_DEPTH_TEST);
glUseProgram
(
sceneShaderProgram);
glActiveTexture
(
GL_TEXTURE0);
glBindTexture
(
GL_TEXTURE_2D, texKitten);
glActiveTexture
(
GL_TEXTURE1);
glBindTexture
(
GL_TEXTURE_2D, texPuppy);
// Dessin du cube
// Liaison du tampon de rendu par défaut et dessine le contenu de notre tampon de rendu
glBindFramebuffer
(
GL_FRAMEBUFFER, 0
);
glBindVertexArray
(
vaoQuad);
glDisable
(
GL_DEPTH_TEST);
glUseProgram
(
screenShaderProgram);
glActiveTexture
(
GL_TEXTURE0);
glBindTexture
(
GL_TEXTURE_2D, texColorBuffer);
glDrawArrays
(
GL_TRIANGLES, 0
, 6
);
Les opérations de dessin 3D et 2D possèdent leurs propres tableaux de sommets (cube contre rectangle), shader program (3D contre post process 2D) et textures. Vous pouvez voir que la liaison de la texture du tampon de couleurs est tout aussi facile que d'utiliser des textures classiques. N'oubliez pas que les appels comme glBindTexture (doc) changent l'état d'OpenGL et sont coûteux en termes de performances, donc essayez d'en faire aussi peu que possible.
Je pense qu'il n'y a pas besoin d'expliquer la structure générale du programme, certains préfèrent simplement regarder le code d'exemple et peut-être effectuer une comparaison avec diff entre ce code et celui du chapitre précédent.
III. Effets Post-processing▲
Je vais maintenant discuter de quelques effets de post-process intéressants, expliquer comment ils fonctionnent et à quoi ils ressemblent.
III-A. Manipulation des couleurs▲
L'inversion des couleurs est une option habituellement présente dans les programmes de manipulation d'images, mais vous pouvez aussi l'implémenter vous-même avec les shaders !
Sachant que les valeurs des couleurs sont des nombres à virgule flottante allant de 0.0 à 1.0, l'inversion d'un canal est aussi simple que de calculer 1.0 - canal. Si vous faites cela pour chaque canal (rouge, vert, bleu), vous allez obtenir la couleur inverse. Cela peut se faire ainsi dans le fragment shader :
outColor =
vec4
(
1
.0
, 1
.0
, 1
.0
, 1
.0
) -
texture
(
texFramebuffer, Texcoord);
Cela va aussi affecter le canal alpha, mais cela n'est pas important, car la transparence est désactivée par défaut.
Afficher une image en nuance de gris peut se faire naïvement en calculant la moyenne d'intensité de chaque canal :
outColor =
texture
(
texFramebuffer, Texcoord);
float
avg =
(
outColor.r +
outColor.g +
outColor.b) /
3
.0
;
outColor =
vec4
(
avg, avg, avg, 1
.0
);
Cela fonctionne bien, mais les humains sont plus sensibles au vert et moins au bleu, donc utiliser des poids par couleur donne une meilleure conversion.
outColor =
texture
(
texFramebuffer, Texcoord);
float
avg =
0
.2126
*
outColor.r +
0
.7152
*
outColor.g +
0
.0722
*
outColor.b;
outColor =
vec4
(
avg, avg, avg, 1
.0
);
III-B. Flou▲
Il y a deux techniques bien connues de floutage : le box blur et le flou gaussien. Le dernier produit des résultats de meilleure qualité, mais le premier est plus facile à implémenter et s'approche assez du flou gaussien.
Le floutage peut être réalisé en échantillonnant les pixels autour du pixel et en calculant la couleur moyenne.
const
float
blurSizeH =
1
.0
/
300
.0
;
const
float
blurSizeV =
1
.0
/
200
.0
;
void
main
(
)
{
vec4
sum =
vec4
(
0
.0
);
for
(
int
x =
-
4
; x <=
4
; x++
)
for
(
int
y =
-
4
; y <=
4
; y++
)
sum +=
texture
(
texFramebuffer,
vec2
(
Texcoord.x +
x *
blurSizeH, Texcoord.y +
y *
blurSizeV)
) /
81
.0
;
outColor =
sum;
}
Vous pouvez voir que 81 échantillons sont utilisés. Vous pouvez modifier le nombre d'échantillons sur les axes des X et Y pour contrôler le floutage. Les variables blurSize sont utilisées pour déterminer la distance entre chaque échantillon. Un nombre plus grand d'échantillons et une distance plus petite donnent une meilleure approximation, mais diminuent rapidement la performance donc essayez de trouver un bon compromis.
III-C. Filtre de Sobel▲
Le filtre de Sobel est souvent utilisé dans les algorithmes de détection de contours. Voyons à quoi il ressemble.
Le fragment shader ressemble à cela :
vec4
top =
texture
(
texFramebuffer, vec2
(
Texcoord.x, Texcoord.y +
1
.0
/
200
.0
));
vec4
bottom =
texture
(
texFramebuffer, vec2
(
Texcoord.x, Texcoord.y -
1
.0
/
200
.0
));
vec4
left =
texture
(
texFramebuffer, vec2
(
Texcoord.x -
1
.0
/
300
.0
, Texcoord.y));
vec4
right =
texture
(
texFramebuffer, vec2
(
Texcoord.x +
1
.0
/
300
.0
, Texcoord.y));
vec4
topLeft =
texture
(
texFramebuffer, vec2
(
Texcoord.x -
1
.0
/
300
.0
, Texcoord.y +
1
.0
/
200
.0
));
vec4
topRight =
texture
(
texFramebuffer, vec2
(
Texcoord.x +
1
.0
/
300
.0
, Texcoord.y +
1
.0
/
200
.0
));
vec4
bottomLeft =
texture
(
texFramebuffer, vec2
(
Texcoord.x -
1
.0
/
300
.0
, Texcoord.y -
1
.0
/
200
.0
));
vec4
bottomRight =
texture
(
texFramebuffer, vec2
(
Texcoord.x +
1
.0
/
300
.0
, Texcoord.y -
1
.0
/
200
.0
));
vec4
sx =
-
topLeft -
2
*
left -
bottomLeft +
topRight +
2
*
right +
bottomRight;
vec4
sy =
-
topLeft -
2
*
top -
topRight +
bottomLeft +
2
*
bottom +
bottomRight;
vec4
sobel =
sqrt
(
sx *
sx +
sy *
sy);
outColor =
sobel;
Tout comme le shader de floutage, quelques échantillons sont pris et combinés d'une manière intéressante. Vous pouvez trouver davantage de détails techniques sur Internet.
IV. Conclusion▲
La bonne chose à propos des shaders est que vous pouvez manipuler les images pixel par pixel en temps réel grâce aux immenses capacités de calcul parallèle de votre carte graphique. Il n'est pas surprenant que les nouvelles versions de Photoshop utilisent les cartes graphiques pour accélérer les opérations de manipulation des images ! Il y a d'autres effets complexes comme le HDR, le flou de mouvement et le SSAO (screen space ambient occlusion), mais ceux-ci nécessitent un peu plus de travail qu'un simple shader et sont donc hors du cadre de ce chapitre.
V. Exercices▲
VI. Remerciements▲
Cet article est une traduction autorisée dont le texte original peut être trouvé sur open.gl.
Navigation▲
Tutoriel précédent : tampons de profondeur et de stencil |
Tutoriel suivant : geometry shader |