OpenGL Moderne

Tutoriel 16 : textures d'ombre (shadow mapping)

Opengl-tutorial.org

Traducteur

Alexandre Laurent

Les textures d'ombre (shadow maps) sont la technique actuelle (en 2012) pour créer des ombres dynamiques. Le bon point à propos de cela est qu'elles sont relativement faciles à mettre en place. En contrepartie, il est extrêmement difficile de les faire fonctionner correctement.

Commentez

- Titre : OpenGL Moderne

- Auteur : Opengl-tutorial.org

- Traducteur : Alexandre Laurent

- Parution : 8 juillet 2014

- 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 opengl-tutorial.org. 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. Introduction

- II. Textures d'ombre basiques

- II-A. Génération de la texture d'ombre

- II-A-1. Configurer la cible de rendu et la matrice MVP

- II-A-2. Les shaders

- II-A-3. Résultat

- II-B. Utiliser la texture d'ombre

- II-B-1. Shader de base

- II-B-2. Résultat - acné d'ombre

- III. Problèmes

- III-A. Acné de l'ombrage

- III-B. Peter Panning

- III-C. Crénelage

- III-C-1. PCF

- III-C-2. Échantillonnage poisson

- III-C-3. Échantillonnage poisson stratifié

- IV. Aller plus loin

- IV-A. Early bailing

- IV-B. Lumières spots

- IV-C. Lumières ponctuelles

- IV-D. Combinaison de plusieurs lumières

- IV-E. Zone automatique de la lumière

- IV-F. Cartes d'ombres exponentielles

- IV-G. Light-space perspective Shadow Maps

- IV-H. Cartes d'ombres en cascade

- V. Conclusion

- VI. Remerciements

- Navigation

Navigation

Tutoriel précédent : textures de lumière

Sommaire

Tutoriel suivant : les rotations

I - Introduction

Dans le quinzième tutoriel, on a appris à créer des textures de lumière (lightmaps), qui incluent l'éclairage statique. Bien que cela produise de belles ombres, cela ne gère pas les modèles animés.

Les textures d'ombre (shadow maps) sont la technique actuelle (en 2012) pour créer des ombres dynamiques. Le bon point à propos de cela est qu'elles sont relativement faciles à mettre en place. En contrepartie, il est extrêmement difficile de les faire fonctionner correctement.

Dans ce tutoriel, on introduira l'algorithme de base, on verra ses défauts et on implémentera quelques techniques pour obtenir de meilleurs résultats. Sachant qu'au moment de l'écriture (2012), les textures d'ombre sont un sujet de recherche d'actualité, on donnera quelques directions pour vous permettre d'améliorer vos propres textures d'ombre, suivant vos besoins.

II - Textures d'ombre basiques

L'algorithme des textures d'ombre de base se décompose en deux passes. Premièrement, la scène est dessinée à partir de la position de la lumière. Seule la profondeur de chaque fragment est calculée. Ensuite, la scène est affichée comme d'habitude, mais avec un test supplémentaire pour vérifier si le fragment actuel est dans l'ombre.

Le test « être dans l'ombre » est vraiment très simple. Si l'échantillon actuel est plus éloigné de la lumière que la texture d'ombre au même point, cela signifie que la scène contient un objet plus proche de la lumière. En d'autres termes, le fragment actuel est dans l'ombrage.

L'image suivante peut vous aider à comprendre le principe :

II-A - Génération de la texture d'ombre

Dans ce tutoriel, on ne considérera que les lumières directionnelles - lumières qui sont si loin que l'on peut considérer les rayons comme étant parallèles. Ainsi, la génération de la texture d'ombre est effectuée avec une matrice de projection orthographique. Une matrice de projection orthographique est exactement comme une matrice de projection en perspective, sauf qu'aucune perspective n'est prise en compte - un objet sera identique indépendamment de sa distance avec la caméra.

II-A-1 - Configurer la cible de rendu et la matrice MVP

Depuis le quatorzième tutoriel, vous savez comment dessiner une scène dans une texture afin d'y accéder plus tard à partir d'un shader.

Ici, on utilise une texture de profondeur 1024 x 1024 sur 16 bits pour stocker la texture d'ombre. 16 bits sont généralement suffisants pour une texture d'ombre. Libre à vous d'expérimenter d'autres valeurs.

On utilise une texture de profondeur et non pas une cible de rendu pour la profondeur, car on a besoin de l'échantillonner par la suite.

