L3IF Images
année 2004-2005
http://www710.univ-lyon1.fr/~jciehl/

TD5 - Lumière, Matière et Ombres


    L'objectif du TD est de visualiser les ombres portées générées par une source ponctuelle et un objet quelconque. Il exite de nombreuses méthodes, nous allons nous interresser aux ombres volumiques et à leur affichage en openGL. Cette méthode est également connue sous les noms : Robust Stencil Shadow Volumes, Zfail ou "Carmack's Reverse", puisque c'est la méthode qui est utilisée dans Doom 3.

    Ce sujet nécessitera sans doute deux séances.


Partie 1 : Principe

    Il suffit de déterminer la partie de la scène à l'ombre de la source de lumière. Déterminer si un point est à l'ombre ou éclairé est un problème de visibilité (entre ce point et la position de la source de lumière). Le vrai problème est la réalisation efficace de ce test pour l'ensemble des points visualisés.

principe ombre volumique
présentation GDC 2002 - nvidia


Partie 2 : Méthode

    Il exite un buffer particulier dans openGL : le stencil buffer, c'est un masque de calcul qui permet d'éliminer d'un calcul les pixels marqués. L'idée est de construire un masque représentant l'ombre telle qu'elle est vue depuis la caméra et d'utiliser ce masque pendant les calculs d'éclairement.

    Exemple de masque :
masque d'éclairement
présentation GDC 2002 - nvidia


    Les opérations autorisées pour la construction du masque sont assez limitées : en gros, il est possible d'incrémenter ou de décrémenter la valeur d'un compteur associé à chaque pixel en fonction du résultat d'un test logique sur les propriétes du pixel.

    Une solution consiste à  compter le nombre de faces du volume d'ombre se projettant sur chaque pixel. Le volume d'ombre étant convexe, la parité du nombre de faces du volume d'ombre se projettant sur chaque pixel suffit à déterminer l'inclusion du point dans le volume.

    En clair : n est le nombre d'intersections du volume d'ombre avec le rayon défini par la camera et le point de la scène associé au pixel  :
    Ce test suffit pour tester l'appartenance à un volume d'ombre. Lorsque plusieurs objets génèrent des ombres, ce test ne fonctionne que lorsque les volumes d'ombres sont disjoints. Pour traiter le cas général, il faut différencier les faces des volumes d'ombres orientés vers la camera de celles qui ne le sont pas. Il suffit alors d'incrémenter n lors d'une intersection avec une face orientée vers la camera et de le décrementer dans l'autre cas. En clair, il suffit de compter le nombre de fois ou le rayon entre et sort des volumes d'ombres. Lorsque tous les volumes d'ombres ont été comptabilisés, si n est nul, le point est éclairé, sinon il est à l'ombre.

    Les schémas suivants devraient vous convaincre :

plusieurs volumes d'ombres
présentation GDC 2002 - nvidia

inclusion : cas 1
présentation GDC 2002 - nvidia
inclusion : cas 2
présentation GDC 2002 - nvidia
inclusion : cas 3
présentation GDC 2002 - nvidia


    Cette méthode souffre toutefois d'un défaut important : les résultats sont faux lorsque la camera se trouve dans une ombre ... L'idée est correcte, c'est le point de réference qui n'est pas valable. Il faut un point de réference qui est toujours à l'extérieur des ombres. Ce point existe : c'est l'infini ! Au lieu de suivre le rayon depuis la camera, il suffit de partir de l'infini et de revenir vers la camera.

    Il faudra modifier la matrice de projection pour représenter numériquement l'infini de manière stable, mais c'est possible et très simple.


