Mapas de Sombra Reflexiva: Parte 2 - Implementação

Olá Habr! Este artigo apresenta uma implementação simples do Reflective Shadow Maps (o algoritmo é descrito em um artigo anterior ). A seguir, explicarei como fiz e quais foram as armadilhas. Algumas possíveis otimizações também serão consideradas.

imagem
Figura 1: Da esquerda para a direita: sem RSM, com RSM, diferença

Resultado


Na Figura 1, você pode ver o resultado obtido usando o RSM . Para criar essas imagens, foram utilizados o "Stanford Rabbit" e três quadrângulos multicoloridos. Na imagem à esquerda, você pode ver o resultado da renderização sem RSM , usando apenas a luz do ponto . Tudo na sombra é completamente preto. A imagem no centro mostra o resultado com o RSM . As seguintes diferenças são notáveis: em todos os lugares há cores mais brilhantes, rosa, inundando o chão e o coelho, o sombreamento não é completamente preto. A última imagem mostra a diferença entre a primeira e a segunda e, portanto, a contribuição do RSM . Bordas e artefatos mais apertados são visíveis na imagem do meio, mas isso pode ser resolvido ajustando o tamanho do núcleo, a intensidade da iluminação indireta e o número de amostras.

Implementação


O algoritmo foi implementado em seu próprio mecanismo. Os shaders são escritos em HLSL e a renderização é no DirectX 11. Eu já configurei sombreamento adiado e mapeamento de sombra para luz direcional (fonte de luz direcional) antes de escrever este artigo. Primeiro, implementei o RSM para luz direcional e somente depois de adicionar suporte para o mapa de sombras e o RSM para luz pontual.

Extensão do mapa de sombra


Tradicionalmente, o Shadow Maps (SM) nada mais é do que um mapa de profundidade. Isso significa que você nem precisa de um sombreador de pixel / fragmento para preencher o SM. No entanto, para o RSM, você precisará de alguns buffers extras. Você precisa armazenar a posição do espaço no mundo, o espaço no mundo normal e o fluxo ( emissão de luz). Isso significa que você precisa de um sombreador de pixel / fragmento com vários destinos de renderização. Lembre-se de que, para esta técnica, é necessário cortar a seleção do rosto , não a frente.

O uso das arestas frontais da separação de faces é uma maneira amplamente usada para evitar artefatos de sombra, mas isso não funciona com o RSM .

Você passa as posições e normais do espaço mundial para o pixel shader e as grava nos buffers apropriados. Se você usar o mapeamento normal , também calcule-os no pixel shader. O fluxo é calculado lá, multiplicando o material albedo pela cor da fonte de luz. Para a luz do ponto, você precisa multiplicar o valor resultante pelo ângulo de incidência. Para luz direcional, é obtida uma imagem não sombreada.

Preparando para o cálculo da iluminação


Há algumas coisas que você precisa fazer para a passagem principal. Você deve vincular todos os buffers usados ​​na sombra como texturas. Você também precisa de números aleatórios. O artigo oficial diz que você precisa pré-calcular esses números e salvá-los no buffer para reduzir o número de operações no passo de amostragem do RSM . Como o algoritmo é pesado em termos de desempenho, concordo totalmente com o artigo oficial. Também é recomendável aderir à coerência temporal (use o mesmo padrão de amostragem para todos os cálculos de iluminação indireta). Isso evitará oscilações quando cada quadro usar uma sombra diferente.

Você precisa de dois números aleatórios de ponto flutuante no intervalo [0, 1] para cada amostra. Esses números aleatórios serão usados ​​para determinar as coordenadas da amostra. Você também precisará da mesma matriz usada para converter posições do espaço do mundo (espaço do mundo) em espaço da sombra (espaço da fonte de luz). Você também precisará desses parâmetros para amostragem, o que dará uma cor preta se você fizer uma amostra além das bordas da textura.

Iluminação de passagem


Agora a parte mais difícil de entender. Eu recomendo que você calcule a iluminação indireta depois de calcular a iluminação direta para uma fonte de luz específica. Isso ocorre porque você precisa de um quad em tela cheia para luz direcional . No entanto, para luzes pontuais e pontuais, geralmente você deseja usar malhas de uma determinada forma com seleção para preencher menos pixels.

No código abaixo, a iluminação indireta é calculada para o pixel. A seguir, explicarei o que está acontecendo lá.

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); } 

O primeiro argumento para a função é P , que é a posição do espaço do mundo (no espaço do mundo) para um pixel específico. DivideByW é usado para a divisão prospectiva necessária para obter o valor Z correto. N é o espaço do mundo normal.

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

Nesta parte do código, a posição do espaço de luz (em relação à fonte de luz) é calculada, a variável de iluminação indireta é inicializada, na qual os valores calculados de cada amostra serão somados e a variável rMax é definida a partir da equação de iluminação do artigo oficial , cujo valor explicarei na próxima seção.

 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; 

Aqui começamos o ciclo e preparamos nossas variáveis ​​para a equação. Para fins de otimização, as amostras aleatórias que calculei já contêm deslocamentos de coordenadas, ou seja, para obter as coordenadas UV, só preciso adicionar rMax * rnd às coordenadas do espaço de luz. Se os UVs resultantes estiverem fora da faixa [0,1], as amostras devem ser pretas. O que é lógico, pois eles vão além do alcance da iluminação.

  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); 

