M2 - Images

TP 1 - openGL3 et shaders


Prise en main

installez gKit et parcourez la doc.

Ce qu'il faut retenir : la classe gk::App permet de construire une application openGL, elle crée une fenetre et un contexte de rendu openGL et permet également de gérer les évènements claviers, souris, joysticks, etc., facilement.

Pour construire une application, il faut dériver la classe gk::App et redéfinir les méthodes suivantes :

Utilisation des objets openGL.

Tous les objets openGL dérivent d'une classe de base gk::GLResource et se manipulent donc de la même manière. 
 
attention : les objets openGL / les GLResources n'appartiennent pas à l'application ! Ils appartiennent au contexte openGL. L'application ne peut donc que les referencer (garder un pointeur sur un objet GLResource).
 
les GLResources sont construits en deux étapes : new/delete permettent de créer/détruire l'objet c++ qui permet de manipuler un objet openGL, et l'objet  openGL lui-même est crée/détruit par createGLResource()/releaseGLResource().
 
    // creer un shader
    // 1. creer l'objet c++ shader
    GLShaderProgram *shader= new gk::GLShaderProgram("simple.vsl", "simple.fsl");
    if(shader == NULL)
        return "erreur";
    // 2. creer effectivement le shader openGL
    if(shader->createGLResource() < 0)
        return "erreur de creation du shader";
 
    // utiliser le shader
    glUseProgram(shader->name());
    // dessiner quelquechose ...
     
    // detruire le shader
    // 1. detruire l'objet openGL
    shader->releaseGLResource();
    // 2. detruire l'objet c++ shader
    delete shader;

Les GLResources doivent être configurés complètement avant d'appeler  createGLResource(), ensuite, ils ne sont plus modifiables, uniquement utilisables par l'application. Cette "contrainte" permet de mieux structurer  l'application en imposant une phase d'initialisation suivie d'une "boucle" d'utilisation. En effet, il est très très (très) lent de créer les objets openGL à chaque utilisation. Pour résumer, les GLResources seront donc crées dans  gk::App::init() et pas dans la fonction gk::App::draw().
 


Fonctions utilitaires pour la gestion des GLResources.

Cette gestion de la durée de vie des objets openGL peut sembler un peu lourde mais plusieurs fonctions permettent de la rendre beaucoup plus pratique. Par exemple, la classe gk::GLManager détruira automatiquement tous les objets openGL crées à la fermeture de l'application.

cf. BufferManager, ShaderManager, EffectShaderManager, TextureManager, SamplerManager, FrambufferManager, etc. pour créer les objects correspondant.


exemple de création de buffer :

    #include "MeshIO.h"
    #include "BufferManager.h"

    // charger un objet
    gk::Mesh *mesh= gk::MeshIO::read( "bigguy.obj" );
    if(mesh == NULL)
        // erreur de lecture de l'objet 3d.
        return -1;

    // utiliser BufferManager pour creer un vertex buffer
    gk::GLAttributeBuffer *positions= gk::createAttributeBuffer(mesh->positionCount(),
        mesh->positionCount() * sizeof(gk::Point), &mesh->positions().front());
    // creer l'objet openGL
    if(positions == NULL || positions->createGLResource() < 0)
        // erreur de creation du buffer.
        return -1;

    le buffer est maintenant géré par gk::GLManager<gk::GLAttributeBuffer>, il n'est plus nécessaire de le détruire dans gk::App::quit() !
    la méthode gk::GLResource::name( ) renvoie l'identifiant de l'objet openGL pour le manipuler directement, par exemple :

    glBindBuffer(GL_ARRAY_BUFFER, positions->name());


Contrôle au clavier, souris, joystick.

La fonction gk::App::key( code ) renvoie l'état d'une touche identifiée par 'code' : 1 si la touche est appuyée, 0 si la touche est relachée.
Cette fonction renvoie une référence sur la valeur, ce qui permet de la modifier, par exemple, key('a')= 0.

Pour tester l'état d'une touche comme les flèches de directions, il suffit d'utiliser les codes SDLK_xxx correspondants. Il est également possible de connaître l'état des touches spéciales : shift, control, etc : SDL_GetModState( ), les codes SDLK_xxx associés sont documentés en bas de la page. Il est également possible de modifier l'état de ces touches spéciales avec SDL_SetModState( ).

La gestion de la souris se fait directement en utilisant les fonctions de libSDL : SDL_GetMouseState( ), SDL_GetRelativeMouseState( ).

Le manuel de programmation complet de libSDL est également disponible.

mesh_viewer fourni dans l'archive gKit donne un exemple assez complet de la gestion clavier / souris.


Exemple complet d'application utilisant gKit (inclus dans l'archive) :

#include "App.h"

#include "Transform.h"

