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.
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 :
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 :
- si n est nul : le rayon n'intersecte pas l'ombre, le pixel est éclairé
- si n est pair : le rayon entre et sort du volume d'ombre, le pixel est éclairé
- si n est impair : le rayon entre dans le volume, le pixel est à l'ombre.
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 :
présentation GDC 2002 - nvidia
présentation GDC 2002 - nvidia
présentation GDC 2002 - nvidia
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 :
- fixer les paramètres de l'éclairage ambient (la lumière présente dans les ombres)
- dessiner la scène (éclairée par l'ambient)
- constuire le volume d'ombre
- 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)
- tracer les faces avant (orientées vers
la camera) du volume d'ombre et compter les intersections pour chaque
pixel (construction du masque)
- fixer les paramètres de la source de lumière
- 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)