OpenGL : les spots

Et voici le deuxième et dernier chapitre sur les bases des lumières. Après avoir bien lu et compris ce tutorial, vous devriez être capables de créer toutes les lumières que vous voulez. Allez, c'est parti : 

1. Les normales

Je dois vous avouer un truc : je vous ai caché quelque chose. Pas une petite chose sans importance, mais vraiment un gros truc, qui mérite pas mal de pages dans un bon bouquin. Et ce truc, c'est les normales. Si vous êtes accros des bons bouquins sur les modeleurs 3D, vous devez en avoir entendu parler, car il n'y a pas de lumière sans normales. Laissez-moi vous expliquer : vous savez déjà que lorsque un objet illuminé, il est coloré par 3 couleurs : ambiente, diffuse et spéculaire. Pour trouver la lumière ambiente, c'est pas dur : on multiplie les composantes ambientes du matériau par celle de la lumière et paf on applique ça à toutes les faces :
Ambiente(R/G/B) = AmbienteMatériau(R/G/B) * AmbienteLumière(R/G/B)
Par contre, pour la lumière diffuse, l'intensité de cette lumière dépend de chaque face : si la face est perpendiculaire à la direction de la lumière, l'intensité de la lumière est maximum. Si par contre la face est en biais, la lumière diffuse est plus faible. Et si la face tourne le dos à la source, alors la lumière diffuse est nulle. Donc en gros, pour calculer la lumière diffuse, on multiplie la composante diffuse de la lumière par la composante diffuse du matériau, et ensuite on multiplie ça par le sinus de l'angle entre la direction de la lumière et la face :
Diffuse(R/G/B) = DiffuseMatériau(R/G/B) * DiffuseLumière(R/G/B) * Sin(alpha)
Si on regarde le dessin, et si on sait ce qu'est un sinus et un cosinus (je vais quand même pas redonner les définitions), on remarquera aisément que Sin(alpha) = Cos(Beta). Je suppose que ce n'est un secret pour personne. Donc maintenant, puisqu'on cherche à faire du temps réel, faudrait voir à trouver une forumle suffisamment rapide et autre qu'une table de sin pour trouver ce cosinus (ou ce sinus). Et là, si vous êtes intelligent (ou si avez déjà lu un bouquin quelconque sur la 3D), vous allez me répondre : le produit scalaire ! En effet, pourvu que deux vecteurs soient unitaires, on récupère directement le cosinus entre les deux ! Puisqu'on connait déjà le vecteur lumière, il suffit de trouver l'autre, en pointillés sur le shéma, orthogonal à la face. Et ce vecteur orthogonal, ou vecteur normal (et oui, c'est ça !), comptez pas sur OpenGL pour le trouver tout seul. Et oui, c'est vous qui devez lui dire quelle est la normale de chaque face. Et si je ne vous en ai jamais parlé auparavant, c'est tout simplement parce que GLUT calculait lui-même les normales dans glutSphere(), donc on n'avait pas à s'en préoccuper. Donc la formule de la lumière diffuse est :
Diffuse(R/G/B) = DiffuseMatériau(R/G/B) * DiffuseLumière(R/G/B) * (VecteurLumière.Normale)
De même, la lumière spéculaire s'obtient en vérifiant si les rayons de lumière, réfléchis par la face, sont dirigés vers la caméra. Et pour savoir dans quelle direction est réfléchie la lumière, il faut aussi connaître la normale de la face.

 

2. Per vertex lighting

En fait, OpenGL ne calcule pas les lumières face par face, mais sommet par sommet. En effet, si la lumière était calculée pour chaque face, alors toutes les faces auraient une couleur uniforme, ce qui ferait apparaître les arêtes entre les faces (ce qu'on appelle le flat shading), et en plus il faudrait calculer le centre de gravité de la face pour connaitre la direction du vecteur lumière. Alors que si la lumière est calculé sommet par sommet, non seulement on connait tout de suite la direction du vecteur lumière, mais en plus si deux sommets sont illuminés différemment, on fait un petit dégradé entre les deux au moment du remplissage de la face, et hop on voit plus les arêtes. C'est ce qu'on appelle le gouraud shading. Mais on voit aussi l'inconvénient : moins il y a de sommets, et moins la tache de lumière sera précise. Laissez-moi vous expliquer ceci en images :
Les deux images sont les mêmes : il s'agit d'un plan illuminé par un spot rouge. Sauf que sur l'image de gauche, le plan est constitué par une grille de quelques points alors qu'à droite les mailles de la grille sont beaucoup plus reserrées. Vous voyez bien que plus il y a de points, et plus l'image est réaliste. Mais aussi s'il y a plus de points, l'image demandera plus de temps de calcul.
En fait, l'idéal serait de faire du per pixel lighting, c'est-à-dire que la lumière serait calculée pour chaque pixel de la face, au moment du remplissage de la face (ca donne ce qu'on appelle le phong shading). Evidemment, là ca serait parfait, quel que soit le nombre de sommets, mais ce serait tellement lent qu'on ne pourrait plus appeler ca du temps réel. C'est donc une technique qui n'est utilisée que dans les modeleurs, où la qualité prévaut sur la vitesse.
En fait je vous dis ça pour une bonne raison : c'est que dans ce tutorial, pour voir les spots, on va s'amuser à éclairer un mur avec des spots et voir ce que ca donne. "Pas compliqué de dessiner un mur : on fait un petit glBegin(GL_QUADS), 4 glVertex(), un glEnd() et c'est fini", me diriez-vous. Et ben non justement. Parce que si on fait ca, on aura juste 4 sommets aux 4 coins, ca risque de faire un peu trop juste pour voir la tache d'un spot. Donc il faut que notre mur soit en fait une grille plane, de façon à ce qu'on distingue bien la lumière du spot sur le mur.

 

3. C'est parti

Et bien allons-y, créons notre mur et notre source de lumière, pour commencer :

float MatSpec[4] = {1.0f, 1.0f, 1.0f, 1.0f};
float MatDif[4] = {1.0f, 1.0f, 1.0f, 1.0f};
float MatAmb[4] = {0.3f, 0.3f, 0.3f, 1.0f};
float Light1Pos[4] = {0.0f, 0.0f, 20.0f, 1.0f};
float Light1Dif[4] = {1.0f, 0.2f, 0.2f, 1.0f};
float Light1Spec[4] = {1.0f, 0.2f, 0.2f, 1.0f};
float Light1Amb[4] = {0.5f, 0.5f, 0.5f, 1.0f};
float Spot1Dir[3] = {0.0f, 0.0f, -1.0f};
void InitGL()
{
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,MatSpec); //On applique les paramètres du matériau
glMaterialfv(GL_FRONT_AND_BACK,GL_DIFFUSE,MatDif);
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT,MatAmb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, Light1Dif); //Et ceux de la lumière
glLightfv(GL_LIGHT0, GL_SPECULAR, Light1Spec);
glLightfv(GL_LIGHT0, GL_AMBIENT, Light1Amb);
glEnable(GL_LIGHTING); //Et on allume la lumière
glEnable(GL_LIGHT0);
}
 
void Draw()
{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(40,40,100,0,0,0,0,1,0);
glLightfv(GL_LIGHT0, GL_POSITION, Light1Pos);
glNormal3i(0,0,1); //Définit la normale commune à tous les sommets
for (int i=-50;i<50;i++)
{
glBegin(GL_QUAD_STRIP);
glVertex2i(i,-50);
glVertex2i(i+1,-50);
for (int j=-50;j<50;j++)
{
glVertex2i(i,j+1);  
glVertex2i(i+1,j+1);  
}
glEnd();
}

SwapBuffers(DC)

// glutSwapBuffers(); pour glut
glutPostRedisplay(); // Uniquement pour GLUT
}

Les autres fonctions restent les mêmes. Repompez-les sur les tutorials précédents.
Ici vous aurez remarqué qu'on n'a rien appris de nouveau, à part comment spécifier les normales pour un sommet : il suffit d'appeler glNormal3{fdbsi}[v]() avant l'appel à glVertex(). Ici, comme la grille est plane, tous les sommets on la même normale : (0,0,1).
Le mur est constitué d'un matériau blanc et mat, c'est-à-dire qu'aucune tache spéculaire n'apparaitra : on ne verra que les couleurs ambiente et diffuse.
Pour ce qui est de la source de lumière, elle est telle qu'on en a déjà vu : omnidirectionnelle, et rouge. Donc si on lance le programme, on verra l'image à gauche:

 

4. Cutoff

Or ce qu'on veut, nous, c'est un spot. Et ben en fait, notre lumière est déjà à moitié un spot. En effet; qu'est-ce qui distingue un spot d'une source omni ? L'angle qui définit son cône de lumière, appelé cutoff. Ce cutoff, il est en réalité déjà défini, car une omni, c'est tout simplement un spot avec un cutoff de 180° ! Donc tout ce qu'on a à faire pour changer notre omni en spot est de changer ce cutoff, disons à 30° (il peut être compris entre 0 et 90, ou 180), grâce bien sûr à glLight(). Rajoutez donc dans InitGL()
glLighti(GL_LIGHT0, GL_SPOT_CUTOFF, 30);
Et relancez le programme : vous obtiendrez un truc dans ce genre :
 

5. Direction

Ca fait un peu bizarre : le spot n'est pas dirigé vraiment perpenciculairement au mur, mais plutôt dans la direction de la caméra. C'est parce qu'on doit aussi dire au spot dans quelle direction éclairer. Et comme on ne lui a pas dit, il éclaire dans la direction par défaut : (0,0,-1), et relativement à la caméra, car le vecteur direction n'a pas subi les transformations de la matrice modelview. Il faut donc lui dire d'éclairer droit vers le mur, donc selon le vecteur (0,0,-1), mais en prenant compte de la transformation de glutLookAt(). Donc, comme pour spécifier la position de la source, il faut spécifier la direction du sopt par glLight() après les transformations appliquées à la matrice modelview. Rajoutez donc les deux lignes suivantes dans Draw(), après glLight() :
float Light1Dir[3] = {0.0f, 0.0f, -1.0f};
glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, Light1Dir);

