TD3 : Éclairage local

Pour démarrer ce TD dans de bonnes conditions, repartez de cette base de code :

curl -O https://www.labri.fr/perso/pbenard/teaching/simg/Ressources/si_tp3.zip

1 - Normales

Pour pouvoir calculer l'éclairage, nous avons besoin de connaître l'orientation de la surface. Cette information est représentée par les normales (vecteurs 3D) que nous noterons $\vx{n}$. Les normales sont transmises par la classe Mesh aux shaders en tant qu'attributs vec3 vtx_normal.

normales

1. Modifiez le vertex shader simple.vert pour qu'il récupère l'attribut vtx_normal en entrée et qu'il le transmette au fragment shader. Modifiez ensuite le fragment shader de façon à afficher les normales sous forme de couleurs.

Attention : les coordonnées des normales sont comprises dans l'intervalle $[-1, 1]$, il faut les transformer dans l'intervalle $[0, 1]$ pour les afficher comme des couleurs.

Testez avec le maillage "cow.obj" dont les normales sont pré-calculées et stockées dans le fichier.

2. Les modèles 3D ne sont pas toujours fournis avec des normales par sommet. Votre tâche est de modifier la méthode Mesh::load pour qu'elle calcule les normales en fonction de la position des sommets, si le maillage chargé ne possède pas de normales (normalId.empty() == true).

La normale d'un sommet peut être obtenue en faisant la moyenne pondérée des normales des faces adjacentes à ce sommet, sachant que la normale $\vx{n}_f $ d'une face de sommets $(\vx{v_1}, \vx{v_2}, \vx{v_3})$ est : $\vx{n}_f = (\vx{v_2} - \vx{v_1}) \times (\vx{v_3} - \vx{v_1})$, avec $\times$ le produit vectoriel. Sa norme $\|\vx{n}_f\|$ vaut alors 2 fois $\Delta_f$, l'aire du triangle $(\vx{v_1}, \vx{v_2}, \vx{v_3})$. Par conséquent, en calculant directement la moyenne des normales $\vx{n}_f$ des faces adjacentes à un sommet et en normalisant cette moyenne, vous obtiendrez une normale moyenne $\vx{n}_p$ pondérée par les aires des triangles adjacents ($w_f = \Delta_f$). Cette pondération est fréquemment utilisée car elle produit globalement de bons résultats tout en étant rapide à calculer.

Testez votre implementation sur le maillage de lemming.

normale lisse

Indices : Si vous utilisez des boucles imbriquées ou une structure de donnée temporaire, vous faites fausse route. L'algorithme consiste à calculer les normales en trois passes :

  1. initialiser toutes les normales au vecteur nul,
  2. calculer les normales des faces et les ajouter à leurs trois sommets incidents au fur à mesure,
  3. normaliser la normale de chaque sommet.

3. Les normales transmises aux shaders sont définies en espace objet. Or, pour le calcul de l'éclairage, nous avons besoin de connaître l'orientation de la normale par rapport à la direction de la lumière incidente et la direction de vue. Pour cela, il faut exprimer tous ces vecteurs dans le même repère.

normales

