• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les Frame Buffer Objects

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous voici arrivés au dernier chapitre de cette deuxième partie. Celui-ci concernera une fonctionnalité très utile pour les effets réalises et constitue donc un incontournable de la programmation avancée OpenGL. Il y aura un peu de technique mais rien de vraiment insurmontable, nous verrons tout ce qu'il y a à voir pas à pas pour ne pas être perdu. :)

Commençons d'ailleurs sans plus tarder ! :pirate:

Qu'est-ce qu'un FBO ?

Définitions

Les différents types de buffer

Avant de nous intéresser au sujet principal de ce chapitre, nous allons faire un petit retour en arrière sur une fonction que nous utilisons depuis pas mal de temps déjà : la fonction glClear() :

// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER | GL_DEPTH_BUFFER);

Celle-ci permet de nettoyer les buffers (zones mémoires ou tampons si vous préférez) qu'ils lui sont donnés en paramètres. Par exemple, nous lui demandons ici de nettoyer le Color ainsi que le Depth Buffer, le premier correspondant simplement aux pixels affichés sur notre fenêtre et le second à la position des objets 3D les uns par rapport aux autres pour pouvoir les cacher si besoin.

Si je vous parle de ça, c'est parce qu'il existe encore un autre buffer dont je ne vous ai pas encore parlé, ce qui est un peu normal vu que c'est celui qui est le moins utilisé. :p Il s'agit du Stencil Buffer ou en français tampon du pochoir. Si vous vous posez la question de savoir ce qu'est un pochoir, sachez que vous en avez déjà probablement utilisé un en maternelle pour dessiner une forme complexe comme un animal par exemple. Je vous laisse regarder sur internet des exemples de pochoirs, vous devriez vite comprendre à quoi ils correspondent en les voyant.

Le Stencil Buffer permet de faire exactement la même chose. Par exemple, au lieu d'afficher une texture carrée représentant un buisson, vous pouvez lui appliquer un pochoir (donc utiliser le Stencil Buffer) pour n'afficher que les pixels qui vous intéressent de façon à ne pas vous retrouver avec un buisson carré mais avec un buisson "réaliste". Je schématise un peu mais vous aurez compris le principe.

Le problème avec ce buffer, c'est qu'il est assez lourd à mettre en place, il faut activer un tableau Vertex Attrib dédié et envoyer ses donnés au shader. Nous ne l'utiliserons pas pour le moment. Si je vous en parle c'est parce que nous devrons gérer le cas où nous en aurions besoin plus tard, vous verrez pourquoi.

Ce qu'il faut retenir c'est qu'il existe 3 types de buffers :

  • Color : qui contient la couleur de chaque pixel de votre fenêtre

  • Depth : qui permet à OpenGL de gérer la profondeur et de cacher les objets

  • Stencil : qui permet de filtrer un rendu avec des pochoirs

Les Frame Buffers

Cette parenthèse étant faite, nous allons pouvoir passer au sujet principal qui concerne les Frame Buffer Objets ou FBO. Vous l'aurez deviné, nous allons encore voir des objets OpenGL. :p

Pour donner une définition simple d'un FBO, on pourrait dire qu'il s'agit d'un écran caché qui utilise les 3 types de buffers que nous avons vus à l'instant :

Image utilisateur

Si j'utilise le terme d'écran caché c'est par les FBO permettent de faire ce que l'on appelle un rendu off-screen (en dehors de l'écran). C'est-à-dire qu'au lieu d'afficher votre scène dans votre fenêtre, vous le ferrez dans un endroit à part dans la carte graphique comme s'il y en avait une deuxième à l'intérieure. Le rendu sera exactement le même sauf que vous ne le verrez pas directement.

À quoi ça sert d'afficher quelque chose si on ne le voit pas ?

C'est quelque chose d'un peu tordu au premier abord mais je pense que vous allez vite aimer les FBO quand vous connaitrez les possibilités qu'ils offrent. En étant associés aux shaders, ils permettent de faire plein d'effets réalistes tels que :

  • Le flou : pour la vitesse ou l'éblouissement

  • Les reflets : pour les miroirs et l'eau

  • Les ombres dynamiques : qui sont modifiées à chaque fois que leur "modèle" bouge

  • La capture vidéo : pour filmer un endroit tout en étant dans un autre (caméra de surveillance, télévision, etc.)

  • ...

Une bonne palette d'effets sympathiques en somme. ^^

Petit détail pour terminer : votre écran lui-même est considéré comme un Frame Buffer. Il reprend donc toutes les caractéristiques dont nous venons de parler, sauf pour les effets réalistes parce qu'il est impossible de modifier les pixels une fois qu'ils sont affichés sur l'écran.

Fonctionnement

Nous allons aller un peu plus loin dans la structure des FBO car ils ont une façon bien particulière de fonctionner, en particulier au niveau de ses 3 buffers qui ne se gèrent pas tous de la même manière. On distinguera d'un coté le Color Buffer et de l'autre le Depth et le Stencil Buffer.

Le Color Buffer

Comme nous l'avons vu un peu plus haut, un FBO peut être considéré comme un écran caché au sein de la carte graphique, nous pouvons donc sans aucun problème faire n'importe quel rendu à l'intérieur. L'intérêt dans tout ça vient du fait que l'on peut modifier les pixels une fois qu'ils sont affichés, ce qui est impossible avec le véritable écran.

Ces fameux pixels sont contenus dans le Color Buffer, celui que nous nettoyons à chaque tour de boucle avec la constante GL_COLOR_BUFFER. Et si je peux vous apprendre quelque chose d'étonnant avec lui, c'est qu'il s'agit en fait d'une simple texture !

Oui, oui vous avez bien lu, le Color Buffer est une texture :

Image utilisateur

C'est un peu logique puisque le but d'OpenGL est d'afficher des pixels sur un écran, et une texture est justement faite pour en contenir plein. L'avantage avec elle, c'est que nous pourrons l'utiliser dans un shader pour la modifier très facilement. Rajouter un effet sera donc un jeu d'enfant. ;)

Ah j'ai une question : le Color Buffer de l'écran est lui-aussi une texture ?

Oui on peut dire ça comme ça. C'es comme si on avait une grosse texture, contenant l'affichage de notre scène, plaquée sur la fenêtre SDL.

Petite précision importante, les FBO peuvent contenir jusqu'à 16 Color Buffers. C'est-à-dire que vous pouvez afficher ou modifier votre rendu 16 fois dans le même FBO :

Image utilisateur

Ceci est particulièrement utile si l'on souhaite stocker des données autres que celles relatives à l'affichage. Les ombres en sont un exemple car elles utilisent plusieurs Color Buffers pour stocker leurs données.

Mais bon, pour le moment nous n'en utiliserons qu'un seul ne vous en faites pas, c'est déjà bien assez.

Le Depth et le Stencil Buffer

Les deux autres buffers (Depth et Stentil) se gèrent différemment du premier. Leurs données sont un peu plus complexes et ne peuvent pas être représentées par des textures. À la place, nous utiliserons ce que l'on appel des Render Buffers, ce sont des espaces mémoires conçues pour accueillir ces types de données :

Image utilisateur
En résumé

Pour résumer un peu tout ce flot d'informations : un Frame Buffer est composé de 3 sous-buffers (Color, Depth et Stencil) :

  • Le premier est représenté pas une texture et contient l'affichage d'une scène. Il peut y en avoir jusqu'à 16.

  • Les deux autres sont représentés par des Render Buffer.

Ce sont les points à connaitre par coeur en ce qui concerne les FBO.

Programme du chapitre

Après ce petit bout de théorie, nous allons pouvoir nous lancer dans la pratique pure et dure.

Je vous retiens encore un peu ici pour vous montrer le programme pour la suite. Nous allons éparpiller le code relatif aux FBO dans plusieurs classes, je préfère donc vous exposer clairement les différentes tâches qui nous attendent.

Celles-ci sont :

  • La modification la classe Texture pour lui permettre de créer des Color Buffers

  • La modification des méthodes de copie pour gérer ce nouveau type de texture

  • L'implémentation des Render Buffers

  • La création une classe Frame Buffer qui réunira tous les buffers

Nous avons donc pas mal de trucs à faire avec tout ceci.

Je vous invite naturellement à commencer par la première tâche qui concerne la modification de la classe Texture. ^^

Le Color Buffer

Les tâches

Pour reprendre très rapidement le programme précédent, notre objectif dans cette partie va être la réalisation des deux premières tâches :

  • La modification la classe Texture pour lui permettre de créer des Color Buffers

  • La modification des méthodes de copie pour gérer ce nouveau type de texture

  • L'implémentation des Render Buffers

  • La création une classe Frame Buffer qui réunira tous les buffers

Je parle bien des deux premières car nous modifierons la classe Texture pour toutes les deux, autant les faire dans la même partie. :)

Commençons par la première.

Créer un Color Buffer

Le header

La fameuse modification de la classe Texture concerne l'ajout d'une nouvelle méthode qui permettra de créer des textures vides. Je dis bien vide parce qu'elle n'est pas chargée depuis une image sur le disque dur, elle ne contient donc aucune donnée au moment de sa création. Ce n'est qu'à l'affichage qu'elle aura ses pixels.

Avant de créer ladite méthode, nous allons tout d'abord ajouter de nouveaux attributs à la classe Texture qui lui permettront de gérer les textures vides.

Dans les chapitres précédents, nous utilisions une structure de type SDL_image pour garder en mémoire pas mal d'informations sur une texture comme ses dimensions, le format interne des couleurs, etc. Vu que nous n'en utilisons pas ici, il faudra recréer toutes ces informations à la main à travers des attributs de classe.

Ces attributs sont les suivants :

  • int largeur : La largeur de la texture

  • int hauteur : Sa hauteur

  • GLenum format : Le format des couleurs (3 ou 4 couleurs en comptant le canal Alpha)

  • GLenum formatInerne : Le format interne (l'ordre des couleurs)

  • bool textureVide : Ce booléen sera utilisé dans les méthodes de copie

En ajoutant le prefix m_ à ces attributs, on a le header suivant :

#ifndef DEF_TEXTURE
#define DEF_TEXTURE


// Includes

....


// Classe Texture

class Texture
{
    public:

    // Méthodes

    ....


    private:

    GLuint m_id;
    std::string m_fichierImage;

    int m_largeur;
    int m_hauteur;
    GLenum m_format;
    GLenum m_formatInterne;
    bool m_textureVide;
};

#endif
Les anciens constructeurs

Les nouveaux attributs doivent évidemment être initialisés dans les différents constructeurs, nous leur donnerons tous la valeur 0 :

// Constructeur par défaut

Texture::Texture() : m_id(0), m_fichierImage(""), m_largeur(0), m_hauteur(0), m_format(0), m_formatInterne(0), m_textureVide(false)
{

}


// Autre constructeur

Texture::Texture(std::string fichierImage) : m_id(0), m_fichierImage(fichierImage), m_largeur(0), m_hauteur(0), m_format(0), m_formatInterne(0), m_textureVide(false)
{

}

Notez que j'ai volontairement occulté le constructeur de copie, nous le verrons à la fin.

Un nouveau constructeur

Le problème avec les constructeurs précédents c'est qu'aucun d'entre eux n'est capable de donner de "vraies" valeurs aux attributs, ce qui est normal car ils n'ont aucuns paramètres qui leur permettent de faire cela.

Pour combler ce manque, nous allons devoir rajouter un nouveau constructeur qui, lui, prendra en paramètres tout ce dont on a besoin pour initialiser nos attributs correctement :

Texture(int largeur, int hauteur, GLenum format, GLenum formatInterne, bool textureVide);

L'implémentation de ce constructeur est assez simple puisqu'il suffit juste de donner le bon paramètre au bon attribut :

Texture::Texture(int largeur, int hauteur, GLenum format, GLenum formatInterne, bool textureVide) : m_id(0), m_fichierImage(""), m_largeur(largeur), 
                 m_hauteur(hauteur), m_format(format), m_formatInterne(formatInterne), m_textureVide(textureVide)
{

}

Grâce à lui, nous pourrons donner de véritables valeurs à nos nouveaux attributs.

La méthode chargerTextureVide()

L'étape suivante consiste à coder la méthode qui nous permettra de créer des textures vides. Elle utilisera pour cela les attributs précédemment initialisés, elle s'appellera tout simplement chargerTextureVide() :

void chargerTextureVide();

Elle ne prendra aucun paramètre.

Son code sera beaucoup plus simple que celui de la méthode charger() car elle n'aura pas besoin de charger une image, d'inverser ses pixels et de déterminer le format des couleurs. Nous avons déjà toutes ces informations grâce à notre nouveau constructeur. :)

