El raymarching es una técnica muy potente que permite no solo usarla como base para el renderizado de objetos, sino que también permite crear efectos especiales de diversa índole, especialmente aquellos que involucran el renderizado de volúmenes. Este proyecto lo hice para poder comprender las bases de la técnica de raymarching. Cabe destacar que en este proyecto no se ha tenido especial atención por el rendimiento ni la adaptabilidad del código, sino que es más bien una disección del proceso de raymarching para poder enteder mejor su funcionamiento.

Kernel del proceso de raymarching. Este código se implementa mediante una Material Function en Unreal Engine.

Código principal que se ejecuta en cada Step del marching de rayos.
Iluminación
El algoritmo calcula las iluminación provocada en la nube haciendo marchar un rayo desde cada punto de evaluación del rayo principal en la dirección de los rayos de la luz direccional. Dicho rayo evalúa la densidad en cada paso de su trayectoria, acumulándola en una variable, que servirá para calcular la cantidad de sombra total en dicho punto del rayo. Cuanta más densidad encuentre en su camino, más ocluido estará el punto evaluado.
También se calculan si el rayo está dentro o fuera del bounding box del volumen, para evitar cálculos innecesarios. También se deja de calcular si el valor acumulado pasa de un cierto valor a partir del cual se considera que ya está suficientemente sombreado, para permitir mayor control sobre el sistema.

Nube dibujada mediante Perlin Noise 3D mostrando auto-sombreado.

Código principal del cálculo de acumulación de iluminación.
Sombreado basado en Distance Fields
Hasta ahora estábamos hablando de auto-sombreado, pero la oclusión externa es importante para lograr un renderizado de volúmenes creíble. Para calcular la influencia de otros objetos en el cálculo de iluminación de la nube, es decir, de la proyección de sombras sobre la misma, recurrimos a los DistanceFields que Unreal ya nos proporciona. Para calcularla, empleamos un método similar al del cálculo de la iluminación, pero en este caso en lugar de usar una línea normal, empleamos un cono que se va ensanchando a medida que nos alejamos de punto original de evaluación, de manera que la distancia del objeto con respecto al punto de evaluación aumente la difusión de la sombra (soft shadows).
Cabe destacar que para poder evaluar la distancia al punto más cercano necesitamos trabajar con coordenadas en WorldSpace. Es por este motivo que realizamos la conversión al comienzo de la función.

Se puede observar como solo la luz que entra por la ventana ilumina la nube.

Código principal del cálculo de acumulación de iluminación.
Tratando de disminuir el banding
Como cualquier aproximación al raymarching, al bajar la cantidad de pasos que hacemos en cada rayo para calcular la representación visual de la densidad, comienzan a aparecer bandas dadas por la naturaleza discreta del proceso al tratar de evaluar una entidad continua como es el volumen.
Para mitigar este efecto, utilizo una técnica de dithering espacial y temporal que introduce un pequeño desplazamiento por píxel y por frame en la posición de muestreo a lo largo del rayo. Este jitter rompe la coherencia espacial de los errores de cuantización, convirtiendo el banding en ruido de alta frecuencia que el ojo percibe como una transición más suave. Este ruido residual se ve además atenuado mediante el uso de técnicas de antialiasing temporal o de superresolución temporal, habituales en los pipelines de renderizado de videojuegos modernos.

Código donde se obtiene un valor de dithering.
Absorción de luz no uniforme.
En el código del cálculo de iluminación en RayStep, donde se calcula la energía lumínica acumulada en lightEnergy, la variable rmParams.ShadowDensity es un vector de 3 dimensiones. Por defecto, este vector tiene el mismo valor en sus tres componentes, pero el shader permite aplicar diferentes valores, lo que afectaría a cómo el objeto absorbe diferentes ondas de luz, efecto que se puede observar en la siguiente imagen. Esto otorga mucho más control para que el equipo artístico configure cómo la luz se transmite por el volumen.

Izquierda: vector de absorción (0, 0.7, 1). Derecha: vector de absorción (1, 0.1, 0).