Gestion dynamique de la lumière avec OpenGL - Partie 6 : éclairage complexe à base de shadersDate de publication : 06/09/2006
Par
Michel de VERDELHAN (mdeverdelhan.developpez.com)
Ce tutoriel représente le dernier de cette série de tutoriaux sur la
gestion de l'éclairage dynamique en temps réel avec OpenGL. Dans ce
tutoriel, je présente un système d'éclairage complexe à base de shaders.
Nous y verrons notamment comment gérer la composante spéculaire de la
lumière, mais aussi une méthode permettant une meilleure impression de
relief sur nos textures : le parallax mapping.
Attention : ce tutoriel requiert d'avoir bien compris les tutoriaux précédents ou d'être familier avec les concepts de la lumière pour être bien compris. De même, des notions sur le fonctionnement des vertex/fragments programs sont utiles pour une bonne compréhension. Ce tutoriel n'a pas pour but d'expliquer le fonctionnement de ces extensions. I. Les concepts lié à la lumière. I-1. L'éclairage spéculaire. I-2. Les materials. I-3. Le parallax mapping. II. Modifications apportée au programme. II-1. Gestion des extensions. II-2. Modification du rendu des modèles. II-3. Les vertex et fragment programs. II-4. Le rendu final. III. Réflexions sur la gestion de la lumière. IV. Conclusions. I. Les concepts lié à la lumière.
Dans cette partie, je vais (re)présenter les concepts
fondamentaux du fonctionnement de la lumière, puis je parlerai
d'une notion très liée à la lumière : la gestion des materials,
et finalement, je parlerai du parallax mapping.
I-1. L'éclairage spéculaire.
Nous avons déjà vu dans les tutoriaux précédents une grande
partie du fonctionnement de la lumière. Nous savons donc
comment calculer l'atténuation de la lumière via une
texture 3D d'atténuation. Nous avons aussi vu comment
prendre en compte l'orientation de la face par pixel grâce
au calcul du produit scalaire via l'extension env_dot3. Il
ne nous reste plus qu'à voir comment fonctionne la
composante spéculaire de la lumière.
La composante spéculaire de la lumière est en fait la
capacité qu'a une surface à refléter la lumière. On parle
souvent de reflets spéculaires. Pour bien voir ce que
représente cette composante spéculaire, il suffit
d'imaginer la différence entre une surface chromée et un
pneu. Les deux surfaces seront noires en l'absence de
lumière, mais si il y a une lumière, la surface chromée
sera plus brillante que le pneu. Ceci est dû au fait que la
surface chromée renvoi plus de lumière que le pneu qui, au
contraire, en absorbe la majeur partie.
Pour calculer la composante spéculaire, il suffit de
calculer le produit scalaire entre la normal à la surface
et le vecteur de demi angle... qu'est ce que c'est que le
vecteur de demi angle ? C'est tout simplement le vecteur
représentant la moitié de l'angle entre le vecteur vers la
lumière et le vecteur vers la camera. Comme ce vecteur
dépend de la position de la camera, nous avons donc bien un
calcul qui va changer en fonction de la camera et donc
donner l'impression de reflet.
![]() Le vecteur de demi angle représente le vecteur median au vecteurs vers la lumière et vers oeil.
Pour calculer le vecteur de demi angle, rien de plus
simple, il suffit d'additionner le vecteur vers la lumière
et le vecteur vers la camera, et on obtient notre vecteur.
Il ne nous reste plus qu'a le normaliser et voila, on peut
calculer notre produit scalaire pour obtenir la composante
spéculaire. Malheureusement, si on effectue simplement le
calcul comme cela, on obtient une surface bien trop
réfléchissante. Pour réduire cet effet de surbrillance, on
utilise une fonction puissance qui va réduire la
réflexivité des pixels peu éclairés tout en préservant la
luminosité des pixels fortement éclairés.
Voici le résultat de la composante spéculaire seule sans
utiliser de fonction puissance :
![]() Composante spéculaire sans utiliser de fonction puissance (et sans atténuation)
Et le résultat avec fonction puissance :
![]() Composante spéculaire avec fonction puissance.
On perçoit nettement mieux l'impression que la lumière se
reflète sur les surfaces avec une fonction puissance. Dans
ce tutoriel, le reflet spéculaire est mis à la puissance 8.
Le but de la composante spéculaire étant de faire saturer
la couleur des pixels aux endroits de forte réflexion, elle
est donc additionnée aux autres composantes de la lumière.
Le calcul de la luminosité finale devient donc :
Luminosité = ambiante + ( (diffuse + spéculaire) *
atténuation)
Le problème avec cette équation, c'est qu'elle impose
d'avoir le même taux de reflets spéculaires sur toute la
surface, or il est intéressant de pouvoir spécifier cela
par pixel pour pouvoir, par exemple, simuler des surface
brillantes par endroit et mat à d'autres endroits. Un bon
exemple pour comprendre l'intérêt de pouvoir moduler la
composante spéculaire est une plaque de métal rouillée. La
ou le métal n'est pas rouillé, on a un reflet spéculaire
maximum, mais la ou il est rouillé, il n'y a pas de
reflets. Pour mieux illustrer l'intérêt de la composante
spéculaire, voici deux images de notre scène finale : la
première sans reflets spéculaires.
![]() Scène sans reflets spéculaires.
Et le deuxième avec des reflets spéculaires.
![]() Scène avec reflets spéculaires.
Pour pouvoir effectuer cela, nous allons utiliser une gloss
map, qui est une des composantes des materials que nous
allons voir maintenant.
I-2. Les materials.
Dans le model d'éclairage d'OpenGL, on peut spécifier à
chaque vertex sa couleur ambiante, sa couleur diffuse, et
sa couleur spéculaire, or maintenant, nous n'utilisons plus
de l'éclairage par vertex mais par pixel. Il est dommage de
devoir spécifier ces composantes par vertex alors que le
reste des calculs d'éclairage va se faire par pixel. C'est
pour cela que nous avons besoin de la même notion de
materials qu'OpenGL, mais cette fois ci par pixel (pour
pouvoir simuler l'effet de la plaque de métal rouillée vu
plus haut par exemple.). Pour cela, nous allons utiliser
plusieurs textures par materials.
![]() Texture diffuse ![]() Normal map ![]() Gloss map ![]() Height map
Vous aurez peut être remarqué qu'il n'y a pas d'ambiante
map... Cela est dû au fait que la lumière ambiante peut
être obtenu à partir de la texture de diffuse.
I-3. Le parallax mapping.
Bien qu'il ne soit pas directement lié à la gestion de la
lumière, le parallax mapping n'est néanmoins utilisable
qu'avec un système d'éclairage par pixel. En effet, le
parallax mapping consiste à déformer les textures en
fonction d'une height map pour obtenir un effet de volume
plus prononcé qu'avec simplement du bump mapping. Il n'y a
donc aucun intérêt à utiliser du parallax mapping avec du
simple light mapping par exemple.
Un gros avantage du parallax mapping est qu'il est très
peu consommateur en ressources et qu'il est facile à
mettre en oeuvre. En effet, il suffit d'avoir une height
map (on peut la stocker dans la composante alpha de la
normal map) et un vecteur du pixel vers la camera pour
calculer les nouvelles coordonnées de textures déformées.
Le parallax mapping consiste simplement à
calculer un décalage dans les coordonnées de textures en
fonction de la direction du pixel par rapport à la camera
et de la hauteur du pixel dans la height map. Ceci se
résume à trois lignes supplémentaires dans le fragment
program.Encore une fois, pour bien voir la différence, rien
ne vaut des images comparatives avec/sans.
![]() Scène sans parallax mapping. ![]() Scène avec parallax mapping.
On peut voir que les pierres on été déformées pour donner
une meilleur impression de volume, mais surtout, cette
déformation se fait par rapport à la position de la camera,
donc si la camera bouge, la déformation va se modifier pour
prendre en compte la nouvelle direction de la camera.
II. Modifications apportée au programme.
Dans cette partie, nous allons voir les modifications apportées
au programme. Nous verrons d'abord la nouvelle gestion des
extensions mise en place pour ce tutoriel, puis nous verrons
les modifications apportées au rendu des modèles. Je
détaillerais ensuite les vertex et fragment program utilisé
dans le tutoriel, puis, finalement, nous verrons comment mettre
en place le rendu final de nos lumières.
II-1. Gestion des extensions.
Depuis le deuxième tutoriel, nous avons ajouté à chaque
nouveau tutoriel de nouvelles extensions. Bien que nous
n'ayons pas chargé toutes les fonctions liées à ces
extensions, notre fonction de chargement des extensions
devient de plus en plus grosse. Il nous faut aussi passer
les fonctions aux classes qui vont les utiliser (la classe
de modèles utilise les fonctions du multitextuing par
exemple), ce qui implique l'utilisation de nombreuses
variables externes. Pour faciliter la gestion des extensions, nous
utiliserons désormais GLEW. GLEW est une bibliothèque qui
permet de gérer facilement et de façon totalement
transparente les extensions OpenGL. En utilisant GLEW,
notre fonction de chargement des extensions devient
simplement ça :
Ici, on vérifie simplement que la carte supporte bien les
extensions, mais on ne les chargent pas, c'est la fonction
glewInit qui s'en est chargé pour nous. Nous pouvons
maintenant utiliser toutes nos fonctions supportées par la
carte comme si elles étaient des fonctions standard OpenGL.
II-2. Modification du rendu des modèles.
Ici, pas de gros changements. Nous allons juste modifier la
méthode d'ajout d'une lumière. Comme nous allons utiliser
un vertex program, l'ensemble des calculs effectués par
vertex se feront dans le vertex program, ce qui va réduire
d'autant notre fonction d'ajout des lumières. La seule
chose à bien comprendre ici, c'est que nous devons envoyer
certaines informations au vertex/fragment program comme les
trois vecteurs de l'espace local du vertex ou encore la
position de la lumière et son rayon. Pour cela, j'utilise
les coordonnées de textures, mais on aurai très bien pu
passer par des tableaux d'attributs... A part cela, la
méthode est assez simple à comprendre puisqu'il ne s'agit
que d'envoyer la géométrie à la carte graphique, plus les
informations nécessaires aux calculs. Le code de la méthode
donne donc ça :
Comme d'habitude, je suppose ici qu'OpenGL est bien
configuré, c'est-à-dire que les vertex/fragment programs
sont bien activé, le blending est en mode additif, et les
unités de textures sont bien positionnées.
II-3. Les vertex et fragment programs.
Dans cette partie, je vais présenter le vertex program et
le fragment program utilisés dans le tutoriel pour calculer
l'éclairage. J'ai préféré utiliser des programs assembleurs
ARB au lieu de vertex/fragment shaders ARB car ceux-ci sont
supporté par moins de cartes, mais aussi car les programs
sont en assembleur, ce qui est plus compliqué à mettre en
oeuvre, mais permet de mieux se rendre compte de l'étendu
des calculs effectués par la carte graphique.
Dans un premier temps, il nous faut envoyer nos programs à
OpenGL pour les compiler et les faire valider par la carte
graphique. C'est le travail effectué par la méthode
initShaders suivante.
Maintenant que nous avons chargé nos programs, nous allons
voir ce qu'ils contiennent. Les explications des programs
sont contenues directement en tant que commentaires pour
éviter d'avoir à les découper en petites parties. D'abord,
voilà le vertex program utilisé. Il effectue tous les
calculs par vertex que nous faisions avant sur le CPU.
Et maintenant, voilà le fragment program qui calcul le
résultat final de notre ajout de lumière.
Si vous n'êtes pas familier avec les vertex/fragment
programs ARB, ceux-ci peuvent vous sembler
incompréhensibles, et pourtant ils ne font que des calculs
assez simples à comprendre. Si vraiment vous ne comprenez
pas, vous pouvez toujours aller regarder une doc sur les
instructions des programs ARB.
Il faut bien se rendre compte que l'ensemble des calculs du
fragment program sont effectué pour chaque pixel, même si
le pixel est finalement noir. On obtient donc un fragment
program relativement coûteux en terme de fill rate
(1)
. On peut réduire ce coût en ne renormalisant pas la normal
map et en effectuant les calculs de normalisation des
vecteurs vers la lumière et la camera au niveau du vertex
program, mais c'est au dépend de la qualité, et on retombe
sur les problèmes d'interpolation si on utilise pas de cube
map de normalisation comme c'est le cas ici.
II-4. Le rendu final.
Maintenant que nous avons toutes les informations
nécessaires, nous pouvons voir comment effectuer le rendu
final de la scène. Le code nécessaire est assez semblable à
celui du tutoriel précédent, mis a part qu'on a plus à
activer l'extension env_dot3 pour l'unité de texture de la
normal map, qu'on doit activer par contre les
vertex/fragment programs et qu'on n'a pas à effectuer de
deuxième passe pour avoir des lumières colorées. On ajoute
aussi l'utilisation de la height map et de la gloss map, ce qui fait que nous
utilisons maintenant 5 unités de textures pour notre rendu
(et nous n'utilisons plus de cube map de normalisation).
Le code de rendu de la scène donne donc :
Et voila le résultat final en image.
![]() Le rendu final de la scène. III. Réflexions sur la gestion de la lumière.
Nous avons vu au cours de cette série de tutoriaux comment
gérer la lumière de plusieurs façons différentes. Cela va de la
plus simple, avec l'éclairage par vertex, à des plus compliquée
comme nous venons de le voir. Comme je le disais dans la
conclusion du tutoriel précédent, l'utilisation d'une méthode
d'éclairage ou d'une autre influe non seulement sur les
performances du moteur 3D, mais aussi sur la quantité de
ressources à fournir pour les artistes. En effet, avec un
système complexe, le créateur de textures aura à crée :
Nous avons donc ici de nombreuses textures à créer pour nos
artistes, ce qui peut considérablement alourdir le processus de
création d'un jeu vidéo pour une équipe d'amateurs.
Un autre point important que je n'est pas traité ici est
l'optimisation du rendu des lumières. Nos lumières étant des
lumières sphériques ponctuelles, on peut effectuer de
nombreuses optimisations avant de les afficher. En dehors des
algorithmes de partitionnement (BSP, octree, portals), la
première optimisation qui vient à l'esprit est de vérifier si
la face est bien orientée vers la lumière avant de la rendre.
En effet, il est inutile de dessiner une face si on sait que
l'ensemble de ses vertex tournent le dos à la lumière. Si ceci
peut être coûteux pour l'éclairage par vertex (le coût de
vérification par face sera supérieur au gain de vitesse), cette
optimisation peut être très intéressant pour les systèmes plus
coûteux en terme de fill rate, c'est-à-dire, quand le dessin
d'un pixel est lent (ce qui est notre cas dans ce tutoriel). On
peut aussi exclure de l'affichage toutes les faces qui sont
trop éloignées de la lumière pour être éclairées (ceci se fait
simplement à l'aide d'une équation de plan par polygone).
Une autre optimisation simple à mettre en oeuvre consiste à
utiliser un rectangle de clipping autour de la lumière. Cette
optimisation n'est utilisable que pour les rendus de lumières
en multi passe, elle n'est donc pas applicable à l'éclairage
par vertex. Elle consiste tout simplement à utiliser la
possibilité des cartes graphiques à utiliser un rectangle de
clipping pour réduire la zone de dessin autorisée. Si, en
calculant les extremum de notre lumière, on en déduit un
rectangle de clipping qui englobe entièrement notre lumière, on
peut ainsi réduire l'affichage des grand polygones touchés par
la lumières, et ainsi sauvegarder du fill rate encore une fois. Un avantage de cette optimisation est qu'elle est aussi très utile pour optimiser le rendu des ombres utilisant l'algorithme des shadow volumes.
Comme je l'ai dit dans le premier tutoriel, je n'ai traité que
des lumières sphériques ponctuelles directes. On peut imaginer
des systèmes d'éclairage plus complexes gérant les spots de
lumières via des projections de textures, et des lumières
directionnelles infinies. De même, on peut espérer voir arriver
dans l'industrie d'ici quelques temps l'utilisation de
l'illumination globale qui permet de gérer les sources de
lumières indirectes. Ceci est encore à l'heure actuelle un
sujet de recherche important, mais les premiers résultats sont
déjà impressionnants.
Un autre point important qui n'est pas traité dans cette série
est la gestion de l'occlusion de la lumière, via des
algorithmes de génération d'ombres portées. En effet, dans ces
tutoriaux, la lumière traverse les objets pour aller éclairer
les autres objets. Ceci est embêtant pour le réalisme de notre
scène. Il peut donc être intéressant d'avoir des ombres portées
sur les autres objets, et pour cela utiliser des techniques
comme les shadow volumes ou les shadow buffers (aussi appelée
shadow maps).
IV. Conclusions.
Ca y est, c'est fini. Si nous avons vu au cours de cette série
de tutoriaux comment gérer la lumière de plusieurs manières, il
ne faut pas oublier que le principal argument pour le faire
dans un moteur 3D amateur est bien de pouvoir dépasser la
limite du nombre maximum de lumières hardware supportées par les cartes
graphiques. Il est donc conseillé de choisir la méthode la plus
adaptée au moteurs que vous souhaitez réaliser. Il n'est pas
forcement utile de mettre en place un système d'éclairage par
pixel pour un moteur simple ou pour un jeu de stratégie par
exemple, alors qu'une bonne gestion de l'éclairage pourra être
très importante dans un FPS. Le principal est que maintenant
vous avez le choix de la technique à utiliser dans votre moteur.
Vous pouvez télécharger les sources de ce tutoriel
ici
ou ici [http]
Une version PDF de ce tutoriel est disponible ici ou ici [http]
|
Les sources présentées sur cette page sont libres de droits, et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une oeuvre intellectuelle protégée par les droits d'auteurs. Copyright © 2006 Michel de VERDELHAN. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.