gKit2 light
rendu multi-passes : shadow maps, post process, framebuffer object

cf tuto_framebuffer.cpp

Dans plusieurs cas, il est nécessaire de "récupérer" le résultat d'un rendu pour le modifier avant l'affichage final. par exemple, on peut vouloir filtrer l'image brute avant de l'afficher. ou ajuster les couleurs pour produire une ambiance chaude ou froide, ajouter un grain dans l'image, etc. il faut donc pouvoir recupérer le color buffer et éventuellement le zbuffer qui sont habituellement affichés directement dans la fenêtre de l'application.

De manière générale, certaines méthodes de rendu sont trop complexes pour être réalisées avec une seule exécution du pipeline graphique. une solution est de découper le rendu complet en plusieurs étapes, chaque étape utilisant l'image produite par l'étape précédente, jusqu'à obtenir le résultat final que l'on peut afficher.

Ce sont des objets openGL, des framebuffer objects, notés FBO, qui permettent de configurer le pipeline pour stocker les résultats des fragment shaders dans des textures crées par l'application, au lieu de les afficher directement.

Leur utilisation est semblable aux autres objets "complexes" d'openGL (comme les vertex array objects, VAO) :

mais bien sur, il faut commencer par créer un objet framebuffer et le sélectionner pour le configurer :

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);

ensuite, il n'y a plus qu'à indiquer quelles textures vont stocker les résultats exportés par le fragment shader et le pipeline. Les sorties d'un fragment shader sont une ou plusieurs couleurs, ainsi que la profondeur du fragment. ces différentes sorties sont identifiées par :

// selectionner le framebuffer a configurer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
GLuint color_texture;
// creer la texture couleur aux dimensions de la fenetre
{ ... }
// associer la texture à une sortie du framebuffer
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT0, /* texture */ color_texture, /* mipmap level */ 0);

reste une dernière option dans la configuration, récupérer l'identifiant d'une sortie déclarée dans le fragment shader et l'associer à une sortie du framebuffer, cf glDrawBuffers( ). il faut lui transmettre un tableau d'identifiants GL_NONE, GL_COLOR_ATTACHMENTxx indexé par l'identifiant de la sortie du fragment shader.

si le fragment shader ne déclare qu'une seule sortie, son identifiant est 0 par convention, il suffit de remplir un tableau contenant la sortie configurée dans le framebuffer à l'indice 0 :

// selectionner le framebuffer a configurer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
GLenum buffers[]= { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, buffers);
MeshBuffer buffers(const MeshData &data)
construction a partir des donnees d'un maillage.
Definition: mesh_buffer.cpp:48

remarque : on peut configurer n'importe quel attachment dans le fbo :

// selectionner le framebuffer a configurer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT4, /* texture */ color_texture, /* mipmap level */ 0);
GLenum buffers[]= { GL_COLOR_ATTACHMENT4 };
glDrawBuffers(1, buffers);

pour "décoder" la configuration de fbo, le plus simple est de partir de la sortie dans le fragment shader : son identifiant est 0, donc la valeur sera écrite dans l'attachment d'indice 0 passé à glDrawBuffers(). et la texture stockant la valeur est celle sélectionnée par glFramebufferTexture( ) sur l'attachment correspondant.

dessiner dans un framebuffer

il suffit de sélectionner le framebuffer sur GL_DRAW_FRAMEBUFFER avec glBindFramebuffer( ) avant de dessiner quelquechose :

GLuint framebuffer= { ... };
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glBindVertexArray( ... );
glUseProgram( ... );
glUniform( ... );
glDrawArrays(GL_TRIANGLES, ... );

attention : glDraw() utilise implicitement plusieurs paramètres : les dimensions du viewport, par exemple (et le framebuffer sélectionné, bien sur). il faudra donc penser à utiliser glViewport() si les dimensions des textures ne sont pas les memes que celles de la fenetre de l'application.

remarque : effacer le framebuffer avant de dessiner :

GLuint framebuffer= { ... };
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Clear( COLOR_BUFFER_BIT ... ) copie la couleur par défaut dans toutes les textures associées aux COLOR_ATTACHMENTxx et Clear( DEPTH_BUFFER_BIT ... ) copie la profondeur par défaut dans la texture associée à DEPTH_ATTACHMENT.