On commence l'implémentation de la méthode en générant un nouvel identifiant d'objet OpenGL grâce à la fonction glGenTextures(). On en profitera au passage pour ajouter le code de vérification d'ID à l'aide la fonction glIsTexture() :

void Texture::chargerTextureVide()
{
    // Destruction d'une éventuelle ancienne texture

    if(glIsTexture(m_id) == GL_TRUE)
        glDeleteTextures(1, &m_id);


    // Génération de l'ID

    glGenTextures(1, &m_id);
}

On ajoute ensuite le verrouillage a l'aide de la fonction glBindTexture() :

void Texture::chargerTextureVide()
{
    // Destruction d'une éventuelle ancienne texture

    if(glIsTexture(m_id) == GL_TRUE)
        glDeleteTextures(1, &m_id);


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);




    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}

Une fois le verrouillage enclenché, nous allons appeler la fonction glTexImage2D() qui permet de définir les caractéristiques d'une texture (dimensions, formats, etc.). Je vous redonne son prototype ainsi que sa floppé de paramètres :

void glTexImage2D(GLenum target,  GLint level,  GLint internalFormat,  GLsizei width,  GLsizei height,  GLint border,  GLenum format,  GLenum type,  const GLvoid * data);
  • target : Le type de la texture auquel nous donnerons, comme toujours, la constante GL_TEXTURE_2D

  • level : Paramètre que nous n'utiliserons pas, nous lui donnerons la valeur 0

  • internalFormat : Le format interne de la texture

  • width : Sa largeur

  • height : Sa hauteur

  • border : Une bordure, nous ne l'utiliserons pas et donnerons la valeur 0

  • format : Le format de la texture

  • type : Type de donnée des pixels (float, int, ...). Nous lui donnerons la constante GL_UNSIGNED_BYTE

  • data : Pixels de la texture, nous n'en avons pas encore et nous donnerons encore la valeur 0

Refaite une petite lecture lentement pour différencier correctement les paramètres. Remarquez aussi que nous utiliserons tous nos nouveaux attributs.

En appelant la fonction glTexImage2D() avec ces paramètres, on a :

// Définition des caractéristiques de la texture

glTexImage2D(GL_TEXTURE_2D, 0, m_formatInterne, m_largeur, m_hauteur, 0, m_format, GL_UNSIGNED_BYTE, 0);

Pour terminer la méthode, il ne nous manque plus qu'à appliquer les filtres avec la fonction glTexParameteriv(). Vous vous souvenez de ce que sont les filtres ? Ils permettent à OpenGL de savoir s'il doit améliorer ou baisser la qualité de la texture en fonction de notre distance par rapport à elle.

On utilisera les mêmes filtres que ceux de la méthode charger() :

// Application des filtres

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

Ces filtres permettent à OpenGL d'affiner l'affichage des textures proches et de pixeliser celui des textures éloignées, le premier étant plus gourmand en calcul que le second.

Si nous réunissons ces bouts de code, on trouve la méthode chargerTextureVide() suivante :

void Texture::chargerTextureVide()
{
    // Destruction d'une éventuelle ancienne texture

    if(glIsTexture(m_id) == GL_TRUE)
        glDeleteTextures(1, &m_id);


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


        // Définition des caractéristiques de la texture

        glTexImage2D(GL_TEXTURE_2D, 0, m_formatInterne, m_largeur, m_hauteur, 0, m_format, GL_UNSIGNED_BYTE, 0);


        // Application des filtres

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}

Grâce à elle, nous pourrons créer tous les Colors Buffers que nous voudrons. :D Il suffira juste de faire attention aux paramètres que nous donnerons.

Le problème de la copie de texture

Étant donné que nous avons rajouté des attributs dans la classe, nous devons penser à modifier les méthodes de copie (constructeur et opérateur =) pour éviter les problèmes plus tard. C'est la deuxième tâche sur notre liste.

Les attributs

On commence par le constructeur.

En reprenant son code actuel, on remarque que celui-ci ne gère pas les nouveaux attributs :

Texture::Texture(Texture const &textureACopier)
{
    // Copie de la texture

    m_fichierImage = textureACopier.m_fichierImage;
    charger();
}

Nous devons donc corriger cela en utilisant l'opérateur = pour chacun d'entre eux :

Texture::Texture(Texture const &textureACopier)
{
    // Copie des attributs

    m_fichierImage = textureACopier.m_fichierImage;

    m_largeur = textureACopier.m_largeur;
    m_hauteur = textureACopier.m_hauteur;
    m_format = textureACopier.m_format;
    m_formatInterne = textureACopier.m_formatInterne;
    m_textureVide = textureACopier.m_textureVide;


    // Chargement de la copie

    charger();
}
Le chargement

Le chargement d'une texture copiée est ce qu'il y a de plus délicat à gérer, surtout dans notre situation car nous avons deux méthodes de chargement : l'une pour les images présentes sur le disque dur et l'autre pour les textures vides.

Le problème avec la copie ici c'est que la classe Texture est incapable de savoir laquelle des deux elle doit appeler. Pour le moment, elle appelle toujours la méthode charger() quelque soit le type de texture (vide ou image), ce qui va nous poser des problèmes quand on utilisera les Color Buffer car eux ont besoin de l'autre méthode.

Pour éviter cela, nous devons définir manuellement les cas de chargement pour appeler la bonne méthode. Pour nous aider, nous allons nous servir du seul attribut que nous n'avons pas encore utilisé : le booléen m_textureVide. Nous lui avions donné une valeur dans tous les constructeurs un peu plus haut.

Si sa valeur est égale à true (c'est-à-dire si la texture est vide) alors nous devons appeler la méthode chargerTextureVide(). Si elle est égale à false (image SDL), alors nous devons appeler la méthode charger(). Un petit bloc if sera nécessaire pour faire cette vérification :

// Si la texture est vide, alors on appelle la méthode chargerTextureVide()
    
if(m_textureVide)
    chargerTextureVide();
    
    
// Sinon, on appelle la méthode charger() par défaut
   
else
    charger();

Le constructeur complet :

Texture::Texture(Texture const &textureACopier)
{
    // Copie des attributs
    
    m_fichierImage = textureACopier.m_fichierImage;
    
    m_largeur = textureACopier.m_largeur;
    m_hauteur = textureACopier.m_hauteur;
    m_format = textureACopier.m_format;
    m_formatInterne = textureACopier.m_formatInterne;
    m_textureVide = textureACopier.m_textureVide;
    
    
    // Si la texture est vide, alors on appelle la méthode chargerTextureVide()
    
    if(m_textureVide)
        chargerTextureVide();
    
    
    // Sinon, on appelle la méthode charger() par défaut
    
    else
        charger();
}

Cette fois, la classe appellera la bonne méthode pour charger la texture. :)

Problème de copie des textures non chargées

Courage, il ne manque plus qu'une condition à gérer et toute cette partie embêtante sera terminée. ^^

Celle-ci concerne les textures qui n'ont pas encore été chargées au moment de la copie, c'est-à-dire celles dont nous n'avons appelé ni la méthode charger(), ni la méthode chargerTextureVide(). Que se passerait-il à votre avis si on essayait d'en copier une dans cette situation ? Et bien la copie serait automatiquement chargée même si l'originale ne l'était pas. Cet automatisme peut causer des problèmes et engendrer des bugs dans certains cas.

Pour éviter cela encore une fois, nous devons rajouter une condition qui vérifiera si la copie doit être chargée ou pas. Nous utiliserons la fonction glIsTexture() qui nous permet justement de savoir ceci :

Texture::Texture(Texture const &textureACopier)
{
    // Copie des attributs
    
    m_fichierImage = textureACopier.m_fichierImage;
    
    m_largeur = textureACopier.m_largeur;
    m_hauteur = textureACopier.m_hauteur;
    m_format = textureACopier.m_format;
    m_formatInterne = textureACopier.m_formatInterne;
    m_textureVide = textureACopier.m_textureVide;
    
    
    // Si la texture est vide, alors on appelle la méthode chargerTextureVide()
    
    if(m_textureVide && glIsTexture(textureACopier.m_id) == GL_TRUE)
        chargerTextureVide();
    
    
    // Sinon, on appelle la méthode charger() par défaut
    
    else if(glIsTexture(textureACopier.m_id) == GL_TRUE)
        charger();
}

Désormais, pour déclencher un des deux chargements, il faudra que la texture originale ait un identifiant valide.

L'opérateur =

Il ne reste plus qu'une seule chose a faire : recopier le code précédent dans la méthode operator=(), en n'oubliant pas de rajouter le return *this bien sûr :

Texture& Texture::operator=(Texture const &textureACopier)
{
    // Copie des attributs
    
    m_fichierImage = textureACopier.m_fichierImage;
    
    m_largeur = textureACopier.m_largeur;
    m_hauteur = textureACopier.m_hauteur;
    m_format = textureACopier.m_format;
    m_formatInterne = textureACopier.m_formatInterne;
    m_textureVide = textureACopier.m_textureVide;
    
    
    // Si la texture est vide, alors on appelle la méthode chargerTextureVide()
    
    if(m_textureVide && glIsTexture(textureACopier.m_id) == GL_TRUE)
        chargerTextureVide();
    
    
    // Sinon, on appelle la méthode charger() par défaut
    
    else if(glIsTexture(textureACopier.m_id) == GL_TRUE)
        charger();


    // Retour du pointeur *this

    return *this;
}

Les modifications dans la classe Texture sont terminées. :)

Les Render Buffers

Les tâches

Reprenons la petite liste de tâches que nous avons à faire pour créer un FBO. Dans cette partie, nous allons nous occuper des deux dernières :

  • La modification la classe Texture pour lui permettre de créer des Color Buffers

  • La modification des méthodes de copie pour gérer ce nouveau type de texture

  • L'implémentation des Render Buffers

  • La création une classe Frame Buffer qui réunira tous les buffers

Ces deux tâches sont liées entre elles car les Render Buffers sont créés à l'intérieur de la classe FrameBuffer. Nous commencerons par parler d'elle et de ses attributs avant d'attaquer les Render Buffers.

La classe FrameBuffer

Le header

La classe FrameBuffer constitue évidemment le coeur des FBO, elle devra gérer la création et l'utilisation de tous les buffers. Elle appellera par la méthode chargerTextureVide() que nous avons codée dans la partie précédente. ;)

Son header de base est celui-ci :

#ifndef DEF_FRAMEBUFFER
#define DEF_FRAMEBUFFER


// Include Windows

#ifdef WIN32
#include <GL/glew.h>


// Include Mac

#elif __APPLE__
#define GL3_PROTOTYPES 1
#include <OpenGL/gl3.h>


// Include UNIX/Linux

#else
#define GL3_PROTOTYPES 1
#include <GL3/gl3.h>

#endif


// Classe FrameBuffer

class FrameBuffer
{
    public:

    private:
};

#endif

