Mapas de sombras reflectantes: Parte 2 - Implementación

Hola Habr! Este artículo presenta una implementación simple de mapas de sombras reflectantes (el algoritmo se describe en un artículo anterior ). A continuación, explicaré cómo lo hice y cuáles fueron las dificultades. Algunas posibles optimizaciones también serán consideradas.

imagen
Figura 1: De izquierda a derecha: sin RSM, con RSM, diferencia

Resultado


En la Figura 1, puede ver el resultado obtenido con RSM . Para crear estas imágenes, se utilizaron el "Conejo de Stanford" y tres cuadrángulos multicolores. En la imagen de la izquierda puede ver el resultado de la renderización sin RSM , utilizando solo luz puntual . Todo a la sombra es completamente negro. La imagen en el centro muestra el resultado con RSM . Las siguientes diferencias son notables: en todas partes hay colores más brillantes, rosa, inundando el piso y el conejo, el sombreado no es completamente negro. La última imagen muestra la diferencia entre el primero y el segundo, y por lo tanto, la contribución de RSM . Los bordes más estrechos y los artefactos son visibles en la imagen central, pero esto se puede resolver ajustando el tamaño del núcleo, la intensidad de la iluminación indirecta y el número de muestras.

Implementación


El algoritmo se implementó en su propio motor. Los sombreadores están escritos en HLSL, y el renderizado está en DirectX 11. Ya configuré sombreado diferido y mapeo de sombras para luz direccional (fuente de luz direccional) antes de escribir este artículo. Primero, implementé RSM para luz direccional y solo después de agregar soporte para el mapa de sombras y RSM para luz puntual.

Extensión del mapa de sombras


Tradicionalmente, Shadow Maps (SM) no es más que un mapa de profundidad. Esto significa que ni siquiera necesita un sombreador de píxeles / fragmentos para llenar SM. Sin embargo, para RSM necesitará algunos buffers adicionales. Necesita almacenar la posición del espacio mundial, el espacio mundial normal y el flujo (salida de luz). Esto significa que necesita un sombreador de píxeles / fragmentos con múltiples objetivos de representación. Tenga en cuenta que para esta técnica debe cortar el sacrificio de la cara , no el frente.

El uso de los bordes frontales de eliminación de caras es una forma muy utilizada de evitar artefactos de sombra, pero esto no funciona con RSM .

Pasa las posiciones y normales del espacio mundial al sombreador de píxeles y las escribe en los búferes apropiados. Si usa el mapeo normal , calcule también en el sombreador de píxeles. El flujo se calcula allí, multiplicando el material albedo por el color de la fuente de luz. Para la luz puntual , debe multiplicar el valor resultante por el ángulo de incidencia. Para luz direccional, se obtiene una imagen sin sombrear.

Preparación para el cálculo de la iluminación.


Hay algunas cosas que debes hacer para el pasaje principal. Debe vincular todos los búferes utilizados en el pase de sombra como texturas. También necesitas números aleatorios. El artículo oficial dice que necesita calcular previamente estos números y guardarlos en un búfer para reducir el número de operaciones en el pase de muestreo RSM . Dado que el algoritmo es pesado en términos de rendimiento, estoy completamente de acuerdo con el artículo oficial. También se recomienda adherirse a la coherencia temporal (use el mismo patrón de muestreo para todos los cálculos de iluminación indirecta). Esto evitará el parpadeo cuando cada cuadro use una sombra diferente.

Necesita dos números aleatorios de coma flotante en el rango [0, 1] para cada muestra. Estos números aleatorios se usarán para determinar las coordenadas de la muestra. También necesitará la misma matriz que usa para convertir las posiciones del espacio mundial (espacio mundial) en el espacio de sombra (espacio fuente de luz). También necesitará dichos parámetros para el muestreo, que le dará un color negro si muestra más allá de los bordes de la textura.

Pasando la iluminación


Ahora la parte difícil de entender. Le recomiendo que calcule la iluminación indirecta después de calcular la iluminación directa para una fuente de luz particular. Esto se debe a que necesita un quad de pantalla completa para la luz direccional . Sin embargo, para la luz puntual y puntual, por lo general, desea utilizar mallas de una determinada forma con selección para llenar menos píxeles.

En el siguiente código, la iluminación indirecta se calcula para el píxel. A continuación, explicaré lo que está sucediendo allí.

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

El primer argumento para la función es P , que es la posición del espacio mundial (en el espacio mundial) para un píxel específico. DivideByW se utiliza para la división prospectiva necesaria para obtener el valor Z correcto. N es el espacio-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; 

En esta parte del código, se calcula la posición del espacio de luz (en relación con la fuente de luz), se inicializa la variable de iluminación indirecta, en la que se sumarán los valores calculados a partir de cada muestra, y la variable rMax se establece a partir de la ecuación de iluminación en el artículo oficial , cuyo valor explicaré en la siguiente sección.

 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; 

Aquí comenzamos el ciclo y preparamos nuestras variables para la ecuación. Para fines de optimización, las muestras aleatorias que calculé ya contienen desplazamientos de coordenadas, es decir, para obtener las coordenadas UV, solo necesito agregar rMax * rnd a las coordenadas del espacio de luz. Si los UV resultantes están fuera del rango [0.1], las muestras deben ser negras. Lo cual es lógico, ya que van más allá del rango de iluminación.

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