// Le tampon d'image, qui regroupe 0, 1, ou plus de textures et 0 ou 1 tampon de profondeur.
gluint FramebufferName = 0;
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);

// Texture de profondeur. Plus lent qu'un tampon de profondeur, mais vous pouvez l'échantillonner plus tard dans votre shader
GLuint depthTexture;
glGenTextures(1, &depthTexture);
glBindTexture(GL_TEXTURE_2D, depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT16, 1024, 1024, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0);

glDrawBuffer(GL_NONE); // Aucun tampon de couleur n'est dessiné

// Toujours vérifier que le tampon d'image est OK
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;

La matrice MVP utilisée pour dessiner la scène à partir de la lumière est calculée comme suit :

- la matrice de projection est une matrice orthographique incluant tout ce qui est compris dans une boîte alignée aux axes de (-10, 10), (-10, 10), (-10, 20) sur les axes X, Y et Z, respectivement. Ces valeurs ont été choisies pour que toute la partie visible de la scène soit toujours visible ; plus d'informations sur ce point dans la section, voir plus loin ;

- la matrice de vue tourne le monde afin que, dans l'espace caméra, la direction de la lumière soit -Z (aimeriez-vous relire le troisième tutoriel ?) ;

- la matrice de modèle est ce que vous souhaitez.

glm::vec3 lightInvDir = glm::vec3(0.5f,2,2);

// Calcule la matrice MVP à partir du point de vue de la lumière
glm::mat4 depthProjectionMatrix = glm::ortho<float>(-10,10,-10,10,-10,20);
glm::mat4 depthViewMatrix = glm::lookAt(lightInvDir, glm::vec3(0,0,0), glm::vec3(0,1,0));
glm::mat4 depthModelMatrix = glm::mat4(1.0);
glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrix * depthModelMatrix;

// Envoie les transformations au shader actuellement lié, dans la variable uniforme "MVP"
glUniformMatrix4fv(depthMatrixID, 1, GL_FALSE, &depthMVP[0][0])

II-A-2 - Les shaders

Les shaders utilisés durant cette passe sont très simples. Le vertex shader est un shader passant les données au fragment shader qui calcule simplement la position des sommets en coordonnées homogènes :

#version 330 core

// Données d'entrées du sommet, différentes pour toutes les exécutions de ce shader.
layout(location = 0) in vec3 vertexPosition_modelspace;

// Valeurs qui restent constantes pour le modèle entier.
uniform mat4 depthMVP;

void main(){
gl_Position =  depthMVP * vec4(vertexPosition_modelspace,1);
}

Le fragment shader est tout aussi simple : il écrit la profondeur du fragment à l'emplacement 0 (c'est-à-dire dans la texture de profondeur).

#version 330 core

// Données de sortie
layout(location = 0) out float fragmentdepth;

void main(){
// Pas vraiment nécessaire, OpenGL le fait de toute façon
fragmentdepth = gl_FragCoord.z;
}

Le rendu d'une texture d'ombre est généralement deux fois plus rapide qu'un rendu normal, car seule une profondeur en faible précision est écrite, à la place de la profondeur et de la couleur. La bande passante mémoire est souvent le plus grand problème de performance sur les GPU.

II-A-3 - Résultat

La texture ressemble à ceci :

Une couleur sombre signifie un petit z ; donc, le coin supérieur droit du mur est proche de la caméra. Au contraire, le blanc signifie z=1 (en coordonnées homogènes), donc que l'élément est très loin.

II-B - Utiliser la texture d'ombre

II-B-1 - Shader de base

Maintenant, on retourne à notre shader habituel. Pour chaque fragment que l'on calcule, on doit tester s'il se trouve « derrière » la texture d'ombre ou non.

Pour ce faire, on doit calculer la position actuelle du fragment dans le même espace que celui que nous avons utilisé lors de la création de la texture d'ombre. Donc, on doit le transformer avec la matrice MVP habituelle et une seconde fois avec la matrice MVP de la passe de profondeur (depthMVP).

Il y a tout de même une petite astuce. La multiplication de la position du sommet avec la matrice depthMVP donnera des coordonnées homogènes, qui sont comprises entre [-1, 1]. Mais l'échantillonnage d'une texture doit être fait entre [0, 1].

Par exemple, un fragment au milieu de l'écran sera en (0, 0) dans l'espace de coordonnées homogènes. Mais comme il devra correspondre au milieu de la texture, les coordonnées UV devront être (0.5, 0.5).