Cette classe possédera pas mal d'attributs pour fonctionner correctement :

  • GLuint m_id : identifiant OpenGL représentant le FBO

  • float m_largeur : largeur du FBO. On peut comparer cet attribut à la largeur de la fenêtre SDL

  • float m_hauteur : même chose pour la hauteur

  • vector m_couleursBuffers : tableau dynamique qui contiendra tous les Colors Buffers désirés

  • GLuint m_depthBufferID : identifiant du Depth Buffer

On ne crée par d'attribut pour le Stencil Buffer ?

Non nous n'en avons pas besoin, vous verrez pourquoi dans la partie suivante. :)

Cette petite flopée d’attributs permettra de gérer tous les aspects de nos futurs FBO. Nous en rajouterons quelqu'uns dans la dernière partie consacrée aux améliorations.

// Attributs 

GLuint m_id;
    
int m_largeur;
int m_hauteur;
    
std::vector<Texture> m_colorBuffers;
GLuint m_depthBufferID;

Le header comportera aussi son constructeur par défaut :

FrameBuffer();

Ainsi qu'un second constructeur qui prendra en paramètres la largeur et la hauteur du FBO. Nous passerons leur valeur aux attributs du même nom :

FrameBuffer(int largeur, int hauteur);

Ce sera ce constructeur que l'on utilisera principalement. ;)

Enfin, nous ajoutons aussi le destructeur qui ne change décidément pas de forme :

~FrameBuffer();

Si on résume tout ça :

#ifndef DEF_FRAMEBUFFER
#define DEF_FRAMEBUFFER


// Include Windows

#ifdef WIN32
#include <GL/glew.h>


// Include Mac

#elif __APPLE__
#define GL3_PROTOTYPES 1
#include <OpenGL/gl3.h>


// Include UNIX/Linux

#else
#define GL3_PROTOTYPES 1
#include <GL3/gl3.h>

#endif


// Includes communs

#include <vector>
#include "Texture.h"


// Classe

class FrameBuffer
{
    public:

    FrameBuffer();
    FrameBuffer(int largeur, int hauteur);
    ~FrameBuffer();


    private:

    GLuint m_id;
    
    int m_largeur;
    int m_hauteur;
    
    std::vector<Texture> m_colorBuffers;
    GLuint m_depthBufferID;
};

#endif
Les constructeurs et le destructeur

Passons tout de suite à l'implémentation des quelques pseudo-méthodes que nous avons déclarés, nous en seront débarrassés. :)

Le premier constructeur est assez simple (comme d'habitude) puisqu'il ne fait que mettre des valeurs nulles aux attributs. Nous leur donnerons tous la valeur 0 :

FrameBuffer::FrameBuffer() : m_id(0), m_largeur(0), m_hauteur(0), m_colorBuffers(0), m_depthBufferID(0)
{

}

Le second quant à lui sera un poil différent puisqu'il prendra 2 paramètres qui correspondent aux dimensions du FBO. Les autres attributs seront initialisés avec la valeur 0 :

FrameBuffer::FrameBuffer(int largeur, int hauteur) : m_id(0), m_largeur(largeur), m_hauteur(hauteur), 
                                                     m_colorBuffers(0), m_depthBufferID(0)
{

}

Pour ce qui est du destructeur, celui-ci ne va pas détruire grand chose pour le moment. Nous le remplirons une fois que nous aurons terminé l'implémentation de toutes les méthodes.

FrameBuffer::~FrameBuffer()
{

}

La méthode creerRenderBuffer()

Explications

Maintenant que nous avons un squelette de classe défini, nous allons pouvoir passer à la troisième tâche que nous attend sur la liste : la création des Render Buffers.

Comme je vous l'ai dit tout à l'heure, les Render Buffers ne seront pas codés dans une classe dédiée, ils ne sont pas aussi importants que les textures donc ils n'ont pas besoin d'être séparés du reste. Nous les utiliserons pour gérer le Depth et le Stencil Buffer qui, contrairement au Color, ne sont pas représentables par des textures (souvenez-vous du petit schéma dans l'introduction).

Pour les créer, nous allons implémenter une méthode qui se chargera de générer et de configurer un Render Buffer à partir d'un identifiant OpenGL. De cette façon, nous initialiserons un buffer très simplement en utilisant seulement une lignes de code. :)

La méthode en question s'appellera creerRenderBuffer() et prendra 2 paramètres :

void creerRenderBuffer(GLuint &id, GLenum formatInterne);
  • id : L'identifiant qui représentera le Render Buffer

  • formatInterne : Format interne du buffer. C'est un paramètre que l'on voit plus souvent avec les textures, nous verrons pourquoi nous l'utilisons ici aussi

Génération de l'identifiant

L'avantage des Render Buffers c'est que ce sont des objets OpenGL, ils se gèrent donc de la même façon que les VBO, les VAO et les textures. C'est-à-dire qu'ils ont besoin d'un identifiant et d'un verrouillage pour leur configuration (et à fortiori pour leur utilisation). Nous retrouverons ainsi, une fois de plus, toutes les fonctions du type glGenXXX(), glBindXXX() etc. ^^

Je vais passer rapidement sur ces deux notions que nous avons l'habitude de voir maintenant. La première est la génération d'identifiant, elle se fait ici avec la fonction glGenRenderbuffers() :

void glGenRenderbuffers(GLsizei number, GLuint *renderbuffers);
  • number : Le nombre d'ID à initialiser. Nous lui donnerons toujours la valeur 1

  • renderbuffers : Un tableau de type GLuint ou l'adresse d'une variable de même type représentant le ou les Render Buffer(s)

Nous appellerons cette fonction en donnant en paramètre la valeur 1 ainsi que l'adresse de la variable id (qui est elle-même un paramètre de la méthode creerRenderBuffer()) :

void FrameBuffer::creerRenderBuffer(GLuint &id, GLenum formatInterne)
{
    // Génération de l'identifiant

    glGenRenderbuffers(1, &id);
}

On profite de cette génération pour inclure la vérification d'un "précédent chargement" qui s'applique à tous les objets OpenGL, et donc aux Render Buffers, afin d'éviter d'éventuelles fuites de mémoire. Nous utiliserons pour cela la fonction glIsRenderbuffer() :

GLboolean glIsRenderbuffer(GLuint renderbuffer);

La fonction renvoie la valeur GL_FALSE si aucun chargement n'a été effectué sur l'objet donné ou GL_TRUE si c'est le cas.

Si nous tombons sur la première valeur, nous n'aurons rien à faire. En revanche, si la fonction nous renvoie GL_TRUE il faudra faire attention à détruire le Render Buffer avant de le charger à nouveau. La fonction permettant cette destruction s'appelle glDeleteRenderbuffers() :

void glDeleteRenderbuffers(GLsizei number, GLuint *renderbuffers);

Elle prend exactement les mêmes paramètres que glGenRenderbuffers(), à savoir :

  • number : Le nombre d'ID à détruire

  • renderbuffers : Un tableau de type GLuint ou l'adresse d'une variable de même type contenant le ou les objet(s) à détruire

Comme à l'accoutumé, nous appellerons les deux fonctions que nous venons de voir juste avant la génération d'ID :

void FrameBuffer::creerRenderBuffer(GLuint &id, GLenum formatInterne)
{
    // Destruction d'un éventuel ancien Render Buffer

    if(glIsRenderbuffer(id) == GL_TRUE)
        glDeleteRenderbuffers(1, &id);


    // Génération de l'identifiant

    glGenRenderbuffers(1, &id);
}
Configuration

Maintenant que nous avons un Render Buffer généré, il ne nous reste plus qu'à le configurer. Cette étape commence évidemment par le "verrouillage" de façon à ce qu'OpenGL sache sur quel objet il doit travailler. Nous utiliserons pour cela la fonction glBindRenderbuffer() :

void glBindRenderbuffer(GLenum target, GLuint renderbuffer);
  • target : Le fameux paramètre target :p qui correspond toujours au type de l'objet que l'on veut verrouiller. Dans notre cas, nous lui donnerons la constante GL_RENDERBUFFER

  • renderbuffer : Identifiant représentant le Render Buffer

Nous appellerons cette fonction deux fois : une fois pour le verrouillage et une autre pour le déverrouillage. Le premier appel prendra en paramètre l'identifiant à verrouiller tandis que le second prendra la valeur 0 :

void FrameBuffer::creerRenderBuffer(GLuint &id, GLenum formatInterne)
{
    // Destruction d'un éventuel ancien Render Buffer

    if(glIsRenderbuffer(id) == GL_TRUE)
        glDeleteRenderbuffers(1, &id);


    // Génération de l'identifiant

    glGenRenderbuffers(1, &id);


    // Verrouillage

    glBindRenderbuffer(GL_RENDERBUFFER, id);


    // Déverrouillage

    glBindRenderbuffer(GL_RENDERBUFFER, 0);
}

Le Render Buffer est à présent verrouillé et prêt à être configuré. Cette opération va d'ailleurs être beaucoup plus simple que celle des autres objets OpenGL car nous n'avons besoin ici que d'une seule et unique fonction.

La fonction en question peut d'ailleurs nous faire penser à glTexImage2D() car elle permet de définir le format et les dimensions du Render Buffer. Elle possède cependant moins de paramètres qu'elle, ce qui la rend tout de même plus agréable à utiliser. :p

Cette fameuse fonction s'appelle glRenderbufferStorage() :

void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
  • target : Type de l'objet que l'on veut verrouiller, donc GL_RENDERBUFFER ici

  • internalformat : Format interne du buffer, nous allons faire un petit aparté dessus dans un instant

  • width : largeur du buffer

  • height : hauteur du buffer

Vous vous souvenez du paramètre formatInterne dont je vous parlé tout à l'heure ? Celui de la méthode creerRenderBuffer() ? Et bien c'est ici que nous allons l'utiliser.

Ce dernier permet de définir le "type" de donnée à stocker dans le buffer. Par exemple, les données relatives à la profondeur (Depth) ne sont pas du tout les mêmes que celles relatives aux pochoirs (Stencil), et pourtant ce sont tous les deux des Render Buffers. Pour marquer cette différence, il faut indiquer dès maintenant le type de donnée qu’accueillera le buffer.

Pour cela, nous allons donner le paramètre formatInterne à la fonction glRenderbufferStorage(). Nous lui donnerons également les dimensions du FBO à l'aide des attributs m_largeur et m_hauteur :

// Configuration du Render Buffer

glRenderbufferStorage(GL_RENDERBUFFER, formatInterne, m_largeur, m_hauteur);

Avec cet appel, nous demandons à créer un Render Buffer du type donné par le paramètre formatInterne (nous verrons ses valeurs possibles dans la partie qui suit) le tout avec les dimensions de notre FBO initial.

Si on ajoute ceci à notre méthode, on obtient :

void FrameBuffer::creerRenderBuffer(GLuint &id, GLenum formatInterne)
{
    // Destruction d'un éventuel ancien Render Buffer

    if(glIsRenderbuffer(id) == GL_TRUE)
        glDeleteRenderbuffers(1, &id);


    // Génération de l'identifiant

    glGenRenderbuffers(1, &id);


    // Verrouillage

    glBindRenderbuffer(GL_RENDERBUFFER, id);


        // Configuration du Render Buffer

        glRenderbufferStorage(GL_RENDERBUFFER, formatInterne, m_largeur, m_hauteur);


    // Déverrouillage

    glBindRenderbuffer(GL_RENDERBUFFER, 0);
}

La méthode creerRenderBuffer() est maintenant terminée, nous pouvons l'utiliser quand bon nous semble. C'est d'ailleurs ce que nous allons faire tout de suite en utilisant tout ce que l'on vient de faire (Color et Render Buffers) dans le but de créer enfin notre premier FBO. :D

Le Frame Buffer

La méthode charger()

Une impression de déjà-vu

Après avoir créé les deux types de buffers dans les parties précédentes, nous allons enfin pouvoir les associer au FBO. Si vous vous sentez un peu perdus entre tous ces buffers, vous pouvez faire une petite analogie avec les shaders : d'un coté vous avez les Color et Render Buffers (les Vertex et Fragment Shaders) et de l'autre le Frame Buffer qui va les utiliser (Le Programme). Je vous redonne le schéma du début pour vous resituer :

Image utilisateur

La comparaison avec les shaders ne s'arrête pas là d'ailleurs car nous retrouvons des notions similaires telles que l'association des buffers avec le FBO, le "attachment", ainsi que la vérification d'intégrité qui sera ici beaucoup moins longue à coder je vous rassure :p

Nous ferons tout ça dans une nouvelle méthode qui s'appellera tout simplement charger() :

bool charger();

Elle retournera un booléen pour confirmer ou non la réussite du chargement. Pensez à inclure ce prototype dans le header de la classe FrameBuffer.

La génération d'identifiant

Allez, on se met dans le bain directement et on commence à coder notre nouvelle méthode dès maintenant. Le début sera très similaire à ce que nous avons fait jusqu'à présent car les Frame Buffers sont eux-aussi des objets OpenGL (et oui encore :p ). On retrouvera donc une fois de plus la génération d'identifiant, la vérification de double chargement et le verrouillage.

La première de ces opérations sera assurée par la fonction glGenFrambuffers() :

void glGenFramebuffers(GLsizei number,  GLuint *ids);
  • number : Le nombre d'ID à initialiser. Nous lui donnerons la valeur 1

  • ids : Un tableau de type GLuint ou l'adresse d'une variable de même type représentant le ou les FBO

Le verrouillage quant à lui, sera effectué par la fonction glBindFramebuffer() :

void glBindFramebuffer(GLenum target, GLuint framebuffer);
  • target : Type d'objet à verrouiller, ici nous lui donnerons toujours la valeur GL_FRAME_BUFFER

  • framebuffer : Identifiant représentant le FBO

Nous utiliserons ces deux fonctions au début de la méthode charger(). L'identifiant à donner sera évidemment l'attribut m_id de notre classe FrameBuffer :

bool FrameBuffer::charger()
{
    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);

  
    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

Remarquez le dernier appel qui permet de déverrouiller le FBO.

Comme d'habitude, on en profite au passage pour inclure le code qui permet d'éviter le double chargement. Nous utiliserons pour cela les fonctions glIsFramebuffer() et glDeleteFramebuffers() :

GLboolean glIsFramebuffer(GLuint framebuffer);

void glDeleteFramebuffers(GLsizei number, GLuint *framebuffers);
  • La première renverra la valeur GL_FALSE si aucun chargement n'a été effectué ou GL_TRUE si c'est le cas.

  • La seconde prendra les mêmes paramètres que la fonction de génération d'ID.

Nous appellerons ces fonctions avec un bloc if de la même façon que d'habitude. Je vous conseille cependant de mettre des accolades à votre bloc car nous rajouterons une instruction dans très peu de temps. ;)

// Vérification d'un éventuel ancien FBO

if(glIsFramebuffer(m_id) == GL_TRUE)
{
    glDeleteFramebuffers(1, &m_id);
}

Si on ajoute ceci au code que nous avions déjà :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);

  
    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
Le Color Buffer

Comme dit un peu plus, la configuration du FBO va être d'une simplicité enfantine. Nous avons déjà tout codé dans les parties précédentes, nous n'avons juste qu'à appeler nos nouvelles méthodes pour créer nos différents buffers.

Le premier dont nous allons nous occuper est le Color Buffer. En théorie, nous devrions gérer le cas où nous en utiliserions 16 comme le permet OpenGL, cependant je ne veux pas vous alourdir encore plus les explications surtout avec tout ce que l'on a vu jusqu'à maintenant. Nous verrons donc cela dans la dernière partie de ce chapitre qui sera consacrée aux améliorations.

En attendant, nous allons gérer le cas où nous utilisons un seul et unique Color Buffer qui correspond, je vous le rappelle, à une texture. La première étape consistera à créer un nouvel objet de type Texture grâce au nouveau constructeur de cette classe.

Texture(int largeur, int hauteur, GLenum format, GLenum formatInterne, bool textureVide);

Celui-ci prendra en paramètres :

  • Les dimensions du FBO : parce qu'il faut bien que les buffers fassent la même taille que lui. Ces dimensions sont représentées par les attributs m_largeur et m_hauteur de notre classe

  • Le format : qui correspond au format des couleurs (3 ou 4 couleurs avec le canal Alpha). Nous lui affecterons la constante GL_RGBA

  • Le format interne des couleurs : qui correspond au interne de la texture (l'ordre des couleurs). Bizarrement, nous lui affecterons la constante GL_RGBA, celle-ci fonctionne pour les deux paramètres

  • Le booléen textureVide : qui sera utile en cas de copie de texture pour savoir quelle méthode appeler (charger() ou chargerTextureVide()). Il faut lui affecter la valeur true évidemment

Nous créons donc un objet Texture en utilisant ce constructeur :

// Création du Color Buffer

Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);

Ensuite, on appelle la méthode chargerTextureVide() de notre nouvel objet de façon à l'initialiser avec les bonnes dimensions et les bons formats :

// Création du Color Buffer

Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
colorBuffer.chargerTextureVide();

Enfin, on l'ajoute au tableau dynamique m_colorBuffers. Ce dernier ne contiendra qu'une seule texture pour le moment mais n'oubliez pas que nous allons gérer les autre cas un peu plus tard :

// Création du Color Buffer

Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
colorBuffer.chargerTextureVide();


// Ajout au tableau

m_colorBuffers.push_back(colorBuffer);

Petit récap :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);


        // Création du Color Buffer

        Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
        colorBuffer.chargerTextureVide();


        // Ajout au tableau

        m_colorBuffers.push_back(colorBuffer);

  
    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