La matrice de vue utilisée dans le vertex shader transforme la position des sommets dans le repère de la vue. Cette position correspond directement au vecteur pointant de la surface vers la caméra (qui est, par définition, placée à l'origine du repère). Il faut donc appliquer une transformation aux normales de façon à les exprimer elles aussi dans le repère de la vue.

La matrice de transformation des normales n'est cependant pas la même que celle des sommets, mais elle peut se déduire de celle-ci grâce à la formule suivante : $$\vx{N} = (\vx{L}^{-1})^\top$$ où $\vx{L}$ est la partie linéaire de la matrice de vue (matrice 3x3 supérieure gauche).

Ajoutez une variable uniform mat3 normalMatrix au vertex shader, et initialisez la correctement dans votre code C++ avec Eigen. Vérifiez que les normales en espace vue affichées sous forme de couleurs produisent un résultat bleuté quel que soit l'orientation du modèle. En effet, les normales des faces visibles sont forcement orientées vers la caméra, elles ont donc une coordonnée z positive.

2 - Calcul de l'éclairage

Dans le modèle de Phong, la luminance réfléchie en un point par une source lumineuse se décompose en une partie ambiante $l_a$, une partie diffuse $l_d$ et une partie spéculaire $l_s$. Si $\vx{n}$ est la normale à la surface au point d'intérêt à ombrer $\vx{p}$, $\vx{l}$ la direction de la source de lumière depuis le $\vx{p}$ et $\vx{v}$ la direction de vue alors :

$$c_a = m_a l_a$$

avec $m_a$ la couleur ambiante du matériau et $l_a$ la couleur ambiante de la lumière.

$$c_d = m_d l_d \cos \theta = m_d l_d \max(\vx{n} \cdot \vx{l}, 0)$$

avec $m_d$ la couleur diffuse du matériau et $l_d$ la couleur diffuse de la lumière.

$$c_s = m_s l_s (\cos \alpha)^s = m_s l_s (\max(\vx{r} \cdot \vx{v}, 0))^s$$

avec $m_s$ et $l_s$ les couleurs spéculaires de l'objet et de la lumière, $\vx{r}$ le vecteur $\vx{l}$ réfléchi par rapport à la normale $\vx{n}$ et $s$ la brillance de l'objet.

La couleur finale de l'objet sera alors $c_a + c_d + c_s$.

eclairage

2.1 - Gouraud vs. Phong shading

1. Implémentez les shaders phongVertex.vert/frag qui doivent calculer l'éclairage du maillage avec le modèle de Phong et l'interpolation de Gouraud (calcul par sommet) pour une source lumineuse directionnelle.

Remarque : vous pouvez spécifier la direction de la lumière "en dur" dans votre shader, ou permettre à l'utilisateur de la définir interactivement et la passer à votre shader dans une variable uniforme.

Afin de bien comprendre comment est formée l'image finale, écrire le vertex shader pour afficher tour à tour les images constituées uniquement de l'éclairement ambiant, puis diffus, et enfin spéculaire :

gouraud  =  gouraud  +  gouraud  +  gouraud
Gouraud shading ambiant diffus spéculaire

2. Créez les shaders phongFragment.vert/frag implémentant l'interpolation de Phong, c'est-à-dire calculant la couleur en chaque fragment. Pour simplifier le passage d'un shader à l'autre, vous pouvez créer un std::vector et contrôler l'index dans ce vecteur à l'aide du clavier. Attention, lorsque vous changez le shader courant, il vous faut ré-appeler la méthode Shape::init.

Vous aurez besoin de passer les vecteurs $\vx{l}$, $\vx{n}$ et $\vx{v}$ du vertex au fragment shader. Ces vecteurs doivent être normalisés, mais où doit être réalisée cette normalisation ?

Quel(s) changement(s) dans l'éclairage constatez-vous ?

Que pensez-vous du surcoût en terme de calcul qu'implique cette interpolation ?

phong
Gouraud et Phong shading

2.3 - Blinn-Phong

Créez de nouveaux shaders blinn.vert/.frag afin d'implémenter le modèle d'éclairage de Blinn-Phong. Pour cette variante, la composante spéculaire s'obtient avec la formule :

$$c_s = m_s l_s (\max(\vx{n} \cdot \vx{h},0))^s$$

où $\vx{h}$ est le demi-vecteur entre le vecteur vue $\vx{v}$ et celui de la source lumineuse $\vx{l}$. Vous devriez obtenir un résultat équivalent à l'éclairement de Phong en multipliant par quatre l'exposant de brillance $s$.

2.4 - Sources ponctuelles

Finalement, on souhaite pouvoir utiliser des sources lumineuses ponctuelles. Dans le shader blinn.frag, ajoutez une variable uniform vec3 light_view correspondant à la position de la lumière en espace vue. Modifiez ensuite la méthode Viewer::display pour ajouter le vecteur correspondant, et transmettez ses coordonnées au shaders.

Modifiez le shader blinn.frag pour gérer la source de lumière ponctuelle. Cela implique de modifier le calcul du vecteur $\vx{l}$ et de prendre en compte l'atténuation liée à la distance entre la source et la surface.