Et maintenant le résultat est bien ce à quoi on s'attend :
 

6. Exposant

Mais ce spot n'est pas super convaincant : la tache a une couleur uniforme, et en plus on distigue trop le crénelage de la grille. Généralement, un spot éclaire plus fort au centre et moins sur le bords. Pour reproduire cet effet, on ne dispose pas comme dans 3DS d'un 'hotspot' et d'un 'falloff', mais on a quand même un truc y ressemblant : l'exposant du spot. En effet OpenGL est capable de diminuer exponentiellement l'intensité de la lumière du spot en fonction de la distance par rapport au centre de la tache. Pour accentuer ce dégradé on peut modifier le facteur expotentiel : plus il est grand et plus la lumière sera faible sur les bords. Un petit exemple pour mieux expliquer :

5 10 20 30 128 (max)





Comme vous pouvez le voir, non seulement la tache du spot est beaucoup plus réaliste, mais en plus on ne distingue plus le crénelage de la grille à partir d'un certain niveau. Pour changer cet exposant, il suffit bien sûr d'appeler glLight() avec comme argument GL_SPOT_EXPONENT. Les spots peuvent également bénéficier de l'atténutation en fonction de la distance, comme pour toute lumière ponctuelle. Notez qu'on ne peut pas faire de spots avec une lumière directionnelle, puisque, la source étant à une distance infinie, on ne peut tenir compte du cutoff.

 

