TD3 - Pipeline programmable
1. Introduction
La bibliothèque OpenGL a subit une évolution majeure suite à l'apparition des
Pour les TDs, nous utiliserons OpenGL 3.3 avec le profile core, ce qui signifie que les fonctionnalités dépréciées sont interdites et provoquent des erreurs à l'exécution en cas d'utilisation. Cela exclut toutes les fonctions bleues de la quick-reference-card. Il faut donc également se méfier de certains tutoriaux et exemples disponibles sur internet qui utilisent les fonctionnalités dépréciées et ne fonctionneront donc pas avec ces TDs. Ce n'est pas un bug.
Téléchargez et compilez le code fourni mds3d_td3.zip en utilisant cmake :
> mkdir build
> cd build
> cmake ../mds3d_td3
> make
La première étape pour utiliser OpenGL est de créer un context et généralement une fenêtre associée capable d'afficher un rendu OpenGL. Ces deux tâches seront effectuées ici par la bibliothèque GLFW qui est inclue avec le projet.
Documentation:
- Pour un aperçu rapide de l'ensemble des fonctions OpenGL triées par catégories, se référer à la quick-reference-card.
- Pour connaître tous les détails d'une fonction OpenGL, se référer aux pages de références.
- La documentation de GLFW est disponible ici: http://www.glfw.org/docs/latest/
Le fichier main.cpp
initialise le contexte OpenGL, crée la fenêtre principale (cf. class Viewer
) et exécute l'application interactive. La fonction main()
appelle:
- la méthode
Viewer::init
après la création de la fenêtre et du contexte OpenGL, - la méthode
Viewer::reshape
lorsque la fenêtre est redimensionnée, - et la méthode
Viewer::updateAndDrawScene
lorsque l'image doit être redessinée, qui elle même appelle la méthodeViewer::drawScene
.
Viewer
. En principe vous n'avez rien à modifier dans ce fichier.
2. Afficher un triangle
OpenGL peut être qualifié de machine à états dans le sens où le contexte lié à l’application possède tout au long de l’exécution un état courant qui peut être altéré au travers de nombreuses fonctions. Généralement, pour activer (resp. désactiver) un état du contexte, OpenGL fournie la fonction glEnable
(resp. glDisable
). D’autres fonctions sont disponibles pour spécifier des états plus complexes (la modification de l’état du viewport, par exemple).
Afin d'obtenir notre premier rendu, il nous faut au strict minimum :
- Définir une zone de tracé au sein de la fenêtre à l'aide de la fonction
glViewport
. Pour l'instant, nous allons utiliser l'ensemble de la fenêtre avec:glViewport(0, 0, _winWidth, _winHeight);
.
TODO : ligne à insérer dans le fichierviewer.cpp
au bon endroit. - Un vertex et un fragment shaders. Des versions minimalistes sont déjà fournies dans les fichiers
data/shaders/simple.*
qui sont chargés et compilés par la fonctionViewer::loadShaders()
à l'aide de la classeShader
à l'initialisation. Les shaders peuvent être re-compilé à la volée avec la touche r. - Définir une géométrie, c'est à dire un maillage triangulaire et le charger en mémoire vidéo. C'est le rôle de la classe
Mesh
où le maillage est stocké sous la forme d'une tableau de sommets (sommet = positions x, y, z, et attributs) et d'un tableau de face (1 face = 3 indices). La fonctionMesh::load(filename)
permet de charger un maillage à partir d'un fichier aux formats ASCII.off
ou.obj
. Après chargement des données, il faut les copier en mémoire vidéo via des Vertex Buffer Object (VBO), un pour les sommets, et un pour les indices, puis les lier dans un unique Vertex Buffer Array (VBA). Cette tache est réalisée par la fonctionMesh::initVBA()
. Un maillage par défaut est chargé dans la fonctionViewer::init
.
Question : regarder le contenu de ce fichier et en déduire la forme qu'il représente.
Viewer::drawScene
. TODO : pour cela vous devez effectuer les trois appels suivants à remettre dans le bon ordre :
_mesh.draw(_shader);
_shader.deactivate();
_shader.activate();
Mesh::draw
en quatre étapes:
- Activer le VBA et les VBO. (fait)
- Spécifier les données à fournir à l'attribut
in vec3 vtx_position;
du vertex shader. (fait) - Tracer les triangles
- Désactiver l'attribut
vtx_position
. (fait)
glDrawElements(GL_TRIANGLES, 3*mFaces.size(), GL_UNSIGNED_INT, 0);
à insérer au bon endroit. Cette fonction émet 3*mFaces.size()
sommets indicés par le tableau d'index courant de type unsigned int
, en commençant par l'index 0
et en interprétant trois sommets consécutifs comme une face triangulaire.
Question : pourquoi cette fonction Mesh::draw
à besoins de connaître le shader actif ?
Vous pouvez maintenant re-compiler et tester. Vous devez obtenir un triangle rouge comme spécifier par le maillage test.off
. Essayez de re-dimensionner la fenêtre, pouvez vous expliquer ce comportement étrange ?
3. Du triangle au maillage complexe