Le Depth et le Stencil Buffer

Bien, on passe maintenant aux Render Buffers.

Dans la partie précédente, nous avons codé une méthode permettant d'en créer un très facilement. Son prototype est le suivant :

void creerRenderBuffer(GLuint &id, GLenum formatInterne);
  • id : référence sur l'identifiant représentant le buffer créé

  • formatInterne : Format interne du buffer

Fut un temps, nous aurions appelé cette méthode deux fois : une pour le Depth Buffer et l'autre pour le Stencil. Cependant, les choses ont changés et ce système ne semble plus fonctionner avec OpenGL 3. Au lieu d'avoir deux buffers séparés, nous n'en avons maintenant plus qu'un seul qui remplit le rôle du Depth et du Stencil.

Notre petit schéma n'est donc plus exact, il doit être modifié pour fusionner les deux Render Buffers :

Image utilisateur

Ce schéma est plus proche de la réalité. :)

En définitif, nous n'appellerons pas la méthode creerRenderBuffer() deux fois mais une seule fois. Nous lui donnerons en paramètre l'attribut m_depthBufferID qui accueillera le "double-buffer" ainsi que la constante GL_DEPTH24_STENCIL8 pour le format interne. Cette constante permet de :

  • Définir une profondeur de 24bits pour le Depth Buffer. Nous avons utilisé le même nombre au moment de configurer le contexte OpenGL avec la fonction SDL_GL_SetAttribute()

  • Même chose pour le Stencil Buffer mais avec seulement 8 bits cette fois-ci. Ce buffer prend moins de place en mémoire

L'appel final ressemble donc à ceci :

// Création du Depth et du Stencil Buffer

creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);

Quelle simplicité, nous venons de créer un double-buffer en seulement une lignes de code. :p

Petite récap :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);


        // Création du Color Buffer

        Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
        colorBuffer.chargerTextureVide();


        // Ajout au tableau

        m_colorBuffers.push_back(colorBuffer);


        // Création du Depth et du Stencil Buffer

        creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);

  
    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
Association (Attachment)

Nos différentes buffers sont maintenant créés et prêts à être utilisés. Cependant ils ne sont pas d'une grande utilité pour le moment, ils sont tous éparpillés dans plusieurs attributs et OpenGL ne connait même pas leur existence. Pour remédier à ce problème, nous allons les associer au FBO, un peu comme nous avons associé les shaders au programme.

Pour faire cela, nous aurons besoin de deux fonctions : l'une est dédiée aux Color Buffers et l'autre aux Render Buffers. Je vais vous demander un peu d'attention pour leur explication car ce sont certainement les fonctions OpenGL les plus importantes de ce chapitre.

La première s'appelle glFramebufferTexture2D() (un peu compliqué comme nom ^^ ) :

void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
  • target : Le paramètre target du FBO, soit GL_FRAMEBUFFER ici

  • attachment : Paramètre SUPER important que nous allons développer dans un instant

  • textarget : Le paramètre target de la texture cette fois, soit GL_TEXTURE_2D

  • texture : L'identifiant de la texture à associer. Nous utiliserons le getter getID() de notre Color Buffer

  • level : Paramètre que l'on a déjà rencontré dans le chapitre sur les textures. Nous l'avions laissé à 0 et c'est ce que nous allons faire ici aussi

Le paramètre auquel il faut faire attention ici est évidemment le paramètre attachment. Ce dernier correspond au point d'attache, ou plus grossièrement à l'index du Color Buffer. N'oubliez pas que nous pouvons en créer jusqu'à 16, il faut qu'OpenGL donne un index à chacun d'entre eux pour pouvoir les reconnaitre au moment de l'affichage.

C'est précisément ce que fait le paramètre attachment, il permet de différencier les buffers au moment de les associer. Les valeurs qu'il peut prendre correspondent aux constantes allant de GL_COLOR_ATTACHMENT0 à GL_COLOR_ATTACHMENT15. Dans notre cas, nous n'utiliserons que la première car nous n'avons qu'un seul buffer à gérer.

Au final, l'appel à la fonction glFramebufferTexture2D() ressemblera à ceci :

// Association du Color Buffer

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffers[0].getID(), 0);

La seconde fonction d'association ressemble fortement à la première, ses paramètres sont assez semblables sauf qu'ils permettent de gérer les Render Buffers. La fonction s'appelle glFramebufferRenderbuffer() :

void glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
  • target : Le paramètre target du FBO, soit GL_FRAMEBUFFER

  • attachment : Paramètre que l'on va développer dans un instant ;)

  • renderbuffertarget : Le paramètre target du Reder Buffer cette fois, celui que nous avons utilisé pour le configurer. Nous lui donnerons donc la constante GL_RENDERBUFFER

  • renderbuffer : L'identifiant du Render Buffer à associer. Nous lui donnerons l'attribut m_depthBufferID qui contient le Depth et le Stencil Buffer

Le principe du paramètre attachment ne change pas vraiment par rapport au précédent on parle toujours du point d'attache, sauf qu'ici OpenGL n'attend plus un index mais le type de Render Buffer que l'on souhaite associer.

Là-aussi, nous devrions en théorie appeler cette fonction deux fois pour le Depth et le Stencil Buffer, cependant avec la fusion de ces derniers, nous n'aurons besoin que d'un seul appel.

Le paramètre attachment prendra une constante assez proche de celle que l'on a utilisée pour la méthode creerRenderBuffer() et qui s'appelle : GL_DEPTH_STENCIL_ATTACHMENT. L'appel à la fonction glFramebufferRenderbuffer() ressemblera donc à ceci :

// Association du Depth et du Stencil Buffer

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);

Avec cet appel, on associe le double-buffer que l'on a créé précédemment avec le point d'attache GL_DEPTH_STENCIL_ATTACHMENT. Le FBO possède maintenant tous les buffers dont il a besoin pour fonctionner. ^^

