M2PRO - Images
Année 2008-2009


TP3 - rendu multi-passes,
application au traitement d'images



  Il est souvent nécessaire d'utiliser plusieurs passes de rendu pour réaliser un effet particulier : le calcul des ombres, par exemple, mais également tous les post-traitements (flous, hdr, etc.).

    L'objectif du tp est de manipuler les éléments du rendu multi-passes : utiliser plusieurs fragment shaders, qui réalisent chacun une étape d'un calcul et plusieurs buffers (render targets) pour stocker les résultats intermédiaires.


    exemple : un effet de flou appliqué sur une partie d'une scène :

exemple

    La première étape dessine la scène normalement, le cube gris ainsi que le carré vert (cf. vignette ligne 2, colonne 1). Une deuxième étape extrait de cette image la partie sur laquelle appliquer le flou : le carré vert (cf. vignette ligne 1, colonne 1). Les étapes suivantes réalisent le flou du carré vert et la dernière étape combine la scène "normale" et la version floue du carré vert.

    Cet affichage est donc composé de 4 étapes :
  1. rendu de la scène complète, résultat buffer0
  2. extraction du carré vert, résultat buffer1 (source buffer0)
  3. filtrage (flou) du carré vert, résultat buffer2 (source buffer1)
  4. composition finale : résultat buffer normal (sources buffer0 et buffer2)
    Chaque étape utilise les résultats de l'étape précédente. Les sections suivantes précisent comment créer de nouveaux buffers et comment réaliser chaque étape.


Partie 1 : rendu dans une texture

    Pour réaliser toutes les étapes, il faudra prévoir 3 textures afin de stocker les résultats des étapes 1, 2 et 3.
    La section suivante explique comment créer une texture "classique" et dessiner une scène dedans.


utilisation des render targets (frame buffer objects)

    Les frame buffer objects permettent de créer des ensembles de textures et de les faire remplir lors du rendu de la scène à la place du buffer d'affichage de la fenêtre. render_target.c (fourni avec sdlkit) crée un buffer contenant deux textures : une couleur et une de profondeur.

exemples d'utilisation (pas d'ordre particulier) :

    #include "render_target.h"

    // avant toute utilisation
    render_target_init();
    // à la fin du programme
    render_target_quit();

    // creation d'un render target (frame buffer object) de dimensions largeur x hauteur
    // contenant un buffer couleur et un buffer de profondeur

    RENDER_TARGET *buffer= render_target_create(largeur, hauteur);

    // creation d'un render target contenant uniquement un buffer couleur
    // (ne permet pas de dessiner correctement des objets, mais permet de traiter tous les pixels d'un buffer)
    RENDER_TARGET *buffer= render_target_create_format(largeur, hauteur, GL_TEXTURE_2D, GL_RGBA, 0);

    // utilisation d'un render target pour afficher une scene : les rendu suivants rempliront 'buffer'
    render_target_bind(buffer);
    // afficher la scene dans buffer
    display( );

    // utilisation d'un render target : retour à la normale : remplissage du buffer d'affichage de la fenêtre
    render_target_disable();
    // afficher la scene dans la fenetre (dans le render target par defaut, crée par opengl lors de la création de la fenetre)
    display( );

    // active la texture couleur du buffer pour l'unite de texture 0
    glActiveTexture(GL_TEXTURE0);
    render_target_bind_texture(buffer);
    // afficher la scène en utilisant le buffer couleur comme texture0
    display( );

    // idem, mais utilise la texture de profondeur, active la texture de profondeur du buffer pour l'unite de texture 1
    glActiveTexture(GL_TEXTURE1); // on peut bien sur utiliser n'importe quelle unite de texture
    render_target_bind_depth_texture(buffer);
    // afficher la scene en utilisant texture1
    display( );

    // detruit le buffer
    render_target_delete(buffer);

    // recuperer les dimensions du buffer
    int viewport[4];
    render_target_get_viewport(buffer, viewport);
    // c'est la même convention qu'openGL
    // viewport[0] = xmin, viewport[1]= ymin
    // viewport[2]= largeur, viewport[3]= hauteur



    Les parties suivantes précisent comment réaliser chaque étape  du rendu.

Partie 2 : extraction

    Le plus simple est de sélectionner tous les pixels dont le gris moyen est supérieur à un seuil choisit par l'application, 160, par exemple.

    rappel : gris moyen = (R + G + B) / 3

    Q1. écrivez un shader qui s'exécute sur tous les pixels d'une texture et qui copie un pixel dans le buffer résultat si son gris moyen est supérieur au seuil. Sinon, le shader écrit un pixel noir dans le buffer résultat. Le seuil sera un paramètre (de quel type ?) du shader.


Partie 3 : filtrage

    Le plus simple est de calculer la couleur moyenne des pixels d'un voisinage 3x3 autour de chaque pixel du buffer d'entrée.

    Q1. écrivez un shader qui s'exécute sur tous les pixels d'une texture et écrit dans le buffer résultat la moyenne des couleurs des pixels voisins.


Partie 4 : composition finale

    Le plus simple est de calculer la couleur moyenne des pixels des 2 buffers. Le shader aura donc 2 entrées et une sortie.

    Q1. écrivez un shader qui s'exécute pour tous les pixels d'une texture et qui calcule la moyenne des pixels correspondants dans les 2 buffers d'entrées.

    Q2. amélioration : calculer la moyenne assombrit les parties de l'image qui ne correspondent pas à l'objet extrait lors de la première étape. Proposez une solution à ce problème.

Partie 5 : optimisation

    Il est très long de filtrer un buffer pleine résolution, une solution couramment utilisée consiste à travailler sur une version basse résolution du buffer d'entrée et à interpoler le résultat.

    Q1. Comme le shader s'exécute sur tous les pixels du buffer résultat, il suffit d'utiliser un buffer résultat plus petit, comme 80x60, pour diminuer le nombre de pixels traités, et gagner énormément de temps.

    Testez cette solution.

    Q2. Filtrer une image avec un voisinage 3x3 demande 9 accès au buffer par pixel, ce qui est couteux. Une solution courante consiste à casser le shader en 2 étapes et à filtrer, d'abord horizontalemment, puis verticalement (ou le contraire, ça n'a pas d'importance dans ce cas). Avec cette solution, le filtre horizontal n'accède qu'à 3 pixels, de même que le filtre vertical, ce qui ne fait plus que 6 accès au lieu de 9.

    De manière générale, pour un voisinage NxN, au lieu de faire N*N accès, on en fait plus que N+N +2 (écriture du résultat du premier filtre et relecture dans le deuxième filtre) ! Par contre, il y a une étape de plus. Faut-il réellement créer un nouveau buffer ? ou est-il possible de travailler sur le même ?

    Ecrivez les 2 passes supplémentaires (horizontales et verticales) qui permettent de filtrer efficacement le buffer.
   

Partie 6 : sélection d'un objet

    Il est également possible d'utiliser la même méthode pour sélectionner un objet d'une couleur connue, rouge, par exemple.

    Q1. écrivez un shader permettant d'extraire un objet d'une couleur particulière d'un buffer. La couleur de l'objet devra être paramétrable.
    Comment comparer la couleur d'un pixel du buffer à la couleur de référence ? comment sélectionner les couleurs "proches" de la couleur de sélection.
    Existe-t-il un espace couleur plus robuste que RGB pour comparer des couleurs ?