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.
Figura 1: De izquierda a derecha: sin RSM, con RSM, diferenciaResultado
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.
Figura 2: Ecuación de iluminancia de un punto con posición xy fuente de luz de n direccional normal de píxeles pEl 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.
OpenglOpenGL 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.
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.
Para | Contra |
Algoritmo fácil de entender. | No son amigos con caché en absoluto |
Se integra bien con el renderizador diferido | Se 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 muestras | FPS para no RSM | FPS para ingenuo RSM | FPS para RSM interpolado |
100 | ~ 700 | 152 | 264 |
200 | ~ 700 | 89 | 179 |
300 | ~ 700 | 62 | 138 |
400 | ~ 700 | 44 | 116 |