Cartes d'ombres réfléchissantes: Partie 2 - Mise en œuvre

Bonjour, Habr! Cet article présente une implémentation simple de Reflective Shadow Maps (l'algorithme est décrit dans un article précédent ). Ensuite, je vais expliquer comment je l'ai fait et quels étaient les pièges. Certaines optimisations possibles seront également envisagées.

image
Figure 1: De gauche à droite: sans RSM, avec RSM, différence

Résultat


Dans la figure 1, vous pouvez voir le résultat obtenu à l'aide de RSM . Pour créer ces images, le «lapin de Stanford» et trois quadrangles multicolores ont été utilisés. Dans l'image de gauche, vous pouvez voir le résultat du rendu sans RSM , en utilisant uniquement la lumière spot . Tout à l'ombre est complètement noir. L'image au centre montre le résultat avec RSM . Les différences suivantes sont perceptibles: partout où il y a des couleurs plus vives, le rose, inondant le sol et le lapin, l'ombrage n'est pas complètement noir. La dernière image montre la différence entre la première et la seconde, et donc la contribution de RSM . Des bords et des artefacts plus serrés sont visibles dans l'image du milieu, mais cela peut être résolu en ajustant la taille du noyau, l'intensité de l'éclairage indirect et le nombre d'échantillons.

Implémentation


L'algorithme a été implémenté sur son propre moteur. Les shaders sont écrits en HLSL, et le rendu est en DirectX 11. J'ai déjà configuré l' ombrage différé et le mapping d'ombre pour la lumière directionnelle (source de lumière directionnelle) avant d'écrire cet article. J'ai d'abord implémenté RSM pour la lumière directionnelle et seulement après avoir ajouté le support pour la carte des ombres et RSM pour la lumière spot.

Extension de la carte d'ombre


Traditionnellement, les Shadow Maps (SM) ne sont rien d'autre qu'une carte de profondeur. Cela signifie que vous n'avez même pas besoin d'un shader de pixels / fragments pour remplir SM. Cependant, pour RSM, vous aurez besoin de quelques tampons supplémentaires. Vous devez enregistrer la position de l' espace-monde, la normale de l' espace-monde et le flux ( flux lumineux). Cela signifie que vous avez besoin d'un shader pixel / fragment avec plusieurs cibles de rendu. Gardez à l'esprit que pour cette technique, vous devez couper l' abattage du visage , pas le devant.

L'utilisation des bords avant de l' élimination des faces est un moyen largement utilisé pour éviter les artefacts d'ombre, mais cela ne fonctionne pas avec RSM .

Vous passez les positions et les normales de l'espace-monde au pixel shader et les écrivez dans les tampons appropriés. Si vous utilisez un mappage normal , calculez-les également dans le pixel shader. Le flux y est calculé, en multipliant le matériau albédo par la couleur de la source lumineuse. Pour le spot, vous devez multiplier la valeur résultante par l'angle d'incidence. Pour la lumière directionnelle, une image non ombrée est obtenue.

Préparation au calcul d'éclairage


Il y a quelques choses que vous devez faire pour le passage principal. Vous devez lier tous les tampons utilisés dans le shadow pass sous forme de textures. Vous avez également besoin de nombres aléatoires. L' article officiel indique que vous devez pré-calculer ces nombres et les enregistrer dans le tampon pour réduire le nombre d'opérations dans la passe d'échantillonnage RSM . Étant donné que l'algorithme est lourd en termes de performances, je suis entièrement d'accord avec l'article officiel. Il est également recommandé d'y respecter la cohérence temporelle (utiliser le même motif d'échantillonnage pour tous les calculs d'éclairage indirect). Cela évitera le scintillement lorsque chaque image utilise une ombre différente.

Vous avez besoin de deux nombres à virgule flottante aléatoires dans la plage [0, 1] pour chaque échantillon. Ces nombres aléatoires seront utilisés pour déterminer les coordonnées de l'échantillon. Vous aurez également besoin de la même matrice que vous utilisez pour convertir les positions de l'espace-monde (espace-monde) en espace-ombre (espace source de lumière). Vous aurez également besoin de ces paramètres pour l'échantillonnage, qui donnera une couleur noire si vous échantillonnez au-delà des frontières de la texture.

Éclairage passant


Maintenant, la partie difficile à comprendre. Je vous recommande de calculer l'éclairage indirect après avoir calculé l'éclairage direct pour une source de lumière particulière. C'est parce que vous avez besoin d'un quadruple plein écran pour la lumière directionnelle . Cependant, pour la lumière ponctuelle et ponctuelle, vous souhaitez généralement utiliser des maillages d'une certaine forme avec élimination pour remplir moins de pixels.

Dans le code ci-dessous, l'éclairage indirect est calculé pour le pixel. Ensuite, je vais expliquer ce qui se passe là-bas.

float3 DoReflectiveShadowMapping(float3 P, bool divideByW, float3 N) { float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); } 

