M2PRO - Images
Année 2007-2008

TP3 - Textures et Shadow Maps






Préambule

    nouveau : shadercc pour compiler vos shaders directement depuis un éditeur de texte.

    Téléchargez et installez les nouvelles versions (débuguées et complétées) des librairies objtoy et sdlkit fournies en Annexes.
    Vérifiez le bon fonctionnement du sdlkit_testmain et de glsl_testmain (dans l'archive tests).

    Téléchargez les objets : bigguy.obj et ground.obj

    Documentation :

    openGL Programming Guide

    openGL Shading Language Specification

    libSDL Programming Guide

    à ne pas manquer :
    openGL man pages
    GLSL quick reference guide [pdf 2 pages]



Rappel / Partie 1 : Texture et shaders

    La création et l'utilisation de textures en openGL sont décrites dans le chapitre 10 du Programming Guide.
   

    Résumé : création d'une texture RGB "standard"

   glGenTextures(1, &texName);	                // crée un objet texture
glBindTexture(GL_TEXTURE_2D, texName); // active une texture 2D pour l'objet

// fixe les paramètres par défaut : une seule application de la texture, pas de répétition
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

// fixe les paramètres par défaut : échantillonnage filtré (interpolation bi-linéaire)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

// remplit la texture et fixe le format des données (couleurs, combien, etc.)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_largeur, texture_hauteur, 0,
// la ligne du dessus décrit le format interne de la texture,
// la ligne du dessous décrit le format des données fournies par l'application,
GL_RGB, GL_UNSIGNED_BYTE, texture_data);
// si elles ne correspondent pas, openGL fera la conversion.

// texture_largeur / hauteur précise les dimensions de la texture, !! attention : ce sont des puissances de 2 !!
// texture_data est un tableau de 3 unsigned char par pixel (largeur * hauteur)

    Résumé : utilisation d'une texture pour afficher un quad

   // active l'unite 0 de gestion de texture, nécessaire pour l'utilisation dans un shader 
glActiveTexture(GL_TEXTURE0);

// active l'utilisation des textures 2D
glEnable(GL_TEXTURE_2D);

// indique comment "mélanger" la couleur de la texture et la couleur de l'objet
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

// indique quelle texture 2D utiliser
glBindTexture(GL_TEXTURE_2D, texName);

// dessiner un quad
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(-2.0, -1.0, 0.0); // chaque sommet est associé à une coordonnée dans la texture

glTexCoord2f(0.0, 1.0);
glVertex3f(-2.0, 1.0, 0.0);

glTexCoord2f(1.0, 1.0);
glVertex3f(0.0, 1.0, 0.0);

glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, -1.0, 0.0);
glEnd();

    La seule 'subtilité' consiste à indiquer quelle unité de texture traite la texture. Le fragment shader utilise directement le numéro de l'unite pour accéder à la texture.

    Résumé : lire une texture dans un fragment shader

    // fragment shader
    uniform sampler2D  texture;

    void main(void)
    {
       // lire la texture aux coordonnées s, t
       gl_FragColor.rgb= texture2D(texture, vec2(s, t));
       gl_FragColord.a= 1.f;
    }

    Résumé : fournir l'unité de texture au shader

    glsl_init()

    GLuint program= glsl_init_program( ... )
    GLuint texture_location= glGetUniformLocation(program, "texture");

    glUniformi(texture_location, 0);

    glActiveTexture(GL_TEXTURE0);
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glBindTexture(GL_TEXTURE_2D, texName);

    // dessiner un quad, shader activé
    glUseProgram(program);

    glBegin(GL_QUADS);
        glTexCoord2f(0.0, 0.0);
        glVertex3f(-2.0, -1.0, 0.0);
        glTexCoord2f(0.0, 1.0);
        glVertex3f(-2.0, 1.0, 0.0);
        glTexCoord2f(1.0, 1.0);
        glVertex3f(0.0, 1.0, 0.0);
        glTexCoord2f(1.0, 0.0);
        glVertex3f(0.0, -1.0, 0.0);
    glEnd();



    Modifiez vos shaders du tp précédent et ajoutez une texture pour modifier l'aspect de bigguy.