Partie 3 : Génération du volume d'ombre

    Maintenant que nous avons une méthode efficace pour déterminer l'inclusion d'un pixel dans l'ombre, il faut construire les volumes d'ombres. Le volume est composé de trois parties : les faces avant, les faces latérales et les faces arrières (situées à l'infini).

    Les faces avant du volume d'ombre sont les faces éclairées de l'objet qui génère l'ombre. Les faces arrières sont les faces non éclairées de l'objet rejettées à l'infini dans la direction de la source de lumière. Les faces latérales relient les faces avant aux faces arrières.

    Il existe plusieurs méthodes pour constuire cette géométrie, une méthode naive et très inefficace, consiste à créer un volume d'ombre pour chaque face éclairée de l'objet. La face de l'objet sera la face avant du volume d'ombre, l'inversion de cette face sera la face arrière du volume d'ombre et une face latérale du volume d'ombre sera construite pour chaque arête de la face de l'objet.

    Le seul point délicat de cette méthode est le respect de l'ordre des sommets des faces. Il ne faut pas non plus oublier que les volumes d'ombres sont infinis et qu'il faut explicitement indiquer les 4 coordonnées de chaque sommet avec glVertex4f().

    Sommets à l'infini

    Les volumes d'ombres sont de dimensions infinies : les sommets des faces arrières ont donc une coordonnée homogène à 0 que l'on peut décrire avec glVertex4f(x, y, z, w).

    Soit A (Ax, Ay, Az, Aw) et B (Bx, By, Bz, Bw) les sommets d'une arète d'une face de l'objet, et L (Lx, Ly, Lz, Lw) la position de la source de lumière. Les faces latérales du volume d'ombre sont composées des 4 sommets :

(Bx, By, Bz, Bw)
(Ax, Ay, Az, Aw)
(AxLw - LxAw, AyLw - LyAw, AzLw - LzAw, 0)
(BxLw - LxBw, ByLw - LyBw, BzLw - LzBw, 0)

   

Partie 4 : Analyse de la géométrie du modèle à ombrer

    La méthode précédente est trop inefficace pour être réellement utilisée, il faut absolument réduire la quantité de géométrie générée. On peut exploiter une propriéte pour réduire la géométrie à traiter : les volumes d'ombres sont portés par les arêtes de la silhouette de l'objet vu depuis la source.

    Il suffit de déterminer cet ensemble d'arêtes pour générer un volume d'ombre beaucoup plus économique. Il y a bien sur plusieurs solutions pour déterminer la silhouette d'un objet. Une solution correcte consiste à déterminer quelles sont les faces éclairées et à rechercher ensuite les paires de faces adjacentes dont une seule face est éclairée. L'arête commune est (potentiellement) sur la silhouette de l'objet vu de la source de lumière. Connaissant la face éclairée et l'arête, il ne reste plus qu'à générer une partie du volume d'ombre. La silhouette étant "normalement" une boucle à la surface de l'objet, le volume d'ombre généré de cette manière sera bien fermé et convexe. Le maillage décrivant la surface de l'objet doit respecter quelques contraintes afin de générer un volume d'ombre fermé et convexe.


Partie 5 :Affichage complet

    Pour afficher correctement les sommets rejettés à l'infini lors de la construction des volumes d'ombres, il est nécessaire de modifier la matrice de projection d'openGL. Pour les détails, consultez l'article Robust Shadow Volume (cf. rubrique Documents).

    GLfloat projmatrix[16];

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// fixe une projection perspective "standard"
gluPerspective(50., 1., 1., 1000.);

/*
Change the Projection matrix to have the far plane at infinity.
In a standard Projection matrix P, the Near and Far plane distances appear only in
entry (2,2) as -(Far+Near)/(Far-Near) and in entry (2,3) as -2*Far*Near/(Far-Near).
As Far goes to infinity, these entries become -1 and, respectively, -2*Near
*/

glGetFloatv(GL_PROJECTION_MATRIX, projmatrix);

projmatrix[10]= -1.;
projmatrix[14]= -2.; // -2.*near,
/*
 near==1 dans ce cas, cf l'appel a gluPerspective ci-dessus
*/

// recharge la matrice de projection modifiee
glLoadMatrixf(projmatrix);

    Le rendu des ombres nécessite de dessiner plusieurs fois l'objet et le volume d'ombre :
  1.     fixer les paramètres de l'éclairage ambient (la lumière présente dans les ombres)
  2.     dessiner la scène (éclairée par l'ambient)
  3.     constuire le volume d'ombre  
  4.     tracer les faces arrières (non orientées vers la camera) du volume d'ombre et compter les intersections pour chaque pixel (construction du masque)
  5.     tracer les faces avant (orientées vers la camera) du volume d'ombre et compter les intersections pour chaque pixel (construction du masque)
  6.     fixer les paramètres de la source de lumière
  7.     dessiner la scène (éclairé par la source) et activer le masque (pour éviter de modifier les parties à l'ombre)


    Voici le détail des appels openGL nécessaires pour le rendu complet :
void objet_affiche_ombres_GEN(MODEL *m)
{
// store current OpenGL state
glPushAttrib(GL_DEPTH_BUFFER_BIT | GL_LIGHTING_BIT | GL_STENCIL_BUFFER_BIT);

/* draw the model without lighting
*/
// depth + ambient dans les ombres
glDisable(GL_LIGHT0);
glCullFace(GL_BACK);

model_display(m);

// store current OpenGL state
glPushAttrib(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT);

glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // do not write to the color buffer
glDepthMask(GL_FALSE); // do not write to the depth buffer

glEnable(GL_CULL_FACE); // enable culling
glEnable(GL_STENCIL_TEST); // enable stencil testing
glStencilFunc(GL_ALWAYS, 0, ~0);
glStencilOp(GL_KEEP, GL_INCR, GL_KEEP);

/* draw only the back faces of the shadow volume
*/
glCullFace(GL_FRONT);
affiche_volumes_ombres(m);


/* draw only the front faces of the shadow volume
*/
glStencilOp(GL_KEEP, GL_DECR, GL_KEEP);
glCullFace(GL_BACK);

affiche_volumes_ombres(m);


/* re-draw the model with the light enabled only where it has previously been drawn
*/
// restore OpenGL state
glPopAttrib();
glDepthFunc(GL_LEQUAL); // GL_LEQUAL cf. ATI hyper-z, optimized rendering

// update the color only where the stencil value is 0
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_EQUAL, 0, ~0);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

// affiche le modele eclaire
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_LIGHT0);

model_display(m);

// restore OpenGL state
glPopAttrib();
}

Partie 6 : Améliorations

    Pour construire de manière efficace le volume d'ombre, il est nécessaire de connaitre pour chaque face quelles sont les faces adjacentes. Il est possible de modifier la description d'une face du modèle et de parcourir le modèle pour construire la liste des faces adjacentes à chaque face.

    Même si la structure d'adjacence du modèle permet de gagner énormément de temps, le parcours exhaustif de l'ensemble d'aretes d'un modèle complexe devient vite trop long, il existe des structures de partitionnement de l'ensemble d'arêtes qui permettent de gagner énormement de temps lors de la recherche de la silhouette.

    Lorsque la scène est composée de plusieurs objets et de plusieurs sources de lumière, il est également interressant de rejetter les ombres qui ne seront pas visibles de la camera (si possible avant de construire le volume d'ombre).

    Il est possible d'améliorer plusieurs parties de l'algorithme de rendu pour éliminer encore de la géométrie, consultez l'article Fast and Robust Shadow Volume (cf. rubrique Documents).


Documents :

    Robust Shadow Volume (nvidia SDK)

    Fast, Pratical, and Robust Shadow Volume (nvidia SDK)