TD6 : Ombres portées

L'objectif de ce TP est de mettre en place la technique de shadow mapping pour le calcul des ombres portées dans le cas des sources lumineuses directionnelles. Cette technique utilise deux passes de rendu. Il faut tout d'abord calculer une carte de profondeur par lampe (la shadow-map) de la scène en effectuant un rendu dans un FBO pour chacune d'elles.

La scène doit ensuite être rendue depuis la caméra. Pour chaque lampe, la profondeur des fragments obtenus selon ce point de vue doit être comparée à celle du depth buffer du FBO correspondant afin de déterminer si la lampe est, ou non, masquée. La difficulté principale est de se placer dans le bon espace pour effectuer ces comparaisons de profondeur. Je vous conseille de convertir la profondeur du fragment depuis le repère de la vue de la caméra vers le repère écran de la lampe (cf. schéma).

transformations

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_tp6.zip

1. Dans un premier temps, vous ne considérerez qu'une seule source directionnelle. Ajoutez une nouvelle caméra shadowCam au Viewer qui correspondra à cette lampe. Ajustez sa position et ses paramètres pour visualiser l'intégralité de la scène depuis le point d'illumination choisi.

shadowmap   shadowmap
Rendu depuis la lampe et shadow-map correspondante

2. Effectuez maintenant le rendu de la scène dans la shadow-map. Pour cela, créez un FBO et initialisez-le à une résolution arbitraire, par exemple 512 x 512. Activez le FBO, réglez le viewport à la même taille que la shadow-map, n'oubliez pas de faire un glClear et dessinez la scène depuis le point de vue de shadowCam en utilisant les shaders simple.vert/frag. Pour vérifier le résultat obtenu, affichez le depth buffer du FBO (inspirez-vous du TD 5, partie 2).

3. Ajoutez une variable uniform sampler2D shadow_map au shader blinn.frag et envoyez-lui le depth buffer du FBO. L'objectif est désormais de calculer les coordonnées permettant d'accéder au point de la shadow-map correspondant au fragment courant. Pour cela, il vous faut reproduire les étapes de projection effectuées par OpenGL lors du rendu de la shadow-map :

  1. application des transformations pour passer les sommets de l'espace objet à l'espace de la shadow-map (en passant par l'espace scène et l'espace vue de la source lumineuse),
  2. division perspective,
  3. remapping des coordonnées obtenues comprises dans l'intervalle $[-1;1]$ dans l'intervalle $[0;1]$ (ce qui correspond aux coordonnées de texture et au depth buffer).

La shadowCam est constituée de deux matrices : l'une permet de passer de l'espace scène à l'espace lumière et la deuxième permet d'effectuer la projection dans l'espace de la shadow-map (en coordonnées homogènes normalisées dans l'intervalle $[-1;1]$). Leur combinaison permet donc de passer de l'espace modèle au repère écran de la lampe (cf. schéma).

Assignez la multiplication de ces deux matrices à une nouvelle variable uniform mat4 shadow_matrix et utilisez-la pour transformer la position du fragment courant de l'esapce monde à l'espace de la shadow-map. Faites aussi la division perspective et le remapping. Pour vérifier le résultat, affichez simplement la shadow-map sur les objets comme une texture. Si tout va bien, elle devrait être alignée sur les objets de la scène.

4. Il ne vous reste plus qu'à utiliser la shadow-map pour calculer les ombres. Ajoutez dans le fragment shader la comparaison des profondeurs. En fonction du résultat de cette comparaison, modifiez le shading pour faire apparaître les ombres. (Le résultat obtenu va souffrir de nombreux artefacts, c'est normal.)

ombres

5. La shadow-map n'est qu'une approximation discrète de la distance entre la scène et la source lumineuse, ce qui engendre de nombreux artefacts. Pour les corriger, il faut ajouter un biais lors de la comparaison des profondeurs. Cela peut être fait de plusieurs façons, à vous d'expérimenter en choisissant parmi les méthodes suivantes (ou un mélange de celles-ci) :

  • Un décalage de la coordonnée $z$ peut être directement intégré au shader lors de l'accès à la shadow-map.
  • Lors du rendu de la shadow-map, les profondeur peuvent être modifiées à l'aide de la fonction glPolygonOffset(factor, units). Pour que cela fonctionne, il faut activer (à l'aide de glEnable) l'état GL_POLYGON_OFFSET_FILL.
  • Pour finir, la profondeur peut être biaisée à l'aide de la méthode glDepthRange qui spécifie quelle portion du depth-buffer est utilisée.
Dans tous les cas, faites attention à désactiver le biais lors du rendu de la scène finale.

6. Réglez les paramètres de filtarge de la shadow-map à GL_LINEAR pour les paramètres de mignification et de magnification. Doublez également la résolution du FBO. Constatez l'impact sur le rendu.

ombres

7. Pour gérer plusieurs sources lumineuses, il est nécessaire de répéter les étapes précédentes pour chaque lampe. Afin d'accumuler la contribution des sources, activez le blending en mode additif après le rendu des shadow-maps, et fixez le depth-test à GL_EQUAL. Itérez alors sur toutes les lampes en effectuant le rendu principal depuis la caméra.

Extensions possibles : Pour les sources ponctuelles, il vous faut faire le rendu de la shadow-map dans une cube-map. Pour anti-aliaser les ombres et les rendre un peu plus douces, vous pouvez implémenter la méthode de Percentage Closer Filtering.