Si on récapitule tout ça :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);


        // Création du Color Buffer

        Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
        colorBuffer.chargerTextureVide();


        // Ajout au tableau

        m_colorBuffers.push_back(colorBuffer);


        // Création du Depth et du Stencil Buffer

        creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);


        // Association du Color Buffer

        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffers[0].getID(), 0);


        // Association du Depth et du Stencil Buffer

        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);

  
    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
Vérification de la construction

En théorie, nous sommes maintenant capables d'utiliser notre classe FrameBuffer sans ajouter de code quelconque. Cependant, faire cela serait assez imprudent car nous ne savons absolument pas si notre FBO est valide ou non.

Pour éviter d'être surpris lors de notre prochain affichage, nous allons coder encore un petit bloc dans la méthode charger() qui nous permettra de vérifier ce que l'on peut appeler l'intégrité du FBO. Si une erreur s'est produite au moment de sa construction (buffer, association, etc.) OpenGL nous le fera savoir, et nous devrons réagir en conséquence.

Mais euh comment on fait pour savoir si le FBO est mal construit ?

La réponse est très simple : il existe une fonction pour nous le dire, tout comme avec les shaders.

Heureusement pour nous, la fonction en question est moins compliquée à utiliser que celle des shaders une fois de plus. Vous vous souvenez qu'avec eux, il fallait vérifier s'il y avait une erreur, récupérer la taille du message et l'afficher. Avec les FBO, nous n'avons pas à faire tout ça. :) Mais en contrepartie, nous n'aurons pas de détails précis sur l'erreur remontée. Ce n'est pas trop grave ici car nous n'avons pas de code source à vérifier, l'erreur est donc moins susceptible de venir de nous.

Cette fonction de vérification s'appelle glCheckFramebufferStatus() :

GLenum glCheckFramebufferStatus(GLenum target);
  • target : Le paramètre target des FBO ! La constante GL_FRAMEBUFFER. Oui c'est un peu spécial mais je vous rassure, la fonction fonctionne correctement.

Elle renvoie plusieurs constantes en cas d'erreur ou GL_FRAMEBUFFER_COMPLETE si tout s'est bien passé. Pour plus de simplicité, nous n'utiliserons que cette dernière. ;)

Nous commençons donc notre code en vérifiant la valeur renvoyée par la fonction glCheckFramebufferStatus(). Si elle est différente de GL_FRAMEBUFFER_COMPLETE alors on entre dans un bloc if :

// Vérification de l'intégrité du FBO

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{

}

Si le programme entre dans ce bloc, c'est qu'il s'est passé quelque chose de mauvais dans la méthode (un Render Buffer mal initialisé par exemple). Si cela arrive, il faudra libérer toute la mémoire prise par les différents objets.

Pour cela, nous allons appeler la fonction glDeleteFramebuffers() pour détruire le FBO et la fonction glDeleteRenderbuffers() pour détruire le double-buffer gérant le Depth et le Stencil :

// Vérification de l'intégrité du FBO

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
    // Libération des buffers

    glDeleteFramebuffers(1, &m_id);
    glDeleteRenderbuffers(1, &m_depthBufferID);
}

Hum au fait, on ne libère pas tout là ... Il manque encore le Color Buffer non ?

Oui c'est exact ! Je l'ai gardé pour la fin celui-la. :p

En fait, ce buffer-la est un peu spécial vu qu'il s'agit d'une texture. Sa libération dépend donc entièrement de son destructeur. Pour l'appeler, il nous suffit simplement de supprimer la texture dans le tableau m_colorBuffers, le programme appellera automatiquement le destructeur concerné. C'est une des bases du C++.

Pour supprimer la texture, nous n'allons pas utiliser la méthode pop_back() contrairement à ce qu'on pourrait penser. A la place, nous allons prendre un peu d'avance et imaginer que nous ayons 7 Color Buffers à détruire. Si nous étions dans ce cas, nous n'appellerions pas la même méthode 7 fois d'affilé. Ce serait une perte de temps, surtout que la classe vector nous fourni une jolie méthode qui permet de vider entièrement son contenu. Cette méthode s'appelle clear().

Si nous l'utilisons, le tableau se videra entièrement et le programme appellera automatiquement les destructeurs de tous les objets qu'il contient. ^^

Donc au final, pour libérer la mémoire prise par toutes les textures, nous appellerons la méthode clear() :

// Vérification de l'intégrité du FBO

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
    // Libération des buffers

    glDeleteFramebuffers(1, &m_id);
    glDeleteRenderbuffers(1, &m_depthBufferID);

    m_colorBuffers.clear();
}

Maintenant, tous nos objets sont détruits proprement et la mémoire est libérée.

Il ne reste plus qu'à afficher un message d'erreur pour conclure le tout et renvoyer la valeur false :

// Vérification de l'intégrité du FBO

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
    // Libération des buffers

    glDeleteFramebuffers(1, &m_id);
    glDeleteRenderbuffers(1, &m_depthBufferID);

    m_colorBuffers.clear();


    // Affichage d'un message d'erreur et retour de la valeur false

    std::cout << "Erreur : le FBO est mal construit" << std::endl;

    return false;
}

Si on récapitule toute notre méthode charger() :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    glGenFramebuffers(1, &m_id);


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, m_id);


        // Création du Color Buffer

        Texture colorBuffer(m_largeur, m_hauteur, GL_RGBA, GL_RGBA, true);
        colorBuffer.chargerTextureVide();


        // Ajout au tableau

        m_colorBuffers.push_back(colorBuffer);


        // Création du Depth et du Stencil Buffer

        creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);


        // Association du Color Buffer

        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffers[0].getID(), 0);


        // Association du Depth et du Stencil Buffer

        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);


        // Vérification de l'intégrité du FBO

        if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        {
            // Libération des buffers

            glDeleteFramebuffers(1, &m_id);
            glDeleteRenderbuffers(1, &m_depthBufferID);

            m_colorBuffers.clear();


            // Affichage d'un message d'erreur et retour de la valeur false

            std::cout << "Erreur : le FBO est mal construit" << std::endl;

            return false;
        }
  

    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

Il ne manque plus que la touche finale : le renvoi de la valeur true pour indiquer que tout s'est bien passé (tant que la condition précédente n'a pas été déclenchée) :

bool FrameBuffer::charger()
{
    // Création du FBO + Vérification

    ....


    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);


    // Si tout s'est bien passé, on renvoie la valeur true

    return true;
}

Cette fois, nous avons enfin terminé tout le codage des Frame Buffer. :D Nous avons créé tout ce dont ils avaient besoin pour fonctionner. Nous avons même inclus un code de vérification en cas d'erreur.

On passe maintenant aux derniers détails à régler avant d'utiliser notre premier FBO dans une scène 3D. :D

Petit rajout

On commence tout de suite les petits détails par la première vérification de la méthode charger(), celle qui concerne le cas des double chargements. Je vous avais demandé de rajouter des accolades à votre bloc if car nous allions rajouter quelques lignes de code :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        glDeleteFramebuffers(1, &m_id);
    }


    // Génération d'un id

    ....
}

Les lignes de code concernées sont en fait celles qui permettent de détruire tous les objets OpenGL. Le but de cette vérification est de nettoyer un éventuel ancien chargement, il faut donc penser à détruire tous les objets qui étaient présents avant.

Cependant, nous n'ajouterons que la destruction des Colors Buffers car les Render Buffers sont détruits automatiquement au début de la méthode creerRenderBuffer(). Il est donc inutile de les détruire une seconde fois. ;)

Donc au final, nous n'ajoutons que l'appel à la méthode clear() pour l'attribut m_colorBuffers :

bool FrameBuffer::charger()
{
    // Vérification d'un éventuel ancien FBO

    if(glIsFramebuffer(m_id) == GL_TRUE)
    {
        // Destruction du Frame Buffer

        glDeleteFramebuffers(1, &m_id);


        // Libération des Color Buffers

        m_colorBuffers.clear();
    }


    // Génération d'un id

    ....
}

Le destructeur

Contrairement au début du chapitre, le destructeur va enfin pouvoir se remplir un peu pour lui permettre de détruire tous les objets OpenGL. Pour nous faciliter la vie en plus, nous allons faire les faignants et reprendre le code de destruction que nous déjà avons fait au moment de vérifier l'intégrité du FBO. :p En effet, ce code-la détruit proprement les différents objets OpenGL utilisés, à savoir tous les Color Buffers, le double Render Buffer (Depth et Stencil) et le FBO en lui-même pour finir :

FrameBuffer::~FrameBuffer()
{
    // Destruction des buffers

    glDeleteFramebuffers(1, &m_id);
    glDeleteRenderbuffers(1, &m_depthBufferID);

    m_colorBuffers.clear();
}

Quelques getters

On termine cette partie avec quelques getters qui nous serviront à manipuler nos FBO sans problème. Nous en aurons exactement besoin de quatre : le premier permettra de récupérer l'identifiant du FBO, le deuxième permettra de récupérer ceux des différents Color Buffer et les deux derniers s'occuperont de renvoyer la largeur et la hauteur. Voici leur prototype :

GLuint getID() const;
GLuint getColorBufferID(unsigned int index) const;

int getLargeur() const;
int getHauteur() const;

L'implémentation de la première méthode se passe de commentaire, il suffit de renvoyer la valeur de l'attribut m_id :

GLuint FrameBuffer::getID() const
{
    return m_id;
}

Au niveau de la seconde méthode, on ne peut pas se contenter de renvoyer l'attribut m_colorBuffers car cela casserait la règle de l'encapsulation (il ne faut pas pouvoir accéder à un attribut en dehors de sa classe d'origine). A la place, nous allons utiliser un index pour récupérer une texture dans ce tableau et appeler ensuite sa propre méthode getID(). De cette façon, on peut renvoyer l'identifiant du Color Buffer souhaité sans avoir à accéder directement à l'attribut m_colorBuffers en dehors de la classe. :)

GLuint FrameBuffer::getColorBufferID(unsigned int index) const
{
    return m_colorBuffers[index].getID();
}

Les deux derniers getters renverront simplement les attributs m_largeur et m_hauteur :

int FrameBuffer::getLargeur() const
{
    return m_largeur;
}

int FrameBuffer::getHauteur() const
{
    return m_hauteur;
}

Grâce à ces getters, nous pourrons utiliser nos FBO facilement. ^^ Et c'est justement ce que nous allons faire maintenant.

Utilisation

Quelques explications

Après toutes les péripéties des précédentes parties, nous allons pouvoir nous reposer un peu. L'utilisation des FBO est une chose assez simple si on la compare à la configuration car il n'y a pas grand chose à faire.

L'objectif de cette partie va être de faire un rendu d'une caisse à l'intérieur d'un carré, que l'on peut comparer un à second écran :

Image utilisateur

Avec ceci, nous ne verrons plus la caisse comme un modèle 3D mais comme une simple image sur un écran de télévision. Nous ajouterons même une petite rotation pour prouver que le rendu se fait en temps réel.

Mais avant cela, nous allons voir ensemble quelques notions relatives à l'utilisation des FBO. Elles ne sont pas compliquées à comprendre mais il faut mieux les voir pour éviter d'être surpris par certains points plus tard.

Les passes

La première chose à savoir à propos de l'utilisation des FBO c'est qu'elle est axée autour de deux étapes, ou de deux passes.

La première passe consiste à effectuer le rendu de ce qui se trouvera dans le FBO. Dans notre cas, ce serait uniquement une caisse. Si nous voulions faire une caméra de sécurité, il y aurait une salle, des personnages, des bureaux, etc. En gros tout ce qui se trouve dans le champ de vision de la caméra.

Tous ces rendus ne s'affichent pas sur l'écran évidemment mais dans le Color Buffer du FBO qui est une simple texture en deux dimensions.

La seconde passe quant à elle consiste à faire le rendu "normal" de la scène 3D, exactement comme nous l'avons toujours fait jusque là. La seule différence sera l'ajout d'un carré, ou tout autre surface, sur laquelle nous afficherons la texture du FBO. Pour reprendre l'exemple de la caméra, nous ajouterions une carré représentant une télévision quelque part dans notre scène sur laquelle viendrait s'afficher le rendu du FBO. Nous pourrions ainsi observer ce qui se trouve dans une salle tout en étant à un autre endroit.

Bien entendu, nous pouvons faire beaucoup plus de choses, notamment des ajouts d'effets sur la texture, mais vous avez au moins un aperçu de ce que sont les passes.

La résolution d'un FBO

La résolution des FBO est notion importante à prendre en compte car elle impacte directement les performances de votre application.

Il faut savoir qu'un FBO fera très rarement la taille de votre véritable écran. Il est tout à fait possible de procéder ainsi mais les ressources consommées seront trop importantes du fait des deux passes. C'est comme si vous travailliez avec des textures de 2048x2048 ...

Le choix de la résolution dépend de ce que vous voulez faire avec votre FBO. Un effet de miroir demandera plus de précision qu'un effet de flou par exemple. Il faudra jauger en fonction des situations.

Dans ce cas, nous utiliserons une résolution de 512x512. Cela nous rapprochera du pack de textures que nous utilisons.

Les matrices

Étant donné que nous avons deux affichages différents, nous allons devoir utiliser deux couples de matrices différents. Nous aurons ainsi une matrice projection et modelview dédiées à la première passe, et un autre couple pour le rendu normal.

Même si l'on fait deux fois le même affichage il vaut mieux séparer les matrices, en particulier la matrice projection car c'est elle qui gère la résolution finale. Souvenez-vous de ce que fait sa méthode perspective().

Première utilisation

Après ces petites explications, nous pouvons passer à la pratique. Le but est d'afficher une caisse dans une sorte de télé, nous allons le faire en faisant attention aux quelques points que nous avons vus à l'instant.

Création du FBO

Premièrement, nous allons reprendre le code du dernier chapitre et ajouter quelques lignes de code pour créer notre FBO. Celui-ci sera contenu dans un objet FrameBuffer et sa résolution de 512x512 pixels :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Frame Buffer

    FrameBuffer frameBuffer(512, 512);
    frameBuffer.charger();


    // Matrices

    ....
}

