M1 - Images
2007-2008
TD 3 - Lumière, Matières et Ombres
objectifs : éclairer une scène, modèle d'illumination locale, ombre portée
Rappel / Partie 1 : Le modèle d'éclairement d'openGL
openGL utilise un modèle d'éclairement simplifié
(cf. cours sur l'éclairement global physiquement réaliste/plausible)
représentant l'énergie et les propriétés des
matières sur 3 canaux : le rouge, le vert et le bleu. Les sources
de lumière sont représentées par une position (un point)
et une description de l'énergie émise dans la scène.
Les propriétés des matières sont également décrites
sur les trois canaux. Le modèle d"éclairement local disponible
dans openGL considère que l'énérgie réfléchie
par les matières des objets est constitué d'un mélange
de plusieurs composantes idéales :
- énergie ambiante : lorsque la lumière interagit avec
un grand nombre d'objets et de matières, openGL consière qu'elle
n'a plus de direction de propagation observable et que l'on peut représenter
son effet sur l'aspect des objets par une constante. Cette énergie
est diffusée dans toutes les directions de la même manière.
- énergie diffusée : lorsque la lumière provient
d'une direction particulière, son effet sur l'aspect des objets diminue
lorsque l'angle d'incidence augmente. Par exemple, la portion de sol directement
sous un plafonnier est plus éclairée qu'une dalle placée
à l'autre extrémité de la pièce. C'est l'angle
entre la normale du sol et la direction vers la source de lumière
qui détermine l'effet observable. Cette énergie est diffusée
dans toutes les directions de la même manière.
- énergie spéculaire : lorsque la lumière provient
d'une direction particulière, elle se réfléchit principalement
vers une direction privilégiée, généralement
symétrique par rapport à la normale de l'objet. Par exemple,
un miroir réfléchit presque toute la lumière incidente
dans une seule direction.
- énergie emise : certaines matières émettent de
l'énergie, les sources de lumières.
Les composants ambiant, diffus et spéculaire
des sources de lumières sont différents selon la contribution
de la source à l'éclairement de la scène. Par exemple,
une source de lumière blanche dans une pièce avec des mûrs
rouges aura une composante ambiante rouge et une composante diffuse blanche.
De la même manière, les propriétés
des matières sont décrits par la quantité
d'énergie réfléchie pour chaque composante idéale
et chaque canal (rouge vert et bleu). Ces valeurs sont comprises entre 0
et 1.
openGL permet de décrire plusieurs sources de
lumière et de les activer séparement les unes des autres.
Partie 2 : Spécifier les propriétés des matières
et des sources de lumière
Il y a plusieurs éléments à spécifier
pour utiliser le modèle d'éclairement de openGL :
- la normale de chaque sommet des polygones à dessiner. Ces normales
permettent de déterminer l'orientation d'un élément
de surface par rapport à la source de lumière et de calculer
la lumière réfléchie vers l'observateur.
- créer, positionner et activer la ou les sources de lumières
- créer et activer un modèle d'éclairement. L'énergie
ambiante globale (en plus de celle associée aux sources de lumière)
doit également être décrite. Plus important la position,
de l'observateur ou de la camera doit être spécifiée.
- spécifier des matières et les associer aux primitives
à dessiner.
Le plus simple est sans doute de parcourir cet
exemple du manuel de programmation pour vous faire une idée des
fonctions à appeler.
Voici les fonctions les plus importantes :
glNormal3f(v_norm) : définit la normale
du prochain sommet (à utiliser entre glBegin(..); ...; glEnd())
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular)
: définit la composante spéculaire d'une matière (à
utiliser entre glBegin(..); ...; glEnd())
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess)
: définit la composante "brillance" d'une matière (à
utiliser entre glBegin(..); ...; glEnd())
glLightfv(GL_LIGHT0, GL_POSITION, light_position) :
définit la position de la source de lumière 0
glEnable(GL_LIGHTING) : active le calcul d'éclairement
glEnable(GL_LIGHT0) : active la source de lumière
0
glShadeModel(GL_SMOOTH) : spécifie le modèle
d'éclairement
Q1. modifiez votre TD précédent.
Comment générer / calculer les normales des faces ?
Reportez vous au man pour connaître les valeurs
possibles des différents paramètres (GL_SPECULAR, GL_AMBIENT,
GL_POSITION ...)
Partie 3 : et les ombres ?
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). Comment
utiliser les fonctionnalités d'openGL pour déterminer la
position des ombres et modifier l'aspect des objets ?
Il existe deux familles de techniques pour calculer
les ombres projettées par une source de lumière : une
famille travaille dans l'espace objet, l'autre directement dans l'image
resultat. La technique géométrique est expliquée
dans les paragraphes suivants.
Dans sa version la plus simple, il suffit de
construire l'objet représentant le volume d'ombre
généré par chaque face de chaque objet de la
scène. Le volume d'ombre est construit par une face avant, des
faces latérales et une face arrière. La face avant est la
face de l'objet, la face arrière, la même face
rejettée à l'infini dans la direction de la source de
lumière, et des faces latérales sont construites pour
chaque arete de la face.
Une fois les volumes d'ombres construits pour toutes
les faces de tous les objets, il ne reste plus qu'a calculer
l'intersection des volumes avec les objets de la scène.
L'intersection est à l'ombre, le reste de la scène est
éclairé par la source.
présentation GDC 2002 - nvidia
Méthode
Il existe 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é 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. Une autre solution
construit un volume d'ombre fini, mais suffisamment grand pour "sortir"
de la scène, ce qui donne le même résultat.
Affichage complet
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(MODEL *m)
{
// store current OpenGL state
glPushAttrib(GL_DEPTH_BUFFER_BIT | GL_LIGHTING_BIT | GL_STENCIL_BUFFER_BIT);
/* draw the model without lighting
*/
glDisable(GL_LIGHT0);
glCullFace(GL_BACK);
affiche_modele(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 par la source 0
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_LIGHT0);
affiche_modele(m);
// restore OpenGL state
glPopAttrib();
}
Partie 6 : Améliorations
Pour un objet maillé proprement, il existe
une frontiere entre les faces à l'ombre et les faces
éclairées, c'est la silhouette de l'objet vu par la
source de lumière. Cette silhouette permet de ne construire
qu'un seul volume d'ombre au lieu d'en construire un par face. Il est
relativement simple de construire les faces latérales du volume
d'ombre en n'utilisant que les arêtes le long de la silhouette de
l'objet. Une arête de l'objet fait partie de la silhouette
lorsque une face est éclairée et l'autre est à
l'ombre.
Annexe : Documents
Robust Shadow Volume (nvidia SDK)
Fast, Pratical, and Robust Shadow Volume (nvidia SDK)
Annexe : Affichage des volumes infinis, modification de la matrice de projection.
Pour afficher correctement les sommets
rejettés à l'infini lors de la construction des volumes
d'ombres (si vous avez choisit cette solution), 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);