Esta es la parte donde se calcula la ecuación de iluminación indirecta ( Figura 2 ), y también se pesa de acuerdo con la distancia desde la coordenada del espacio de luz hasta la muestra. La ecuación parece intimidante y el código no ayuda a comprenderlo todo, por lo que explicaré con más detalle.

La variable Φ (phi) es el flujo de luz, que es la intensidad de la radiación. El artículo anterior describe el flujo con más detalle.

Escalas de flujo con dos ilustraciones escalares. El primero es entre la normalidad de la fuente de luz (texel) y la dirección de la fuente de luz a la posición actual. El segundo es entre el vector actual normal y el de dirección desde la posición actual hasta la posición de la fuente de luz (texel). Para no hacer una contribución negativa a la iluminación (resulta que si el píxel no está iluminado), los productos escalares se limitan al rango [0, ∞]. En esta ecuación, la normalización se realiza al final, supongo, por razones de rendimiento. Es igualmente aceptable normalizar los vectores de dirección antes de realizar productos escalares.

imagen
Figura 2: Ecuación de iluminancia de un punto con posición xy fuente de luz de n direccional normal de píxeles p

El resultado de este pase se puede mezclar con un backbuffer (iluminación directa), y el resultado será como en la Figura 1 .

Trampas


Al implementar este algoritmo, me encontré con algunos problemas. Hablaré sobre estos problemas para que no pisen el mismo rastrillo.

Muestra incorrecta


Pasé una cantidad considerable de tiempo descubriendo por qué mi iluminación indirecta parecía repetitiva. Las texturas de Crytek Sponza están ocultas, por lo que necesita una muestra envuelta para ello. Pero para RSM no es muy adecuado.

Opengl
OpenGL establece texturas RSM en GL_CLAMP_TO_BORDER

Valores personalizados


Para mejorar el flujo de trabajo, es importante poder cambiar algunas variables con solo presionar un botón. Por ejemplo, la intensidad de la iluminación indirecta y el rango de muestreo ( rMax ). Estos parámetros deben ajustarse para cada fuente de luz. Si tiene un rango de muestreo grande, obtiene iluminación indirecta de todas partes, lo que es útil para escenas grandes. Para obtener más iluminación indirecta local, necesitará un rango más pequeño. La Figura 3 muestra la iluminación indirecta global y local.

imagen
Figura 3: Demostración de la dependencia de rMax .

Pasaje separado


Al principio, pensé que podía hacer iluminación indirecta en un sombreador, en el que considero la iluminación directa. Para la luz direccional, esto funciona porque todavía dibujas un quad de pantalla completa. Sin embargo, para la luz puntual y puntual, debe optimizar el cálculo de la iluminación indirecta. Por lo tanto, consideré la iluminación indirecta como un pasaje separado, que es necesario si también quieres hacer una interpolación de espacio de pantalla .

Caché


Este algoritmo no es compatible con el caché en absoluto. Realiza muestreo en puntos aleatorios en varias texturas. El número de muestras sin optimizaciones también es inaceptablemente grande. Con una resolución de 1280 * 720 y el número de muestras RSM 400, obtendrá 1.105.920.000 muestras para cada fuente de luz.

Los pros y contras


Voy a enumerar los pros y los contras de este algoritmo de cálculo de iluminación indirecta.
ParaContra
Algoritmo fácil de entender.No son amigos con caché en absoluto
Se integra bien con el renderizador diferidoSe requiere configuración variable
Se puede usar en otros algoritmos ( LPV )Elección forzada entre iluminación indirecta local y global

Optimizaciones


Hice varios intentos para aumentar la velocidad de este algoritmo. Como se describe en el artículo oficial , puede implementar la interpolación de espacio de pantalla . Hice esto y rendericé un poco más rápido. A continuación, describiré algunas optimizaciones y haré una comparación (en cuadros por segundo) entre las siguientes implementaciones, usando una escena con 3 paredes y un conejo: sin RSM , implementación ingenua de RSM , interpolada por RSM .

Z-check


Una de las razones por las que mi RSM funcionó de manera ineficiente fue porque también calculé la iluminación indirecta para los píxeles que formaban parte del skybox. Skybox definitivamente no lo necesita.

Muestreo aleatorio de la CPU


El cálculo preliminar de las muestras no solo le dará una mayor coherencia temporal, sino que también le ahorrará tener que volver a calcular estas muestras en el sombreador.

Interpolación espacio-pantalla


Un artículo oficial sugiere usar un objetivo de renderizado de baja resolución para calcular la iluminación indirecta. Para escenas con muchas normales suaves y paredes rectas, la información de iluminación se puede interpolar fácilmente entre puntos con menor resolución. No describiré la interpolación en detalle para que este artículo sea un poco más corto.

Conclusión


A continuación se muestran los resultados para un número diferente de muestras. Tengo algunos comentarios sobre estos resultados:

  • Lógicamente, el FPS permanece alrededor de 700 para un número diferente de muestras cuando no se realiza el cálculo de RSM .
  • La interpolación genera cierta sobrecarga y no es muy útil con un pequeño número de muestras.
  • Incluso con 100 muestras, la imagen final se veía bastante bien. Esto puede deberse a la interpolación, que "difumina" la iluminación indirecta.

Recuento de muestrasFPS para no RSMFPS para ingenuo RSMFPS para RSM interpolado
100~ 700152264
200~ 70089179
300~ 70062138
400~ 70044116

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


All Articles