N'oubliez pas d'inclure le header FrameBuffer.h pour ne pas avoir d'erreur au moment de la compilation. N'oubliez pas non plus d'appeler la méthode charger().

Les matrices

Maintenant que le FBO est créé, nous pouvons passer à la création du second couple de matrices, celles qui seront dédiées à la première passe. Ces matrices porteront quasiment le même nom que celles que nous avons l'habitude de manipuler :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Frame Buffer

    FrameBuffer frameBuffer(512, 512);
    frameBuffer.charger();


    // Matrices (première passe)

    mat4 projectionFBO, modelviewFBO;

    ....
}

L'initialisation de ces matrices sera elle-aussi quasiment identique. La seule chose qui va différer dans cette initialisation c'est le rapport des pixels pour la matrice de projection (le ratio).

De base, celui-ci est relatif à la taille de votre fenêtre, tout dépend de ce que vous avez mis avant. Cependant le rendu de la première passe s'effectue dans le FBO, le rapport doit donc est relatif à la taille de ce dernier. Vu que nous avons spécifié une taille de 512x512 pixels, le rapport sera donc de 512 / 512. Nous utiliserons les getters sur la largeur et la hauteur du FBO pour avoir accès à ces valeurs, ce qui sera utile en cas de changement de ces dimensions :

// Matrices (première passe)

mat4 projectionFBO;
mat4 modelviewFBO;


// Initialisation

projectionFBO = perspective(70.0, (double)frameBuffer.getLargeur() / frameBuffer.getHauteur(), 1.0, 100.0);
modelviewFBO = mat4(1.0);

Si vous vous posez la question, sachez que l'autre couple de matrices n'a pas besoin d'être modifié, elles sont très bien comme ça. :)

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Frame Buffer

    FrameBuffer frameBuffer(512, 512);
    frameBuffer.charger();


    // Matrices (première passe)

    mat4 projectionFBO;
    mat4 modelviewFBO;

    projectionFBO = perspective(70.0, (double)frameBuffer.getLargeur() / frameBuffer.getHauteur(), 1.0, 100.0);
    modelviewFBO = mat4(1.0);


    // Matrices (seconde passe)

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);



    // Caméra

    ....
}
La première passe

Tous nos objets sont maintenant en place, il ne manque plus qu'à passer aux deux passes. Dans la première, nous allons juste afficher une caisse dans le FBO. Créez donc, si ce n'est déjà fait, un objet Caisse. Ajoutez également une variable de type float, elle nous permettra de le faire pivoter sur lui-même :

void SceneOpenGL::bouclePrincipale()
{
    // Frame Buffer, Matrices et Caméra

    ....


    // Objet Caisse

    Caisse caisse(2.0, "Shaders/textureMVP.vert", "Shaders/texture.frag", "Textures/Caisse2.jpg");
    caisse.charger();

    float angle = 0.0;


    // Boucle principale 

    ....
}

Étant donné que les FBO se comportent comme des écrans, nous n'avons pas à modifier notre manière d'effectuer nos rendus. Il nous faut toujours nettoyer les buffers avec la fonction glClear(), repositionner la caméra, etc. La seule chose qui ne sera pas présente ce sera la limitation Frame Rate qui elle concerne l'application entière et pas juste un FBO.

Pour être plus précis, nous devons :

  • Nettoyer les buffers

  • Ré-initialiser la matrice modelview

  • Replacer la caméra

  • Afficher la caisse, rotation comprise

Nous avons déjà vu ce code pas mal de fois, je passerai donc les explications pour celui-ci. ^^ Faites juste attention à utiliser les matrices réservées au FBO.

// Boucle principale

while(!m_input.terminer())
{
    // Gestion des évènements

    ....


  
    /* ***** Première passe ***** */


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Placement de la caméra

    modelviewFBO = lookAt(vec3(3, 0, 3), vec3(0, 0, 0), vec3(0, 1, 0));


    // Gestion de la rotation de la caisse

    angle += 2;

    if(angle > 360)
        angle -= 360;


    mat4 sauvegardeModelviewFBO = modelviewFBO;

            modelviewFBO = rotate(modelviewFBO, angle, vec3(0, 1, 0));
            caisse.afficher(projectionFBO, modelviewFBO);

    modelviewFBO = sauvegardeModelviewFBO;


    // Actualisation de la fenêtre

    ....
}

Remarquez ici que l'on n'utilise pas la caméra mobile, on se place au niveau d'un point fixe. Si nous faisons cela, c'est pour éviter que le contenu du FBO ne change en fonction des mouvements de la caméra. Si vous faites un test avec elle, vous remarquez que votre cube n'apparait qu'à certains endroits car votre point de vue est modifié en permanence.

Mais si on ne change rien ce code, le rendu va se faire sur l'écran non ?

Oui évidemment si on ne dit rien à OpenGL il va comprendre qu'il doit tout afficher sur l'écran.

Pour lui dire d'utiliser le FBO, nous allons devoir ajouter deux choses :

  • Premièrement, nous allons devoir encadrer toutes les étapes précédentes par le verrouillage du FBO que l'on veut remplir. Il faudra donc appeler la fonction glBindFramebuffer() juste avant l'appel à glClear() et une autre fois après avoir affiché tous nos objets.

  • Ensuite, nous allons redimensionner virtuellement la fenêtre, ou plutôt le contexte OpenGL. Nous devons faire cela car celui-ci considère toujours qu'il est dans une fenêtre dont les dimensions sont celles que nous avons spécifiées au début du programme. Une petite fonction lui permettra de redimensionner son espace d'affichage pour correspondre aux dimensions du FBO.

Le verrouillage va être très simple à faire car il nous suffit d'appeler la fonction glBindFramebuffer() au début et à la fin de l'affichage de la première passe :

// Boucle principale

while(!m_input.terminer())
{
    // Gestion des évènements

    ....


  
    /* ***** Première passe ***** */


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer.getID());


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Placement de la caméra

        modelviewFBO = lookAt(vec3(3, 0, 3), vec3(0, 0, 0), vec3(0, 1, 0));


        // Gestion de la rotation de la caisse

        angle += 2;

        if(angle > 360)
            angle -= 360;


        // Affichage de la caisse

        mat4 sauvegardeModelviewFBO = modelviewFBO;

            modelviewFBO = rotate(modelviewFBO, angle, vec3(0, 1, 0));
            caisse.afficher(projectionFBO, modelviewFBO);

        modelviewFBO = sauvegardeModelviewFBO;


    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);


    // Actualisation de la fenêtre

    ....
}

Le redimensionnement de la fenêtre est une notion qui peut vous paraitre un peu bizarre, mais pour vous donner un exemple voici ce qui se passerait si nous ne faisions pas cette étape :

Image utilisateur

Dans ce cas, OpenGL et le FBO ne travaillent pas avec les mêmes dimensions, la caisse peut se retrouver alors déformée, tronquée, ou je ne sais quoi d'autre.

Pour éviter cela, nous allons dire à OpenGL de travailler temporairement avec les dimensions du FBO (512x512 ici). Ceci se fait grâce à la fonction glViewport() :

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
  • x : abscisse où commence le redimensionnement. Le point de coordonnées (0; 0) correspond au coin inférieur gauche de votre fenêtre

  • y : ordonnée où commence le redimensionnement

  • width : nouvelle largeur de la zone d'affichage

  • height : nouvelle hauteur

Cette fonction est à appeler juste après la fonction de nettoyage glClear() :

// Boucle principale

while(!m_input.terminer())
{
    // Gestion des évènements

    ....



    /* ***** Première passe ***** */


    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer.getID());


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Redimensionnement de la zone d'affichage

        glViewport(0, 0, frameBuffer.getLargeur(), frameBuffer.getHauteur());


        // Placement de la caméra

        modelviewFBO = lookAt(vec3(3, 0, 3), vec3(0, 0, 0), vec3(0, 1, 0));


        // Gestion de la rotation de la caisse

        angle += 2;

        if(angle > 360)
            angle -= 360;


        // Affichage de la caisse

        mat4 sauvegardeModelviewFBO = modelviewFBO;

            modelviewFBO = rotate(modelviewFBO, angle, vec3(0, 1, 0));
            caisse.afficher(projectionFBO, modelviewFBO);

        modelviewFBO = sauvegardeModelviewFBO;


    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);


    // Actualisation de la fenêtre

    ....
}

Si tout s'est bien passé jusque là, vous devriez avoir le rendu de votre caisse à l'intérieur de votre FBO, et plus précisément à l'intérieur de la texture qu'est le Color Buffer.

Si vous pensez qu'il y a trop de choses à apprendre, ne vous inquiétez pas nous allons faire un point récapitulatif à la fin de cette partie. ^^

La seconde passe

La seconde passe est la dernière étape d'implémentation d'un FBO, elle consiste simplement à afficher le contenu de son Color Buffer sur une surface. Si vous avez une scène 3D, c'est le moment de l'afficher également.

Il n'y a pas de nouvelles notions à apprendre ici, il faut effectuer le rendu comme d'habitude. Le seul petit ajout sera le redimensionnement de la zone d'affichage qui est réduite à 512x512 pour le moment. Il faudra remettre les dimensions de la fenêtre SDL.

Pour commencer, nous allons afficher un carré qui accueillera la texture du FBO. Ajoutez donc le code suivant après déclaré la caisse. Je passe un peu son explication, le chapitre est assez dense comme ça, il permet juste d'afficher un carré avec une gestion des VBO/VAO. Les coordonnées de textures sont également présentes, elles permettront de faire le lien avec la texture du FBO.

