Tutoriel 17 - Éclairage ambiant

Ogldev

Traducteur

Sylvain DOREMUS

Ce tutoriel est le premier de la série sur l'éclairage.

Il commence par l'éclairage ambiant.

Commentez

- Titre : Tutoriel 17 - Éclairage ambiant

- Auteur : Ogldev

- Traducteur : Sylvain DOREMUS

- Parution : 31 octobre 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

- V. Navigation

Navigation

Tutoriel précédent : application basique de texture

Sommaire

Tutoriel suivant : lumière diffuse

I - Contexte

L'éclairage est un des sujets les plus importants dans le domaine de la 3D. Le modéliser correctement ajoute beaucoup à l'aspect visuel de la scène rendue. La raison du mot « modéliser » vient du fait que nous ne pouvons pas simuler exactement ce que fait la nature. La lumière réelle est faite d'une énorme quantité de particules, appelées « photons », qui agissent à la fois comme des ondes et des particules (la dualité onde-particule de la lumière). Si nous essayons de calculer l'effet de chaque photon dans notre programme, nous allons très rapidement être à court de puissance de calcul.

Par conséquent divers modèles d'éclairage ont été développés au fil des années pour reproduire l'effet principal de la lumière sur les objets qu'elle atteint, en les rendant visibles. Ces modèles d'éclairage sont devenus de plus en plus complexes, au fur et à mesure de la progression des connaissances en rendu 3D et de l'augmentation de la puissance de calcul. Pendant l'étude des quelques tutoriels suivants, nous verrons le modèle d'éclairage de base, plus simple à implémenter, mais qui contribue énormément à l'atmosphère globale d'une scène.

Le modèle d'éclairage de base est appelé « Ambiante/Diffuse/Spéculaire ». La lumière ambiante est le type de lumière que l'on voit lorsque nous allons dehors, en un jour de soleil habituel. Même si le soleil traverse le ciel et que les rayons lumineux qu'il projette arrivent à différents angles, au cours de la journée, la plupart des choses seront visibles, même dans l'ombre. Comme la lumière rebondit sur chaque chose touchée, elle finit par tout toucher, ainsi les objets qui ne sont pas directement face au soleil finissent par être éclairés. Même une ampoule dans une pièce agit comme le soleil en ce sens et répand une lumière ambiante, car si la pièce n'est pas trop grande, tout est éclairé uniformément. La lumière ambiante est modélisée comme une lumière sans origine, sans direction et qui a un effet identique sur tous les objets de la scène.

La lumière diffuse accentue le fait que l'angle dans lequel la lumière touche la surface affecte l'intensité de l'éclairage de l'objet. Quand la lumière atteint un objet sur un côté, ce côté est plus clair que l'autre (celui qui n'est pas directement en face de la lumière). Nous venons de voir que le soleil répand une lumière ambiante qui n'a pas de direction spécifique. Cependant, la lumière du soleil a aussi des propriétés de diffusion. Quand elle touche un bâtiment, nous pouvons généralement observer qu'un côté du bâtiment est plus éclairé qu'un autre. La propriété la plus importante de la lumière diffuse est sa direction.

La lumière spéculaire est plus une propriété de l'objet, que de la lumière elle-même. C'est ce qui fait que des portions d'objets brillent, lorsque la lumière les atteint dans un angle spécifique et que l'on regarde depuis un point spécifique. Les objets métalliques ont souvent une propriété spéculaire. Par exemple, une voiture pendant une journée ensoleillée brille parfois au niveau de ses arêtes. Pour calculer la lumière spéculaire, nous devons prendre en considération la direction d'arrivée de la lumière (et de son rebond), ainsi que la position de l'observateur.

