jueves, 10 de junio de 2010

Sombras nada mas....

Si buscan sombras + directx en la web van a encontrar miles de artículos, ejemplos y código sobre el tema. Los 2 métodos mas comunes son el shadow mapping y el volumen shadow.

El que quiero mostrar ahora es el shadow mapping.
Aca esta todo explicado como se hace:
http://en.wikipedia.org/wiki/Shadow_mapping

Basicamente, el algoritmo dibuja la escena desde el pto de vista de la luz. Pero en lugar de dibujar colores RGB, genera la profundidad. El resultado es un mapa en donde cada punto de la textura representa la distancia entre el ocluder mas cercano y la fuente luminosa. Ese mapa se llama shadow map.
En una segunda etapa se dibuja la escena, y se usa el shadow map para saber si el pixel esta iluminado o esta en la sombra, comparando la distancia a la fuente luminosa con la que esta almacenada en el shadow map.

Es bastante trillado, pero esta bueno dedicarle un tiempo a interpretarlo porque esta herramienta (dibujar desde otro lugar y almacenar informacion en una textura) se usa para todo. Asi hay mapa de normales, mapa de luces, mapas de entorno, y de lo que se les ocurra. (Yo estoy usando un mapa de coordenadas xyz para ponerle texturas a un ray-tracing, y un mapa de “ids” de polígonos para hacer un experimento de oclussion culling).

Cosas interesantes de la implementacion del algoritmo:
- utiliza una textura como rendertarget ( SetRenderTarget)
- necesita crear un stencil buffer manualmente, para asegurarse que tenga el mismo tamaño que el mapa, que no necesaramiente es igual al de la pantalla.
- utiliza el formato de floating point textures, por ejemplo el D3DFMT_R32F en el cual cada texel representa un float de 32bits en el canal rojo. Esto porque se necesita mucha precisión para almacenar la profundidad en el mapa.
- como lo que se esta dibujando son valores de profundidad, hay que tener cuidado con el sampler de la textura. No queremos que el directx le aplique algun filtro standard y nos arruine la informacion.

El sampler deberia ser algo asi:
sampler2D g_samShadow =
sampler_state
{
Texture = ;
MinFilter = Point;
MagFilter = Point;
MipFilter = NONE;
};

Con esto me aseguro que cuando leo un punto, me traiga ese punto, y que no haga un promedio de profundidades (que no tiene significado real)

Lo primero que uno quiere hacer cuando no les anda el shadow map (no les creo si me dicen que les anduvo de una).... es tratar de verlo por pantalla.
Pero eso no va a funcionar asi de simple, porque el pixel shader genera profundidades y no colores que un bmp pueda almacenar. Hay que tocar el pixel shader para que dibuje colores y modificar para que salgan por pantalla. Hay que tener en cuenta que solo hay una escala de 0..255, con lo cual hay que hacer algún artificio para que no se vea todo del mismo color.

Con esas modificaciones hechas asi se ve un shadowmap: esta es la escena desde el pto de vista de la fuente luminosa. La escala de grises representa la profundidad de los puntos, cuando mas oscuros mas lejos estan de la fuenta luminosa.



Y asi se ve la escena con sombras proyectadas:



Anti-Aliasing del ShadowMap
El problema mas groso que hay con el shadow map, es que como toda solución que se basa en texturas, presenta problemas de sampling. Estos se hacen mas evidentes en algunos lugares de la escena cuando la sombra ocupa mucho espacio.
Ejemplo: aca se ve el efecto de "serrucho" en el borde la sombra, eso se debe a que varios pixels de pantalla van a parar al mismo texel del shadow map.





La solucion mas facil es darle mas precisión al shadow map, pero eso en general no resuelve el problema ya que en determinados ángulos se requiere cada vez más precisión. Otra solución standard es hacer una interpolacion bi-lineal, entre los 4 texels mas cercanos. El anti-aliasing del shadowmap no se puede resolver automáticamente como con cualquier otra textura. No se puede aplicar un filtro lineal al propio mapa ya que promediar las profundidades en el mapa no tiene ningún sentido real. Lo que hay que hacer es promediar las muestras en un entorno alrededor del pixel actual en el pixel shader que dibuja la escena.

Este es un ejemplo del pixel shader que toma un cierto radio r (en texels) sobre el shadowmap y genera un total de M= (2r+1)^2 de muestras.

float I = 0; // intensidad total
float r = 2; // radio de muestreo
for(int i=-r;i<=r;++i)
for(int j=-r;j<=r;++j)
I += (tex2D( g_samShadow, CT + float2((float)i/SMAP_SIZE, (float)j/SMAP_SIZE) ) + EPSILON < r =" 2" r ="2">

Como se puede ver, cada muestra puede estar en luz o sombra, con lo cual genera 2 valores posibles (0 o 1), con lo cual la cantidad de posiblidades son 2^M posibilidades. En este ps sencillo todas las muestras tienen el mismo peso, pero habria que ponderar mas las muestras centrales que las periféricas. En la práctica no note diferencia, y asi como esta hecho anda más rapido. (Lo perfecto es enemigo de lo bueno)
La misma imagen con anti-aliasing de muestreo directo con r = 2




Problemas:

- todavia hay un salto discreto entre pixel y pixel, pero eso se puede resolver interpolando bi-linealmente.
- con r =2 es sumamente lento, y se hace exponencialmente mas lento a medida que incrementamos r. Entonces estoy estudiando hacer un muestro variable: tomando solo algunos puntos y si estan todos en 0 o todos en 1 no seguir tomando muestras, si en cambio hay algun punto diferente tomar otros 4 puntos, y asi que la cantidad de muestras depende de la desviación standard.

Otros temas que cuando tenga tiempo los posteo:
El anti-aliasing esta relacionado con las sombras suaves (soft shadows) que a su vez estan relacionadas con fuentes de luz superficiales (a diferencia de las fuentes de luz puntuales que producen sombras precisas (hard shadows)
Con respecto a eso estoy implementando una forma de hacer sombras suaves en base a la distancia entre el pixel y el objeto que produce sombras. La idea es que cuanto mas lejos este el punto del objeto que le produce sombras mas muestras hay que tomar, para que produzca una area de penumbras mayor. A medida que esa distancia se acorta la sombra se tiene que hacer mas compacta (hard)

1 comentario:

  1. Muy bueno el post. Hay mucha informacion junta. Hay que leerlo varias veces. Cada parrafo es un mundo.

    ResponderEliminar