void SceneOpenGL::bouclePrincipale()
{
    // Frame Buffer, Matrices et Caméra

    ....


    // Objet Caisse

    ...


    // Vertices

    float vertices[] = {0, 0, 0,   4, 0, 0,   4, 4, 0,     // Triangle 1
    			0, 0, 0,   0, 4, 0,   4, 4, 0};    // Triangle 2

    float coordTexture[] = {0, 0,   1, 0,   1, 1,          // Triangle 1
			    0, 0,   0, 1,   1, 1};         // Triangle 2



    /* ***** Gestion du VBO ***** */

    GLuint vbo;
    int tailleVerticesBytes = 18 * sizeof(float);
    int tailleCoordTextureBytes = 12 * sizeof(float);


    // Génération du VBO

    glGenBuffers(1, &vbo);


    // Verrouillage

    glBindBuffer(GL_ARRAY_BUFFER, vbo);

	// Remplissage

	glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes + tailleCoordTextureBytes, 0, GL_STATIC_DRAW);
	glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);
	glBufferSubData(GL_ARRAY_BUFFER, tailleVerticesBytes, tailleCoordTextureBytes, coordTexture);


    // Déverrouillage

    glBindBuffer(GL_ARRAY_BUFFER, 0);


    /* ***** Gestion du VAO ***** */

    GLuint vao;


    // Génération du VAO

    glGenVertexArrays(1, &vao);


    // Verrouillage du VAO

    glBindVertexArray(vao);

	// Verrouillage du VBO

	glBindBuffer(GL_ARRAY_BUFFER, vbo);


		// Vertex Attrib 0 (Vertices)

		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
		glEnableVertexAttribArray(0);


		 // Vertex Attrib 0 (Vertices)

		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(tailleVerticesBytes));
		glEnableVertexAttribArray(2);


	// Déverrouillage du VBO

	glBindBuffer(GL_ARRAY_BUFFER, 0);


    // Déverrouillage du VAO

    glBindVertexArray(0);


    // Shader

    Shader shaderTexture("Shaders/textureMVP.vert", "Shaders/texture.frag");
    shaderTexture.charger();


    // Boucle principale

    while(!m_input.terminer())
    {
        ....
    }
}

Dans le prochain chapitre, nous créerons une classe dédiée pour les carrés. Cela évitera d'avoir à inclure des gros bouts de code comme celui-là. :)

Une fois le carré déclaré, il me manque plus qu'à l'afficher. Pour cela nous allons répéter les mêmes opérations d'affichage que l'on a l'habitude de faire, à savoir:

  • Activer le shader

  • Verrouiller le VAO

  • Envoyer les matrices

  • Verrouiller la texture

  • Afficher le tout

// Boucle principale

while(!m_input.terminer())
{
    // Gestion des évènements

    ....



    /* ***** Première passe ***** */


    ....



    /* ***** Seconde passe ***** */


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Gestion de la caméra

    camera.lookAt(modelview);


    // Activation du shader

    glUseProgram(shaderTexture.getProgramID());


	// Verrouillage du VAO

	glBindVertexArray(vao);

	
	    // Envoi des matrices

            shaderTexture.envoyerMat4("modelviewProjection", projection * modelview);


	    // Verrouillage de la texture

	    glBindTexture(GL_TEXTURE_2D, frameBuffer.getColorBufferID(0));


		// Rendu

		glDrawArrays(GL_TRIANGLES, 0, 6);

		
	    // Déverrouillage de la texture

            glBindTexture(GL_TEXTURE_2D, 0);
	

        // Verrouillage du VAO

        glBindVertexArray(0);


    // Désactivation du shader

    glUseProgram(0);


    // Gestion du Frame Rate

    ....
}

N'oublions pas la touche finale qui consiste à redimensionner de la zone d'affichage. Nous utiliserons les attributs m_largeurFenetre et m_hauteurFenetre avec la fonction glViewport() pour régler ce petit détail :

/* ***** Seconde passe ***** */


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


// Redimensionnement de la zone d'affichage

glViewport(0, 0, m_largeurFenetre, m_hauteurFenetre);


// Gestion de la caméra

camera.lookAt(modelview);
Récapitulatif de la méthode bouclePrincipale()

La méthode bouclePrincipale() accueille pas mal de code en ce moment, nous allons tout récapituler pour être sûr qu'il ne vous manque pas un bout quelque part.

Voici les objets à déclarer au début de votre méthode (attention au shader de la caisse je le répète ;) ) :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Frame Buffer

    FrameBuffer frameBuffer(512, 512);
    frameBuffer.charger();


    // Matrices (première passe)

    mat4 projectionFBO;
    mat4 modelviewFBO;

    projectionFBO = perspective(70.0, (double)frameBuffer.getLargeur() / frameBuffer.getHauteur(), 1.0, 100.0);
    modelviewFBO = mat4(1.0);


    // Matrices (seconde passe)

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Caméra mobile

    Camera camera(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0), 0.5, 0.5);
    m_input.afficherPointeur(false);
    m_input.capturerPointeur(true);


    // Objet Caisse

    Caisse caisse(2.0, "Shaders/textureMVP.vert", "Shaders/texture.frag", "Textures/Caisse2.jpg");
    caisse.charger();

    float angle = 0.0;


    // Vertices

    float vertices[] = {0, 0, 0,   4, 0, 0,   4, 4, 0,     // Triangle 1
    			0, 0, 0,   0, 4, 0,   4, 4, 0};    // Triangle 2

    float coordTexture[] = {0, 0,   1, 0,   1, 1,          // Triangle 1
			    0, 0,   0, 1,   1, 1};         // Triangle 2


    /* ***** Gestion du VBO ***** */

    GLuint vbo;
    int tailleVerticesBytes = 18 * sizeof(float);
    int tailleCoordTextureBytes = 12 * sizeof(float);


    // Génération du VBO

    glGenBuffers(1, &vbo);


    // Verrouillage

    glBindBuffer(GL_ARRAY_BUFFER, vbo);

	// Remplissage

	glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes + tailleCoordTextureBytes, 0, GL_STATIC_DRAW);
	glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);
	glBufferSubData(GL_ARRAY_BUFFER, tailleVerticesBytes, tailleCoordTextureBytes, coordTexture);


    // Déverrouillage

    glBindBuffer(GL_ARRAY_BUFFER, 0);


    /* ***** Gestion du VAO ***** */

    GLuint vao;


    // Génération du VAO

    glGenVertexArrays(1, &vao);


    // Verrouillage du VAO

    glBindVertexArray(vao);

	// Verrouillage du VBO

	glBindBuffer(GL_ARRAY_BUFFER, vbo);


		// Vertex Attrib 0 (Vertices)

		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
		glEnableVertexAttribArray(0);


		 // Vertex Attrib 0 (Vertices)

		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(tailleVerticesBytes));
		glEnableVertexAttribArray(2);


	// Déverrouillage du VBO

	glBindBuffer(GL_ARRAY_BUFFER, 0);


    // Déverrouillage du VAO

    glBindVertexArray(0);


    // Shader

    Shader shaderTexture("Shaders/textureMVP.vert", "Shaders/texture.frag");
    shaderTexture.charger();


    // Boucle principale

    while(!m_input.terminer())
    {
        ....
    }
}

Et voici ce que contient la boucle principale :

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
        break;

    camera.deplacer(m_input);



    /* ***** Première Passe ***** */
 

    // Verrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer.getID());


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Redimensionnement de la zone d'affichage

        glViewport(0, 0, frameBuffer.getLargeur(), frameBuffer.getHauteur());


        // Placement de la caméra

        modelviewFBO = lookAt(vec3(3, 0, 3), vec3(0, 0, 0), vec3(0, 1, 0));


        // Gestion de la rotation de la caisse

        angle += 2;

        if(angle > 360)
            angle -= 360;


        // Affichage de la caisse

        mat4 sauvegardeModelviewFBO = modelviewFBO;

            modelviewFBO = rotate(modelviewFBO, angle, vec3(0, 1, 0));
            caisse.afficher(projectionFBO, modelviewFBO);

        modelviewFBO = sauvegardeModelviewFBO;


    // Déverrouillage du Frame Buffer

    glBindFramebuffer(GL_FRAMEBUFFER, 0);



    /* ***** Seconde Passe ***** */


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Redimensionnement de la zone d'affichage

    glViewport(0, 0, m_largeurFenetre, m_hauteurFenetre);


    // Gestion de la caméra

    camera.lookAt(modelview);


    // Activation du shader

    glUseProgram(shaderTexture.getProgramID());


	// Verrouillage du VAO

	glBindVertexArray(vao);

	
	    // Envoi des matrices

            shaderTexture.envoyerMat4("modelviewProjection", projection * modelview);


	    // Verrouillage de la texture

	    glBindTexture(GL_TEXTURE_2D, frameBuffer.getColorBufferID(0));


		// Rendu

		glDrawArrays(GL_TRIANGLES, 0, 6);

		
	    // Déverrouillage de la texture

            glBindTexture(GL_TEXTURE_2D, 0);
	

        // Verrouillage du VAO

        glBindVertexArray(0);


    // Désactivation du shader

    glUseProgram(0);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);


    // Calcul du temps écoulé

    finBoucle = SDL_GetTicks();
    tempsEcoule = finBoucle - debutBoucle;


    // Si nécessaire, on met en pause le programme

    if(tempsEcoule < frameRate)
        SDL_Delay(frameRate - tempsEcoule);
}

Si vous êtes prêts, vous pouvez compiler tout ça pour voir ce que cela donne. :D

Image utilisateur

C'est un peu bizarre ton truc on ne voit pas grand chose. :(

Bon j'avoue, on ne voit pas grand chose.

On va ajouter un appel à la fonction glClearColor() juste avant d'appeler glClear() au niveau de la première passe. Cette fonction permet de donner une couleur par défaut à votre affichage au moment où les buffers sont nettoyés :

void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha)
  • red : composante rouge de la couleur par défaut

  • green : composante verte

  • blue : composante bleue

  • alpha : composante alpha

Appelons cette fonction dans la première passe juste avant glClear(), nous donnerons une couleur grise par défaut :

/* ***** Première passe ***** */


// Nettoyage de l'écran

