TD2 : Shadow Volumes

Dans ce TD, vous allez mettre en œuvre la technique des shadow volumes pour calculer les ombres portées d'un maillage éclairé par une source ponctuelle.

Le principe général de cette méthode est de calculer – indépendamment du point de vue – la frontière entre les régions de l'espace éclairées et celles dans l'ombre (c'est-à-dire visibles ou invisibles depuis la lumière ponctuelle). Dans un second temps, en fonction de la vue, l'idée est de compter le nombre d'entrées et de sorties dans ces volumes d'ombres.

Pour démarrer dans de bonnes conditions, télécharger le squelette de code suivant : sia_td2.zip

1. Construction des volumes d'ombres

La première étape va consister à détecter, pour chaque objet, sa silhouette vue depuis la lumière, puis à extruder ces silhouettes pour générer les volumes d'ombre. Pour cela, vous aurez besoin de parcourir les arêtes d'un maillage et d'accéder aux faces adjacentes à cette arête. La structure de demi-arrête (half edge) facilite grandement cette tâche ; vous utiliserez la bibliothèque Surface_mesh (doc)

1. Pour vous (re-)familiariser avec la syntaxe de Surface_mesh, observez et comprenez la méthode Mesh::load. Vous y découvrirez en particulier comment parcourir les sommets du maillage (Vertex_iterator) et accéder à leurs attributs (position, normale, couleur et coordonnées de texture). Vous pourrez de la même façon parcourir les arêtes (Edge_iterator) ou accéder à la normale des faces (get_face_property()).

2. Implémentez la méthode Mesh::computeShadowVolume qui prend en paramètre la position de la source lumineuse ($L$) et doit retourner un nouveau maillage correspondant au volume d'ombre. Cette méthode doit parcourir chaque arêtes du maillage, et déterminer s'il s'agit d'une silhouette vue depuis la lumière, c'est-à-dire si elle est adjacente à une face éclairée et une face dans l'ombre.

Remarque : Une face est éclairée si la source lumineuse est du même côté que le plan orienté par la normale de la face. Soit le plan défini par le point $\vx{p}$ et la normale unitaire $\vx{n}$ ; alors la distance signée d'un point $\vx{x}$ à ce plan peut être calculée par $(\vx{x} - \vx{p}) \cdot \vx{n}$.

Pour chaque arête-silhouette, il vous faut alors créer un quad dont les deux premières extrémités sont les sommets $\vx{v_0}, \vx{v_1}$ de l'arête, et les deux autres extrémités correspondent à $\vx{v_0}, \vx{v_1}$ envoyés à l’infini par rapport à la source lumineuse, c'est-à-dire aux vecteurs $(\vx{v_0} - L)$ et $ (\vx{v_1} - L)$ dont la coordonnée homogène vaut $w = 0$. Vous pouvez utiliser la méthode Mesh::addQuad pour créer le quad (deux triangles) à partir de ces positions.

3. La position de la lumière étant fixe, appelez Mesh::computeShadowVolume dans Viewer::init() sur le maillage "sphere.off. Pour visualiser le volume d'ombre généré, dessinez ce nouveau maillage dans Viewer::display() à l'aide du shader _simplePrg (attention à model_matrix).

Vous observerez un décalage entre le volume d'ombre calculé et la sphère. En effet, la matrice de transformation de la sphère n'a pas été prise en compte. Deux solutions : soit transformer les sommets de la sphère dans le repère monde lors du calcul des volumes, soit transformer la position de la lampe dans le repère de l'objet (par la transformation inverse). Dans le second cas, les volumes d'ombres seront eux-même calculés dans le repère de l'objet ; il faudra donc leur appliquer la même transformation que la sphère lors de l'affichage.

Remarques : pour un rendu en fil de fer (wireframe), appelez la commande glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) avant de dessiner les volumes d'ombre. Pour revenir au rendu standard, appelez glPolygonMode(GL_FRONT_AND_BACK, GL_FILL).

shadow volumes shadow volumes
Shadow volumes visualisés en rendu fil de fer.

