M2 - Images

TP2 - Lumière, matière et Ombres



documents à rendre pour mi janvier :

Envoyez en utilisant le mail de l'université une archive des sources de votre tp ainsi qu'un rapport de quelques pages précisant les tansformations et algorithmes utilisés pour chaque exercice (notamment les transformations nécessaires pour les ombres). Si vous n'avez pas terminé l'exercice 3, rédigez quand même l'algorithme en précisant les transformations nécessaires pour trouver les sources de lumières utilisées pour éclairer indirectement chaque fragment.


Vous préciserez vos noms, prénoms et numéros d'étudiants dans le corps du mail.



Exercice 1 : matières et sources de lumières

On souhaite afficher un objet et lui donner un aspect particulier, en "simulant" les interactions de la lumière avec sa matière.

Chargez un objet et ses descriptions de matières :

    #include "MeshIO.h"
    #include "GLTexture2DIO.h"

    gk::Mesh *mesh= gk::MeshIO::read("bigguy.obj");
    if(mesh == NULL)
        return "erreur de chargement";
   
    for(int i= 0; i < mesh->subMeshCount(); i++)
    {
        const gk::MeshMaterial& material= mesh->subMeshMaterial(i);
       
        // eventuellement, charger la texture diffuse associée à la matière, lorsqu'elle existe :
        if(material.diffuse_texture.empty() == false)
        {
            GLTexture2D *texture= gk::GLTexture2DIO::read(material.diffuse_texture);
        }
    }

Un subMesh est un groupe de triangles partageant la même matière, tous les triangles d'un objet Mesh sont triés par matière et groupés dans des subMesh, il y a donc un subMesh par matière dans un objet Mesh. Un subMesh est décrit par une séquence d'indices : begin, end et un indice de matière.

Pour afficher tous les groupes / submesh, il faut donc découper le dessin de l'objet en plusieurs fois, et paramètrer correctement le glDrawXXX( ).
Pour un submesh il y a (end - begin) indices à dessiner et ils sont placés dans le buffer à partir de la position (begin * sizeof(type des indices)).

Exemple pour afficher un objet gk::Mesh :

    #include "MeshIO.h"

    gk::Mesh *mesh= gk::MeshIO::read("bigguy.obj");
    if(mesh == NULL)
        return "erreur de chargement";

    glUseProgram(program);
   
    for(int i= 0; i < mesh->subMeshCount(); i++)
    {
        const gk::subMesh& submesh= mesh->subMesh(i);
        const gk::MeshMaterial& material= mesh->subMeshMaterial(i);
   
        // parametrer le shader avec la description de matiere :
        // par exemple, pour une matiere diffuse
        // gk::setUniform(program->uniform("kd"), material.kd);
        // gk::setUniform(program->uniform("diffuse"), material.diffuse.r, material.diffuse.g, material.diffuse.b);
   
        const int count= submesh.end - submesh.begin;
        const long int offset= submesh.begin * sizeof(unsigned int);
        glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, (const GLvoid *) offset);
    }

1.1 Ecrivez une paire de vertex / fragment shaders permettant de représenter une matière diffuse (modèle de Lambert) et une autre paire de shaders pour une matière réfléchissante (modèle de Blinn-Phong).

Les différents paramètres sont disponibles dans la classe gk::MeshMaterial, cf. gk::Mesh::subMeshMaterial() :


1.2 Chargez les textures référencées par les matières de l'objet et utilisez-les pour "moduler" le résultat précédent.
Utilisez gk::GLTextureIO::read() pour charger une image (.png, .jpg, .bmp, .tga) et créer la texture 2d openGL correspondante.

remarque : Vous pouvez comparer vos modèles de matières à une implementation de référence avec Brdf Explorer.

remarque : les matières sont décrites dans un fichier texte, "simple.mtl", il est très simple de le modifier pour tester vos shaders.
Par exemple, la matière par défaut s'appelle "white", elle est déclarée :

newmtl white
Kd 0.75  0.75  0.75

ce qui correspond à une matière diffuse grise qui réflechit 75% de la lumière incidente. La couleur décrite par Kd dans le fichier mtl est conservée dans le champ diffuse de la structure gk::MeshMaterial.

pour utiliser une texture pour moduler le résultat, il suffit d'ajouter la ligne map_Kd nom_image, par exemple :

