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 :

Meshmesh; contient le maillage du modèle à afficher (voir plus loin). Le code pour le charger est déjà fourni.
AABBoxbbox; 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.
boolshowSilhouettes_ 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)).
boolshowShadowQuads_
boolshowShadows_

La fonction importante est la fonction void Viewer::draw(). C'est elle qui indique ce que le Viewer doit dessiner. Elle appelle les fonctions :

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 
  • le sommet d'origine (from vertex);
  • le sommet d'arrivée (to vertex);
  • la face sur sa gauche;
  • la demi-arête inverse (opposite halfedge).
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 

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.