Clear( ) utilise implicitement les dimensions fournies par glViewport( ), donc il faut penser à configurer le viewport avant...

si l'on souhaite utiliser des valeurs / couleurs différentes selon le buffer, il faut utiliser glClearBuffer(). par exemple pour "effacer", le buffer 0 :

GLuint framebuffer= { ... };
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT0, /* texture */ color_texture, /* mipmap level */ 0);
GLenum buffers[]= { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, buffers);
Color color(1, 0, 0);
glClearBufferfv(GL_COLOR, /* draw buffer */ 0, /* value */ &color.x);
representation d'une couleur (rgba) transparente ou opaque.
Definition: color.h:14

DRAW_BUFFER0 est l'indice dans le tableau buffers, buffers[0] contient COLOR_ATTACHMENT0, et c'est la texture color_texture qui sera finalement modifiée / effacée.

et glViewport( ) ?

il faut aussi configurer le pipeline en fonction des dimensions des textures associées au framebuffer.

GLuint framebuffer= { ... };
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glViewport(0, 0, framebuffer_width, framebuffer_height);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glClear( ) et glDraw( ) utilisent implicitement les dimensions fournies par glViewport( ), donc il faut configurer viewport avant clear et draw...

dessiner dans la fenetre ? (framebuffer par défaut)

il suffit de sélectionner le framebuffer 0 avant de dessiner :

glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glViewport(0, 0, window_width(), window_height());
glClear(GL_COLOR_BUFFER_BIt | GL_DEPTH_BUFFER_BIT);
glBindVertexArray( ... );
glUseProgram( ... );
glUniform( ... );
glDrawArrays(GL_TRIANGLES, ... );
int window_height()
renvoie la hauteur de la fenetre de l'application.
Definition: window.cpp:29
int window_width()
renvoie la largeur de la fenetre de l'application.
Definition: window.cpp:25

récupérer l'identifiant d'une sortie du fragment shader

c'est glGetFragDataLocation( ) qui renvoie l'identifiant du varying déclaré par le fragment shader :

GLuint program= ... ;
GLint location= glGetFragDataLocation(program, "fragment_color");

et on peut utiliser cette valeur pour configurer le framebuffer, cf glDrawBuffers() :

GLuint program= ... ;
GLuint framebuffer = .... ;
GLenum buffers[8]= { GL_NONE, GL_NONE, GL_NONE, GL_NONE, GL_NONE, GL_NONE, GL_NONE, GL_NONE };
GLint location= glGetFragDataLocation(program, "fragment_color");
if(location >= 0)
buffers[location]= GL_COLOR_ATTCHMENT0;
// selectionner le fbo, si necessaire
// glBindFamebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glDrawBuffers(8, buffers);

créer une texture de profondeur / zbuffer

Jusqu'à présent cf textures, samplers et pipeline, les textures peuvent stocker des couleurs (dont les valeurs sont comprises entre 0 et 1) sur plusieurs canaux r, g, b (et alpha, éventuellement). il est aussi possible de créer des textures pour stocker une valeur de profondeur par texel avec le format GL_DEPTH_COMPONENT (un entier normalise entre 0 et 1) :

GLuint zubffer;
glGenTextures(1, &zbuffer);
glBindTexture(GL_TEXTURE_2D, zbuffer);
glTexImage2D(GL_TEXTURE_2D, 0,
GL_DEPTH_COMPONENT, width, height, 0,
GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr);

ou GL_DEPTH_COMPONENT32F (un réel 32bits, un float classique) :

GLuint zubffer;
glGenTextures(1, &zbuffer);
glBindTexture(GL_TEXTURE_2D, zbuffer);
glTexImage2D(GL_TEXTURE_2D, 0,
GL_DEPTH_COMPONENT32F, width, height, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);

utiliser une texture attachée à un framebuffer

Dessiner dans un framebuffer, et dans les textures associées, ne modifie que le niveau de mipmap attaché au framebuffer, cf glFramebufferTexture( ... level), le mipmap 0, le plus souvent. Mais lorsque l'on utilise cette texture dans l'étape suivante, ses autres mipmaps n'ont pas changés de valeurs. Et selon le mode filtrage utilsé par le shader pour accéder à la texture, les résultats peuvent être surprenants...