Remarques : vous pouvez constater que les silhouettes sont assez irrégulières. Pour résoudre ce problème (sans augmenter la résolution du maillage), il faudrait détecter les silhouettes à l'intérieur des faces et non le long des arêtes. Nous verrons comment le faire dans le cours sur le rendu expressif.

4. Il est capital pour la suite que les quad soient correctement orientés. Activez le back face culling (GL_CULL_FACE) et visualisez les volumes d'ombre en l'effectuant le culling sur les faces arrières avec glCullFace(GL_BACK), puis sur les faces avants (GL_FRONT). Assurez-vous que tous les quads sont orientés de façon consistante. Testez également sur un maillage plus complexe comme "tw.off".

2. Comptage des entrées et sorties

Durant cette seconde étape, vous allez utiliser le stencil buffer pour compter le nombre d'entrées et de sorties dans les volumes d'ombre depuis le point de vue courant. Pour cela, il faut rastériser les quads délimitant les volumes d'ombre et mettre à jour le stencil buffer en fonction du résultat du test de profondeur avec le reste de la scène. Vous utiliserez ici l'approche z pass.

1. Pour effectuer le test de profondeur, il faut tout d'abord remplir le depth buffer. Faites un rendu des objets de la scène en n'écrivant que dans le depth buffer ; désactivez l'écriture de la couleur avec glColorMask.

2. Pour les quads orientés vers l'avant, il faut incrémenter le stencil si le test de profondeur a réussi (on entre dans un nouveau volume d'ombre), et inversement pour les quads orientés vers l'arrière (on en sort). Ce test doit s'effectuer quelle que soit la valeur dans le stencil. Utilisez les fonctions glStencilOpSeparate et glStencilFunc pour obtenir cette configuration.

N'oubliez pas d'activer le GL_STENCIL_TEST, de remettre à zéro le stencil buffer avec glClear, et dessinez le volumes d'ombre (pleins, pas en wireframe). Assurez-vous que le back-face culling est bien désactivé (les faces avants et arrières doivent être rastérisées) et que le depth buffer n'est pas mis à jour (utilisez glDepthMask).

3. À l'issue de la passe précédente, les zones dans l'ombre correspondent aux régions où le stencil est supérieur à 0. Configurez le test du stencil pour rejeter ces fragments (avec glStencilFunc et glStencilOp), et redessinez les objets de la scène. N'oubliez pas de réactiver l'écriture de la couleur et du depth buffer, et de réinitialiser ce dernier.

shadow volumes shadow volumes
Ombres portées opaques.

Les ombres obtenues sont complètement noires ; pour ajouter une contribution ambiante, vous pouvez effectuer la première passe de rendu (étape 1) avec un shader spécifique n'écrivant que la couleur ambiante des objets (activez alors l'écriture de la couleur).

4. Animez la position de la lumière et/ou du maillage (rotation dans le plan horizontal, par exemple). Où devez-vous calculer les volumes d'ombres ?

Bonus : Placez la caméra à l'intérieur du volume d'ombre, qu'observez-vous ? Pour résoudre ce problème, il faut utiliser l'approche z fail qui consiste à placer le plan far de la caméra très loin et à inverser la condition de mise à jour du stencil (cf. cours). Il vous faudra également boucher les volume d'ombre en y incluant les faces éclairées et leur version envoyée très loin (mais un peu moins que le far).

Bonus : Si vous souhaitez intégrer les ombres portées au rendu en deferred shading du TD précédent, vous aurez besoin d'accéder à la profondeur stockée dans le g-buffer pour mettre à jour le stencil lors du rendu des volumes d'ombres. Cela n'étant pas directement possible en OpenGL, il vous faut effectuer une copie du depth buffer du FBO vers celui de l'affichage principal. Vous pouvez réaliser cette copie avec les commandes :

glBindFramebuffer(GL_READ_FRAMEBUFFER, mFBO.id());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0,0,WIDTH,HEIGHT,0,0,WIDTH,HEIGHT,GL_DEPTH_BUFFER_BIT,GL_NEAREST);
shadow volumes
Ombres portées combinées au deferred shading.