1. Introduction▲
1-A. Avant tout▲
Pour lire ce tutoriel, il est nécessaire de connaître le fonctionnement d'OpenGL. Je ne reviendrai pas sur les bases d'un programme OpenGL affichant une scène ni sur l'architecture de cette bibliothèque reposant sur une machine à états. Le pipeline fixe étant devenu obsolète, je passerai volontairement quelques explications par rapport à cet ancien modèle. Sachant que les Frame Buffer Objects sont étroitement liés avec les textures OpenGL, il est donc nécessaire de savoir créer et utiliser des textures avec OpenGL. Finalement, il peut être utile de lire les articles sur les extensions OpenGL et sur les shaders GLSL.
1-B. Qu'est-ce que c'est ?▲
Tout d'abord, regardons ce diagramme simplifié du pipeline graphique :
Nous voyons que le programme OpenGL commence le calcul de l'image en passant par le vertex shader, puis le fragment shader. Une fois le fragment shader exécuté, plusieurs canaux sont remplis :
- pour la couleur ;
- pour la profondeur ;
- pour le stencil buffer ;
- pour l'accumulation.
L'ensemble de ces canaux est appelé un FrameBuffer (littéralement un tampon d'image). Par défaut, le canal « couleur » est copié vers l'écran.
Dans les jeux vidéo, certaines techniques demandent à lire les pixels affichés à l'écran afin de les réutiliser comme source d'information (telle que la technique de calcul des ombres, appelées « shadow mapping ») ou d'appliquer des effets supplémentaires sur l'image (post process). Pour ce faire, il était nécessaire de demander à OpenGL de lire le tampon mémoire de sortie et de renvoyer les informations au programme OpenGL. Cette technique est coûteuse, car elle repose sur la vitesse de transfert GPU/CPU. Une autre technique mise en place à l'époque a été d'utiliser des pixel buffers (pbuffers) afin d'avoir un espace de stockage pour le rendu. Le problème de ce dernier est que les pbuffers étaient gérés par le système de fenêtrage, forçant l'utilisateur à créer différents contextes pour les différents formats de texture voulus. De plus, le changement entre la fenêtre principale et ce second buffer pouvait être lent.
Afin de répondre à une demande de plus en plus importante, le consortium autour d'OpenGL a décidé de faire une extension simple et optimisée permettant de faire du rendu hors écran.
L'extension GL_ARB_framebuffer_object, apporte de nouveaux éléments au sein d'OpenGL. Le premier, nommé « image attachable au renderbuffer » correspond à un objet pouvant être attaché à un framebuffer afin qu'il soit utilisé comme source et destination du framebuffer. L'information de remplissage est soit la couleur, la profondeur ou le stencil. Actuellement, il existe deux objets de ce type :
- les textures ;
- les renderbuffers.
Les buffers de profondeur et les stencil buffers possédaient des formats de pixels non représentables dans de simples textures. Afin de conserver ces buffers à la fin du rendu, les renderbuffers ont été intégrés. Il est possible de les utiliser lorsque nous n'avons pas besoin de texture dans la suite de notre rendu.
Ainsi, pour faire un rendu vers une texture, il suffit d'attacher cette texture à l'un des canaux et d'effectuer le rendu. Ensuite, comme toute autre texture, elle est utilisable dans le fragment shader.
Le second élément est le FrameBuffer Object. C'est un objet OpenGL permettant de gérer facilement les collections d'attachements. Grâce à cet objet, il est facile d'avoir plusieurs ensembles de textures de destination ou encore de changer simplement les textures attachées.
1-C. Lexique▲
- rendering : convertir des données en une image visible à l'écran.
pixel : point lumineux à l'écran.
fragment : informations telles que la couleur, la profondeur… correspondant à un pixel. Devient pixel après blending, depth test, etc.
buffer : tableau de fragments. Exemples de buffers : colors buffer, depth buffer, stencil buffer, accumulation buffer . Les informations contenues dans chaque fragment correspondent au type de buffer. Par exemple, pour un color buffer, chaque fragment contient une couleur, codée selon le système RGB (les intensités des canaux rouge, vert et bleu d'un pixel sont codées sur un octet chacune).
Buffer logique : un des canaux du frame buffer (couleurs, profondeur, stencil).
Frame buffer : un ensemble de buffers logiques et d'états définissant l'emplacement où le rendu OpenGL sera effectué.
Frame buffer object : objet représentant un frame buffer.
Render buffer : nouvel espace de stockage contenant un simple tableau 2D de pixels pouvant être utilisé comme cible de rendu.
Renderbuffer object : objet représentant un render buffer.
Attacher : le fait de lier un objet avec un autre. Dans le cas du frame buffer, le fait d'attacher un objet au frame buffer permettra à celui-ci de l'utiliser comme endroit où faire le rendu.
Passe(s) : nous parlons de passe, le fait d'effectuer une opération de dessin sur la carte graphique.
Rendu direct : le rendu direct est un rendu en une seule et unique passe. C'est le rendu que nous utilisons très couramment afin d'effectuer un affichage simple des objets.
Rendu différé : le rendu différé est un rendu constitué de plusieurs passes. Généralement, dans les premières passes, la scène est rendue dans des textures qui seront utilisées dans les passes suivantes.
1-D. Est-ce vraiment plus rapide ?▲
Les frame buffer objects ont l'avantage de permettre de conserver et d'utiliser les informations calculées par le GPU dans la mémoire du GPU. En effet, avec les anciennes méthodes, il fallait faire des allers-retours entre le programme OpenGL et le GPU. Maintenant, les données restent sur le GPU, les programmes ne subissent plus des latences à cause du bus de données.
Pour certaines techniques de rendu, telles que le calcul des lumières, il est plus efficace de conserver toutes les informations de la scène (position dans le monde, couleurs, normales) dans des textures et des les réutiliser pour toutes les sources de lumière. De plus, si vous souhaitez effectuer des effets sur l'image finale, il vous suffit de dessiner la scène dans une texture et de lui appliquer l'effet à l'aide d'un shader.
2. Implémentation des Frame Buffer Objects▲
2-A. Fonctionnement▲
Un FBO est un objet OpenGL qui correspond au buffer à remplir en sortie du pipeline graphique. Celui-ci possède plusieurs canaux de sortie dans lesquels le fragment shader écrira. Parmi ceux-ci, nous comptons :
- les canaux de couleurs (lorsque nous dessinons à l'écran, nous n'en utilisons qu'un seul, mais les FBO permettent d'en avoir plusieurs) ;
- le canal de profondeur ;
- le canal stencil.
Le nombre de canaux de couleurs dépend de la carte graphique. Le minimum défini par la spécification est quatre.
Pour chaque canal, vous pouvez soit attacher une texture, soit un render buffer.
Voici un petit schéma récapitulatif du fonctionnement :
Lorsque le FBO est actif, le shader écrira dans les canaux. Pour les canaux attachés à des textures ou des render buffer, les informations iront directement dans ces objets.
2-B. La bibliothèque OpenGL et les FBO▲
2-B-1. Évolution des versions▲
Au début, les FBO ont été ajoutés à OpenGL 2.x sous la forme d'une extension appelée « GL_ARB_framebuffer_object ». Depuis OpenGL 3, les frame buffers sont directement intégrés comme fonctionnalités du cœur d'OpenGL. Entre les deux versions, seuls les noms des fonctions et énumérations changent en perdant le suffixe ARB depuis la normalisation de la fonctionnalité. Cet article est écrit en OpenGL 3.
2-B-2. Énumérations▲
Une série d'énumérations a été créée pour permettre la gestion des FBO. Voici un tableau des valeurs les plus utilisées. Consulter la documentation pour plus d'informations :
Valeurs |
Description |
Fonctions |
GL_FRAMEBUFFER |
Description du buffer à utiliser. GL_FRAMEBUFFER est une union de GL_READ_FRAMEBUFFER et GL_DRAW_FRAMEBUFFER. |
GlFramebufferTexture |
GL_RENDERBUFFER |
Désigne un render buffer. |
BindRenderbuffer |
RENDERBUFFER_WIDTH |
Indicateurs pour récupérer des informations sur les render buffers à passer au paramètre pname. |
GetRenderbufferParameteriv |
FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE |
Indicateurs pour récupérer des informations sur le frame buffer, à passer au paramètre pname de GetFramebufferAttachmentParameteriv. |
GetFramebufferAttachmentParameteriv |
COLOR_ATTACHMENT0 COLOR_ATTACHMENT1 COLOR_ATTACHMENT2 COLOR_ATTACHMENT3 COLOR_ATTACHMENT4 COLOR_ATTACHMENT5 COLOR_ATTACHMENT6 COLOR_ATTACHMENT7 COLOR_ATTACHMENT8 COLOR_ATTACHMENT9 COLOR_ATTACHMENT10 COLOR_ATTACHMENT11 |
Valeurs désignant les différents canaux des FBO. |
FramebufferTexture |
FRAMEBUFFER_COMPLETE FRAMEBUFFER_INCOMPLETE_ATTACHMENT FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER FRAMEBUFFER_INCOMPLETE_READ_BUFFER |
Valeurs identifiant les erreurs liées aux FBO. |
CheckFramebufferStatus |
INVALID_FRAMEBUFFER_OPERATION |
Nouvelle valeur identifiant une erreur liée aux FBO. |
glGetError() |
2-B-3. Fonctions▲
- void glGenFramebuffers (Glsizei n, Gluint* ids) :
créer un ou des FBO.
n : nombre de FBO à créer.
ids : tableau où les identifiants des FBO créés se trouveront.
- void glBindFramebuffer (Glenum target, Gluint framebuffer) :
lie le frame buffer au contexte cible 'target' (les appels suivants aux fonctions OpenGL seront effectués sur ce FBO).
Target : la cible sur laquelle le FBO sera attaché. Cela peut être GL_READ_FRAMEBUFFER afin de désigner le framebuffer de lecture, ou GL_DRAW_FRAMEBUFFER pour celui d'écriture. Si vous voulez attacher un FBO aux deux buffers à la fois, vous pouvez utiliser GL_FRAMEBUFFER.
framebuffer : l'identifiant du framebuffer que vous souhaitez attacher.
- void glGenRenderbuffers (Glsizei n, Gluint* renderbuffers) :
génère un ou des render buffers.
n :nombre de render buffers à créer.
renderbuffers : tableau où les identifiants des render buffers créés se trouveront.
- void glBindRenderbuffer (Glenum target, Gluint renderbuffer) :
lie le render buffer au contexte courant.
target : doit être GL_RENDERBUFFER.
renderbuffer : l'identifiant du render buffer à lier.
- void glRenderbufferStorage (Glenum target, Glenum internalformat, Glsizei width, Glsizei height) :
génère un render buffer à partir du format et des dimensions.
target : doit être GL_RENDERBUFFER.
internalformat : le format du render buffer.
width : la largeur du render buffer.
height : la hauteur du render buffer.
- void glFramebufferTexture (Glenum target, Glenum attachment, Gluint texture, Glint level) :
indique la texture à attacher au FBO.
target : le FBO cible (en lecture et/ou en écriture).
attachment : le canal auquel nous attachons la texture (GL_COLOR_ATTACHMENT, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT ou GL_DEPTH_STENCIL_ATTACHMENT)
texture : l'identifiant de texture à attacher au FBO.
level : indique le niveau de mipmap de la texture attachée.
- GLsync glFramebufferRenderbuffer (Glenum target, Glenum attachment, Glenum renderbuffertarget, Gluint renderbuffer) :
indique le render buffer à attacher au FBO.
target : le FBO cible (en lecture et/ou en écriture).
attachment : le canal auquel nous attachons le render buffer.
renderbuffertarget : GL_RENDERBUFFER.
renderbuffer : l'identifiant sur le render buffer à attacher.
- Glenum glCheckFramebufferStatus (Glenum target) :
vérifie si le FBO est correctement construit.
target : GL_DRAW_FRAMEBUFFER ou GL_READ_FRAMEBUFFER ou GL_FRAMEBUFFER
Renvoie un code d'erreur.
- Void glDrawBuffers (Glsizei n, const Glenum* bufs) :
spécifie la liste des canaux de couleurs qui seront utilisés.
n : le nombre de canaux dans la liste.
bufs : pointeur sur les canaux dans lesquels le fragment shader écrira
- void glBlitFramebuffer (Glint srcX0, Glint srcY0, Glint, srcX1, Glint srcY1, Glint dstX0, Glint dstY0, Glint dstX1, Glint dstY1, Glbitfield mask, Glenum filter) :
copie le FBO lié à GL_READ_FRAMEBUFFER dans le frame buffer lié à GL_DRAW_FRAMEBUFFER. Cette fonction n'est accessible qu'à partir d'OpenGL 3. Pour l'utiliser avec une version plus ancienne d'OpenGL, il vous faudra charger l'extension GL_EXT_framebuffer_blit.
SrcX0 srcY0 : coin supérieur gauche à partir duquel l'image sera copiée.
srcX1 srcY1 : coin inférieur droit jusqu'où l'image sera copiée.
dstX0 dstY0 : coin supérieur gauche à partir duquel l'image sera collée.
dstX1 dstY1 : coin inférieur droit jusqu'où l'image sera collée.
mask : le canal à copier (GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT et/ou GL_STENCIL_BUFFER_BIT).
filter : interpolation appliquée durant la copie (GL_NEAREST ou GL_LINEAR).
2-C. Mise en place▲
Maintenant que nous avons pris connaissance des fonctions pour les FBO, nous pouvons commencer à les utiliser.
Le schéma suivant récapitule les différentes actions afin d'utiliser les FBO.
Il est aussi possible de changer de texture et de render buffer dans la boucle d'affichage. Pour cela, il suffit d'attacher la nouvelle ressource avec glFramebufferTexture ou glFramebufferRenderbuffer et de valider celle-ci avec glCheckFramebufferStatus.
2-D. Les FBO dans les shaders▲
Pour finaliser l'intégration des FBO, il est aussi nécessaire de faire quelques modifications dans le fragment shader, lui permettant ainsi de sauvegarder des informations dans les multiples textures attachées aux FBO.
2-D-1. En OpenGL 2▲
Dans le GLSL < 150, lorsque nous devons envoyer des informations au FBO, nous utilisons gl_FragData[]. C'est un tableau dont la taille est « gl_MaxDrawBuffers », soit le nombre maximum d'attachements que nous pouvons assigner. Son utilisation est simple, le premier élément du tableau correspond au premier attachement de couleur 'GL_COLOR_ATTACHMENT0', le deuxième correspond à 'GL_COLOR_ATTACHMENT1' et ainsi de suite.
Il n'est pas possible, dans le même fragment shader, d'utiliser gl_FragColor et gl_FragData. En effet, gl_FragData[0] est identique à gl_FragColor lors de l'utilisation d'un unique FBO. Cela veut aussi dire que, si vous avez un FBO avec GL_COLOR_ATTACHMENT0, l'utilisation de gl_FragColor fonctionnera.
2-D-2. En OpenGL 3▲
À partir du GLSL 330, glFragData n'existe plus. Finalement, cela est un avantage, car les variables de sortie du fragment shader peuvent être nommées comme nous le souhaitons. Pour spécifier une variable de sortie, nous utilisons le mot clé 'out'. Maintenant, votre fragment shader aura plusieurs variables out et pour spécifier laquelle correspond à quel attachement de couleur, vous devez utiliser « layout(location=0) » comme suit :
layout
(
location=
0
) out
vec4
positionOut;
Ainsi, la variable positionOut permettra d'écrire dans la cible de rendu attachée à la couleur 0 dans le FBO.
Si vous ne mettez aucune indication de « location » par défaut, la variable écrira dans l'attachement de couleur 0 (ou sur l'écran s'il n'y a pas de FBO).
2-E. Exemple d'utilisation▲
Voici un petit exemple simple de l'utilisation des FBO avec OpenGL. Le programme est simplifié à son minimum. Pour les shaders, j'ai en partie utilisé le code de ce tutoriel. Pour le VBO, je me suis inspiré du tutoriel de raptor70.
Le programme effectue un premier rendu (ou première passe) d'un cube coloré dans une texture. Dans la passe suivante, le programme dessine un cube et utilise la texture créée lors de la première passe. De plus, le résultat de la première passe est affiché sur l'écran à l'aide de la fonction glBlitFramebuffer().
Afin de compiler le projet, la lecture du README.txt est recommandée.
Afin de faciliter le débogage des FBO, il est conseillé de modifier la couleur de nettoyage (avec glClearColor()), ce qui permettra de reconnaître quel FBO est affiché.
Il est nécessaire d'utiliser un render buffer afin que le cube de la première passe utilise le buffer de profondeur. Sans celui-ci vous aurez les artefacts visuels suivants :
3. Techniques utilisant les Frame Buffer Objects▲
Grâce aux FBO, certaines techniques sont devenues possibles et d'autres ont été facilitées. Voici une liste non exhaustive de techniques utilisant les FBO.
3-A. Flou (Blur)▲
Un flou simple est très aisé à mettre en place. Il suffit de faire un rendu de la scène dans une texture. Ensuite, pour chaque pixel nous calculons la moyenne des couleurs alentour. Pour ce faire, nous dessinons la texture générée directement sur l'écran, à l'aide d'un rectangle de la taille de la fenêtre. Ainsi, le pixel shader va être exécuté sur chaque pixel. Voici un exemple du calcul de la couleur moyenne effectué dans le pixel shader :
float
blurValue =
0
.0015
;
vec4
color =
texture2D
(
fboTex, vec2
(
io_texCoord.s +
blurValue, io_texCoord.t +
blurValue));
color +=
texture2D
(
fboTex, vec2
(
io_texCoord.s -
blurValue, io_texCoord.t +
blurValue));
color +=
texture2D
(
fboTex, vec2
(
io_texCoord.s +
blurValue, io_texCoord.t -
blurValue));
color +=
texture2D
(
fboTex, vec2
(
io_texCoord.s -
blurValue, io_texCoord.t -
blurValue));
gl_FragColor =
color /
4
.0
;
Vous pouvez trouver ici le programme d'exemple affichant un cube légèrement flou. On remarquera sur l'image ci-dessous que les bords du cube sont plus lisses.
Cet effet de flou peut être utilisé pour remplacer la technique d'antialiasing et cela pour un faible coût.
3-B. Shadow Mapping▲
Le shadow mapping est une technique permettant de calculer les ombres pour les objets d'une scène.
La technique est décomposée en deux passes : le calcul des ombres, puis le rendu proprement dit. Pour le calcul des ombres, il faut placer la caméra à l'emplacement de la lumière, l'orienter avec la direction de la lumière, faire un rendu de la scène et ne garder que la profondeur (distance entre la lampe et le mesh, donc). Celle-ci est conservée dans l'espace homogène. Grâce aux FBO ce résultat est stocké dans une texture, appelée shadow map (littéralement : carte des ombres). Nous devons créer la shadow map pour chaque source, afin de connaître les ombres produites par chaque lumière.
Dans la seconde passe, la scène est rendue normalement, sauf que dans son pixel shader, on calculera si la distance entre le point actuel et la lampe est supérieure à la valeur retournée par la shadow map pour la même position. Si la valeur de la texture est inférieure, cela indique qu'il y a un objet entre la lampe et ce pixel, donc, qu'il doit être ombré.
3-C. Point Light▲
Avec le pipeline fixe, nous étions limités à huit sources de lumière. Avec les shaders, le nombre de sources n'est plus limité, mais le calcul des lumières pour tous les pixels et toutes les lampes est lent. Heureusement, il est possible de faire beaucoup mieux.
Cette technique se décompose en deux passes.
Dans la première, il faut afficher la scène, comme nous le ferions habituellement. Par contre, nous faisons le rendu dans un FBO qui contiendra au minimum trois textures attachées. Lors de l'exécution du shader, la position du vertex, la normale et la couleur de l'objet seront sauvegardées dans les textures (une texture pour chaque donnée).
La seconde passe utilise les trois textures précédemment créées. Par contre, nous allons éviter de calculer l'influence de toutes les lampes pour tous les pixels. Pour ce faire, nous allons limiter l'exécution du pixel shader aux seuls endroits où la lampe influe sur les pixels. Cela est possible en dessinant des rectangles de la taille du rayon de la lumière (puissance de la lampe). Ainsi, le pixel shader, qui contiendra l'algorithme pour l'éclairage ne sera exécuté que pour les pixels réellement éclairés. De plus, pour le cas où deux lampes affectent les mêmes pixels, il suffit de régler le mode de fondu (blend mode) afin d'avoir une composition.
4. Conclusion▲
Cet article présente les Frame Buffer Objects et explique comment s'en servir. Après une présentation de la documentation, nous avons mis en place notre premier programme utilisant les FBO. Celui-ci est simple, mais il pose les bases d'une fonctionnalité maintenant présente dans tous les moteurs 3D. Pour terminer, nous avons rapidement exploré quelques techniques utilisant les FBO afin de montrer leur utilité.
5. Liens▲
Documentation OpenGL 3.2 (Section 4.4)
Documentation GLSL 1.5 (Section 4.3.6)
6. Remerciements▲
Je souhaite remercier gbdivers. Finalement, je remercie aussi jacques_jean pour ses corrections.