par exemple, les paramètres par défaut de filtrage, cf LINEAR_MIPMAP_LINEAR, interpolent 2 mipmaps et 4 texels par mipmap, mais seul le mipmap 0 a ete modifie, et le résultat sera faux.

il y a 2 solutions, soit utiliser un filtrage NEAREST_MIPMAP_LINEAR et MAX_LEVEL = 0 qui n'utilise que le mipmap 0, soit recalculer tous les mipmaps de la texture, cf glGenerateMipmap( ).

remarque : pour les textures de profondeur, il n'est pas possible d'utiliser glGenerateMipmap(), donc il faut utiliser un mode de filtrage (cf GL_NEAREST) qui n'accède qu'aux données disponibles : le niveau de mipmap dans lequel on vient de dessiner.

// selectionner l'unite de texture a modifier, si necessaire
// glActiveTExture(GL_TEXTURE0 + unit);
// desactiver le filtrage (pour les textures de profondeur, mais pas que... )
// acces direct aux donnes sans interpolation :
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// equivalent, avec un sampler, à utiliser avec glBindSampler(unit, sampler);
GLuint sampler= { ... };
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

attention

Autre subtilité, si une texture est active sur une unité de texture et attachée en même temps au framebuffer actif, glDraw() génèrera une erreur : le pipeline ne peut pas lire et écrire la même texture en même temps. il faut donc bien penser à nettoyer les unités de texture pour eviter ce problème...

GLuint color= { ... };
GLuint framebuffer= { ... };
init( )
{
// configuration framebuffer / texture
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT0, /* texture */ color, /* mipmap level */ 0);
GLenum buffers[]= { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, buffers);
// nettoyage
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
render( )
{
// passe 1, dessiner dans la texture color, attachee au framebuffer
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
glViewport( ... );
glClear( ... );
glBindVertexArray( ... );
glUseProgram( ... );
glUniform( ... );
glDraw( ... );
// passe 2, utiliser la texture de la passe 1 et dessiner dans la fenetre
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glViewport( ... );
glClear( ... );
// mettre a jour les mipmaps de la texture
glBindTexture(GL_TEXTURE_2D, color);
glGenerateMipmap(GL_TEXTURE_2D);
glBindVertexArray( ... );
glUseProgram( ... );
glUniform( ... );
glDraw( ... );
// nettoyage *obligatoire*, la texture color *ne doit pas* etre active sur une unite de texture et attachee au framebuffer selectionne
glBindTexture(GL_TEXTURE_2D, 0);
}
int init(std::vector< const char * > &options)
Definition: shader_kit.cpp:92

exemple complet dans cf tuto_framebuffer.cpp.

et avec plusieurs textures ?

dans certains cas, il est nécessaire de stocker plusieurs informations, en plus de la couleur et de la profondeur du fragment. la démarche est la meme, il suffit de déclarer les sorties dans le fragment shader (8 au maximum), de créer des textures pour stocker les résultats, de les attacher au framebuffer, et enfin de les associer aux sorties du fragment shader avec glDrawBuffers().

comme d'habitude, les différentes sorties sont numérotées. l'application peut choisir l'identifiant de chaque sortie avec glBindFragDataLocation(program, numero, varying ), mais il faut ensuite (re-) linker le shader program, ce qui n'est pas très pratique. il est aussi possible d'indiquer le numéro directement dans le source du fragment shader avec layout(location= ...), ce qui est plus simple à manipuler (c'est la même chose pour les attributs des sommets dans le vertex shader).

// fragment shader
layout(location= 0) out vec4 fragment_color; // declare la sortie 0
layout(location= 1) out vec3 fragment_normal; // declare la sortie 1
void main( )
{
fragment_normal= { ... };
fragment_color= { ... };
}
// application
GLuint color_texture= { ... };
GLuint normal_texture= { ... };
GLuint depth_texture= { ... };
GLuint framebuffer= { ... };
// selectionner le framebuffer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
// attacher les textures au framebuffer
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT0, /* texture */ color_texture, /* mipmap level */ 0);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT1, /* texture */ normal_texture, /* mipmap level */ 0);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_DEPTH_ATTACHMENT, /* texture */ depth_texture, /* mipmap level */ 0);
// associer les sorties du fragment shader aux textures attachees au framebuffer
GLenum buffers[]= { /* sortie / location 0 */ GL_COLOR_ATTACHMENT0, /* sortie / location 1 */ GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, buffers);
vecteur generique, utilitaire.
Definition: vec.h:146
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168