7. Le LightModel

Notre petit tour sur les lumière ne serait pas complet sans avoir parlé du LightModel. Qu'est-ce que c'est (je dois bien déjà avoir posé cette question un bon nombre de fois) ? Et bien c'est un ensemble de variables d'état qui définissent comment est gérée l'illumination dans OpenGL, sans se rapporter à une source de lumière spécifique. Ces variables sont au nombre de 3 : la lumière ambiente (GL_LIGHT_MODEL_AMBIENT) de la scène, le modèle de vue (GL_LIGHT_MODEL_LOCAL_VIEWER) et le modèle double face (GL_LIGHT_MODEL_TWO_SIDE). Pour changer une de ces variables, on appelle la fonction glLightModel{if}[v](paramètre, valeur), où paramètre est le nom de la variable à modifier (GL_LIGHT_MODEL_BIDULE), et valeur la valeur à assigner à cette variable.

  • La variable GL_LIGHT_MODEL_AMBIENT est, comme son nom a l'air de l'indiquer, la lumière ambiente de la scène entière. En fait, lorsque la gestion des lumière est activée, il y une source qui est toujours présente, et qui émet une lumière ambiente sur tout les objets. La valeur par défaut de cette lumière est (0.2, 0.2, 0.2, 1.0) (ne vous préoccupez pas du 4e paramètre). Pour la modifier, il suffit d'appeler glLightModel{if}v() avec comme nouvelle valeur un tableau de 4 valeurs (int ou float, suivant la fonction).
  • La variable GL_LIGHT_MODEL_LOCAL_VIEWER est le pendant des lumières directionnelles/ponctuelles pour la caméra. En effet, lorsque la lumière spéculaire d'une face est calculée, elle résulte d'une opération entre le vecteur lumière->objet et le vecteur caméra->objet. Dans la réalité, tous les vecteurs caméra->objet rayonnent à partir du centre de la caméra, un peu comme une lumière ponctuelle rayonne à partir de sa position. On dit alors que le point de vue est local. Ceci oblige OpenGL à recalculer le vecteur caméra->objet à chaque fois qu'il a besoin de calculer la composante spéculaire d'un sommet. Pour accélérer un peu les choses, on peut lui dire que tous les vecteurs caméra->objet sont parallèes à l'axe z de la caméra, comme pour une source directionnelle, où tous les veteurs lumière sont parallèles. Cela diminue le réalisme, mais la vitesse de rendu est sensiblement augmentée. Plus le FOV de la caméra sera grand, et moins le calcul de la lumière semblera réaliste. Par défaut, le modèle local est désactivé (GL_LIGHT_MODEL_LOCAL_VIEWER = 0). Pour l'activer, il suffit d'affecter à la variable une valeur différente de 0.
  • Et enfin, GL_LIGHT_MODEL_TWO_SIDE définit si on travaille en illumination simple ou double face. Attention, si vous êtes en simple-face, cela ne veut pas dire qu'OpenGL ne dessinera que la face avant et pas la face arrière (pour cela il faut utiliser glEnable(GL_CULL_FACE)), mais que le matériau attribué à la face avant sera utilisé pour la face arrière, et que la normale sera la même pour les deux cotés. Si par contre l'illumination est en double-face, la face arrière sera dessinée avec son propre matériau, et sa normale sera l'opposée de celle de la face avant. Pour définir à quel coté (avant ou arrière) de la face on affecte tel ou tel matériau, il faut spécifer GL_FRONT, GL_BACK, ou GL_FRONT_AND_BACK comme premier paramètre de glMaterial() (se reporter aux tutorials précédents). Si GL_LIGHT_MODEL_TWO_SIDE est à 0 (défaut), le modèle simple-face est défini. Sinon, on est en mode double-face.