1. Afin de résoudre le problème précédent, nous devons effacer l'image de destination avant de tracer notre scène. Pour cela, utilisez les commandes suivantes (au bon endroit et dans le bon ordre) de manière à avoir un fond gris :
glClear(GL_COLOR_BUFFER_BIT);
: efface le tampon de couleur.glClearColor(r, g, b, a);
: spécifie la couleur RGBA d'effacement (chaque composante est unfloat
entre 0 et 1).
2. Modifiez le fichier test.off
de manière à tracer une petite maison comme la figure ci-contre.
3. Beaucoup d'efforts pour afficher 3 triangles... mais votre code permet déjà d'afficher des maillages beaucoup plus complexe :
- Ouvrez le logiciel Blender, cliquez sur la fenêtre et supprimer le cube par défaut avec la touche suppr.
- Ajoutez un maillage de singe avec space bar, entrez
monk
et valider. - Lissage : onglet modifier, ajouter une Subdivision Surface, et mettre les niveaux à 3.
- Personnalisation : passez en mode édition avec la touche tab, sélectionner un sommet avec un clique droit, et déplacer le sommet en cliquant/déplaçant les flèches du repère
- Export : menu File->export->Wavefront (.obj), dans les options valider Triangulate faces, et enregistrez dans votre dossier
data/models/monkey.obj
. - Dans la fonction
Viewer::init()
, chargez votre nouveau maillage à la place du fichier de test, compilez et testez.
4. Attributs aux sommets
1. La classe Mesh
permet de définir des couleurs par sommet. Pour cela nous devons charger un maillage possédant cette information, tel que le fichier quad.off
représentant un carré avec des couleurs différentes pour chacun des sommets. Testez. Pourquoi le carré est-il uniformément rouge ?
2. Afin de prendre en compte cette information de couleur dans notre rendu nous avons un peu de travail :
- Au niveau des shaders :
Modifiez le vertex shader pour prendre en entrée (in
) un attribut de couleur, par exemple unvec3 vtx_color
. Nous devons ensuite passer cette information au fragment shader via le raster. Pour cela, ajoutez une variable en sortie du vertex shader, par ex.out vec3 var_color;
. Réalisez la copie dans la fonction main, et dans le fragment shader ajoutez une variable en entrée (in
) ayant le même type et le même nom que la variable en sortie du vertex shader. La valeur de cette variable sera calculée automatiquement par l'étage de raster en interpolant les valeurs aux sommets. Mettez à jour la fonction main pour recopier la valeur en sortie. - Au niveau de la classe Mesh :
Nous devons maintenant mettre à jour le code de la fonctionMesh::draw
afin de spécifier où prendre les valeurs pour le nouvel attributvtx_color
. Pour cela, inspirez vous de ce qui est déjà fait pour les positions et les normales, et de la déclaration de la classeMesh::Vertex
.

3. Testez avec le modèle lemming.off
. Pourquoi le rendu ne semble pas correct ?
4. Activez le z-buffer avec la commande glEnable(GL_DEPTH_TEST);
. Observez bien ce qui se passe au démarrage de l'application. Qu'avons nous oublié ?
5. Shaders
1. Modifiez le ou les shaders pour que le personnage nous fasse face, c'est à dire, faire une symétrie par rapport au plan X-Y. Vous pouvez accéder aux composantes x, y, et z d'un vec3
avec v.x, v.y, v.z
. Vous pouvez aussi faire du swizzling: v.xy, v.yxz, v.xxyy
, ... Vous pouvez re-compiler à chaud les shaders avec la touche r.
2. Modifiez le ou les shaders pour que le personnage soit 2 fois plus grand.
3. Nous allons maintenant contrôler le niveau de zoom au clavier (au lieu d'éditer et recompiler à chaque fois le shader). Pour cela :
- Dans le shader, déclarez une variable globale
uniform float zoom;
, et mettez à jour la fonction main pour l'exploiter. - La valeur de la variable zoom doit être définie par l'application C++ via l'instruction :
glUniform1f(_shader.getUniformLocation("zoom"), value);
. Testez avec une valeur arbitraire (par exemple 1.5), puis rendez cette valeur contrôlable au clavier (ajoutez un attribut à la classeViewer
, initialisez-la dans le constructeur, et modifiez sa valeur dans la fonctionViewer::keyPressed
via les touches page up/down).
4. Ajoutez une translation 2D contrôlée au clavier via les flèches. Côté Viewer
, vous pouvez utiliser un Eigen::Vector2f
pour stocker un vecteur 2D, et côté shaders un vec2
, ainsi que la fonction glUniform2fv(int,int,float*)
pour faire le lien.
6. Affichage en fils de fer

1. Afin de visualiser le maillage, nous allons effectuer un rendu en fils-de-fer révélant ses arêtes. Pour cela, vous pouvez utiliser la commande glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
. Testez.
2. Afin de rendre la visualisation plus lisible, nous allons superposer les arêtes visibles au rendu classique. Pour cela vous devez :
- Tracer deux fois le modèle ; une première fois de manière classique, une seconde fois en mode fils-de-fer.
- Modifier la fonction de test du z-buffer pour que le second tracé soit effectif (voir
glDepthFunc
). - Tracer les lignes d'une couleur uniforme, par exemple blanche (vous pouvez utiliser un 2ème shader spécialisé, ou utiliser une variable uniforme pour passer du rendu classique à un rendu en blanc uniforme).
- Pensez à restaurer les états entre deux images !
- Pour un meilleur rendu, vous pouvez activer l'antialiasing des lignes avec
glEnable(GL_LINE_SMOOTH);
- Ajoutez un raccourci clavier pour activer/désactiver ce mode de rendu.

7. Fenêtre d'affichage
Utilisez la commande glViewport
pour scinder la fenêtre en deux, et afficher l'objet selon une vue de face et une vue de profil. Comme précédemment, vous pouvez au choix utiliser deux vertex shaders différents ou passer par une variable uniforme pour générer les 2 points de vue. Nous verrons au prochain TP comment généraliser ces transformations.