Tutoriel 20 - Lumières ponctuelles

Ogldev

Traducteur

Sylvain DOREMUS

Quatrième chapitre sur l'éclairage, ce tutoriel étudie les sources de lumières ponctuelles.

Commentez

- Titre : Tutoriel 20 - Lumières ponctuelles

- Auteur : Ogldev

- Traducteur : Sylvain DOREMUS

- Parution : 30 décembre 2014

- Public visé :

- Licence : 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 œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Etay Meiri. 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'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Contexte

- II. Explication du code

- III. Sources

- IV. Remerciements

- Navigation

Navigation

Tutoriel précédent : lumière spéculaire

Sommaire

Tutoriel suivant : projecteurs

I - Contexte

Nous avons étudié les trois modèles de l'éclairage basique (ambiant, diffus, spéculaire), sous le couvert de la lumière directionnelle. La lumière directionnelle est un type de lumière caractérisé par un simple vecteur direction et l'absence d'origine. Par conséquent, son éclairage ne devient pas plus faible avec la distance (en fait, on ne peut pas définir la distance entre la source lumineuse et la cible éclairée). Nous allons maintenant voir le type de lumière appelé « lumière ponctuelle », qui a un point d'origine ainsi qu'un effet d'affaiblissement, qui augmente à mesure que les objets s'en éloignent. Un exemple classique de lumière ponctuelle est une ampoule. Nous ne pouvons pas bien nous rendre compte de l'effet d'affaiblissement lorsque l'ampoule est dans une pièce, mais, en la plaçant à l'extérieur, nous voyons bien comme sa puissance est limitée. Notons que la direction de la lumière, qui est une constante de scène pour une lumière directionnelle, devient dynamique pour une lumière ponctuelle. C'est parce qu'une lumière ponctuelle brille de la même manière dans toutes les directions, donc la direction doit être calculée pour chaque objet, en récupérant le vecteur entre l'objet et la source lumineuse. C'est pour cela que nous spécifions l'origine plutôt que la direction des lumières ponctuelles.

L'effet d'affaiblissement des lumières ponctuelles est généralement appelé « atténuation ». L'atténuation pour les lumières réelles est régie par la loi des carrés inverses, qui dit que l'intensité de la lumière est inversement proportionnelle au carré de la distance à la source de la lumière. Ce concept est décrit de manière mathématique par la formule suivante :

Cette formule ne donne pas de bons résultats en graphisme 3D. Par exemple, comme la distance devient plus petite, l'intensité de la lumière s'approche de l'infini. En outre, le développeur n'a pas de contrôle sur le résultat, à part en définissant l'intensité initiale de la lumière. C'est trop limité. Par conséquent, nous ajoutons quelques facteurs à la formule ci-dessus pour la rendre plus flexible :

Nous avons ajouté trois facteurs d'atténuation de la lumière au dénominateur : un facteur constant, un facteur linéaire et un facteur exponentiel. La formule physiquement correcte est retrouvée en mettant les facteurs linéaire et constant à zéro et le facteur exponentiel à un. Nous pourrons trouver utile de mettre le facteur constant à un et les deux autres à des fractions plus petites. En mettant le facteur constant à un, nous garantissons que l'intensité de la lumière atteint son maximum (en fait, la valeur configurée dans le programme) à la distance zéro et décroît avec l'augmentation de la distance, car le dénominateur devient plus grand que un. En réglant correctement les facteurs, linéaire et exponentiel, nous atteindrons l'effet désiré de lumière qui s'affaiblit rapidement ou lentement, suivant la distance.

Résumons les étapes requises pour le calcul d'une lumière ponctuelle :

1. Calculer le terme ambiant, de la même manière que pour une lumière directionnelle ;