Et bien voilà, on a fait le tour complet des sources de lumières. Vous pouvez maintenant faire ce que vous voulez avec les lumières pour créer votre petite disco personnelle. Pour ceux qui se poseraient des questions, je rapelle quand même qu'OpenGL est un moteur temps réel basique, et que même s'il est professionnel, il ne gère tout de même pas les ombres ou autres réflections. Si vous en voulez, vous serez obligé de les calculer vous-même. Il existe plusieurs méthodes, plus ou moins rapides et réalistes, peut-être en parlerais-je plus tard. Pour les impatients, allez voir sur le site de sgi, vous aaurez pas mal d'infos là-dessus (en anglais, bien sûr). En attendant le prochain tutorial, sur les textures, je vous propose de lire la petite annexe ci-dessous :

 

Annexe : Calculer les normales

Dans l'exemple que je viens de vous montrer, on n'a pas vraiment eu de mal à calculer les normales des points du mur. Mais il se pourrait bien que vous ayez quelques fois des normales à calculer autrement plus complexes, notemment si vous importez des modèles 3DS ou ASE, où les normales ne sont pas forcément spécifiées. Voici donc quelques techniques couramment utilisées pour calculer les normales.
Essayons d'abord de calculer une normale unique pour tous les sommets d'un triangle. Si vous avez des notions de maths, vous aurez remarqué qu'un vecteur normal à un plan peut être obtenu à partir de deux vecteurs de ce plan, du moment qu'ils ne sont pas colinéaires. Et bien nous somme gâtés : dans un triangle, il y a trois côtés, donc 3 vecteurs : il suffit d'en prendre 2 et d'en faire le produit vectoriel, et nous avons notre normale (attention à l'ordre du produit vectoriel : il faut que la normale soit orientée dans le bon sens !). Il faut ensuite la normaliser (c'est-à-dire faire en sorte que sa norme soit égale à 1) pour que le produit scalaire donne directement le cos(Beta), en divisant chaque composante par la norme du vecteur, car sinon les calculs seront faussés. Notez qu'OpenGL peut normaliser les normales pour vous (il suffit pour cela de faire glEnable(GL_NORMALIZE) ), mais ce sera au détriment de la rapidité. Donc maintenant que nous avons la normale de notre face, il suffit de l'appliquer aux 3 sommets et c'est fini.
Mais ce n'est pas totalement satisfaisant : cela nous donnera du flat shading : il n'y aura pas de dégradé entre les sommets, et on verra distinctement les arêtes, comme dans la piètre image que vous voyez au dessus. Pour qu'on ne voie pas les arêtes, ce qu'il faudrait, c'est que deux sommets ayant les mêmes coordonnées aient la même normale, quelle que soit la face à laquelle ils appartiennent. Donc il faut parcourir les sommets et lorsqu'on en trouve plusieurs au même endroit, on fait la moyenne des normales de ces sommets et on leur attribue à tous cette normale moyenne. Et là ch'est machik : cha marche !

Voilà le tutorial est terminé. Si vous avez des questions, vous savez à qui vous adresser. Dans les sources à télécharger se trouve un petit programme qui montre 3 spots se courant après.

Antoche
 


← Les lumières↑ Tutoriaux OpenGL ↑Les textures →