Master Informatique 2017-2018
M3DS



TP 07 : Light et Shadow Map

1  Introduction

L’objectif est d’ajouter un éclairement par pixel et les ombres portées :

2  Eclairement par vertex/éclairement par pixel

Question 1. On travaille pour le moment dans le shader perVertexLighting.vert et perVertexLighting.frag (vous pouvez éditer ces fichiers directement sous Qt : ils se trouvent dans Autres fichiers dans la liste des fichiers du projet à gauche). Pour le moment seul le diffus y est calculé.

Comprenez le calcul de V, N et L (et, bien sûr, quelle est leur signification/rôle), le calcul de l’intensité, le rôle de materialDiffuse et materialAmbient. Constatez également que la position de la source lumineuse est fournie, dans le shader, dans le repère de l’observateur.

Remarque : on ne gère pas l’éclairage "double coté" (i.e. seul le coté de la normale est susceptible d’être éclairé; les objets qui apparaissent sont correctement orientés).

Question 2. Ajoutez l’éclairement spéculaire dans le vertex shader (calcul de R, calcul de l’intensité spéculaire, ajout à la couleur du sommet fColor). Pour la brillance, prenez par exemple 50. Le matériel spéculaire (le ks du cours) est donné par l’uniform materialSpecular.

Testez et constatez le mauvais effet du spéculaire calculé au sommet (on le voit surtout lorsque vous animez la scène en cliquant sur "animate" : l’effet est probant sur la sphère).

Question 3. On utilise à présent un éclairement par pixel (diffus et spéculaire) : travaillez dans les fichiers perPixelLighting.vert et perPixelLighting.frag. Reportez tout le calcul d’éclairement dans le fragment shader perPixelLighting.frag (c’est quasiment du copier coller : il faut interpoler L,V,N en in/out, puis faire le calcul d’éclairement dans le fragment shader avec ces vecteurs interpolées).

Testez (il faut cliquer sur "perPixel Lighting" pour que le tracé s’effectue avec ce shader dans l’application). Comparez le résultat et comprenez l’ensemble. Notamment, pourquoi le sol semble plus obscur avec l’éclairement par vertex ? (répondez dans le Readme.txt).

3  Ombre par Depth Map


Exercice 1. Texture et changement de repères

Lors des tests utilisez click-gauche + mouvement pour orienter la vue : cela est nécessaire pour bien comprendre les effets constatés (vous pouvez également bouger la caméra avec ’q’,’d,’s’,’z’; l’appui sur ’espace’ change le mode de navigation de la caméra).

Question 1. Lorsque vous cliquez sur "Texture Transform" le tracé de chaque objet se fait avec un shader qui correspond aux fichiers "textureTransform.vert" et "textureTransform.frag" (le shader effectuant l’éclairage est désactivé).

L’objectif de ce shader est d’affecter la couleur du fragment avec la couleur d’une texture. Les coordonnées de textures vont être calculées (et non pas directement données par un attribut).

Nous allons calculer ces coordonnées de textures en fonction de la position des sommets (à partir de l’attribut position). L’objectif final sera de calculer les coordonnées de textures correctement pour qu’elles correspondent à la projection d’une diapositive.

Pour le moment, testez le résultat obtenu pour fTexCoord=gl_Position dans le vertex shader (après l’affectation de gl_Position bien sûr).

Constatez et comprenez le résultat : pourquoi l’image est fixe quand on bouge la caméra ? pourquoi uniquement dans le quart haut droit ? pourquoi ne voit on pas la texture sur le fond de la scène ? (répondre dans le Readme.txt).

Remarques :

Question 2. En guise de remarque, les textures peuvent être définies avec répétition ou sans répétition :