Partie 2 : Shadow map

    Comment projetter une ombre ? Le pipeline graphique traite les vertex et les fragments indépendemment les uns des autres, il n'est donc pas possible de savoir directement si un objet se trouve entre la source de lumière et l'objet en cours d'affichage.

    La solution standard consiste à afficher deux fois la scène : une fois vue de la source de lumière afin de stocker les fragments éclairés (et leur distance à la source), et une autre fois depuis la caméra. Lors de l'affichage depuis la caméra, il suffit de recalculer la position de chaque sommet dans le repère (projectif) de la source de lumière et de comparer cette distance à celle stockée dans la première étape. Si la distance est inférieure ou égale à celle stockée : le sommet est visible de la source, il est donc éclairé, dans l'autre cas, le sommet n'est pas le plus proche de la source et il est donc à l'ombre.

    Voici un schema pour illustrer le principe :


   



    Une shadow map est donc le zbuffer de la scène vue par la source de lumière. Lors de la deuxième étape, le vertex shader calcule deux transformations par sommet traité : une classique utilisée pour afficher les objets vus par la caméra et l'autre pour retrouver la position du sommet dans le repère de la source de lumière. Les deux résultats sont transmis au fragment shader qui termine la projection dans le repère de la source de lumière et compare la profondeur stockée dans la shadow map à celle du fragment. La comparaison se fait automatiquement en accédant à une texture de type shadow2D.  L'accès à une texture se fait en utilisant des coordonnées de textures ( !! ), donc le plus simple consiste à transmettre les coordonnées du sommet (dans le repère projectif de la source de lumière) en utilisant les varying gl_TexCoord[0].stpq dans le vertex shader.

    En résumé, voici les différentes étapes :
       1. placer la source de lumière dans le monde,
       2. afficher la scène vue par la source de lumière,
       3. stocker le zbuffer dans une texture (la shadow map),
       4. activer la texture shadow_map,
       5. afficher la scène vue par la camera,
       6. le vertex shader calcule la transformation classique des sommets dans le repère projectif de la caméra,
       7. le vertex shader calcule également la transformation des sommets dans le repère projectif de la camèra utilisée dans l'étape 2 et stocke le sommet transformé dans gl_TexCoord[0],
       8. le fragment shader récupère les coordonnées, termine la projection et accède à la texture pour comparer les profondeurs  du point stocké dans la shadow map et du fragment,
       9. le fragment shader calcule la couleur du fragment en fonction du résultat : fragment éclairé ou à l'ombre.


    Remarques : une texture contenant des profondeurs n'est pas manipulée de la même manière qu'une texture de couleur :

       1. elle se déclare uniform sampler2DShadow texture;

       2. elle est utilisé avec shadow2D(texture, vec3(s, t, p)) ou shadow2DProj(texture, vec4(s, t, p, q))

       3. il faut activer le type de comparaison (dans l'application, après le glBind()):
            résultat vrai si p < au texel (ou p / q pour shadow2DProj)

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

            ou vrai si p > au texel (ou p / q pour shadow2DProj)

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_GEQUAL);

       4. les coordonnées de texture sont des réels entre 0 et 1, le résultat de la projection du sommet est entre -1 et 1, comment corriger ce dernier changement de repère ??

       5. pour dessiner directement dans la texture à l'étape 2, parcourez l'annexe 'utilisation des framebuffer objects'.


Partie Subsidiaire : Ambient occlusion,
ou "comment pré-calculer ça ?"



  L'idée est d'éclairer l'objet par un grand nombre de sources de lumière disposées tout autour. Ensuite pour chaque texel de la surface de l'objet, il faut compter le nombre de sources de lumières visibles. Par exemple, le sommet du crane de bigguy est éclairé par quasiment toutes les sources, au contraire des autres parties plus sombres.




 
    Il suffit de calculer une shadow map par source et de comptabiliser les texels qui sont éclairés. Au lieu de dessiner bigguy en 3D avec des coordonnées de texture 2D comme paramètres, il suffit de faire le contraire. C'est à dire, dessiner la surface en 2D et d'utiliser les coordonnées 3D des sommets comme attributs des sommets 2D.

 