Essa é a parte em que a equação de iluminação indireta é calculada ( Figura 2 ) e também pesada de acordo com a distância da coordenada espaço-luz à amostra. A equação parece intimidadora, e o código não ajuda a entender tudo, então vou explicar com mais detalhes.

A variável Φ (phi) é o fluxo de luz, que é a intensidade da radiação. O artigo anterior descreve o fluxo com mais detalhes.

Escalas de fluxo com duas obras de arte escalares. O primeiro é entre o normal da fonte de luz (texel) e a direção da fonte de luz para a posição atual. O segundo está entre o vetor corrente normal e o vetor de direção da posição atual para a posição da fonte de luz (texel). Para não fazer uma contribuição negativa à iluminação (se o pixel não estiver aceso), os produtos escalares são limitados ao intervalo [0, ∞]. Nesta equação, a normalização é feita no final, suponho, por razões de desempenho. É igualmente aceitável normalizar vetores de direção antes de executar produtos escalares.

imagem
Figura 2: Equação da iluminância de um ponto com posição xe fonte de luz de pixel direcional n normal p

O resultado desse passe pode ser misturado com um backbuffer (iluminação direta) e o resultado será como na Figura 1 .

Armadilhas


Ao implementar esse algoritmo, tive alguns problemas. Vou falar sobre esses problemas para que você não pise no mesmo rake.

Amostrador errado


Passei um tempo considerável descobrindo por que minha iluminação indireta parecia repetitiva. As texturas da Crytek Sponza estão ocultas, então você precisa de um amostrador para isso. Mas para o RSM não é muito adequado.

Opengl
OpenGL define texturas RSM para GL_CLAMP_TO_BORDER

Valores personalizados


Para melhorar o fluxo de trabalho, é importante poder alterar algumas variáveis ​​pressionando um botão. Por exemplo, a intensidade da iluminação indireta e o intervalo de amostragem ( rMax ). Esses parâmetros devem ser ajustados para cada fonte de luz. Se você possui uma ampla faixa de amostragem, obtém iluminação indireta de qualquer lugar, o que é útil para cenas grandes. Para mais iluminação indireta local, você precisará de um alcance menor. A Figura 3 mostra a iluminação indireta global e local.

imagem
Figura 3: Demonstração da dependência do rMax .

Passagem separada


No começo, pensei em fazer iluminação indireta em um sombreador, no qual considero a iluminação direta. Para luz direcional, isso funciona porque você ainda desenha um quad em tela cheia. No entanto, para luzes pontuais e pontuais, é necessário otimizar o cálculo da iluminação indireta. Portanto, considerei a iluminação indireta uma passagem separada, necessária se você também quiser fazer a interpolação do espaço da tela .

Cache


Esse algoritmo não é amigável com o cache. Ele realiza amostragem em pontos aleatórios em várias texturas. O número de amostras sem otimizações também é inaceitavelmente grande. Com uma resolução de 1280 * 720 e o número de amostras RSM 400, você fará 1.105.920.000 amostras para cada fonte de luz.

Os prós e contras


Vou listar os prós e contras desse algoritmo de cálculo de iluminação indireta.
ParaContra
Algoritmo fácil de entenderNão é amigo do cache
Integra-se bem ao renderizador diferidoConfiguração variável necessária
Pode ser usado em outros algoritmos ( LPV )Escolha forçada entre iluminação indireta local e global

Otimizações


Fiz várias tentativas para aumentar a velocidade desse algoritmo. Conforme descrito no artigo oficial , você pode implementar a interpolação do espaço da tela . Eu fiz isso e processando um pouco mais rápido. Abaixo, descreverei algumas otimizações e farei uma comparação (em quadros por segundo) entre as seguintes implementações, usando uma cena com 3 paredes e um coelho: sem RSM , implementação ingênua do RSM , interpolada pelo RSM .

Z-check


Uma das razões pelas quais meu RSM funcionou de maneira ineficiente foi porque também calculei a iluminação indireta para pixels que faziam parte do skybox. Skybox definitivamente não precisa disso.

Amostragem aleatória da CPU


O cálculo preliminar de amostras não apenas fornecerá maior coerência temporal, como também evitará que você precise recalcular essas amostras no shader.

Interpolação espaço-tela


Um artigo oficial sugere o uso do alvo de renderização em baixa resolução para calcular a iluminação indireta. Para cenas com muitas normais suaves e paredes retas, as informações de iluminação podem ser facilmente interpoladas entre os pontos com menor resolução. Não descreverei a interpolação em detalhes, para que este artigo seja um pouco mais curto.

Conclusão


Abaixo estão os resultados para um número diferente de amostras. Tenho alguns comentários sobre esses resultados:

  • Logicamente, o FPS permanece em torno de 700 para um número diferente de amostras quando o cálculo do RSM não é executado.
  • A interpolação fornece alguma sobrecarga e não é muito útil com um pequeno número de amostras.
  • Mesmo com 100 amostras, a imagem final parecia muito boa. Isso pode ser devido à interpolação, que "borra" a iluminação indireta.

Contagem de amostrasFPS sem RSMFPS para Naive RSMFPS para RSM interpolado
100~ 700152264
200~ 70089179
300~ 70062138
400~ 70044116

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


All Articles