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 :
  1. é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.

  2. é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.

  3. é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.

  4. é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 :
  1. 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.

  2. créer, positionner et activer la ou les sources de lumières

  3. 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.

  4. 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.

principe ombre volumique
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 :
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é 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. 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 :
  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(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);