Dans les applications 3D, nous ne créons généralement pas directement de lumière ambiante, diffuse ou spéculaire. Nous utilisons plutôt des sources lumineuses tels le soleil (en extérieur), une ampoule (à l'intérieur) ou une lampe de poche (dans une grotte). Ces sources lumineuses peuvent avoir différentes combinaisons d'intensités ambiante, diffuse et spéculaire, ainsi que d'autres propriétés spécifiques. Par exemple la lampe de poche a un cône lumineux et les objets qui en sont éloignés ne sont pas éclairés du tout.

Dans les tutoriels suivants, nous développerons quelques types de sources lumineuses très utiles et étudierons le modèle d'éclairage de base, tout en progressant.

Nous allons commencer par une source lumineuse appelée « lumière directionnelle ». Une lumière directionnelle a une direction, mais pas d'origine spécifique. Cela signifie que tous les rayons lumineux sont parallèles les uns aux autres. La direction d'une lumière est définie par un vecteur et ce vecteur est utilisé pour calculer la lumière sur tous les objets de la scène, quelle que soit leur position. Le soleil rentre particulièrement bien dans la catégorie des lumières directionnelles. Si nous essayons de calculer l'angle précis avec lequel la lumière du soleil atteint deux bâtiments mitoyens, nous retrouverons deux valeurs presque identiques (la différence entre elles sera une fraction minuscule). C'est parce que le soleil est situé à quelque 150 millions de kilomètres de distance. Par conséquent, nous ne nous attardons pas sur sa position et prenons uniquement la direction en compte.

Une autre propriété importante d'une lumière directionnelle est que l'éclairage reste le même, quelle que soit la distance de l'objet éclairé. C'est une différence avec une autre source lumineuse que nous étudierons dans les tutoriels à venir, la lumière ponctuelle, dont l'éclairage est de plus en plus faible avec l'éloignement (par exemple : une ampoule).

L'image suivante illustre une lumière directionnelle :

Nous venons de voir que la lumière du soleil possède des propriétés ambiantes ainsi que de diffusion. Nous allons développer la partie ambiante ici et la partie diffusion dans le tutoriel suivant.

Dans le tutoriel précédent, nous avons appris comment échantillonner la couleur d'un pixel à partir d'une texture. La couleur possède trois canaux (rouge, vert et bleu) et chaque canal est représenté par un octet. Cela signifie que la valeur d'un canal peut aller de 0 à 255. Différentes combinaisons de canaux représentent donc différentes couleurs. Quand tous les canaux sont à zéro, la couleur est noire. Quand ils sont tous à 255, la couleur est blanche. Toutes les autres couleurs se situent entre les deux. En multipliant tous les canaux par la même valeur, nous obtenons la même couleur de base, mais pouvons la rendre plus claire ou plus sombre (en fonction de la valeur).

Lorsque la lumière blanche atteint une surface, la couleur reflétée est simplement la couleur de la surface. Elle peut être plus claire ou plus sombre, en fonction de la puissance de la source lumineuse, mais cela reste la même couleur de base. Si la source lumineuse est rouge pur (255, 0, 0), la couleur reflétée ne pourra être qu'une sorte de rouge. C'est parce que la lumière n'a pas de canaux vert et bleu qui pourraient être reflétés par la surface. Si la surface est bleu pur, le résultat sera donc noir. La conclusion est que la lumière peut uniquement exposer la couleur d'un objet, pas le « peindre ».

Nous allons définir la couleur des sources lumineuses comme un trio de nombres flottants dans l'intervalle [0,1]. En multipliant la couleur de la lumière par la couleur d'un objet, nous obtiendrons la couleur reflétée. Cependant, nous souhaitons aussi prendre en compte l'intensité ambiante de la lumière. Par conséquent, l'intensité ambiante sera définie par un simple nombre flottant dans l'intervalle [0,1], qui sera lui aussi multiplié par tous les canaux de la lumière reflétée que nous venons de calculer. Ce sera la couleur finale. L'équation suivante résume le calcul de la lumière ambiante :

Dans le code d'exemple de ce tutoriel, nous pourrons jouer avec les touches « a » et « s », pour augmenter ou réduire l'intensité de la lumière ambiante et voir l'effet que ça a sur la pyramide texturée du tutoriel précédent. Comme ce n'est que la partie ambiante de la lumière directionnelle, la direction elle-même n'a pas encore été impliquée. Cela changera dans le prochain tutoriel où nous étudierons la lumière diffuse. Pour l'instant nous allons voir que la pyramide est éclairée de la même manière, quelle que soit la manière dont on la regarde.

La lumière ambiante est considérée par beaucoup comme quelque chose à éviter autant que possible. C'est parce qu'elle semble quelque peu artificielle et la simplicité de son implémentation ne contribue pas vraiment au réalisme de la scène. En utilisant des méthodes avancées, telles que l'illumination globale, on peut éliminer le besoin de lumière ambiante, car la lumière qui est reflétée sur des objets et qui atteint d'autres objets peut aussi être prise en compte. Comme nous ne sommes pas encore là, nous aurons généralement besoin d'un peu de lumière ambiante, pour éviter les cas où un côté d'un objet est éclairé et l'autre côté est dans le noir complet. Pour simuler une lumière de fin de journée, il faut beaucoup jouer avec les paramètres et beaucoup de travail d'affinage.

II - Explication du code

Nos exemples de code vont devenir de plus en plus complexes avec le temps et cette tendance va s'accentuer. Dans ce tutoriel, en plus d'implémenter l'éclairage ambiant, nous allons aussi restructurer le code. Cela va placer le code dans une meilleure forme pour les tutoriels à venir. Les changements majeurs sont :

1. encapsulation de la gestion des shaders dans la classe Technique. Cela inclut les actions telles que la compilation et la liaison. À partir de maintenant, nous implémenterons nos effets visuels dans des classes dérivées de la classe Technique ;

2. déplacement de l'initialisation de GLUT et de la gestion des callbacks, dans le composant GLUTBackend. Ce composant s'enregistre lui-même pour recevoir les appels aux callbacks GLUT et les transfère à l'application, en utilisant une interface C++ appelée ICallbacks ;

3. déplacements des fonctions et variables globales, depuis le ficher cpp principal vers une classe, qui peut être considérée comme « l'application ». Dans le futur, nous étendrons ceci à une classe de base pour toutes les applications et fournirons les fonctionnalités communes à toutes. Cette approche est très populaire dans de nombreux moteurs de jeux et frameworks.

La majorité du code de ce tutoriel (excepté le code spécifique à la lumière) n'est pas neuf, mais a juste été réorganisé conformément aux principes du plan décrit ci-dessus. Par conséquent, seuls les nouveaux fichiers d'en-têtes sont revus.

1. void GLUTBackendInit(int argc, char** argv);

3. bool GLUTBackendCreateWindow(unsigned int Width, unsigned int Height, unsigned int bpp, bool isFullScreen, const char* pTitle);

Beaucoup de code spécifique à GLUT a été déplacé vers le composant « GLUT backend », qui rend plus faciles l'initialisation de GLUT et la création d'une fenêtre en utilisant les fonctions ci-dessus.

1. void GLUTBackendRun(ICallbacks* pCallbacks);

Après que GLUT a été initialisé et qu'une fenêtre a été créée, l'étape suivante est d'exécuter la boucle principale de GLUT, dans la fonction suivante. L'ajout ici est l'utilisation de l'interface ICallbacks, qui aide à l'enregistrement des fonctions de callback GLUT. Au lieu de demander à chaque application d'enregistrer ses callbacks de son côté, le composant « GLUT backend » enregistre ses propres fonctions privées et transmet les événements à l'objet spécifié dans l'appel à la fonction ci-dessus. La classe principale de l'application va souvent implémenter de son côté cette interface et se passer elle-même en tant que paramètre de l'appel à GLUTBackendRun. Cette approche a aussi été choisie pour ce tutoriel.

1. class Technique

2. {

3. public:

5. Technique();

7. ~Technique();

9. virtual bool Init();

11. void Enable();

13. protected:

15. bool AddShader(GLenum ShaderType, const char* pShaderText);

17. bool Finalize();

19. GLint GetUniformLocation(const char* pUniformName);

21. private:

23. GLuint m_shaderProg;

25. typedef std::list<GLuint> ShaderObjList;

26. ShaderObjList m_shaderObjList;

27. };

Dans les tutoriels précédents, toute la corvée de compiler et lier les shaders était le lot de l'application. La classe Technique aide en intégrant toutes les fonctionnalités communes et en permettant aux classes dérivées de se concentrer sur le cœur de l'effet (la technique).

Chaque technique doit d'abord être initialisée en appelant la fonction Init(). Une technique dérivée doit appeler Init() de la classe de base (qui crée l'objet du programme OpenGL) et peut ajouter ses initialisations privées ici.

