Faculté des sciences
Département d’informatique
[Link]
IMN401 - Infographie et jeux vidéo - TP 2
Guillaume GILET - [Link]@[Link]
1. Objectif
Pour ce TP, nous disposons d'un squelette d'application volontairement très simple pour automatiser
certaines tâches. L’objectif est d’intégrer dans cette application le pipeline graphique vu en cours afin
d’afficher de manière interactive un objet avec un modèle d’éclairage classique, comme le modèle de Phong.
Ce travail est assez conséquent et de nombreuses étapes vous sont proposées pour avancer pas à pas (il est
fortement recommandé de suivre ces étapes).
Prise en main
Prenez un peu de temps pour vous familiariser avec l'architecture du code fourni. Cette application est
composée de plusieurs classes qui interagissent entre-elles pour automatiser le chargement des objets et
l’interface utilisateur.
Le projet est fourni avec un CMake, il est recommandé d’utiliser “gen_vs_22_solution.bat” pour
générer la solution. Ce script doit être exécuté depuis une “Developer Command Prompt” de Visual Studio
(ou alors simplement exécuté si vous avez CMake d’installé).
Pour faciliter l’ajout de fichier dans Visual Studio, les fichiers doivent être créer dans l’explorateur de fichier
puis “gen_vs_22_solution.bat” doit être exécuté afin de mettre à jour la solution.
Architecture Générale
Dans notre scène 3D, chaque objet 3D sera exprimé par un nœud : une instance de la classe Node, qui est
un objet C++ contenant un modèle 3D (classe ModelGL), un repère 3D (classe Frame) et un matériau
contenant une fonction de rendu et les shaders permettant l’affichage (classe MaterialGL). Chaque objet
nœud, matériau, et modèle sera identifié par un identifiant unique (une chaine de caractères)
Élément central de l’application, la classe Scene est un singleton contenant des pointeurs sur les éléments
de la scène (Nœuds, modèles 3D, caméra...). Le rendu de la scène est effectué par la classe EngineGL. La classe
Application est responsable du chargement de la fenêtre graphique, des extensions OpenGL ainsi que de la
gestion des actions de l’utilisateur.
Classe Application
Cette classe contient principalement une fonction mainLoop implémentant la boucle principale. Son rôle
est de traiter les interactions utilisateurs et d’appeler la fonction de rendu de l’objet EngineGL. Elle contient
également plusieurs fonctions callback appelée par la librairie GLFW lorsque l’utilisateur interagit avec
l’application. C’est dans cette classe qu’il faudra traiter les interactions souris/clavier de l’utilisateur.
Faculté des sciences
Département d’informatique
[Link]
Classe Scene
Cette classe est un singleton. C’est un objet unique dont on peut récupérer son unique instance depuis
toute fonction du code en utilisant la fonction Scene::getInstance(). Elle contient un nœud Root, qui sera le
repère de référence de la scène 3D, auquel sera attaché une caméra (classe Caméra), ainsi qu’un nœud
SceneNode, auquel seront attachés tous les autres nœuds de la scène 3D.
La gestion des nœuds s’effectue via un manager de ressource : La fonction getNode("monNode") renvoie
un pointeur vers le nœud nommé "monNode" (Le nœud sera créé s'il n’existe pas encore). Cela permet de
retrouver tout nœud de la scène à tout moment dans le code. De la même manière, chaque modèle 3D sera
créé par la fonction getModel("filename") avec filename, le chemin vers un fichier contenant un modèle 3D
Classe EngineGL
C’est dans cette classe que se passent la plupart des choses en OpenGL. Elle contient une fonction init
responsable de la création de la scène 3D (créer les nœuds, les modèles, les matériaux…). Ses deux autres
fonctions majeures sont la fonction animate, qui appelle la fonction animate de chaque nœud (afin de faire
évoluer les paramètres des objets et gérer les déplacements, animations…) et la fonction render qui contient
la boucle de rendu OpenGL. Cette dernière fonction nettoie la fenêtre graphique et appelle la fonction de
rendu de chaque nœud, qui sera en charge de l’affichage de son modèle en fonction de son matériau.
Classe Node
Cette classe est principalement un conteneur modélisant un objet 3D. Chaque objet est composé d’un
repère (classe Frame), éventuellement d’un modèle 3D (classe ModelGL) et d’un matériau (classe
MaterialGL). Chaque nœud se place ainsi dans une hiérarchie et peut "adopter" d’autres nœuds. Les repères
3D de chaque nœud sont ainsi liés dans une hiérarchie de transformations (cf cours). Cette classe contient
une fonction animate pour gérer les animations ainsi qu’une fonction render , dédiée à l’affichage. Cette
fonction appelle la fonction render du matériau associé. L’ensemble des traitements et commandes pour
afficher le modèle d’un nœud sera effectuée dans la fonction render du matériau.
Classe Frame
Cette classe modélise un repère 3D (une matrice de transformation). Chaque repère est "lié" à un repère
parent menant jusqu’au nœud Root de la scène. Cette classe contient toutes les fonctions de changement de
repère pour exprimer un point ou une direction d’un repère dans un autre (tous les repères doivent être
attachés au repère Root). Une fonction getModelMatrix() permet de récupérer la matrice contenant
l’ensemble des transformations depuis ce repère vers le repère Root.
Classe ModelGL
Cette classe modélise un modèle 3D chargé depuis un fichier. Elle contient les différents tableaux OpenGL
(VBO, VA, …) et est responsable de les initialiser. Une fonction drawGeometry() permet d’envoyer la
géométrie dans le pipeline OpenGL.
Classe MaterialGL
Cette classe modélise la méthode de rendu de chaque objet. Elle contient un objet GLProgramPipeline
encapsulant l’objet OpenGL correspondant (le pipeline graphique). Chaque nouvelle méthode de rendu devra
Faculté des sciences
Département d’informatique
[Link]
être une nouvelle classe héritant de celle-ci. Le constructeur de cette classe devra spécifier les shaders à
utiliser (classe GLProgram), les lier au pipeline, valider ce dernier et gérer ses propres variables uniform. La
fonction render devra être responsable du rendu de son modèle 3D associé. Finalement, la fonction
animate() sera appelée régulièrement et sera responsable des mises à jour des variables uniform dont la
valeur change à chaque frame. (Note : ces deux fonctions reçoivent en paramètre o : un pointeur vers le nœud
sur lequel le matériau sera appliqué).
La classe BaseMaterial est un exemple de matériau utilisé pour ce TP. N'hésitez pas à créer vos propres
matériaux (un nouveau fichier classe, héritant de MaterialGL) dans son propre répertoire et à rajouter des
fonctions (par exemple pour modifier les variables uniform depuis le reste du code).
Attention: chaque matériau contient son propre objet OpenGL Pipeline, ses propres shaders et son
propre espace mémoire GPU. Il faudra donc créer une nouvelle instance d’un matériau si plusieurs objets se
partagent le même matériau.
Classe GLProgramPipeline et GLProgram
Ces classes encapsulent les objets OpenGL ProgramPipeline et Program. Ces classes s’occupent de
charger et de compiler les shaders passés en paramètre. Elles contiennent notamment les fonctions
permettant d’afficher l’état de l’objet (erreurs de compilation…) ainsi qu’une fonction getId() renvoyant
l’identifiant OpenGL correspondant.
Classe Camera
Cette classe modélise une caméra et doit être liée au nœud Root. (Cela est fait lors de son initialisation).
Elle contient un repère (une Frame) ainsi que les fonctions permettant de spécifier les différentes
projections utilisées. Vous utiliserez notamment sa fonction getViewMatrix() et getProjectionMatrix()
pour récupérer ses matrices de vue et de projection.
Faculté des sciences
Département d’informatique
[Link]
2. Objectif
Voici les objectifs a atteindre pour ce travail :
- Un objet Bunny, ayant pour géométrie [Link], éclairé à l’aide d’un matériau reproduisant le modèle
d’illumination de Phong (les paramètres sont libres mais doivent permettre un éclairage cohérent). La
source de lumière doit être indépendante du code du matériau et provenir de la sphère L en rotation
autour de l’objet. L’objet doit présenter une déformation de sa géométrie en fonction du temps
- Un objet Sol, ayant pour géométrie [Link], positionné afin de représenter un sol de grande taille, sous
l’objet Bunny. Ce sol devra être éclairé à l’aide d’un matériau reproduisant le modèle d’illumination de
Phong (les paramètres sont libres mais doivent permettre un éclairage cohérent). La source de lumière
doit être indépendante du code du matériau et provenir de la sphère L.
- Un objet sphère L, ayant pour géométrie [Link] en rotation autour du modèle du Bunny et au-dessus
du Sol. Cet objet doit avoir un matériau permettant d’afficher sa normale géométrique en chaque point.
Cet objet devra être la source de lumière des autres objets.
- Optionnel : Une interface utilisateur pour chaque matériau basé sur ImGUI (déjà intégré dans le code).
Cela est non-obligatoire mais peut simplifier les tests de votre code. Inspirez-vous de la documentation
ImGUI, des interfaces déjà existantes ([Link],…), de votre auxiliaire d’enseignement et
remplissez pour chaque matériau la fonction displayInterface().
Si vous remettez votre projet, pensez à nettoyer le projet avant le rendu afin de limiter la taille de l’archive.
3. Marche à suivre (partie 1) : Premier affichage et shaders
Notre première scène consiste en un objet Bunny, auquel est associé un matériau de base BaseMaterial.
Nous allons, pas à pas, modifier le matériau pour afficher l’objet, le déformer, et visualiser sa normale. Nous
finirons cette partie en créant un nouvel objet sphère que nous placerons en rotation autour de l’objet
Bunny.
Premier affichage
Commencez par regarder le vertex shader [Link] associé au matériau BaseMaterial. Ce shader devra
utiliser les trois matrices View, Proj et Model pour transformer la position des sommets en espace image.
Avant de pouvoir afficher notre modèle, il faut transmettre aux shaders ces variables.
Modifiez le contenu de la fonction animate de la classe BaseMaterial pour transmettre ces matrices au vertex
shader. Il faudra pour cela récupérer les matrices Model, View et Proj correspondantes dans l’objet Frame
associé au nœud o ainsi que dans l’objet Camera de la scène (Rappel : La classe scène contient une fonction
camera() renvoyant un pointeur sur la caméra. La fonction animate a également pour paramètre un pointeur
vers le nœud o à afficher). Une fois ces variables transmises au vertex shader, il faut modifier celui-ci pour
remplir la variable gl_Position avec la position du sommet en espace image.
À cette étape, vous devez pouvoir observer et manipuler le bunny avec une interaction souris (clic gauche
pour faire tourner l’objet et clic droit pour effectuer une translation). Entrainez-vous à manipuler le noeud
bunny ainsi que le nœud scene.
Faculté des sciences
Département d’informatique
[Link]
Visualiser la normale
Modifions maintenant l’apparence du modèle en visualisant sa normale. Pour cela, il faut définir la couleur
en chaque sommet du Bunny et transmettre cette information au fragment shader.
Créez pour cela dans le vertex shader une variable out contenant une couleur RGB. Cette variable devra
recevoir comme valeur la valeur absolue de la normale en chaque sommet (Le vecteur normal en chaque
point correspond au canal 2. Les VAO et VBO de la classe ModelGL sont déjà configurés pour envoyer cette
information. Il suffit donc de la récupérer). Modifiez en conséquence le fragment shader pour afficher le
résultat.
À cette étape, vous devez pouvoir observer et manipuler le bunny, avec chaque fragment ayant comme
couleur la normale par sommet interpolée.
Déformation
Nous souhaitons maintenant déformer le modèle de notre Bunny. En vous inspirant du TP1, modifiez le
vertex shader afin de déplacer chaque sommet du modèle le long de sa normale (chaque sommet doit être
déplacé d’une valeur X le long de sa normale).
Animez cette déformation à l’aide d’une fonction sinusoïdale et d’une variable temps que vous prendrez
soin de créer dans le matériau et de transmettre au GPU.
À cette étape, vous devez pouvoir observer et manipuler le bunny, avec chaque fragment ayant comme
couleur la normale par sommet interpolée. Le bunny doit se déformer en fonction du temps (attention à ne
pas mettre une déformation trop rapide ou avec trop d’amplitude)
Animations et autres objets
Finalement, nous ajouterons un nouvel objet sphère en rotation autour de l’objet Bunny. Créez un nouvel
objet L, de géométrie [Link] et de matériau BaseMaterial. Afin d’effectuer une rotation, nous utiliserons
le principe de la hiérarchie de repère. Cet objet L devra être attaché à un repère A, qui sera lui-même attaché
au repère du Bunny. Ce repère A sera lui en rotation autour de son axe Y. Tous les nœuds attachés à ce repère
subiront donc la rotation.
Créez pour cela un nœud vide A et attachez celui-ci au repère du Bunny. Effectuez une translation de 0.2
unité le long de l’axe x au repère de votre nœud L et attachez ce nœud au repère A.
Pour mettre notre objet L en rotation, il suffit maintenant d’effectuer une rotation sur le nœud A. Créez
un nouveau matériau rotation en ajoutant une nouvelle classe à l’architecture. Ce matériau ne contiendra pas
de shaders et sa méthode render sera vide. La méthode animate, est cependant appelée par EngineGL à
chaque frame et devra effectuer une rotation du repère de l’objet autour de son axe Y.
A cette étape, vous devez pouvoir observer et manipuler le bunny en déformation, avec chaque fragment
ayant comme couleur la normale par sommet interpolée. Une sphère en rotation doit être visible autour du
bunny. Essayez de manipuler avec la souris uniquement le nœud bunny, ainsi que le nœud sphère (utilisez
pour cela l’interface du moteur).
Faculté des sciences
Département d’informatique
[Link]
4. Marche à suivre (partie 2) : Illumination
Nous allons maintenant ajouter progressivement l’éclairage de Phong à notre scène. Dans un premier
temps, nous travaillerons uniquement sur notre objet bunny. Mettez en commentaire la sphère en rotation
ainsi que la déformation du bunny. Nous vous conseillons à cette étape de créer un nouveau matériau Phong
et de l’assigner au nœud Bunny.
Éclairage diffus et ambiant
La première étape consiste à travailler dans le vertex shader pour définir un éclairage simple avec une
lumière fixe, dont la position est définie « en dur » dans le shader.
Déclarez dans le vertex shader une variable locale posLum, symbolisant la position de la lumière, de
valeur (0, 5, 0). Déterminez pour chaque sommet, le vecteur L correspondant à la direction de la lumière.
Calculez ensuite les terme diffus (en fonction de la couleur de l’objet) et ambiant du modèle de Phong ainsi
que la valeur de couleur finale de l’objet et transmettez cette valeur au fragment program (qui devra bien
entendu l’assigner à chaque fragment).
Une fois l’affichage correct de l’objet, déclarez les valeurs de couleur de l’objet, ainsi que les paramètres
du modèle de Phong comme des variables uniform et transmettez-les depuis le constructeur du matériau.
À cette étape, nous avons un objet Bunny, éclairé par un modèle diffus et ambiant. La lumière est
cependant fixe, sur le dessus du bunny (la lumière tourne avec le bunny. Il est donc impossible d’éclairer le
dessous du bunny).
Indépendance lumière objet
Nous allons maintenant séparer ces deux éléments, c’est à dire définir la position de la lumière dans le
repère de la scène, indépendamment du repère de l’objet. Nous souhaitons maintenant que la lumière se
trouve aux coordonnées P=(0, 5, 0) dans le repère de la scène. Il va donc falloir exprimer ce point P (exprimé
dans le repère scène) dans le repère de l’objet.
Déclarez posLum comme une variable uniform (sans valeur dans le shader donc). Créez le point P de
coordonnées (0,5,0) dans la fonction animate du matériau et transmettez sa valeur au shader. Le résultat
doit être identique à l’étape précédente.
Maintenant, nous allons devoir effectuer un changement de repère pour exprimer P comme un point du
repère scene converti dans le repère de l’objet o. A l’aide les fonctions de conversion de la classe Frame et en
utilisant le repère de la scène et de l’objet, convertissez le point P dans le repère de l’objet et transmettez le
résultat au shader.
À cette étape, nous devrions pouvoir faire tourner le bunny tout en gardant la lumière fixe dans la scène.
Il nous est désormais possible d’éclairer la face du dessous du bunny.
Une lumière indépendante
L’objectif de cette étape est d’exprimer la lumière, non pas comme un point dans le repère de la scène
directement mais comme un nœud Lumiere.
Faculté des sciences
Département d’informatique
[Link]
Commencez par créer un nœud Lumiere lors de l’initialisation et faites-le adopter par la scène. Afin de
mieux visualiser la source lumière, assignez-lui la géométrie de la sphère et un matériau basique.
Depuis votre matériau Phong, il est possible de récupérer un pointeur sur ce nœud Lumière (Rappel : la
fonction getNode(string s) de la classe Scene retourne un pointeur vers le nœud s s'il existe ou crée un nœud
s s'il n’existe pas encore). Exprimez maintenant la position de la lumière comme le point (0,0,0) du nœud
Lumière exprimé dans le repère de l’objet et transmettez cette valeur au shader.
A cette étape, nous devrions pouvoir manipuler l’objet Bunny ainsi que la source de lumière de manière
totalement indépendante.
Éclairage spéculaire
Il est temps maintenant d’ajouter les réflexions spéculaires du modèle de Phong. Pour cela, nous aurons
besoin de la position de la caméra exprimée dans le repère de l’objet. Depuis votre matériau Phong,
calculez l’expression du point (0,0,0) du repère de la caméra dans le repère de l’objet et transmettez cette
valeur dans une variable uniform posCam dans le shader.
À partir de posCam, il est maintenant possible de calculer V, la direction de la caméra. Calculez ensuite R,
la direction de la réflexion idéale en utilisant la fonction glsl reflect, la direction de lumière incidente (-L) et
N.
A l’aide de V et R, vous pouvez maintenant calculer l’intensité spéculaire Is et mélanger sa contribution à
la couleur finale. Attention, Is correspond à la lumière spéculaire et ne doit pas multiplier la couleur de l’objet.
Note : Il est possible que des résultats incorrects lors de l’éclairage proviennent d’une valeur négative du
produit scalaire. Corrigez l’expression de Is si ce n’est pas déjà fait (cf. cours).
À cette étape, nous devrions pouvoir manipuler l’objet Bunny, la source de lumière, et observer des reflets
spéculaires sur le bunny.
Calcul par fragment
Ajoutons maintenant un objet final et déplaçons les calculs du modèle de Phong dans le fragment shader.
Ajoutez un objet Sol à la scène, de géométrie [Link] et assignez-lui votre matériau de Phong. A l’aide
de rotation et changement d’échelle, placez l’objet sous le bunny pour simuler un sol.
Vous pouvez constater que l’éclairage de Phong, sur ce nouveau modèle, montre des artefacts liés à sa
géométrie sous-jacente. Pour résoudre ce problème, il faut déplacer les calculs de Phong dans le fragment
shader. Pour cela, transmettez les vecteurs (non-normalisés) V et L au fragment shader et réalisez les calculs
d’éclairage dans le fragment shader (pensez à renormaliser L et V dans le fragment shader).
Finalement, nous devons pouvoir observer chacun des objets, avec une lumière cohérente et sans
artefacts.