Cela peut être corrigé en ajustant les coordonnées directement dans le fragment shader, bien qu'il soit plus efficace de multiplier les coordonnées homogènes avec la matrice suivante, qui divise simplement les coordonnées par 2 (la diagonale : [-1, 1] → [-0.5, 0.5]) et les déplace (la ligne du bas : [-0.5, 0.5] → [0, 1]).

glm::mat4 biasMatrix(
0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.5, 0.5, 0.5, 1.0
);
glm::mat4 depthBiasMVP = biasMatrix*depthMVP;

On peut maintenant écrire le vertex shader. Il est identique au précédent, mais on sort deux positions au lieu d'une :

- gl_Position est la position du sommet tel qu'il est vu par la caméra ;

- ShadowCoord est la position du sommet tel qu'il est vu à partir de l'ancienne caméra (la lumière).

// Position de sortie du sommet, dans l'espace clip : MVP * position
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

// Pareil, mais avec la matrice de vue de la lumière
ShadowCoord = DepthBiasMVP * vec4(vertexPosition_modelspace,1);

Le fragment shader est très simple :

- texture(shadowMap, ShadowCoord.xy).z est la distance entre la lumière et l'objet le plus proche ;

- ShadowCoord.z est la distance entre la lumière et le fragment actuel.

Donc si le fragment actuel est plus loin que l'objet le plus proche, cela signifie que l'on se trouve dans l'ombre (du plus proche objet) :

float visibility = 1.0;
if ( texture( shadowMap, ShadowCoord.xy ).z  <  ShadowCoord.z){
visibility = 0.5;
}

On a juste besoin d'utiliser cela pour modifier l'ombrage. Bien sûr, la couleur ambiante n'est pas modifiée, car son but est d'imiter une lumière arrivant même lorsqu'on se trouve dans l'ombrage (ou sinon tout serait complètement noir).

color =
// Ambiante : simule l'éclairage indirect
MaterialAmbientColor +
// Diffuse : "coleur" de l'objet
visibility * MaterialDiffuseColor * LightColor * LightPower * cosTheta+
// Spéculaire : surbrillance réflective, comme un miroir
visibility * MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5);

II-B-2 - Résultat - acné d'ombre

Voici le résultat du code actuel. Évidemment, l'idée générale est présente, mais la qualité est inacceptable.

Regardez chacun des problèmes de l'image. Le code possède deux projets : shadowmaps et shadowmaps_simple : commencez par celui que vous préférez. La version simple est tout aussi laide que l'image ci-dessus, mais plus facile à comprendre.

III - Problèmes

III-A - Acné de l'ombrage

Le problème le plus évident s'appelle acné de l'ombrage :

Ce phénomène est facilement explicable avec une image :

Le « correctif » habituel pour cela consiste à utiliser une marge d'erreur : on n'ajoute l'ombre que si la profondeur du fragment actuel (encore une fois, dans l'espace de la lumière) est réellement loin de la valeur de la texture de lumière. On fait cela en ajoutant un biais :

float bias = 0.005;
float visibility = 1.0;
if ( texture2D( shadowMap, ShadowCoord.xy ).z  <  ShadowCoord.z-bias){
visibility = 0.5;
}

Le résultat est déjà beaucoup plus beau :

Par contre, vous pouvez remarquer qu'à cause de notre biais, l'artefact entre le sol est le mur s'est empiré. Qui plus est, un biais de 0.005 semble trop important pour le sol, mais pas assez pour les surfaces arrondies : quelques artefacts sont toujours visibles sur le cylindre et la sphère.

Une approche commune consiste à modifier le biais suivant la pente :

float bias = 0.005*tan(acos(cosTheta)); // cosTheta est dot( n,l ), réduit entre 0 et 1
bias = clamp(bias, 0,0.01);

L'acné d'ombrage n'est plus là, même sur les surfaces arrondies :

Une autre astuce qui peut ou non fonctionner suivant vos modèles est d'afficher seulement les faces arrière dans la texture d'ombre. Cela force à utiliser une scène spécifique (voir la prochaine section - Peter Panning) avec les murs fins. Mais au moins, l'acné sera sur les surfaces qui sont dans l'ombre :

Lors du rendu de la texture d'ombre, supprimez les faces avant des triangles :

// On n'utilise pas de biais dans le shader, mais à la place on dessine les faces arrière,
// qui sont déjà séparées des faces avec une faible distance
// (si la scène est construite de cette façon)
glCullFace(GL_FRONT); // Suppression des faces avant des triangles -> ne dessine que les faces arrière

Et ensuite, affichez la scène avec un rendu normal (suppression des faces arrière) :