Après qu'un objet Technique a été créé et initialisé, la séquence habituelle pour la classe de technique dérivée est d'appeler la fonction protégée AddShader() sur autant de shaders GLSL (fournis dans une chaîne de caractères) que nécessaire. Enfin, Finalize() est appelée pour lier les objets. La fonction Enable() ne fait pour l'instant qu'appeler glUseProgram()() et doit donc être appelée à chaque fois que l'on change de technique et que l'on appelle la fonction de dessin.

Cette classe garde les objets intermédiaires compilés et les détruit après la liaison, en utilisant glDeleteShader(). Cela aide à réduire le nombre de ressources consommées par notre application. Pour de meilleures performances, les applications OpenGL compilent souvent tous les shaders pendant le chargement et non pendant l'exécution. En supprimant les objets directement après la liaison, nous aidons à garder basse la consommation de ressources OpenGL par notre application. L'objet du programme lui-même est supprimé dans le destructeur, en utilisant glDeleteProgram().

1. class Tutorial17 : public ICallbacks

2. {

3. public:

5. Tutorial17()

6. {

7. ...

8. }

10. ~Tutorial17()

11. {

12. ...

13. }

15. bool Init()

16. {

17. ...

18. }

20. void Run()

21. {

22. GLUTBackendRun(this);

23. }

25. virtual void RenderSceneCB()

26. {

27. ...

28. }

30. virtual void IdleCB()

31. {

32. ...

33. }

35. virtual void SpecialKeyboardCB(int Key, int x, int y)

36. {

37. ...

38. }

40. virtual void KeyboardCB(unsigned char Key, int x, int y)

41. {

42. ...

43. }

45. virtual void PassiveMouseCB(int x, int y)

46. {

47. ...

48. }

50. private:

52. void CreateVertexBuffer()

53. {

54. ...

55. }

56. void CreateIndexBuffer()

57. {

58. ...

59. }

61. GLuint m_VBO;

62. GLuint m_IBO;

63. LightingTechnique* m_pEffect;

64. Texture* m_pTexture;

65. Camera* m_pGameCamera;

66. float m_scale;

67. DirectionalLight m_directionalLight;

68. };