Le premier argument de la fonction est P , qui est la position de l'espace-monde (dans l'espace-monde) pour un pixel spécifique. DivideByW est utilisé pour la division prospective nécessaire pour obtenir la valeur Z correcte. N est l'espace normal du monde.

 float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; 

Dans cette partie du code, la position de l'espace lumineux (par rapport à la source de lumière) est calculée, la variable d'éclairage indirect est initialisée, dans laquelle les valeurs calculées à partir de chaque échantillon seront additionnées, et la variable rMax est définie à partir de l'équation d'éclairage dans l' article officiel , dont je vais expliquer la valeur dans la section suivante.

 for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; 

Ici, nous commençons le cycle et préparons nos variables pour l'équation. À des fins d'optimisation, les échantillons aléatoires que j'ai calculés contiennent déjà des décalages de coordonnées, c'est-à-dire que pour obtenir les coordonnées UV, il me suffit d'ajouter rMax * rnd aux coordonnées espace-lumière. Si les UV résultants sont hors de portée [0,1], les échantillons doivent être noirs. Ce qui est logique, car ils vont au-delà de la gamme d'éclairage.

  float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); 

C'est la partie où l'équation d'éclairage indirect est calculée ( figure 2 ), et également pesée en fonction de la distance entre la coordonnée espace-lumière et l'échantillon. L'équation semble intimidante et le code n'aide pas à tout comprendre, je vais donc vous expliquer plus en détail.

La variable Φ (phi) est le flux lumineux, qui est l'intensité du rayonnement. L'article précédent décrit le flux plus en détail.

Flux échelles avec deux illustrations scalaires. Le premier est entre la normale de la source lumineuse (texel) et la direction de la source lumineuse à la position actuelle. La seconde se situe entre la normale actuelle et le vecteur de direction de la position actuelle à la position de la source lumineuse (texel). Afin de ne pas apporter de contribution négative à l'éclairage (il s'avère que le pixel n'est pas éclairé), les produits scalaires sont limités à la plage [0, ∞]. Dans cette équation, la normalisation se fait à la fin, je suppose, pour des raisons de performances. Il est également acceptable de normaliser les vecteurs directionnels avant d'effectuer des produits scalaires.

image
Figure 2: Équation de l'éclairement lumineux d'un point avec la position x et la source de lumière pixel directionnelle n normale p

Le résultat de cette passe peut être mélangé avec un backbuffer (éclairage direct), et le résultat sera comme dans la figure 1 .

Pièges


Lors de la mise en œuvre de cet algorithme, j'ai rencontré quelques problèmes. Je vais parler de ces problèmes pour ne pas marcher sur le même râteau.

Mauvais échantillonneur


J'ai passé beaucoup de temps à comprendre pourquoi mon éclairage indirect semblait répétitif. Les textures de Crytek Sponza sont cachées, vous avez donc besoin d'un échantillonneur enveloppé pour cela. Mais pour RSM, ce n'est pas très approprié.

Opengl
OpenGL définit les textures RSM sur GL_CLAMP_TO_BORDER