newmtl white
Kd 0.75  0.75  0.75
map_Kd simple.png

Voici les correspondances entre le fichier mtl et les valeurs stockées dans un objet MeshMaterial :

Kd r g b
MeshMaterial::diffuse= Normalize(r,g,b)
MeshMaterial::kd= Length(r,g,b)
map_Kd image
MeshMaterial::diffuse_texture= image
Ks r g b
MeshMaterial::specular= Normalize(r,g,b)
MeshMaterial::ks= Length(r,g,b)
map_Ks image
MeshMaterial::specular_texture= image
Ns MeshMaterial::n= Ns

Avec les valeurs de l'exemple précédent :

gk::MeshMaterial material;
material.name= "white"
material.kd= 0.75;
material.diffuse= gk::Energy(1.0, 1.0, 1.0);
material.map_Kd= "simple.png"

Exercice 2 : ombres

Le calcul des ombres se fait en 2 étapes, la première construit un zbuffer de la scène du point de vue de la source de lumière et la deuxième étape utilise cette information pour déterminer quels sont les fragments visibles de l'observateur et éclairés par la source de lumière.

cf gk::createFramebuffer(w, h, gk::COLOR0_BIT | gk::DEPTH_BIT); pour créer une texture couleur et une texture de profondeur et les attacher  dans un framebuffer. L'annexe en fin de sujet présente plus en détails la classe gk::GLFramebuffer.

Etape 1 : construire le point de vue de la source de lumière.

La source de lumière doit voir les objets visibles de la caméra pour que la deuxieme étape puisse déterminer quels fragments visibles sont à l'ombre ou éclairés. Il faut donc calculer les paramètres du point de vue à utiliser.

2.1 Déterminer un englobant des objets visibles par la caméra. Si la source de lumière ne se trouve pas dans l'englobant des objets visibles, il faut également dessiner les objets pouvant projetter une ombre visible, c'est à dire ceux qui se trouvent "entre" la source de lumière et les objets visibles, cf exercice bonus.

2.2 Placer et orienter une "camera" sur la source de lumière et dessiner les objets (du point de vue de la source de lumière). Si la source est à l'extérieur de la scène, il suffit de construire une projection perspective en fonction de la distance et des dimensions de l'englobant des objets visibles (paramètre fovy de gk::Perspective). Les valeurs 'znear' et 'zfar' correspondent aux distances minimales et maximales des objets.

Lorsque la source est à l'intérieur de l'englobant des objets visibles, un seul point de vue ne suffit pas toujours, cf exercice bonus. Si la source de lumière émet dans une seule direction (un spot, par exemple), il aussi possible de reculer le point de vue.

2.3 Configurer un framebuffer object pour dessiner les objets selectionnés et conserver leur distance à la caméra dans une texture de profondeur.

indication : utiliser gk::createFramebuffer( largeur, hauteur, ... DEPTH_BIT )) et la classe gk::GLFramebuffer pour simplifier la création et la configuration des textures et du framebuffer.

Etape 2 : afficher les objets du point de vue de l'observateur.

2.4 Dessiner les objets du point de vue de l'observateur et déterminer si chaque fragment est éclairé ou à l'ombre à l'aide de la texture de profondeur construite lors de la première étape. Ecrivez la paire de shaders qui éclaire chaque objet.

indication : attention, la texture de profondeur est accessible avec des coordonnées (s, t) comprises entre 0 et 1, pas avec les coordonnées d'un fragment (x, y) comprises entre [0, largeur[ et [0, hauteur[.

exercice bonus : source de lumière "à l'interieur" de la scène...
la source voit "toute" la scène, mais on peut pas utiliser une projection perspective avec un angle de 180° (ou plus)... pourquoi ?
comment régler le problème ?

exercice bonus : source de lumière "à l'exterieur" de la scène, dessiner les objets dont l'ombre est visible.

exercice bonus : stabiliser la paramétrisation du point de vue de la source...
si les paramètres du point de vue de la source sont recalculés à chaque image et que la caméra se déplace légèrement ou que les objets de la scène bougent, les ombres vont "fourmiller"... comment régler ce problème ?


Exercice 3 : rendu indirect

