Sombreado dinámico en 3D

Programando un motor 3D desde cero - Brakeza3D

Sombreado dinámico en 3D

Bienvenid@s de nuevo! Llevo varios meses de parón, tras un par de cambios de trabajo en los que no he podido invertir ni el más mínimo tiempo a Brakeza3D. Afortunadamente ya me encuentro más estabilizado y con muchísimas ganas de retomar este desarrollo!. Así que vuelvo con un plato fuerte: La generación de sombras en tiempo real.

 Existen diversas técnicas para conseguir el efecto de sombreado en tiempo real, hoy voy a presentaros la más sencilla a mi modo de ver: El Shadow Mapping.

Vamos a empezar por diferenciar el sombreado en tiempo real del de las sombras “estáticas” generadas por un proceso de Lightmapping. Recomiendo pegar un vistazo al artículo en el que hablamos de esta técnica para iluminar geometría de forma estática. Para los recién llegados, una imagen vale más que mil palabras:

Recordar que partíamos de una geometría con texturas UV:

Junto con unas texturas que pre-calculan la intensidad de la luz que recibe dicha geometría:

Cuyo resultado final, superpondremos con algún algoritmo que haga el “blending” entre ambos colores.

Como podéis observar la aplicación de esta sencilla técnica de iluminado ya genera sombras/sombreados.

La ventaja del lightmapping es su alta velocidad de procesamiento al estar toda la proyección de luz (y su ausencia, es decir, las sombras), almacenada en una textura, que llevaremos a memoria desde el primer momento. Su principal desventaja es que es estática, es decir, no varía en el tiempo. Pero no podremos apoyarnos en esta técnica para sombrear elementos dinámicos de nuestra geometría, véase, enemigos en movimiento.

¿Cómo nos aproximamos al sombreado en tiempo real?

El sombreado en tiempo real no abre un capítulo de teoría por si mismo, supone ir un paso más allá sobre algunas técnicas que ya hemos implementado en nuestro sistema de iluminación. Dicho de otra forma: no tiene sentido hablar de sombras, sin hablar de luces.

Dicho esto os recomiendo repasar el artículo en el que hablamos de como implementar un sistema de luces desde cero. Resumamos que en un mundo digital disponíamos de tres tipos de luces: la ambiental, las estáticas y las dinámicas.

Vamos a centrarnos en las dinámicas, ya que como ya hemos mencionado las luces estáticas generan sombras estáticas que podemos implementar mediante el LightMapping. Sin embargo, por su naturaleza, no podremos pre-cachear ningún calculo lumínico para una luz dinámica, ya que su posición, color e intensidad puede variar en el tiempo.

¿Qué es una luz y que una sombra?

Una luz viene determinada por su color e intensidad y opcionalmente su dirección, las cuales determinan un color! Este color será mezclado con los colores naturales de la textura que deseamos iluminar y eso es todo!.

Una sombra, supone la ausencia total o parcial de luz, lo cual también determinará un color final! Que también mezclaremos con los colores naturales de la textura.

Recordar que los colores son al fin y al cabo números, con los que podemos realizar aritmética de colores, sumando restando, mezclando etc.

¿Cómo saber si un punto en el espacio está sombreado o iluminado?

Al contrario de lo que pudiera parecer en un principio, resolver esta cuestión no supondrá ahondar en complicados cálculos matemáticos. Por el contrario, reciclaremos una técnica que ya hemos presentado en post anteriores. El Z-Buffer.

Recomiendo consultar el artículo en el que hablamos específicamente del Z-Buffering como técnica de dibujado para la gestión de intersecciones en profundidad.

El Z-Buffer consiste en lanzar un rayo para cada pixel en pantalla (x, y), calculando la profundidad (eje Z), hasta encontrar la colisión más cercana, para la cual almacenaremos su distancia.

Si os paráis a pensar un segundo: Por cada pixel (x, y) de nuestro monitor, hay hipotéticamente infinitos Z “hacia dentro”, la idea es que el Z-Buffer solo se actualice para un pixel (x, y) cuando la distancia sea menor del que ya existiese en dicho buffer. Al terminar de calcular el Z-Buffer y si lo representamos como una imagen, observaremos un degradado, donde las cosas más cercanas estarán más oscuras y la mas lejos más claras. En definitiva habremos obtenido un mapeo de la profundidad desde la cámara hasta la geometría más inmediata.

La generación de un Z-Buffer siempre genera una imagen con degradado:

De igual forma que para el Z-Buffer obteníamos el mapa de profundidad desde el punto de vista de la cámara, para el shadow mapping, obtendremos los mapas de profundidad existentes para cada luz dinámica en la escena

Modificando nuestro rasterizador

Los más veteranos en el blog ya conoceréis la función del rasterizador, ese lugar, donde transformamos los vértices 3D en pixeles 2D. Lo importante es comprender que en el rasterizador ya disponemos de la coordenada (X, Y, Z) del punto que estamos dibujando.

Lo único que necesitamos ahora es preguntarle a cada luz dinámica, si ese punto (X, Y, Z) está iluminado por dicha luz. Para ellos nos apoyaremos en el Z-Buffer generado para dicha luz dinámica. Podremos concluir que si la distancia Z del punto que estamos dibujando hacia su luz es mayor que el dato almacenado en su Z-Buffer, ese vértice no se encuentra iluminado por esa luz. Veamos un ejemplo con una única luz:

Pseudocódigo

Por cada luz dinámica:
   – Generamos su Z-Buffer

En el rasterizador, por cada pixel (x, y):
       Recorremos cada luz dinámica:
        – Si la distancia entre la luz dinámica y el vértice es mayor a la almacenada en su Z-Buffer el pixel tiene sombra
        – Si la distancia entre la luz dinámica y el vértice es igual o menor a la almacenada en su Z-Buffer el pixel está iluminado.

    

Optimizaciones

Como podéis imaginar, generar sombras dinámicas en tiempo real en un motor 3D hecho para CPU es un proceso muy costoso. Por su naturaleza se convierte en un cálculo per-pixel sin mucho margen de optimización más allá de nuestra imaginación. Existen algunas técnicas que pueden mejorar nuestro rendimiento, como el evitar la generación de sombras en cada frame por segundo, limitándolo a un framerate específico.

Otra opción sería la generación de un Z-Buffer de una resolución menor a la de nuestra pantalla, lo que agilizaría los cálculos de estos, pero otorgaría una menor precisión en la sombra generada (generando el típico problema de dientes de sierra en los sombreados que podemos ver en cualquier juego)

 

Resumen

Hoy hemos presentado el ShadowMapping, una técnica con la que podremos afrontar los sombreados generados por luces dinámicas, que sumados a las técnicas ya existentes para sombrear geometría estática, nos aportan todo lo necesario para crear un sistema de iluminación lo más realista posible.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.