#include "MeshIO.h"
#include "BufferManager.h"
#include "ShaderManager.h"


class TP : public gk::App
{
    gk::GLShaderProgram *program;
   
public:
    TP( )
        :
        gk::App(1024, 768)
    {}
   
    ~TP( ) {}
   
    int init( )
    {
        // gk::MeshIO::read( mesh_filename ) pour lire un objet 3d
        // gk::createAttributeBuffer( ) pour creer un vertex buffer
        // gk::createIndexBuffer( ) pour creer un index buffer
       
        // pour creer un shader program en compilant les fichiers sources vertex et fragment
        program= gk::createShaderProgram( "vertex.vsl", "fragment.fsl" ); 
        if(program == NULL || program->createGLResource() < 0)
            // erreur de chargement des fichiers sources ou de compilation des shaders, sortir de l'application
            return -1;
       
        return 0;       // tout c'est bien passe, sinon renvoyer -1
    }
   
    int quit( )
    {
        return 0;
    }
   
    int draw( )
    {
        if(key(SDLK_ESCAPE))
            // fermer l'application si l'utilisateur appuie sur ESCAPE
            Close();
       
        // activer le shader
        glUseProgram(program->name());
       
        // parametrer le shader, par exemple, mvpMatrix
       
        // activer les buffers d'attributs
        // activer le buffer d'indice
        // draw
       
        // afficher le buffer de dessin
        SDL_GL_SwapBuffers();
        return 1;       // continuer
    }
};


int main( int argc, char **argv )
{
    TP app;
    app.run();
   
    return 0;
}

Partie 1 : Affichage avec vertex buffer / index buffer.

Les vertex et index buffers seront donc crées et initialisés dans la méthode init( ) de gk::App.
Quelques méthodes utiles de gk::Mesh :
Si les normales ne sont pas disponibles, il est possible de les calculer avec la méthode gk::Mesh::buildNormals( ).
En résumé, pour charger un objet et récupérer ses normales :

     gk::Mesh *mesh= gk:MeshIO::read("xxx.obj");
    if(mesh->normalCount() != mesh->positionCount())
        mesh->buildNormals();


exercice :

Chargez un objet et affichez-le en utilisant un vertex buffer et un index buffer.
Vous utiliserez un shader minimaliste (couleur uniforme, par exemple).
Structurez correctement votre code, en utilisant les methodes init( ), quit( ) et draw( ) en dérivant la classe gk::App.

La méthode draw( ) commence par redimensionner et effacer le buffer de dessin. Ensuite, activez le shader, décrire la projection de la caméra, puis pour chaque objet à afficher : décrire la matrice repère local vers repère camera (modelview), et affichez l'objet.


Derniere remarque : pensez à désactiver tous les états openGL modifiés pour afficher chaque objet, vous gagnerez du temps.


Partie 2 : Afficher plusieurs objets / élimination des objets occultés.

Un objet affichable est a priori représenté par un ensemble de buffers, une transformation pour le placer dans le repère de la scène et une boite englobante.


exercice 1 : visibilité.

Testez la visibilité de la boite englobante d'un objet avant de l'afficher.

Ecrivez une fonction permettant de déterminer la visibilité d'un englobant par rapport au volume de vision de la camera, c'est à dire connaissant la transformation model * view * projection :

    bool occluded( const gk::BBox& object_bbox, const gk::Transform& model_view_projection );

ou en travaillant dans le repère de la scène :

    bool occluded( const gk::BBox& world_bbox, const gk::Transform& view_projection );

exercice bonus : visibilité hiérarchique.

Construisez une hiérarchie d'englobants afin de tester des groupes d'objets proches et d'éviter de tester individuellement tous les objets de la scène.


Partie 3 : Shaders et matières.

Modifiez les shaders utilisés pour afficher les objets et utilisez les paramètres du modèle Blinn-Phong stockés dans une structure gk::MeshMaterial.
Dans quel repère faire les calculs d'éclairement ?
Quels sont les paramètres uniforms des shaders ?
Quels sont les attributs de sommets ?


utilitaire : utilisez shadercc (fourni avec l'archive gKit) pour vérifier que vos shaders compilent avant le lancer votre application.

indications : utilisez gk::Mesh::triangleMaterial( ) pour recupérer une description de matière.
Les maillages peuvent utiliser plusieurs matières, dans ce cas, les triangles associés à la même matière sont regroupés dans un gk::SubMesh, cf gk::Mesh::subMeshCount(), gk::Mesh::subMeshes() et gk::subMeshMaterial( ) pour obtenir la description des différentes parties du maillage.


exercice bonus : textures.
Chargez les textures associées aux matières, cf. gk::MeshMaterial::diffuse_texture et gk::MeshMaterial::specular_texture et utilisez-les pour moduler le résultat des calculs d'éclairement.