bien sur, on peut utiliser glGetFragDataLocation( ) pour récupérer l'identifiant d'une sortie du fragment shader, mais par construction, le résultat est la valeur que l'on a indiqué avec la déclaration layout(location= ...).

rappel : le paramètre attachment de glFramebufferTexture est libre, on obtient exactement le meme résultat de cette maniere :

// selectionner le framebuffer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
// attacher les textures au framebuffer
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT4, /* texture */ color_texture, /* mipmap level */ 0);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_COLOR_ATTACHMENT2, /* texture */ normal_texture, /* mipmap level */ 0);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_DEPTH_ATTACHMENT, /* texture */ depth_texture, /* mipmap level */ 0);
// associer les sorties du fragment shader aux textures attachees au framebuffer
GLenum buffers[]= { /* sortie / location 0 */ GL_COLOR_ATTACHMENT4, /* sortie / location 1 */ GL_COLOR_ATTACHMENT2 };
glDrawBuffers(2, buffers);

vérifier la configuration du framebuffer

pour vérifier que le framebuffer est correctement configuré et que le pipeline est capable d'écrire toutes les données aux formats demandés, on peut utiliser glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER).

// selectionner le framebuffer, si necessaire
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
GLenum code= glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
switch(code)
{
case GL_FRAMEBUFFER_COMPLETE; /* ok */ break;
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: /* un draw buffer n'a pas de texture associee */ break;
case GL_FRAMEBUFFER_UNSUPPORTED: /* cet ensemble de formats de textures n'est pas compatible avec la carte graphique */ break;
...
}

les autres erreurs possibles sont dans la doc openGL.

formats de textures pour stocker des données, profondeur, position, normale, etc.

les textures couleurs classiques stockent des entiers sur 8bits re-normalisés entre 0 et 1. si on souhaite stocker des réels, il faut utiliser un format de texture avec assez de précision. les floats sur 16 bits (suffixe 16F) et sur 32 bits (suffixe 32F) sont supportés par la plupart des cartes graphiques. voila les formats courants :

attention : toutes les combinaisons de formats ne sont pas supportées par la carte graphique. vérifiez avec glCheckFramebufferStatus().

effacer le framebuffer / valeurs par défaut

les framebuffers classiques sont "effacés" / (re-) initialisés par glClear() avec les valeurs fournies par glClearColor(), glClearDepthf(). lorsque plusieurs textures de formats différents sont utilisées et que l'on souhaite les initialiser avec des valeurs différentes, il faut utiliser la famille de fonctions glClearBuffer(). les formats de textures entiers utilisent glClearBufferiv(), les formats float, glClearBufferfv().

Les textures attachées au framebuffer ne sont pas identifiées directement, mais à travers leur association à une sortie du fragment shader, cf les paramètres passés à glDrawBuffers() lors de la configuration du framebuffer. Les valeurs par défaut sont toujours fournies par un vec4, même si la texture utilise un format avec moins de composantes. Le zbuffer est un cas particulier, il suffit de passer un seul float.

// couleur par défaut : gris, pour la texture associee à la sortie 0 du fragment shader
vec4 color= vec4(.2, .2, .2, 1);
glClearBufferfv(GL_COLOR, /* draw buffer / sortie / location */ 0, /* const float[4] */ &color.r);
// normale par défaut : vec3(0, 0, 0), pour la texture associee à la sortie 1 du framgent shader
vec4 normal= vec4(0, 0, 0, 0);
glClearBufferfv(GL_COLOR, /* draw buffer / sortie / location */ 1, /* const float[4] */ &normal.x);
// profondeur par defaut
float one= 1;
glClearBufferfv(GL_DEPTH, /* draw buffer 0 */ 0, /* const float* */ &one);