glCullFace(GL_BACK); // Suppression des faces arrière des triangles → dessine seulement les faces avant

Cette méthode est utilisée dans le code, en plus du biais.

III-B - Peter Panning

On n'a plus d'acné d'ombrages, mais on a toujours un mauvais ombrage sur le sol, faisant comme si les murs volaient (d'où le terme « Peter Panning »). En fait, en ajoutant le biais, c'est devenu pire.

Celui-ci est très facile à corriger : évitez tout simplement les géométries fines. Cela a deux avantages :

- premièrement, cela corrige le « Peter Panning » : si votre géométrie est plus profonde que le biais, tout est bon ;

- deuxièmement, vous pouvez réactiver la suppression des faces arrière lors du rendu de la texture de lumière, car maintenant, il y a un polygone du mur qui fait face à la lumière et donc cache l'autre côté, qui n'aurait pas été affiché avec la suppression des faces arrière.

L'inconvénient est que vous avez plus de triangles à afficher (deux fois par image !).

III-C - Crénelage

Mais avec ces deux astuces, vous allez remarquer qu'il y a toujours du crénelage sur le bord de l'ombre. En d'autres termes, un pixel est blanc et le prochain noir, sans même de transition douce entre les deux.

III-C-1 - PCF

La façon la plus simple d'améliorer cela est de changer le type d'échantillonnage de la texture d'ombre en shadow2DShadow. La conséquence est que, lorsque vous échantillonnez une fois, le matériel fera en réalité aussi un échantillonnage des pixels voisins, une comparaison entre eux et retournera un nombre à virgule flottante compris dans [0, 1] avec un filtrage bilinéaire du résultat de la comparaison.

Par exemple, 0.5 signifie que deux échantillons sont dans l'ombre et deux échantillons dans la lumière.

Ce n'est pas la même chose qu'un échantillonnage simple d'une texture de profondeur filtrée ! Une comparaison retourne toujours vrai ou faux ; PCF donne une interpolation de quatre « vrai ou faux ».

Comme vous pouvez le voir, les bordures de l'ombre sont douces, mais la texture d'ombre est toujours visible.

III-C-2 - Échantillonnage poisson

Une méthode simple pour le gérer est d'échantillonner la texture d'ombre N fois au lieu d'une seule. En combinaison avec le PCF, cela peut donner de très bons résultats, même avec un petit N. Voici le code pour quatre échantillonnages :

for (int i=0;i<4;i++){
if ( texture2D( shadowMap, ShadowCoord.xy + poissonDisk[i]/700.0 ).z  <  ShadowCoord.z-bias ){
visibility-=0.2;
}
}

poissonDisk est un tableau constant qui peut être défini comme suit :

vec2 poissonDisk[4] = vec2[](
vec2( -0.94201624, -0.39906216 ),
vec2( 0.94558609, -0.76890725 ),
vec2( -0.094184101, -0.92938870 ),
vec2( 0.34495938, 0.29387760 )
);

De cette façon, suivant le nombre de passes d'échantillonnage de la texture d'ombre, le fragment généré sera plus ou moins sombre :

La constante 700.0 définit la « diffusion » des échantillons. Si la diffusion est trop petite, vous obtenez du crénelage ; si la diffusion est trop importante, vous obtenez des bandes (cette capture n'utilise pas PCF pour accentuer l'effet, mais utilise 16 échantillons à la place).

III-C-3 - Échantillonnage poisson stratifié

On peut supprimer cet effet de bande en choisissant différents échantillons pour chaque pixel. Il y a deux méthodes principales : le poisson stratifié ou le poisson tourné. Le stratifié choisit différents échantillons, la rotation toujours les mêmes, mais avec une rotation aléatoire afin qu'ils semblent différents. Dans ce tutoriel, je vais expliquer seulement la version stratifiée.

La seule différence avec la version précédente est que l'on indexe poissonDisk avec un indice aléatoire :

for (int i=0;i<4;i++){
int index = // Un nombre aléatoire entre 0 et 15, différent pour chaque pixel (et chaque i !)
visibility -= 0.2*(1.0-texture( shadowMap, vec3(ShadowCoord.xy + poissonDisk[index]/700.0,  (ShadowCoord.z-bias)/ShadowCoord.w) ));
}