C'est un squelette de la classe de l'application principale, qui encapsule le code restant qui nous est familier. Init() prend en charge la création de l'effet, le chargement de la texture et la création des tampons d'indices et de sommets. Run() appelle GLUTBackendRun() et passe l'instance elle-même en paramètre. Comme cette classe implémente l'interface ICallbacks, tous les événements GLUT sont traités dans les bonnes méthodes de la classe. De plus, toutes les variables, qui étaient préalablement dans la section globale du fichier, sont maintenant des attributs privés de la classe.

1. struct DirectionalLight

2. {

3. Vector3f Color;

4. float AmbientIntensity;

5. };

C'est le début de la définition de la lumière directionnelle. Pour l'instant, seule la partie ambiante existe et la direction est absente. Nous ajouterons la direction dans le tutoriel suivant, lorsque nous traiterons la lumière diffuse. La structure contient deux champs : une couleur et l'intensité ambiante. La couleur détermine quels canaux de couleur des objets seront reflétés et en quelle intensité. Par exemple, si la couleur vaut (1.0, 0.5, 0.0), alors le canal rouge de l'objet sera complètement reflété, le canal vert sera reflété à moitié et le canal bleu ne sera pas reflété du tout. C'est parce qu'un objet ne peut que refléter la lumière qui l'atteint (les sources lumineuses sont différentes, elles émettent la lumière et doivent être gérées séparément). Dans le cas du soleil, la couleur habituellement choisie est le blanc pur (1.0, 1.0, 1.0).

AmbientIntensity définit à quel point la lumière est sombre ou claire. Nous pouvons avoir une lumière blanche dont l'intensité est 1.0, donc l'objet est complètement éclairé, ou une intensité de 0.1 qui signifie que l'objet sera visible, mais apparaîtra très sombre.