L'exercice précédent permet d'afficher des objets éclairés ainsi que leurs ombres. Mais la lumière est réfléchie dans plusieurs directions à chaque interaction avec une matière, comment calculer rapidement la quantité de lumière qui éclaire un objet après avoir rebondi sur un autre objet ? Autre manière de se poser la question : lorsque l'étape 1 de l'exercice précédent calcule les objets visibles par la source de lumière, que manque-t-il pour calculer la lumière réfléchie par ces objets éclairés ?

Dans l'exemple ci-dessus, quelle information faut-il garder lors de la passe 1, pour pouvoir éclairer xq avec la lumière réfléchie par xp (lors de la passe 2) ?
indication :
l'energie incidente et les paramètres de la matière ?

Comment trouver les fragments qui peuvent réfléchir de la lumière vers le fragment q ?
indication :
ils sont proches de q dans l'image de la scène du point de vue de la source de lumière...

remarque : toutes les paires de points ne peuvent pas échanger de l'énergie, quel critère peut on utiliser pour déterminer que x et y ne peuvent pas éclairer xq, alors que xp peut éclairer xq ?

3.1 Modifier la construction de la shadow map de l'exercice 2 pour stocker les informations supplémentaires pour chaque pixel.

3.2 Ecrivez une paire de shader qui choisit un ensemble de points xp, qui vérifie que chaque point peut éclairer le fragment en cours de traitement et accumule l'énergie indirecte reçue par le fragment.


question bonus :

Comment évaluer efficacement la visibilité entre les points xp et le fragment en cours de traitement ?


détails supplémentaires dans :

"Reflective Shadow Maps"
C. Dachsbacher, M. Stamminger,
I3D 2005


Annexe : rappels sur les framebuffers et les textures gérés par gKit

créer des textures et les attacher à un framebuffer :

utilisez FramebufferManager :

    #include "FramebufferManager.h"
    gk::createFramebuffer( largeur, hauteur, buffers, color_texture_format, depth_texture_format );

    cette fonction crée une ou plusieurs textures couleurs/profondeur, un framebuffer, et associe les textures au framebuffer.
    le parametre buffers décrit la liste des textures à créer et à attacher : utilisez une combinaison des constantes gk::COLOR0_BIT, gk::COLOR1_BIT, gk::DEPTH_BIT, etc.

    gk::GLFramebuffer *framebuffer= gk::createFramebuffer( largeur, hauteur, gk::COLOR0_BIT | gk::DEPTH_BIT );
    if(framebuffer == NULL)
        return "erreur"

    les parametres color_texture_format et depth_texture_format permettent de décrire le type/format de texture à créer, cf gk::TextureFormat. Plusieurs format courants sont déja déclarés :

gk::TextureRGBA :     texture couleur standard, 4 canaux rgba,
gk::TextureRGBA16F :  texture couleur 4 canaux rgba, codés sur des float 16bits,
gk::TextureRGBA32F :  texture couleur 4 canaux rgba, codés sur des float (32 bits),

gk::TextureDepth :    texture de profondeur standard,
gk::TextureDepth24 :  texture de profondeur codée sur 24bits,
gk::TextureDepth32 :  texture de profondeur codée sur 32bits,

gk::TextureR32UI :    texture 1 canal, codé sur un unsigned int 32bits,
gk::TextureR16UI :    texture 1 canal, codé sur un unsigned short 16bits,

gk::TextureRG16UI :   texture 2 canaux, codés sur des unsigned shorts 16bits,


pour déclarer un format différent, il suffit de créer un objet TextureFormat( ) et de l'utiliser lors de la création des textures :
par exemple pour créer une texture 3 canaux rgb codés sur des float 16bits, gk::TextureFormat( GL_RGB16F, GL_RGB, GL_FLOAT );
les combinaisons de formats possibles sont décrites sur la page glTexImage2D, par exemple.

pour le tp vous aurez besoin d'un framebuffer pour la première étape du rendu des shadow maps :

    gk::GLFramebuffer *framebuffer1= gk::createFramebuffer(largeur, hauteur, gk::COLOR0_BIT | gk::DEPTH_BIT, gk::TextureRGBA16F);
    if(framebuffer1 == NULL)
        return "erreur"

une fois la configuration du framebuffer terminée, il faut créer l'objet openGL :

    if(framebuffer1 == NULL || framebuffer1->createGLResource() < 0)
        return "erreur"