Valeurs personnalisées


Pour améliorer le flux de travail, il est important de pouvoir modifier certaines variables en appuyant sur un bouton. Par exemple, l'intensité de l'éclairage indirect et la plage d'échantillonnage ( rMax ). Ces paramètres doivent être ajustés pour chaque source lumineuse. Si vous avez une large plage d'échantillonnage, vous obtenez un éclairage indirect de partout, ce qui est utile pour les grandes scènes. Pour un éclairage indirect plus local, vous aurez besoin d'une plage plus petite. La figure 3 montre l'éclairage indirect global et local.

image
Figure 3: Démonstration de la dépendance rMax .

Passage séparé


Au début, je pensais que je pouvais faire un éclairage indirect dans un shader, dans lequel je considère l'éclairage direct. Pour la lumière directionnelle, cela fonctionne car vous dessinez toujours un quadruple plein écran. Cependant, pour la lumière ponctuelle et ponctuelle, vous devez optimiser le calcul de l'éclairage indirect. Par conséquent, j'ai considéré l'éclairage indirect comme un passage séparé, ce qui est nécessaire si vous souhaitez également effectuer une interpolation écran-espace .

Cache


Cet algorithme n'est pas du tout compatible avec le cache. Il effectue un échantillonnage à des points aléatoires dans plusieurs textures. Le nombre d'échantillons sans optimisation est également trop élevé. Avec une résolution de 1280 * 720 et le nombre d'échantillons RSM 400, vous ferez 1.105.920.000 échantillons pour chaque source lumineuse.

Le pour et le contre


Je vais énumérer les avantages et les inconvénients de cet algorithme de calcul d'éclairage indirect.
PourContre
Algorithme facile à comprendrePas du tout amis avec cache
S'intègre bien avec le rendu différéRéglage variable requis
Peut être utilisé dans d'autres algorithmes ( LPV )Choix forcé entre éclairage indirect local et global

Optimisations


J'ai fait plusieurs tentatives pour augmenter la vitesse de cet algorithme. Comme décrit dans l' article officiel , vous pouvez implémenter l'interpolation espace écran . J'ai fait ça, et rendu un peu plus vite. Ci-dessous, je décrirai quelques optimisations et ferai une comparaison (en images par seconde) entre les implémentations suivantes, en utilisant une scène avec 3 murs et un lapin: sans RSM , implémentation naïve de RSM , interpolée par RSM .

Z-check


L'une des raisons pour lesquelles mon RSM a fonctionné de manière inefficace est que j'ai également calculé l'éclairage indirect pour les pixels qui faisaient partie de la skybox. Skybox n'en a définitivement pas besoin.

Échantillonnage aléatoire du processeur


Le calcul préliminaire des échantillons donnera non seulement une plus grande cohérence temporelle, mais vous évitera également d'avoir à recalculer ces échantillons dans le shader.

Interpolation espace écran


Un article officiel suggère d'utiliser une cible de rendu basse résolution pour calculer l'éclairage indirect. Pour les scènes avec de nombreuses normales lisses et des murs droits, les informations d'éclairage peuvent facilement être interpolées entre des points avec une résolution inférieure. Je ne décrirai pas l'interpolation en détail afin que cet article soit un peu plus court.

Conclusion


Voici les résultats pour un nombre différent d'échantillons. J'ai quelques commentaires concernant ces résultats:

  • Logiquement, le FPS reste autour de 700 pour un nombre différent d'échantillons lorsque le calcul RSM n'est pas effectué.
  • L'interpolation donne une surcharge et n'est pas très utile avec un petit nombre d'échantillons.
  • Même avec 100 échantillons, l'image finale était plutôt bonne. Cela peut être dû à l'interpolation, qui «brouille» l'éclairage indirect.

Nombre d'échantillonsFPS pour No RSMFPS pour Naive RSMFPS pour RSM interpolé
100~ 700152264
200~ 70089179
300~ 70062138
400~ 70044116

Source: https://habr.com/ru/post/fr440570/


All Articles