2. Calculer la direction de la lumière comme étant le vecteur allant du pixel (dans l'espace monde) au point d'origine de la source lumineuse. Nous pouvons maintenant calculer les termes de diffusion et de spéculaire, de la même manière que pour une lumière directionnelle, en utilisant cette fois la direction calculée ;

3. Calculer la distance entre le pixel et le point d'origine de la source lumineuse et l'utiliser pour calculer la valeur totale d'atténuation ;

4. Ajouter les trois termes et diviser cette somme par l'atténuation pour obtenir la couleur finale.

II - Explication du code

1. struct BaseLight

2. {

3. Vector3f Color;

4. float AmbientIntensity;

5. float DiffuseIntensity;

6. };

7. .

8. .

9. .

10. struct PointLight : public BaseLight

11. {

12. Vector3f Position;

14. struct

15. {

16. float Constant;

17. float Linear;

18. float Exp;

19. } Attenuation;

20. }

En dépit de leurs différences, les sources lumineuses directionnelles et ponctuelles ont beaucoup en commun. Ces données communes ont été déplacées dans la structure BaseLight, dont les deux types de lumière dérivent maintenant. La structure de lumière directionnelle ajoute la direction tandis que la structure de lumière ponctuelle ajoute la position (dans l'espace monde) et les trois facteurs d'atténuation.

1. void SetPointLights(unsigned int NumLights, const PointLight* pLights);

En plus de montrer comment implémenter une lumière ponctuelle, ce tutoriel montre aussi comment utiliser plusieurs sources lumineuses. Nous considérons ici qu'il n'y a généralement qu'une seule lumière directionnelle (jouant le rôle de « soleil ») et plusieurs sources lumineuses ponctuelles possibles (des ampoules dans des pièces, des torches dans un donjon, etc.). Cette fonction prend en paramètre un tableau de structures PointLight ainsi que sa taille et met à jour le shader avec leurs valeurs.

1. struct {

2. GLuint Color;

3. GLuint AmbientIntensity;

4. GLuint DiffuseIntensity;

5. GLuint Position;

6. struct

7. {

8. GLuint Constant;

9. GLuint Linear;

10. GLuint Exp;

11. } Atten;

12. } m_pointLightsLocation[MAX_POINT_LIGHTS];

Afin de supporter plusieurs lumières ponctuelles, le shader contient un tableau de structures identiques à la structure PointLight (en GLSL, cette fois). Il y a deux méthodes pour mettre à jour un tableau de structures dans les shaders :

1. Nous pouvons récupérer la position de chaque membre de la structure, pour chaque élément du tableau (donc un tableau de cinq structures avec quatre champs amène à vingt positions de variables uniformes) et définir la valeur de chaque champ séparément ;

2. Nous pouvons récupérer la position des champs du premier élément et utiliser une fonction OpenGL, qui définit les valeurs d'un tableau de variables, pour chaque type d'attribut spécifique à un champ. Par exemple, le premier champ est un flottant et le second est un entier, donc on peut définir toutes les valeurs du premier champ en donnant un tableau de flottants en un appel et définir le second champ avec un tableau d'entiers en un second appel.

La première méthode est la moins rentable en termes de nombre de positions de variables uniformes à stocker, mais est plus flexible à l'utilisation. Elle permet de mettre à jour n'importe quelle variable du tableau en accédant simplement à sa position et ne requiert pas la transformation de données qu'impose la seconde méthode.

La seconde méthode requiert moins de gestion de positions de variables uniformes ; cependant, si nous voulons mettre à jour plusieurs tableaux d'éléments à la fois et que l'utilisateur donne un tableau de structures (comme le fait la méthode SetPointLights()), nous allons devoir les transformer en une structure de tableaux, à cause du fait que les positions de variables uniformes représentent des tableaux de variables de même type. Lors de l'utilisation d'un tableau de structures, il y a un espace en mémoire entre les positions du même champ de deux structures consécutives dans le tableau, ce qui requiert de les rassembler dans un même tableau.

Dans ce tutoriel, nous allons utiliser la première méthode. Vous pouvez vous amuser avec les deux pour décider laquelle fonctionne le mieux dans votre cas.

La constante MAX_POINT_LIGHTS limite le nombre de lumières ponctuelles pouvant être utilisées et doit être synchronisée avec la valeur correspondante dans le shader. La valeur par défaut est 2. En augmentant le nombre de sources lumineuses dans votre application, vous pouvez rencontrer des problèmes de performances, qui empirent avec l'augmentation du nombre de lumières. Ce problème peut être contourné en utilisant une technique appelée « rendu différé » qui sera traitée plus tard.

1. vec4 CalcLightInternal(BaseLight Light, vec3 LightDirection, vec3 Normal)

2. {

3. vec4 AmbientColor = vec4(Light.Color, 1.0f) * Light.AmbientIntensity;

4. float DiffuseFactor = dot(Normal, -LightDirection);

6. vec4 DiffuseColor = vec4(0, 0, 0, 0);

7. vec4 SpecularColor = vec4(0, 0, 0, 0);

9. if (DiffuseFactor > 0) {

10. DiffuseColor = vec4(Light.Color, 1.0f) * Light.DiffuseIntensity * DiffuseFactor;

11. vec3 VertexToEye = normalize(gEyeWorldPos - WorldPos0);

12. vec3 LightReflect = normalize(reflect(LightDirection, Normal));

13. float SpecularFactor = dot(VertexToEye, LightReflect);

14. SpecularFactor = pow(SpecularFactor, gSpecularPower);

15. if (SpecularFactor > 0) {

16. SpecularColor = vec4(Light.Color, 1.0f) *

17. gMatSpecularIntensity * SpecularFactor;

18. }

19. }

21. return (AmbientColor + DiffuseColor + SpecularColor);

22. }

Il n'est pas surprenant de remarquer que nous avons beaucoup de code en commun, entre les lumières directionnelles et les lumières ponctuelles. La plupart des algorithmes sont les mêmes. La différence réside dans le fait que nous devons ajouter le facteur d'atténuation uniquement pour la lumière ponctuelle. De plus, la direction de la lumière est fournie par l'application dans le cas de la lumière directionnelle alors qu'elle est calculée par pixel dans le cas de la lumière ponctuelle.

La fonction ci-dessus encapsule le code commun aux deux types de lumières. La structure BaseLight contient les intensités et la couleur. Le membre LightDirection est fourni séparément à cause de la raison décrite précédemment. La normale au sommet est aussi fournie, car nous la normalisons une fois en entrée du fragment shader puis l'utilisons plusieurs fois dans cette fonction.

1. vec4 CalcDirectionalLight(vec3 Normal)

2. {

3. return CalcLightInternal(gDirectionalLight.Base, gDirectionalLight.Direction, Normal);

4. }

Avec la mise en place de la fonction commune, la fonction de calcul de la lumière directionnelle devient une simple enveloppe pour celle-ci, renseignant la plupart des paramètres à partir de variables globales.

lighting.fs

1. vec4 CalcPointLight(int Index, vec3 Normal)

2. {

3. vec3 LightDirection = WorldPos0 - gPointLights[Index].Position;

4. float Distance = length(LightDirection);

5. LightDirection = normalize(LightDirection);

7. vec4 Color = CalcLightInternal(gPointLights[Index].Base, LightDirection, Normal);

8. float Attenuation = gPointLights[Index].Atten.Constant +

9. gPointLights[Index].Atten.Linear * Distance +

10. gPointLights[Index].Atten.Exp * Distance * Distance;

12. return Color / Attenuation;

13. }

Calculer une lumière ponctuelle est un peu plus complexe que calculer une lumière directionnelle. Cette fonction sera appelée pour chaque source ponctuelle donc elle prend l'indice de la source en paramètre et l'utilise comme index du tableau de lumières ponctuelles. Elle calcule le vecteur partant de la source (fournie dans l'espace monde par l'application) vers la position du pixel dans l'espace monde, fourni par le vertex shader. La distance de la source au pixel est calculée en utilisant la fonction interne length(). Maintenant que nous avons la distance, nous normalisons le vecteur de direction de la lumière. Rappelons-nous que CalcLightInternal() s'attend à ce qu'il soit normalisé et, dans le cas d'une lumière directionnelle, la classe LightingTechnique s'en occupe. Nous récupérons la couleur en sortie de la fonction CalcLightInternal() et utilisons la distance récupérée auparavant pour calculer l'atténuation. La couleur finale de la source lumineuse ponctuelle est calculée en divisant la couleur obtenue par l'atténuation.

1. void main()

2. {

3. vec3 Normal = normalize(Normal0);

4. vec4 TotalLight = CalcDirectionalLight(Normal);

6. for (int i = 0 ; i < gNumPointLights ; i++) {

7. TotalLight += CalcPointLight(i, Normal);

8. }

10. FragColor = texture2D(gSampler, TexCoord0.xy) * TotalLight;

11. }

Maintenant que nous avons toute l'infrastructure en place, le fragment shader devient très simple. Il effectue la normalisation de la normale au sommet et accumule les résultats d'application des sources lumineuses. Le résultat est multiplié par la couleur échantillonnée et est utilisé comme couleur finale du pixel.

1. void LightingTechnique::SetPointLights(unsigned int NumLights, const PointLight* pLights)

2. {

3. glUniform1i(m_numPointLightsLocation, NumLights);

5. for (unsigned int i = 0 ; i < NumLights ; i++) {

6. glUniform3f(m_pointLightsLocation[i].Color, pLights[i].Color.x, pLights[i].Color.y, pLights[i].Color.z);

7. glUniform1f(m_pointLightsLocation[i].AmbientIntensity, pLights[i].AmbientIntensity);

8. glUniform1f(m_pointLightsLocation[i].DiffuseIntensity, pLights[i].DiffuseIntensity);

9. glUniform3f(m_pointLightsLocation[i].Position, pLights[i].Position.x, pLights[i].Position.y, pLights[i].Position.z);

10. glUniform1f(m_pointLightsLocation[i].Atten.Constant, pLights[i].Attenuation.Constant);

11. glUniform1f(m_pointLightsLocation[i].Atten.Linear, pLights[i].Attenuation.Linear);

12. glUniform1f(m_pointLightsLocation[i].Atten.Exp, pLights[i].Attenuation.Exp);

13. }

14. }

Cette fonction met à jour les valeurs des lumières ponctuelles dans le shader, en itérant sur le tableau d'éléments et en passant une à une les valeurs de chaque élément. C'est la « méthode 1 » décrite précédemment.

La démonstration de ce tutoriel montre deux lumières ponctuelles se poursuivant sur le terrain. Les déplacements d'une des sources sont basés sur la fonction cosinus et les déplacements de l'autre le sont sur la fonction sinus. Le terrain est un simple quadrilatère formé par deux triangles. La normale est un vecteur pointant directement vers le haut.

III - Sources

Vous pouvez télécharger les sources de ce projet en suivant ce lien :

Récupérer les sources

IV - Remerciements

Merci à Etay Meiri de nous permettre de traduire son tutoriel.

Merci à LittleWhite pour ses corrections et à ced pour sa relecture.

Navigation

Tutoriel précédent : lumière spéculaire

Sommaire

Tutoriel suivant : projecteurs