M1 synthèse d'images
2023

TP2 - prise en main openGL


Installation

mettez à jour gkit3, puis dans le répertoire de gKit3, clonez gKit3GL, la partie openGL de gKit :

cd gkit3
git pull
premake5 gmake
git clone https://forge.univ-lyon1.fr/JEAN-CLAUDE.IEHL/gkit3GL
cd gkit3GL
premake5 gmake
make tp1
bin/tp1

vous pouvez compiler l'exemple tp1 et vérifier que tout est correct.

pour windows, et linux, il faut installer les librairies openGL, les différentes étapes sont résumées sur le dépot.


Partie 1 : openGL et application

pour dessiner un objet avec openGL, il faut créer une fenètre avec les bons paramètres pour qu'openGL puisse dessiner dedans. cettte étape n'est pas très interressante, mais surtout elle est différente d'un système à l'autre, ie X11 ne crée pas les fenêtres de la même manière que Wayland, ni que Windows et encore moins que Macos, ou Android ou WebGL, ou, etc...

on va donc utiliser une librairie pour faire ça de manière portable, par exemple : SDL.

l'utilisation de la librairie n'est pas très interressante non plus, il suffit de lire la doc sur la création des fenêtres et du contexte openGL et d'appeler les fonctions dans le bon ordre. et comme c'est toujours la même chose, il n'a pas été trop difficile de créer 2 fonctions utilitaires : create_window() et create_context() définies dans window.h

pour les curieux, il y a un résumé dans la doc en ligne, cf écrire une application openGL

maintenant que la fenêtre est créée, il suffit de dessiner dedans et de recommencer tant que l'application existe...
mais pour savoir que l'utilisateur veut fermer l'application, il faut le tester explicitement. ca s'appelle la gestion des évènements...

c'est un peu barbare, mais ce n'est pas compliqué, par contre, il y a pleins d'évènements, cf la doc.

pour savoir si la souris vient de cliquer sur le bouton 'fermer' de la fenêtre de l'application, il faut surveiller l'évènement SDL_WindowEvent, on peut aussi surveiller le clavier, ie SDL_KeyboardEvent, pour détecter que la touche 'echap' est enfoncée ou que 'ctrl'+'w' / 'ctrl'+'q' est enfoncé. ie toutes les manières classiques de fermer la fenêtre d'une application.

c'est SDL_PollEvent() qui permet de récupérer les évènements et de réagir, le code ressemblera à :

bool close= false;

// recuperer un evenement a la fois
// PollEvent() renvoie faux lorsque tous les evenements ont ete traite
SDL_Event event;
while(SDL_PollEvent(&event))
{
    if(event.type == SDL_QUIT)
        close= true;  // sortir si click sur le bouton 'fermer' de la fenetre

    else if(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)
        close= true;  // sortir si la touche esc / echapp est enfoncee
}

projets/tp1.cpp est un exemple complet et fonctionnel.

une application openGL est en général composée de 3 parties. la boucle qui dessine toutes les images bien sur, mais il faut en général initialiser openGL, charger un objet, des textures, des shaders, etc. on regroupe tout ce code dans une fonction init(). sans surprises, toutes ces ressources doivent être libérées à la fin de l'exécution de l'application, il y a donc également une fonction quit() qui nettoye tout. l'organisation du code de projets/tp1.cpp doit être un peu plus claire.

en résumé, voila ce que fait tp1.cpp :
étape 1 : création de la fenêtre et du contexte openGL

    Window window= create_window(1024, 576);
    Context context= create_context(window);

étape 2 : initialisation, chargement d'un objet 3d au format wavefront, ie un fichier .obj

    std::vector<Point> positions;
    std::vector<unsigned> indices;
   
    // lit le fichier, et recupere un tableau de positions de sommets et un tableau d'indice des sommets des triangles de l'objet
    if(!read_indexed_positions("../data/robot.obj", positions, indices))
        return false;   // erreur de lecture

    count= indices.size();
   
    // utilitaire. alloue de la memoire gpu et transfere les tableaux positions et indices sur le gpu. necessaire pour dessiner...
    vao= create_buffers(positions, indices);

étape 3 : boucle principale et gestion des évènements

    bool close= false;
    while(!close)
    {
        SDL_Event event;
        while(SDL_PollEvent(&event))
        {
            if(event.type == SDL_QUIT)
                close= true;  // sortir si click sur le bouton 'fermer' de la fenetre
            else if(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)
                close= true;  // sortir si la touche esc / echapp est enfoncee
        }
       
        // dessiner
        draw();

        // presenter / montrer le resultat
        SDL_GL_SwapWindow(window);
    }

étape 4 : libération des ressouces / nettoyage

    release_buffers(vao);

et bien sur, (enfin !!) l'affichage de l'objet chargé dans init() :

    /* utilitaire. dessine des TRIANGLES avec count indices (ie indices/3 triangles...)
        en appliquant les transformations model, view et projection aux coordonnées des sommets.

     */
    draw(vao, GL_TRIANGLES, count, model, view, projection);


Partie 2 : openGL et transformations