Cette répétition est contrôlée en OpenGL, pour la texture courante, avec l’instruction glTexParameter et les constantes GL_TEXTURE_WRAP... (cf man glTexParameter . Dans le squelette, avec la classe Texture fournie, il suffit de faire : _univLille1.wrap(GL_REPEAT) ou _univLille1.wrap(GL_CLAMP_TO_BORDER) (instruction présente dans GLApplication::initialize). Testez ces 2 possibilités et comprenez le résultat.

Question 3. Remettez _univLille1.wrap(GL_CLAMP_TO_BORDER); avant de poursuivre.

Question 4. Dans le fragment shader textureTransform.frag, reportez les coordonnées de fTexCoord.xyz, qui sont donc comprises dans [-1,1], sur [0,1] (notez au passage la division par fTextCoord.w, car en coordonnées homogènes). Testez (la texture doit maintenant correspondre à tout l’écran). Expliquez pourquoi (toujours dans le Readme.txt).

Remarque : la coordonnée de texture z n’est pas encore exploitée, mais elle sera importante dans la suite.

Question 5. Testez (et comprenez) :

  1. fTexCoord=vec4(position,1); (cliquez sur "Animate" pour constater que la texture est fixe par rapport aux objets). Pouquoi ? (Readme.txt).
  2. fTexCoord=positionEye;. Expliquez (Readme.txt). Constatez que ce test correspond à une bonne voie pour l’effet d’une projection d’une diapositive.

Question 6. Le passage des coordonnées de la position des sommets (qui sont donnés au vertex shader dans le repère local) aux coordonnées de textures est, en fait, un changement de repère. On peut donc généraliser ce calcul en indiquant une matrice 4x4 qui représentera le passage MTexturerepère sommets.

Pour l’effet de diapositive recherché il faudra placer le repère de texture par rapport au repère Eye (et non par rapport au repère local des objets). On va donc travailler sur une matrice MTextureEye.

C’est le rôle de l’uniform textureEyeMatrix déjà définie dans le vertex shader textureTransform.vert.

Testez alors la transformation fTexCoord=textureEyeMatrix*positionEye (pour l’instant textureEyeMatrix est affectée à l’identité, donc cela donne le même résultat que l’exemple fTexCoord=positionEye).

Question 7. Dans l’application, l’uniform textureEyeMatrix du shader correspond à l’attribut _textureEyeMatrix (de la classe GLApplication). Son affectation est assurée par le squelette.

Pour modifier sa valeur, allez dans GLApplication::update

et à la suite de _textureEyeMatrix.setIdentity(), faites par exemple _textureEyeMatrix.translate(4,0,0).

Testez et comprenez le changement de repère effectué.

Remarque : attention à l’interprétation. Intuitivement, l’interprétation serait plus aisée avec MEyeTexture (i.e. comment on place la texture par rapport à l’oeil). Mais c’est bien MTextureEye dont on a besoin pour calculer les coordonnées de texture à partir de la position Eye (faire l’analogie avec le placement de la caméra et MEyeWorld).

Question 8. Cumulez ce translate en faisant juste à la suite _textureEyeMatrix.rotate(_moveAngle,0,0,1). Testez et comprenez (il faudra cliquer sur "Animate" ou appuyer sur la touche du clavier <A> à l’exécution pour faire évoluer _moveAngle).

Question 9. Agrandissez 2 fois la texture (m.scale(k) permet de cumuler à droite un changement d’échelle k à la matrice m).

Question 10. Une fois bien compris le principe, il est aisé de simuler un vidéoprojecteur avec la projection d’une diapositive (i.e. projection de la texture).

Le vidéo-projecteur est représenté par l’objet bleu de la scène. Son orientation est contrôlable avec click droit.

Dans le code, le placement du vidéoprojecteur est donné par l’attribut _projectorMatrix, et correspond au passage MWorldProjecteur.

Affectez alors _textureEyeMatrix (toujours au début de GLApplication::update, à la place des réponses précédentes) pour que le repère Texture corresponde au repère du vidéoprojecteur (_textureEyeMatrix doit donc à présent traduire MProjecteurEye).

Remarques : MWorldEye (i.e. placement de la caméra) est donné par _camera.worldLocal() (de type Matrix4); _camera.localWorld() donne le passage inverse MEyeWorld; m1=m2.inverse() donne la matrice inverse de la matrice m2; m3=m1*m2 donne le produit de 2 matrices.

Testez. Le projecteur s’oriente avec click-droit + mouvement souris. Le placement de la texture doit être cohérent avec le mouvement du projecteur. Il manque encore l’aspect projection (la texture est projeté de façon orthogonale pour l’instant).

Question 11. Pour rendre réaliste la projection de la diapositive, il manque la projection perspective. Il suffit de cumuler un frustum au changement de repère précédent. Faites-le (l’appel à Matrix4::fromFrustum(-0.1,0.1,-0.1,0.1,0.1,100), par exemple, vous donne une matrice de projection perspective). Jouez avec les paramètres du frustum pour avoir une projection ni trop petite, ni trop grande (laissez cependant le near=0.1 et le far=100, ce qui aura son importance à l’exercice suivant).

Testez (en manipulant l’orientation du projecteur avec click-droit) : le résultat doit être "réaliste" (semblable à la projection d’une diapositive).

Notez que le projecteur projette dans les 2 sens (retournez le projecteur à 180 degrés par exemple) : dans le fragment shader on peut limiter en affectant la couleur uniquement si fTexCoord.z>=-1 && fTexCoord.z<=1 (i.e. à l’intérieur du volume défini par le frustum en coordonnées normalisées).


Exercice 2. Ombres portées par depth map

Question 1. Pour réaliser les ombres portées (issues du projecteur) nous allons partir des shaders précédents, mais pour conserver l’effet diapositive, nous allons travailler dans les shaders shadowMap.vert et shadowMap.frag.

Copiez-collez le code de textureTransform.vert dans shadowMap.vert (écrasez tout son contenu qui ne servait qu’à ce que le squelette du tp compile et s’exécute) et le code de textureTransform.frag dans shadowMap.frag (idem : écrasez tout le contenu).

Renommez alors sampler2D image1; en sampler2D depthTexture (et donc changez également le texture(image1,...) en texture(depthTexture,...)). depthTexture est en effet l’uniform qui est affecté par l’application pour ce shader. Si vous testez vous devez obtenir une projection blanche (tous les pixels de la texture depthTexture sont à la valeur 1).

Question 2. Dans l’application C++, la texture depthTexture correspond à l’attribut _depthTexture (définie dans GLApplication).

Plutôt que d’initialiser la texture _depthTexture avec une image quelconque (jpg ou autre; comme nous l’avons fait à l’exercice précédent avec UL1-IEEA.png), nous allons l’initialiser avec un tracé OpenGL.

En OpenGL nous pouvons en effet rediriger toutes les commandes de tracé dans une texture (plutôt que dans la fenêtre graphique). Ce principe s’appelle "Render To Texture". Techniquement, la mise en oeuvre s’appuie sur les Frame Buffer Objects. La classe FrameBuffer du squelette permet d’abstraire les détails techniques, et vous disposez d’un membre FrameBuffer _rtt; utilisé ainsi :

Autrement dit tous les pixels de la rtt sont initialisés par le glClear à la couleur blanche (la couleur blanche est fixée par un glClearColor dans initialize).

Vous pouvez visualiser le contenu de _depthTexture en cliquant sur le bouton "Display depth texture (switch)" (s’incruste en bas à gauche; il s’agit simplement du tracé d’un carré texturé avec _depthTexture).

Testez : pour l’instant l’incrustation est blanche (le tracé dans la texture fait par GLApplication::renderToTexture se contente de faire un glClear).

Question 3. Testez dans GLApplication::renderToTexture le tracé suivant (c’est la scène vue depuis la caméra) :

p3d::projectionMatrix=_camera.projectionMatrix(); p3d::modelviewMatrix=_camera.localWorld(); _currentShader=&_perVertexLighting; // les tracés qui suivent se feront avec le shader _perVertexLighting (premier exercice) // on trace toute la scène dans la rtt sauf le projecteur : lightPosition(); drawGround(); drawEarth(); drawObject();

Comprenez l’incrustation obtenue ainsi que la projection de texture obtenue en cliquant sur "Shadow Map" (ce qui active le shader shadowMap). Cliquez avec la souris pour bouger la scène.

Remarque : pour renderToTexture() ni la sphère ni le sol ne sont tracés avec leurs textures (inutile pour l’objectif final).

Question 4. Modifiez le point de vue (i.e. modifiez le p3d::projectionMatrix=... et le p3d::modelviewMatrix=... du GLApplication::renderToTexture) pour obtenir une texture correspondant à la scène vue depuis le projecteur. Testez (si vous mettez le même frustum que pour _textureEyeMatrix, la texture doit se calquer sur les objets de la scène : autrement dit les objets de la scène sont "éclairés" avec la projection de la texture). La projection est maintenant fixe lors du déplacement de la caméra.

Question 5. Dans les questions précédentes, la texture _depthTexture a été utilisée en tant que Color Buffer pour le "Render to Texture". Elle peut également servir de Depth Buffer : il suffit de faire _rtt.rtt(0,&_depthTexture) au lieu de _rtt.rtt(&_depthTexture,0); (instruction dans GLApplication::initialize).

Ainsi, une fois effectué le tracé de GLApplication::renderToTexture(), _depthTexture contiendra les valeurs de profondeur de tous les pixels de la scène (et non plus la couleur). Testez : constatez que l’incrustation et la projection apparaissent blanche. Cela est dû au fait que les valeurs de profondeurs sont stockées dans les composantes rouge/vert/bleu de la texture (valeur identique pour les 3 composantes). Dans l’incrustation, on peut percevoir les objets plus proches en plus sombres (car la profondeur est plus faible).

Remarque : sur certaines configurations l’image peut apparaitre en dégradé rouge plutôt qu’en dégradé de gris (i.e. profondeur stockée sur la composante rouge uniquement).

Pour avoir des valeurs de profondeur un peu plus visibles effectuez les 2 étapes suivantes :

  1. Pour la projection : dans shadowMap.frag mettez comme couleur de sortie : fragColor=1.0-clamp(50.0*(1.0-fragColor),0,1); (formule empirique : permet d’étaler un peu plus les couleurs sur [0,1] pour qu’elles soient un peu plus visibles).
  2. Pour l’incrustation : dans la méthode GLApplication::drawIncrustation(), mettez true en dernier paramètre de p3d::drawTexture (augmentera artificiellement le contraste pour le tracé de l’incrustation).

Re-testez

Question 6. La texture depthTexture contient donc les valeurs de profondeurs de chaque pixel de la scène vue depuis le projecteur. Il reste à exploiter cette texture pour obtenir les ombres portées : dans shadowMap.frag à quoi correspondent texCoord.z et fragColor.r pour le pixel tracé ?

Répondre dans le Readme.txt. Vous pouvez vous appuyer sur le schéma suivant (notamment en BD à quoi correspond fragColor.r ? lors du tracé du pixel BE à quoi correspondent fragColor.r et texCoord.z ?).

Attention : la valeur de profondeur stockée dans la texture est dans [0,1] (window coordinate) alors que la valeur calculée dans texCoord.z est dans [−1,1] (coordonnée normalisée).

Question 7. Modifiez alors shadowMap.frag pour que fragColor soit affecté à la couleur noire vec4(0,0,0,1) si le pixel tracé est dans une ombre (pixel non vu depuis le projecteur) vec4(1,1,1,1) si le pixel tracé est eclairé (pixel vu depuis le projecteur). Testez.

Vous devez constatez l’effet lié aux erreurs de précision (il n’y a pas de correspondance exacte entre 1 pixel projeté sur la texture et 1 pixel de la depth map : il peut donc y avoir un décalage entre les profondeurs calculées et celles stockées).

Question 8. Pour résoudre il suffit de faire le test à une erreur près. Testez par exemple avec texCoord.z-0.001. Vous devez avoir à présent des ombres portées correctes.

Question 9. Pour mixer l’éclairement avec les ombres, on choisit la démarche suivante : la scène est d’abord tracé avec le shader pour l’éclairement (1ère passe), puis "par dessus" on retrace la scène mais cette fois ci avec le shader shadowMap (2nde passe). Les couleurs doivent être mélangées entre elles (utilisation du "blending"; c’est le même principe que pour la transparence constatée sur le tp des BSP; nous détaillerons le blending lors d’un prochain chapitre).

Tout est déjà fait dans le squelette (constatez rapidement l’existence des 2 passes dans GLApplication::draw() avec l’activation du blending). Il reste seulement à donner une "transparence" à l’ombre portée : modifiez shadowMap.frag pour que la couleur éclairée soit vec4(0,0,0,0) (alpha =0, c’est-à-dire complètement transparent : la couleur du pixel tracé n’influencera donc pas la couleur finale) et la couleur d’ombre soit vec4(0,0,0,0.6) (couleur noire légèrement transparente). Testez

Question 10. Un dernier détail : les pixels dont les coordonnées de texture calculées sont en dehors de la depth map apparaissent ombrés. Il suffit de forcer une couleur vec4(0,0,0,0) pour toute coordonnée de texture non comprise dans [0,1]. Testez.

Quelques remarques pour finir :


Ce document a été traduit de LATEX par HEVEA