glClearColor(0.5, 0.5, 0.5, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Le problème avec cette fonction est qu'elle applique la couleur par défaut à tous les FBO, écran compris. Pour remettre la couleur qu'il y avait avant il faut donc faire la même chose qu'avec glViewport() et rappeler la fonction une seconde fois au moment de la seconde passe.

C'est donc ce que nous allons faire en remettant cette fois la couleur noir :

/* ***** Seconde passe ***** */


// Nettoyage de l'écran

glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Recompilons maintenant tout cela :

Image utilisateur
Image utilisateur

C'est déjà un peu mieux.

Faites le tour de votre carré pour voir que votre caisse n'existe pas en 3D, elle est contenue dans une simple texture 2D. Grâce aux FBO, nous sommes en théorie capables d'appliquer n'importe quel effet sur cet affichage à l'aide des shaders (déformation, miroir, flou, ombre, etc.).

Cet exemple n'est pas bien parlant, mais le TP qui va arriver juste après ce chapitre, vous devrez ên faire une utilisation plus utile. ^^

Ce qu'il faut retenir

Piouf, nous aurons vu pas mal de choses dans ce chapitre. Entre la création d'un FBO et son utilisation finale, il y a plein de petites notions auxquelles il faut faire attention. Nous allons résumé tout cela en quelques points pour synthétiser toute cette partie.

Les préparatifs

Premièrement, l'utilisation d'un FBO est divisée en deux étapes que l'on appelle passe. La première contient les éléments affichés uniquement dans le FBO. La seconde quant à elle se concentre sur l'affichage normal de la scène (plus le contenu du FBO).

Avant même de commencer la première passe, il faut déterminer les dimensions du FBO. Plus elles seront grandes, meilleur sera votre rendu. Cependant cela impactera sur les performances de votre application final. Il est préférable de choisir des dimensions moins importantes pour économiser au maximum vos ressources.

Enfin, le dernier point à retenir pour les préparatifs est la création d'un second couple de matrices spécialement dédié à la première passe. On évite ainsi d'éventuelles erreurs en rapport avec la seconde. La matrice projection doit prendre en compte le rapport des dimensions du FBO dans la méthode perspective().

La première passe

La première passe commence par le verrouillage du FBO à l'aide de la fonction glBindFramebuffer(). Incluez le déverrouillage immédiatement après pour ne pas oublier de le faire.

Le rendu se comporte ensuite de la même façon qu'un rendu classique. Cela va de la fonction glClear(), qui permet de nettoyer les buffers du FBO jusqu'au dernier objet que vous souhaitez afficher. Petite précision importante : Il est préférable de se positionner d'un point de vu fixe, et non mobile. Préférez donc la méthode lookAt() de la matrice modelview.

Pensez à appeler la fonction glViewport() pour redimensionner la zone d'affichage en fonction des dimensions du FBO.

La seconde passe

Le rendu de la second passe se concentre sur le rendu normal de la scène 3D. On retrouve ainsi tous les objets qui la composent, même ceux faisant partie du FBO.

Pensez la encore à appeler la fonction glViewport() pour redimensionner la zone d'affichage en fonction des dimensions de votre fenêtre.

Affichez enfin le contenu du Color Buffer du FBO sur une surface comme vous le feriez pour une texture classique.

Améliorations simples

Dans cette partie, nous allons faire plusieurs modifications dans notre classe FrameBuffer. Nous allons tout d'abord voir comment économiser un peu de ressources en évitant d'utiliser le Stencil Buffer lorsque l'on n'en a pas besoin. Nous implémenterons ensuite le constructeur de copie, vous savez déjà à quoi il peut bien service. ^^ Enfin, nous verrons ensemble, dans une ultime partie, comment gérer plusieurs Color Buffers. Ce sera un peu technique, je ferai donc un point récapitulatif dessus à la fin.

Éviter la création du Stencil Buffer

Un nouvel attribut

Le premier point que nous allons gérer concerne l'économie de ressources relatives au Stencil Buffer. En effet celui-ci n'est pas souvent utilisé, d'ailleurs nous ne l'avons jamais utilisé, nous pouvons donc éviter sa création dans la méthode charger() lorsque nous n'en avons pas besoin.

Pour cela, nous allons ajouter un nouvel attribut nommé m_utiliserStencilBuffer de type bool :

// Classe

class FrameBuffer
{
    public:

    ....


    private:

    GLuint m_id;

    int m_largeur;
    int m_hauteur;

    std::vector<Texture> m_colorBuffers;
    GLuint m_depthBufferID;

    bool m_utiliserStencilBuffer;
};

Celui-ci permettra de dire si oui ou non, nous devons créer le Stencil Buffer.

Modification du constructeur

Pour lui donner une valeur au moment au moment de créer un objet FrameBuffer, nous allons légèrement modifier le constructeur. Pour le moment, le prototype est le suivant :

FrameBuffer(int largeur, int hauteur);

Nous allons ajouter un autre paramètre pour lui permettre de donner une valeur à notre nouveau booléen. De préférence, nous l'ajouterons avec une valeur par défaut égale à false car le Stencil Buffer n'est pas souvent utilisé :

FrameBuffer(int largeur, int hauteur, bool utiliserStencilBuffer = false);

Bien entendu, il faut modifier le constructeur dans le fichier .cpp. Il faut ajouter ce nouveau paramètre ainsi que l'initialisation du booléen :

FrameBuffer::FrameBuffer(int largeur, int hauteur, bool utiliserStencilBuffer) : m_id(0), m_largeur(largeur), m_hauteur(hauteur),
                                                                                 m_colorBuffers(0), m_depthBufferID(0),
                                                                                 m_utiliserStencilBuffer(utiliserStencilBuffer)
{

}

Pour compléter les modifications, pensons à initialiser le booléen dans le constructeur par défaut avec une valeur égale à false :

FrameBuffer::FrameBuffer() : m_id(0), m_largeur(0), m_hauteur(0), m_colorBuffers(0), m_depthBufferID(0), m_utiliserStencilBuffer(false)
{

}
Création des Render Buffers

Maintenant que le booléen possède une valeur (soit false par défaut, soit true par le programmeur) nous pouvons l'utiliser dans la méthode charger() pour nous permettre de bloquer ou non le chargement du Stencil Buffer.

Mais avant cela, nous allons revenir à la création du double-Render Buffer et se demander : comment peut-on séparer le Depth et le Stencil Buffer ? Car je vous rappelle que les deux se trouve dans le même buffer.

La réponse est en fait très simple et se situe au niveau du format interne, celui que nous donnons à la méthode creerRenderBuffer() :

// Création du Depth et du Stencil Buffer

creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);

Pour le moment, nous lui donnons la constante GL_DEPTH24_STENCIL8 pour dire à OpenGL de créer un double-RenderBuffer contenant le Depth et le Stencil.

Pour ne garder que le Depth, il suffit juste de changer la constante en GL_DEPTH_COMPONENT24. Avec elle, OpenGL comprendrait qu'il ne doit créer que le Depth Buffer. L'appel à la méthode creerRenderBuffer() ressemblerait donc à ceci :

// Création du Depth Buffer

creerRenderBuffer(m_depthBufferID, GL_DEPTH_COMPONENT24);

En définitif, si le booléen m_utiliserStencilBuffer est égal à la valeur true alors on conserve le double-Render Buffer, sinon on utilise la nouvelle constante pour ne garder que le Depth Buffer :

bool FrameBuffer::charger()
{
    // Début de la méthode

    ....


    // Création du Depth Buffer et du Stencil Buffer (si besoin)

    if(m_utiliserStencilBuffer == true)
        creerRenderBuffer(m_depthBufferID, GL_DEPTH24_STENCIL8);

    else
        creerRenderBuffer(m_depthBufferID, GL_DEPTH_COMPONENT24);


    // Association du Color Buffer

    ....
}
Association des Renders Buffers

L'appel à la fonction glFramebufferRenderbuffer() doit également être modifié. En effet, son paramètre attachment permet de spécifier le point d'attache du Render buffer, sa valeur est pour le moment égale à la constante GL_DEPTH_STENCIL_ATTACHMENT :

// Association du Depth et du Stencil Buffer

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);

Dans le cas où le booléen est à false, il faudrait changer cette constante car le Stencil Buffer n'existe pas. Celle-ci deviendrait alors GL_DEPTH_ATTACHMENT :

// Association du Depth Buffer

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);

En utilisant le booléen, l'association du Render Buffer ressemblerait donc à :

bool FrameBuffer::charger()
{
    // Début de la méthode

    ....


    // Association du Color Buffer

    ....


    // Création du Depth Buffer et du Stencil Buffer (si besoin)

    if(m_utiliserStencilBuffer == true)
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);

    else
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthBufferID);


    // Vérification de l'intégrité du FBO

    ....
}
Création d'un objet FrameBuffer

Au final, si vous souhaitez utiliser le Stencil Buffer avec un FBO vous devrez ajouter la valeur true au constructeur :

// Frame Buffer avec Stencil Buffer

FrameBuffer frameBuffer(512, 512, true);

Si vous ne souhaitez pas l'utiliser, soit vous ne mettez pas de booléen, soit vous mettez la valeur false :

// Frame Buffer sans Stencil Buffer

FrameBuffer frameBuffer(512, 512);

Le constructeur de copie

Le constructeur de copie est une pseudo-méthode que nous avons l'habitude de rencontrer maintenant. Vous savez que les objets OpenGL ne peuvent être copiés comme de simples variables, ils doivent être totalement rechargés dans leur copie exactement comme s'il s'agissait de pointeurs. On ne risque ainsi pas de perdre des données si un objet original est détruit.

Le constructeur de copie est assez simple puisqu'il ne prend en paramètre qu'une référence constante sur un objet de même type, ici FrameBuffer :

FrameBuffer(const FrameBuffer &frameBufferACopier);

Son implémentation commencera comme d'habitude par la copie des attributs variables. Si on regarde notre liste d'attributs, on remarque qu'il n'y en a que trois : m_largeur, m_hauteur et m_utiliserStencilBuffer.

FrameBuffer::FrameBuffer(const FrameBuffer &frameBufferACopier)
{
    // Copie de la largeur, de la hauteur et du booléen

    m_largeur = frameBufferACopier.m_largeur;
    m_hauteur = frameBufferACopier.m_hauteur;
    m_utiliserStencilBuffer = frameBufferACopier.m_utiliserStencilBuffer;
}

Ces trois attributs peuvent être copiés directement. En revanche, les autres sont( ou contiennent) des objets OpenGL ils ne peuvent donc pas être copiés par le signe =. La méthode qui leur donne une valeur est la méthode charger(). C'est elle qui charge le FBO et qui permet de gérer un identifiant à tous ces attributs.

Nous l'appellerons donc cette méthode pour simuler une copie de ces attributs :

FrameBuffer::FrameBuffer(const FrameBuffer &frameBufferACopier)
{
    // Copie de la largeur, de la hauteur et du booléen

    m_largeur = frameBufferACopier.m_largeur;
    m_hauteur = frameBufferACopier.m_hauteur;
    m_utiliserStencilBuffer = frameBufferACopier.m_utiliserStencilBuffer;


    // Chargement de la copie du Frame Buffer

    charger();
}

Le constructeur de copie est maintenant complet. :)

Gérer plusieurs Color Buffers

Cette partie est en cours de ré-écriture, sa précédente version était trop lourde et inutilement complexe. Néanmoins, vous avez déjà pratiquement tout vu sur les FBO. ^^

Télécharger (Windows, UNIX/Linux, Mac OS X) : Code Source C++ du chapitre sur les Frame Buffers Objects

Ce chapitre était un peu compliqué et il y avait pas mal de notions à assimiler en une fois. Vous comprendrez pourquoi je l'ai placé à la fin des notions avancées.

Les Frame Buffers permettent de faire pas mal de choses mais il faut avoir un minimum de connaissances en OpenGL pour connaitre leur fonctionnement. Je vous rassure tout de suite, si je vous ai fait un chapitre dessus c'est parce que vous êtes tout à fait capables de les utiliser. ^^

Après tout ce que vous avez vu jusqu'à présent, vous avez une bonne vu d'ensemble de ce que propose OpenGL. Il y a plein d'autres fonctionnalités à voir évidemment, mais à partir de maintenant nous n'allons pas vraiment apprendre de nouvelles notions pures et dures mais nous allons plutôt utiliser tout ce que nous avons vu pour faire de véritables rendus. Nous allons apprendre à charger des modèles 3D statiques et animés, utiliser des SkyBox, des heigh maps, et bien évidemment des effets ralistes avec les shaders.

Avant de passer à ce programme, je vous invite à faire un petit TP, basé sur le premier que vous avez déjà fait, pour mettre en pratique ce que nous avons vu dans cette deuxième partie. Il y aura des VBO, des VAO, des shaders et bien évidemment des FBO. :p

Ce tutoriel est loin d'être terminé, nous avons encore pas mal de choses à voir et à apprendre ensemble ^^ . Pour vous donner un avant-goût des prochains chapitres, sachez que :

  • La troisième partie sera consacrée aux techniques du jeu vidéo en général, comme le chargement de modèles 3D animés et non-animés, les polices d'écriture, ...

  • La quatrième partie sera consacrée à l'élaboration d'effets avancés comme la lumière, le bump mapping, l'eau, ...

Si vous avez des remarques ou des éléments que vous aimeriez que je développe, merci de m'en faire part dans les commentaires de ce tutoriel. Toute critique (constructive) sera la bienvenue :) .

Exemple de certificat de réussite
Exemple de certificat de réussite