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.
Figure 1: De gauche à droite: sans RSM, avec RSM, différenceRé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.
Figure 2: Équation de l'éclairement lumineux d'un point avec la position x et la source de lumière pixel directionnelle n normale pLe 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é.
OpenglOpenGL 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.
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.
Pour | Contre |
Algorithme facile à comprendre | Pas 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'échantillons | FPS pour No RSM | FPS pour Naive RSM | FPS pour RSM interpolé |
100 | ~ 700 | 152 | 264 |
200 | ~ 700 | 89 | 179 |
300 | ~ 700 | 62 | 138 |
400 | ~ 700 | 44 | 116 |