attacher une texture à un framebuffer :

    utilisez la fonction gk::GLFramebuffer::attachTexture( buffer, texture );
    le paramètre buffer est une constante gk::COLOR0, gk::COLOR1, gk::DEPTH, etc.

    les fonctions gk::GLFramebuffer::texture( buffer ) et gk::GLFramebuffer::zbuffer( ) permettent de recupérer les GLTexture attachées à un framebuffer.

dessiner dans un framebuffer :

    l'application doit activer l'utilisation d'un framebuffer déja configuré avec :

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer1->name());

    si plusieurs textures couleurs sont attachées au framebuffer, il faut également configurer l'écriture dans ces textures avec glDrawBuffers( ). La classe gk::GLFramebuffer détermine les paramètres à transmettre à openGL (l'ensemble des color buffers dans lesquels dessiner) :

    const std::vector<GLenum>&buffers= framebuffer1.drawBuffers( );
    glDrawBuffers(buffers.size(), &buffers.front());

désactiver le dessin dans un framebuffer :

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

utiliser une texture dans un shader

    l'application doit sélectionner une unite de texture GL_TEXTURE0 + index, activer la texture sur l'unité de texture, paramétrer le mode de filtrage et indiquer au shader sur quelle unite de texture se trouve la texture.

    #include "ShaderManager.h"
    #include "SamplerManager.h"
    #include "TextureManager.h"
    #include "FramebufferManager.h"

    gk::GLSampler *sampler= ...;    // creer un sampler dans init()
    gk::GLShaderProgram *program= ...;    // creer un shader program dans init()
    gk::GLFramebuffer *framebuffer1= ...;    // creer un framebuffer et une texture couleur attache dans init()

    glUseProgram(program->name());    // active le shader program
   
    gk::GLTexture *texture= framebuffer1->texture(gk::COLOR0);    // recupere la texture a utiliser, dans un framebuffer, par exemple...
    glActiveTexture(GL_TEXTURE0 + unit);    // selectionne une unite de texture
    glBindTexture(texture->target(), texture->name());    // active la texture sur l'unite de texture

    glBindSampler(unit, sampler->name());    // active un sampler et parametre le filtrage sur l'unite de texture

    gk::setSamplerUniform(program->sampler("color_texture"), unit);    // indique au shader sur quelle unite de texture se trouve la texture

    gk::Transform mvp= ...;
    gk::setUniform(program->uniform("mvpMatrix"), mvp.matrix());    // finir de parametrer le shader program

    glDrawXXX( );    // dessiner quelquechose


    dans le fragment shader, l'acces à la texture ressemble à ça :

    #version 330     // par exemple
   
    uniform sampler2D color_texture;    // declare le type de la texture, 2D, 1D, 3D, CUBE...

    in vec2 vertex_texcoord;    // coordonnees de texture interpolees depuis le vertex shader
    out vec4 fragment_color;    // couleur en sortie du fragment shader

    void main( )
    {
        // copie simplement la couleur lue dans la texture dans le pixel
        vec4 color= texture(color_texture, vertex_texcoords);

        fragment_color= color;
    }

    il est également possible d'utiliser gk::setTexture( const ProgramSampler& sampler, gk::GLTexture *texture, gk::GLSampler *sampler ) pour simplifier l'utilisation des textures par un shader :

    #include "ShaderManager.h"
    #include "SamplerManager.h"
    #include "TextureManager.h"

    #include "FramebufferManager.h"

    gk::GLSampler *sampler= ...;    // creer un sampler dans init()
    gk::GLShaderProgram *program= ...;    // creer un shader program dans init()
    gk::GLFramebuffer *framebuffer1= ...;    // creer un framebuffer et une texture couleur attache dans init()

    glUseProgram(program->name());    // active le shader program
   
    gk::GLTexture *texture= framebuffer1->texture(gk::COLOR0);    // recupere la texture a utiliser, dans un framebuffer, par exemple...

    gk::setTexture(program->sampler("color_texture"), texture, sampler);


    gk::Transform mvp= ...;
    gk::setUniform(program->uniform("mvpMatrix"), mvp.matrix());    // finir de parametrer le shader program

    glDrawXXX( );    // dessiner quelquechose