TD5: shadow volumes
Le but de ce TD est d'implémenter l'algorithme Z-pass des shadow volumes, de voir quels sont les problèmes qu'il pose, et si le temps le permet d'implémenter le Z-fail qui les corrige.
Pour récupérer le TP, taper :
tar zxvf ~holzschu/td5/td5.tar.gz cd td5 qmake
Puis pour compiler le programme et le lancer, taper :
make ./td5 ~holzschu/usr/share/data/shadow.om
Au départ, le programme ne fait rien d'autre que charger le modèle donné sur la ligne de commande, mais ne l'affiche pas.
Squelette du code fourni
La classe Viewer
Comme chaque fois, nous utilisons le QGLViewer. Nous dérivons notre propre Viewer dans les fichiers viewer.h et viewer.cpp. Ce sont les seuls fichiers à modifier. Une instance de la classe possède plusieurs variables membres :
Mesh | mesh; | contient le maillage du modèle à afficher (voir plus loin). Le code pour le charger est déjà fourni. |
AABBox | bbox; | contient une boîte englobante du modèle; permet par exemple de connaître le diamètre (bbox.sizeMax()) du modèle. Le code pour calculer cette boîte englobante est déjà fourni. |
qglviewer::ManipulatedFrame* | light; | représente un point dans l'espace que l'on peut manipuler avec la souris (voir plus loin). On s'en sert pour représenter la position de la source lumineuse. |
bool | showSilhouettes_ | drapeaux indiquant si on doit afficher ou non certaines choses. Le squelette contient déjà le code pour basculer ces booléens quand l'utilisateur presse les touches E, Q et A respectivement (fonction Viewer::keyPressEvent(QKeyEvent*e)). |
bool | showShadowQuads_ | |
bool | showShadows_ |
La fonction importante est la fonction void Viewer::draw(). C'est elle qui indique ce que le Viewer doit dessiner. Elle appelle les fonctions :
- void renderModel() const;
- void renderSilhouettes() const;
- void renderShadowQuads() const;
C'est le code de ces fonctions que vous allez devoir écrire.
La classe Mesh
Pour représenter le maillage décrivant le modèle, nous utilisons une halfedge datastructure. Une telle structure permet de représenter efficacement un manifold et les relations d'ajacences entre sommets, arêtes et faces. Dans un manifold, chaque arête a au plus deux faces adjacentes. Nous considérerons qu'elles en ont exactement 2, autrement dit, le modèle est constitués d'objets "fermés" (qui délimitent un intérieur et un extérieur). Une telle structure est composée de
vertices | ce sont les sommets du maillage. Un sommet connaît la liste des demi-arêtes (voir ci-dessous) dont il est l'origine. |
edges | ce sont les arêtes du maillage; Une arête connaît les deux demi-arêtes (voir ci-dessous) qui la compose. |
halfedges | ce sont les demi-arêtes du maillage. Une demi-arête est orientées et
connaît
|
faces | ce sont les triangles du maillage. Chaque triangle connaît les trois demi arêtes qui l'ont comme face sur la gauche. |
Cette représentation permet d'accéder à toute les informations d'ajacences: liste des faces autour d'un sommet, liste des sommets d'une face, face gauche et droite d'une arête, etc... Nous utilisons pour cette structure la classe Mesh proposée par la librairie OpenMesh. Nous découvrirons son API au cours du TD.
Prise en main
Affichage de la lampe
Au début de draw(), écrire le code OpenGL permettant d'afficher un gros point jaune à l'endroit de la lampe (la variable Vec L contient cette position). On utilisera glPointSize(8.0f) avant le glBegin() pour régler la taille du point affiché à 8 pixels.
Lancer le programme et vérifier qu'en cliquant avec le bouton droit de la souris sur le point rouge et en déplacant la souris le bouton appuyé, vous pouvez déplacer la lampe dans l'espace. C'est la magie du ManipulatedFrame de QGLViewer qui est capable de grabber les actions de la souris quand celle-ci passe dessus. Pour rendre cela encore plus flagrant, utiliser la fonction booléenne light->grabsMouse() pour afficher la lampe avec une taille de 12 pixels quand elle est grabbée.
Affichage du modèle
Écrire le code de la fonction renderModel() pour qu'il affiche le modèle (cette fonction est appellée automatiquement par Viewer::draw()). Pour cela on itèrera à travers les faces du maillage de la façon suivate
for (Mesh::ConstFaceIter f=mesh.faces_begin();f!=mesh.faces_end();++f) { // // // }
La variable f fournie une poignée (handle) sur une face. Cette poignée permet de demander des informations sur la face au maillage. Par exemple pour obtenir la normale de la face :
Vec n = Vec(mesh.normal(f));
Avec cette poignée, on peut maintenant itérer à travers les sommets de la face :
for (Mesh::ConstFaceVertexIter v=mesh.cfv_iter(f);v;++v) { // // // }
Cette fois ci, v est une poignée sur un sommet du maillage, qui permet de demander à ce dernier des informations sur le sommet. Par exemple pour obtenir la position 3D du point :
Vec p = Vec(mesh.point(v));
Découverte OpenGL
Éclairage
Dans la fonction draw(), juste avant l'appel à la fonction qui affiche le modèle, changer le glEnable(GL_LIGHTING) en glDisable(GL_LIGHTING), relancer le programme et regarder ce qu'il se passe. Réactiver l'éclairage et rajouter glDisable(GL_LIGHT0). Relancer et regarder ce qu'il se passe.
Face Culling
Dans la fonction draw(), juste avant l'appel à la fonction qui affiche le modèle, rajouter :
glEnable(GL_CULL_FACE); glCullFace(GL_FRONT);
Relancer le programme et regarder ce qu'il se passe. Réessayer ensuite avec glCullFace(GL_BACK);. On rajoutera ce qu'il faut pour afficher le modèle en fil de fer afin de mieux voir ce qu'il se passe.
Stencil buffer
On va utiliser le stencil buffer pour ne dessiner que la moitié gauche de l'écran. Pour cela, on affiche un quad rouge qui couvre la moitié de l'écran et met à 1 le stencil pour tous les pixels correspondants (par défaut le stencil est à 0 partout). Ensuite on affiche le modèle en indiquant que seuls les fragments où le stencil est 1 doivent être affichés.
Toujours dans la fonction draw(), juste avant l'appel à la fonction qui affiche le modèle, ajouter :
glEnable(GL_STENCIL_TEST); glStencilFunc(GL_ALWAYS,1,~0); glStencilOp(GL_REPLACE,GL_REPLACE,GL_REPLACE);
Regarder les man pages pour la doc de ces fonctions. Ensuite rajouter le code suivant pour dessiner un rectangle rouge couvrant la droite de l'écran
startScreenCoordinatesSystem(); glDisable(GL_LIGHTING); glDepthMask(false); int w=width()/2; int h=height()/2; glBegin(GL_QUADS); glColor3f(1.0f,0.0f,0.0f); glVertex2i(0,h); glVertex2i(w,h); glVertex2i(w,0); glVertex2i(0,0); glEnd(); glEnable(GL_LIGHTING); glDepthMask(true); stopScreenCoordinatesSystem();
Pour finir, modifier le stencil test juste avant l'appel à renderModel() pour n'afficher que ce qui est dans le rectangle. On rajoutera aussi juste après renderModel() l'instruction glDisable(GL_STENCIL_TEST);.
Rendu des ombres
Écrire le code de la fonction renderSilhouettes() pour qu'il affiche les arêtes qui sont silhouettes pour la lampe (dont la position est donnée par L). Pour cela, on traversera la liste des arêtes du maillage. Pour chaque poignée e sur une arête, on obtiendra des poignées sur les deux demi-arêtes qui la compose avec :
Mesh::HalfedgeHandle h0 = mesh.halfedge_handle(e,0); Mesh::HalfedgeHandle h1 = mesh.halfedge_handle(e,1);
À partir de ces poignées, on obtiendra des poignées sur les faces adjacentes avec mesh.face_handle(hi) et des poignées sur les sommets de l'arête avec mesh.from_vertex_handle(hi). Pour faire le produit scalaire entre deux Vec a et b on utilisera a*b.
Lancer le programme et appuyer sur E pour afficher les arêtes.
Affichage des shadow quads
Écrire le code de la fonction renderShadowQuads() pour qu'il affiche les arêtes de silhouette extrudées vers l'infini. On fera attention à bien orienter les quadrilatères.
Lancer le programme et appuyer sur Q pour afficher les arêtes.
Rendu des ombres par z-fail
Dans la méthode draw(), écrire le code qui rend la scène avec les ombres lorsque showShadows est vrai. On rappelle que l'on doit faire les étapes suivantes
- afficher la scène une première fois avec seulement l'affichage ambiant activé;
- rendre les front facing shadow quads en incrémentant le stencil quand le z-test passe;
- rendre les front facing shadow quads en decrémentant le stencil quand le z-test passe;
- re-rendre la scène avec l'éclairage diffus activé et le stencil test qui ne passe que si le stencil est à 0.
Lancer le programme et appuyer sur A pour afficher les ombres. Essayer d'identifier des points de vues qui posent problème.
Rendu des ombres par z-pass
Si vous avez le temps, implémenter le z-pass.