espace de la surface de l'objet
(ou espace texture) : les faces de bigguy sont affichées en 2D en utilisant les coordonnées de texture.
shadow map dans l'espace de la surface de l'objet
(éclairage par le dessus)



Résultat avec accumulation de toutes les sources de lumières (à gauche sans tenir compte de la direction de la source de lumière, à droite, en calculant la direction de la source de lumière et la normale de chaque point visible) :


            



Partie Subsidiaire : Ombres douces et pénombres.


    La méthode de base décrite dans la première partie du sujet ne 'fonctionne' correctement qu'avec des sources ponctuelles, ce qui produit des ombres 'dures' : deux pixels voisins dans l'image peuvent être l'un à l'ombre alors que l'autre est en pleine lumière.

    Une méthode simple pour des ombres douces correctes est décrit dans le sdk nvidia, exemple 'Soft Shadows'

Partie Subsidaire : Anti-aliasing,
ou "mes ombres, elles sont toutes carrées ..."


    Une méthode filtrage simple et élégante est décrite dans 'Variance Shadow Maps'.

Partie Subsidiaire : construction de la caméra associée à la source de lumière
ou "mes ombres, elles sont toutes carrées, mais pas tout le temps !!"


    C'est encore lié à un problème d'échantillonnage entre le rendu camera et le rendu depuis la source de lumière. Une méthode interressante pour limiter le problème : 'Parallel Split Shadow Maps'.


    Autre technique simple pour améliorer la qualité des shadow maps et limiter les problèmes de précisions du zbuffer : 'Linear Zbuffer'.




Annexes : utilisation des 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 la nouvelle version de 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 frame buffer object de dimensions largeur x hauteur
    RENDER_TARGET *buffer= render_target_create(largeur, hauteur);

    // les rendu suivants rempliront 'buffer'
    render_target_bind(buffer);

    // retour à la normale : remplissage du buffer d'affichage de la fenêtre
    render_target_disable();

    // active la texture couleur du buffer pour l'unite de texture 0
    glActiveTexture(GL_TEXTURE0);
    render_target_bind_texture(buffer);

    // 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);

    // 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





Annexe : utilitaires (nouvelles versions)

Installation de sdlkit + render_target + glsl2 + shadercc

Téléchargez sdlkit + render_target + glsl2 + shadercc

créez un répertoire et dézippez l'archive dedans
cd <répertoire>
tar -zxvf sdlkit_latest.tar.gz
make
make install

si vous souhaitez installer les exemples :
créez un répertoire tests dans le répertoire sdlkit et dézippez l'archive dedans
cd tests
tar -zxvf sdlkit_tests_latest.tar.gz
make

shadercc

    Pour compiler vos shaders et vérifier qu'il n'y a pas d'erreurs de syntaxe et de casts avant d'éxécuter votre programme complet :

        ~/local/bin/shadercc -c shader.vsl

    Pensez à ajouter #version 110 au début du source des shaders pour détecter le maximum d'erreurs / warnings. shadercc affiche également la ligne du source correspondant à l'erreur détectée par le driver et en utilisant le format d'erreur de gcc, résultat, il s'intègre facilement à un éditeur ...

    Démonstration :

intégration shadercc avec SciTE


Intégration de shadercc dans SciTE

    Ouvrez les propriétés utilisateur : menu 'Options', option 'Open User Options File', copiez - collez les lignes suivantes :

# extensions de fichiers reconnues
file.patterns.cpp=*.c;*.cpp;*.cs;*.h;*.hpp;*.fsl;*.vsl

# compilation des shaders glsl
shadercc=shadercc -c $(FileNameExt)
command.compile.*.vsl=$(shadercc)
command.compile.*.fsl=$(shadercc)

    Enregistrez le fichier, fermez SciTE, rouvrez-le et éditez un shader. Dans le menu Tools, l'option Compile est maintenant disponible (Ctrl-F7). Appuyez ensuite sur F4 pour visualiser les différentes erreurs (éventuelles, bien sur !!).

Installation de objtoy

Téléchargez objtoy

créer un répertoire et dézippez l'archive dedans
cd <répertoire>