1. class LightingTechnique : public Technique

2. {

3. public:

5. LightingTechnique();

7. virtual bool Init();

9. void SetWVP(const Matrix4f& WVP);

10. void SetTextureUnit(unsigned int TextureUnit);

11. void SetDirectionalLight(const DirectionalLight& Light);

13. private:

15. GLuint m_WVPLocation;

16. GLuint m_samplerLocation;

17. GLuint m_dirLightColorLocation;

18. GLuint m_dirLightAmbientIntensityLocation;

19. };

Voici le premier exemple de l'utilisation de la classe Technique. LightingTechnique est une classe dérivée qui implémente la lumière, en utilisant les fonctionnalités communes de compilation et liaison fournies par la classe de base. La fonction Init() doit être appelée après que l'objet a été créé. Il appelle simplement Technique::AddShader() et Technique::Finalize() pour générer le programme GLSL.

1. #version 330

3. in vec2 TexCoord0;

5. out vec4 FragColor;

7. struct DirectionalLight

8. {

9. vec3 Color;

10. float AmbientIntensity;

11. };

13. uniform DirectionalLight gDirectionalLight;

14. uniform sampler2D gSampler;

16. void main()

17. {

18. FragColor = texture2D(gSampler, TexCoord0.xy) *

19. vec4(gDirectionalLight.Color, 1.0f) *

20. gDirectionalLight.AmbientIntensity;

21. }

Le vertex shader reste inchangé dans ce tutoriel. Il continue de passer la position (après l'avoir multipliée par la matrice WVP) et les coordonnées de texture. Toute la nouvelle logique se situe dans le fragment shader. L'ajout ici est l'utilisation du mot-clef « struct » afin de définir la lumière directionnelle. Comme nous pouvons le voir, ce mot-clef est utilisé pratiquement de la même manière qu'en C/C++. La structure est identique à celle que nous avons dans le code de l'application et nous devons la garder ainsi afin que l'application et le shader puissent communiquer.

Il y a maintenant une nouvelle variable uniforme de type DirectionalLight, dont l'application a besoin pour la mise à jour. Cette variable est utilisée dans le calcul de la couleur finale du pixel. Comme avant, nous échantillonnons la texture pour récupérer la couleur de base. Nous la multiplions ensuite, par la couleur et l'intensité ambiante, selon la formule ci-dessus. Cela termine le calcul de la lumière ambiante.

1. m_WVPLocation = GetUniformLocation("gWVP");

2. m_samplerLocation = GetUniformLocation("gSampler");

3. m_dirLightColorLocation = GetUniformLocation("gDirectionalLight.Color");

4. m_dirLightAmbientIntensityLocation = GetUniformLocation("gDirectionalLight.AmbientIntensity");

Afin d'accéder à la variable uniforme DirectionalLight à partir de l'application, nous devons récupérer la position de ses deux champs, indépendamment. La classe LightingTechnique garde quatre Gluint pour les positions des variables uniformes dans le vertex shader et le fragment shader. La matrice WVP et la position de l'échantillonneur sont récupérées d'une manière familière. La couleur et l'intensité ambiante sont récupérées de la manière que nous voyons : en spécifiant le nom de la variable uniforme, suivi d'un point puis du nom du champ dans la structure elle-même. La définition de la valeur de ces variables est faite de la même manière que pour n'importe quelle autre variable. La classe LightingTechnique fournit deux méthodes pour définir la lumière directionnelle et la matrice WVP. La classe Tutorial17 les appelle avant chaque dessin, pour mettre les valeurs à jour.

Ce tutoriel nous permet de jouer avec l'intensité ambiante, en utilisant les touches « a » et « s », qui l'augmentent et la réduisent, respectivement. Regardez la fonction KeyboardCB() dans la classe Tutorial17 pour voir comment c'est fait.

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 à ClaudeLELOUP pour sa relecture.

V - Navigation

Tutoriel précédent : application basique de texture

Sommaire

Tutoriel suivant : lumière diffuse