On peut générer un nombre aléatoire avec une ligne comme celle-ci, qui retourne un nombre entre [0, 1[ :

float dot_product = dot(seed4, vec4(12.9898,78.233,45.164,94.673));
return fract(sin(dot_product) * 43758.5453);

Dans ce cas, seed4 sera une combinaison de i (faisant que l'on échantillonne à quatre emplacements différents) et… quelque chose d'autre. On peut utiliser gl_FragCoord (l'emplacement du pixel sur l'image) ou Position_worldspace :

//  - Un échantillon aléatoire, basé sur l'emplacement du pixel sur l'écran.
//    Pas de bandes, mais l'ombre se déplace avec la caméra, ce qui est étrange.
int index = int(16.0*random(gl_FragCoord.xyy, i))%16;
//  - Un échantillon aléatoire, basé sur la position du pixel dans l'espace monde.
//    La position est arrondie au millimètre pour éviter le crénelage.
//int index = int(16.0*random(floor(Position_worldspace.xyz*1000.0), i))%16;

Cela fera que les motifs de l'image ci-dessus vont disparaître, au détriment d'un bruit visuel. Bien qu'un bruit correctement appliqué soit souvent moins désagréable que ces motifs.

Voir le fichier tutorial16/ShadowMapping.fragmentshader pour les trois exemples d'implémentation.

IV - Aller plus loin

Même avec toutes ces astuces, il reste de nombreuses, très nombreuses méthodes pour améliorer vos ombres. Voici les plus répandues :

IV-A - Early bailing

Au lieu de prendre seize échantillons pour chaque fragment (encore une fois, c'est beaucoup), on prend quatre échantillons distants. S'ils sont tous dans la lumière ou dans l'ombre, vous pouvez sûrement considérer que les seize échantillons auraient donné le même résultat : vous pouvez arrêter tout de suite (bail early). Si certains sont différents, vous êtes probablement sur une bordure d'ombre, donc les seize sont nécessaires.

IV-B - Lumières spots

La gestion des lumières spots nécessite quelques petites modifications. La plus évidente est de changer la matrice de projection orthographique entre une matrice de projection en perspective :

glm::vec3 lightPos(5, 20, 20);
glm::mat4 depthProjectionMatrix = glm::perspective<float>(45.0f, 1.0f, 2.0f, 50.0f);
glm::mat4 depthViewMatrix = glm::lookAt(lightPos, lightPos-lightInvDir, glm::vec3(0,1,0));

Même chose, mais avec une pyramide tronquée de matrice de perspective à la place de celle de la matrice orthographique. Utilisez texture2Dproj pour prendre en compte la division de la perspective (voir les notes de bas de pages du troisième tutoriel sur les matrices).

La seconde étape consiste à prendre en compte la perspective dans le shader (voir les notes de bas de pages du troisième tutoriel sur les matrices). En résumé, une matrice de projection perspective n'a en fait pas besoin d'effectuer de perspective. Cela est effectué par le matériel, lors de la division avec la coordonnée projetée w. Ici, on émule la transformation dans le shader, donc on doit effectuer la division de la perspective. D'ailleurs, une matrice orthographique génère toujours des vecteurs homogènes avec w = 1, faisant qu'elle ne produit jamais de perspective.

Voici deux façons de faire cela en GLSL. La seconde utilise la fonction du langage textureProj, mais les deux méthodes produisent le même résultat.

if ( texture( shadowMap, (ShadowCoord.xy/ShadowCoord.w) ).z  <  (ShadowCoord.z-bias)/ShadowCoord.w )
if ( textureProj( shadowMap, ShadowCoord.xyw ).z  <  (ShadowCoord.z-bias)/ShadowCoord.w )

IV-C - Lumières ponctuelles

Même chose, mais avec une carte cubique (cubemap) de profondeur. Une carte cubique est un ensemble de six textures, une pour chaque côté du cube. De plus, l'accès ne se fait pas avec des coordonnées UV standard, mais avec un vecteur 3D représentant une direction.

La profondeur est conservée pour chaque direction dans l'espace, rendant possible la projection des ombres tout autour de la lumière ponctuelle.

IV-D - Combinaison de plusieurs lumières

L'algorithme gère plusieurs lumières, mais gardez à l'esprit que chaque lumière nécessite un rendu supplémentaire de la scène afin de produire la carte d'ombres. Cela nécessitera une quantité énorme de mémoire lors de l'application des ombres et vous pouvez être très rapidement limité par la bande passante.

IV-E - Zone automatique de la lumière

Dans ce tutoriel, la zone de lumière est produite à la main pour contenir toute la scène. Bien que cela fonctionne dans cet exemple restreint, c'est à éviter. Si votre carte s'étend sur 1 km x 1 km, chaque texel de votre carte d'ombres 1024 x 1024 prendra un mètre carré, ce qui est nul. La matrice de projection de la lumière doit être aussi compacte que possible.

Pour les lumières spots, cela peut être facilement modifié en jouant sur sa portée.

Pour les lumières directionnelles, comme le soleil, c'est plus compliqué : elles illuminent réellement toute la scène. Voici une façon de calculer cette pyramide tronquée :

1. Les Potential Shadow Receivers, ou PSR, sont des objets qui appartiennent en même temps à la zone de lumière, à celle de la vue et à la boîte englobante de la scène. Comme leur nom le suggère, ces objets sont susceptibles d'être dans l'ombre : ils sont visibles par la caméra et la lumière ;

2. Les Potential Shadow Casters, ou PSC, sont tous les Potential Shadow Receivers, plus tous les objets qui se tiennent entre eux et la lumière (un objet peut ne pas être visible mais produire une ombre visible).

Donc, pour calculer la matrice de projection de la lumière, prenez tous les objets visibles, retirez ceux qui sont trop loin et calculez leur boîte englobante. Ajoutez les objets se tenant entre la boîte englobante et la lumière, et calculez la nouvelle boîte englobante (mais cette fois, alignée suivant la direction de la lumière).

Le calcul précis de ces ensembles implique le calcul d'intersections d'enveloppes convexes, mais cette méthode est bien plus facile à implémenter.

Cette méthode provoquera des apparitions soudaines lorsque les objets disparaîtrons de la zone, car la résolution de la texture d'ombre diminuera brusquement. Les textures d'ombre en cascade ne souffrent pas de ce problème, mais sont plus compliquées à implémenter et vous pouvez toujours compenser cela en adoucissant les valeurs sur le temps.

IV-F - Cartes d'ombres exponentielles

Les cartes d'ombres exponentielles tentent de limiter le crénelage en supposant qu'un fragment se situant dans l'ombre, mais proche de la surface éclairée, est en fait « quelque part entre les deux ». Cela est lié au biais, sauf que le test n'est plus binaire : le fragment devient plus sombre lorsque la distance de la surface éclairée augmente.

C'est de la triche, évidemment, et des artefacts peuvent apparaître lorsque deux objets se recouvrent.

IV-G - Light-space perspective Shadow Maps

Les LiSPSM ajustent la matrice de projection de la lumière afin d'obtenir une plus grande précision pour les objets proches de la caméra. C'est très important dans les cas de « duelling frustra » : vous regardez dans une direction, mais la lumière spot « semble » dans la direction opposée. Vous avez une grande précision pour la carte d'ombres près de la lumière, soit loin de vous et une faible résolution proche de la caméra, là où vous en avez le plus besoin.

Par contre, les LiSPSM sont délicates à implémenter. Lisez les références pour des détails d'implémentation.

IV-H - Cartes d'ombres en cascade

Les CSM gèrent le même problème que les LiSPSM mais d'une manière différente. Elles utilisent simplement plusieurs (2-4) cartes d'ombres standards pour les différentes parties de la zone vue. La première gère les premiers mètres, donc vous allez obtenir une bonne résolution pour une petite zone. La prochaine carte d'ombres gère les objets plus loin. La dernière carte d'ombres gère la grosse partie de la scène, mais à cause de la perspective, elle ne sera pas aussi importante visuellement que la zone la plus proche.

Les cartes d'ombres en cascade ont, au moment de l'écriture (2012), le meilleur compromis complexité/qualité. C'est la solution dans bien des cas.

V - Conclusion

Comme vous pouvez le voir, les cartes d'ombres sont un sujet compliqué. Chaque année, de nouvelles variations et améliorations sont publiées, et aujourd'hui, aucune solution n'est parfaite.

Heureusement, les solutions présentées peuvent être mélangées : il est parfaitement possible d'avoir des cartes d'ombres en cascade dans une perspective dans l'espace lumière, adoucie avec le PCF… Essayez d'expérimenter toutes ces techniques.

Comme conclusion, je vous suggère de rester avec les cartes de lumières précalculées autant que possible et d'utiliser les cartes d'ombres seulement pour les objets dynamiques. Et assurez-vous que la qualité visuelle des deux soit équivalente : il n'est pas bon d'avoir un environnement statique parfait et des ombres dynamiques atroces.

VI - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.

Navigation

Tutoriel précédent : textures de lumière

Sommaire

Tutoriel suivant : les rotations