pour dessiner un objet, il faut le placer dans le monde, ainsi que placer et définir une camera pour l'observer. il y a plusieurs solutions pour décrire ces transformations / ces changements de repères, mais les api 3d comme openGL, Vulkan, DirectX ou Metal imposent d'utiliser des matrices 4x4 homogènes. c'est un outil qu'il faut apprendre à manipuler.

pour placer un objet dans le monde, on modifie la matrice model : avec une translation, une rotation, un changement de taille, ou la composition de plusieurs transformations. mat.h définit les transformations usuelles, cf Translation, Rotation, Scale...

pour décrire une camera, on utilise aussi une matrice pour représenter le passage entre le repère du monde et le repère de la camera. on construit ce changement de repère comme pour placer les objets dans le monde, ie avec des translations, rotations, etc. la projection réalisée par la camera est également décrite par une matrice de projection, cf Perspective, par exemple.

la suite de la lecture dans la doc en ligne : premiers objets et transformations, à partir de la section "placer un objet dans le monde". les parties précédentes ne sont pas adaptées à gkit3.

remarque : la doc et ses exemples sont adaptés à gkit2light, il y a donc des différences. par exemple, la classe Mesh n'existe plus dans gkit3, elle est remplacée par la fonction create_buffers(). les paramètres de la fonction draw(), qui affiche les objets, sont également différents.

voici comment adapter les exemples de la doc, par exemple le 1er :

// code gkit2
Transform model= Translation(0, 2, 0);
draw(m_objet, model, camera());

// version gkit3
Transform model= Translation(0, 2, 0);
draw(vao, GL_TRIANGLES, count, model, view, projection);

m_objet est de type Mesh et contient, entre autre, le vao, count, le nombre d'indices de sommets et le type de primitives à dessiner, des GL_TRIANGLES, en général.
la fonction camera() renvoie les 2 matrices qui décrivent la camera, view et projection.

gkit3 est plus explicite... il y a moins de raccourcis, passer les exemples vers les fonctions de gkit3 ne devrait pas poser de probleme.

une fois que l'essentiel des transformations et de leur composition est compris, comment écrire une camera simplifiée qui permet de tourner autour du centre d'un objet ? en controlant un seul angle de rotation ? et avec 2 angles de rotations, ce serait plus intuitif, non ?
comment pourrait on approcher la camera du centre de l'objet ? ou l'éloigner ? et comment serait-il possible de déplacer le centre de rotation de la camera ?
comment calculer la distance de la camera pour voir la totalité de l'objet dans la fenêtre ?

bravo ! vous venez d'écrire une camera de type orbiter.
et une camera type première personne ? que faut-il changer ? est ce quce type de camera est adaptée à l'observation d'un seul objet ?
vous connaissez probablement au moins 2 autres types de cameras couramment utilisées dans les jeux video, comment les realiser ?

remarque : pour utiliser key_state() et clear_key_state(), il faut utiliser la fonction utilitaire de gestion d'évènements events() définie dans window.h.
il n'est pas trop difficile d'écrire une fonction générique pour gérer les évènements.
projets/tp3.cpp donne un exemple complet qui utilise key_state() pour déplacer un objet.

Partie 3 : openGL et shaders

maintenant que vous savez afficher des objets à peu près comme vous voulez, il est temps de regarder comment fonctionnent les shaders.

projets/tp4.cpp et tp4.glsl fournissent un premier exemple minimaliste.

que faut-il modifier pour colorier les pixels avec une autre couleur ?
que faut-il faire pour que l'application modifie la couleur ? rappel : les shaders ont plusieurs types de paramètres...

dessinez chaque objet avec une couleur différente.

c'est quand même moche ces aplats de couleurs constante. que faut-il faire pour utiliser un modèle de matière, Lambert par exemple ?
il faut faire un peu de gymnastique, mais la démarche est la même que d'habitude.
que veut-on calculer ?
quels paramètres ?
quelles données ? quels résulats ?
comment déclarer les différents paramètres ?

rappel : voila la situation, un point p éclairé par une source de lumière visible dans la direction l, et une matière toute simple, juste une couleur.





que doit on calculer ?

\[
L_r(p, \vec{o}) = \frac{\mathrm{color}}{\pi} \cdot L_i(p, \vec{l}) \cdot \cos \theta
\]

il suffit de calculer le cosinus de l'angle entre la normale en p et la direction vers la lumière, et de se rappeler que les calculs sur les points et les vecteurs doivent se faire avec des coordonnées dans le même repère.

en résumé, il faut connaitre, dans le même repère :

quel shader fait le calcul ? et oui, il faudra probablement transformer certaines coordonnées.

solution 1 : vertex shader
on va commencer par faire les calculs dans le vertex shader. 

comment récupérer toutes ces informations dans le vertex shader ? une fois que les coordonnées de tous ces points et vecteurs sont connues dans le même repère, il ne reste plus qu'à faire le calcul...

dernière question, comment transmettre la couleur que l'on vient de calculer au fragment shader ? et pourquoi vouloir faire ça ? y-a-t-il une autre solution ?

solution 2 : fragment shader
que faut-il modifier pour faire les calculs pixel par pixel, dans le fragment shader ?

le résultat est-il différent ? ou, pourquoi ça marche ?


quelle est la meilleure solution ?