martes, 3 de agosto de 2010

Z-BUFFER Vectorial

El z-buffer es un invento magnífico, especialmente para todos aquellos que alguna vez tuvimos que resolver "a mano" el problema de las caras ocultas, para figuras arbitrarias. Cuando se trata de una superficie cerrada, se pueden ocultar facilmente las caras invisibles si se sabe de antemano en que orden están los vertices(horario o antihorario,pero todos iguales) . Si la proyección mantiene el orden la cara es visible (y si no esta oculta). (ver. Back-Face Culling). Pero para un conjunto arbitrario de caras, que es el caso más común en un escenario, el tema no es nada facil. Una de las formas de hacerlo es dibujando las objetos en orden z: primero los que estan mas alejados del pto de vista. Este algoritmo se llama del pintor. Para poder implementarlo es necesario una estructura especial con los caras, usualmente se utiliza en BSP (busquen binary space partition en el google, que hay cientos de ejemplos). En sintesis: es un despelote.

Todo esto se resuelve sencillamente con el directX activando el zbuffer, y con una resolucion de 24bits para la profundidad, es mas que suficiente para cualquier escenario tipico.

Ahora..si alguna vez intentaron trabajar con z-buffer y transparencias al mismo tiempo, la cosa se vuelve a complicar. Si bien en el directx permite configurar el alpha blending muy facilmente, el z-buffer asi como esta planteado no es compatible con el concepto de transparencia.
Una manera sencilla (y físicamente incorrecta) de resolverlo es dibujar primero todos los objetos opacos con el zbuffer activado, y luego desactivar el z-buffer y dibujar los objetos transparentes. Sin embargo, (y se puede demostrar facilmente) las ecuaciones que definen la transparencia NO SON CONMUTATIVAS, (salvo en el caso improbable que todos los objetos tengan el mismo color). En el caso general, la luz tiene que pasar por todos los objetos transparentes en el orden correcto (desde el más lejano hasta el más cercano) antes de alcanzar el punto de vista. Esto requiere que tengamos que dibujar los objetos transparentes en el orden z, por ejemplo usando una estructura de BSP, justamente lo que habíamos logramos evitar con el zbuffer!! . (El zbuffer surgió para evitar calcular estructuras como el BSP).

La solución que presento a continuacion es una prueba de concepto para extender el zbuffer. El zbuffer "normal" (y el color buffer) se puede pensar como "escalar" en el sentido que cada elemento del mismo contiene un valor único, que representa la profundidad, usualmente el valor de z que corresponde al punto más cercano al near plane.
Lo que necesitamos para implementar un alpha blending correcto, es que el zbuffer tenga todos los puntos al mismo tiempo, para poder ordenarlos por profundidad y asi generar la transparencia. Es decir un z-buffer "vectorial" y de ahi el titulo del post. La idea es simular este comportamiento "vectorial" de la siguiente manera:

1- Usar una textura auxiliares (con formato F32R) que va a representar el zbuffer, la vamos a llamar DepthMap.
2- Otra textura auxiliar (con formato RGB) va a representar el color buffer.
3- En el primer paso se dibuja toda la escena con el zbuffer activado, con un shader especial, para que grabe la profundidad en el depth map y el color en el colorbuffer. Este paso puede requerir 2 pasadas y 4 texturas, dependiendo de la implementación, pero esto no viene al caso. (Por ejemplo si no se usan multiples render targets). Luego de este paso, cada punto del depthMap y del ColorBufer contiene la profundidad y color respectivamente, del punto MAS cercano al near plane.
4- En el segundo paso, se vuelve a dibujar la escena, tambien con el z-buffer activado, pero con un pixel shader especial que hace uso del depthmap previamente generado y de la posibilidad que se introduce en el shader 3.0 de la semantica DEPTH. Esta permite que el pixel shader genere una profundidad como salida. (Siempre me pregunte para que podria llegar a servir eso...). El ps pregunta si la profundidad del pixel actual, es menor o igual a la que esta previamente grabada en el DepthMap. Si es asi, eso indica que ese punto ya fue "procesado", su color esta en el ColorBuffer y su profundidad en el DepthMap, Aprovechando que el ps puede cambiar la profundidad y que este punto no lo queremos volver a procesar, el ps asigna la profundidad en -1, haciendo al pto invisible. Y permitiendo que el directx ubique otro punto en dicho lugar. Si la profundidad por el contrario es mayor, se trata de un pto diferente, asi que devuelve la misma profundidad, para que el ztest sea el standard, y el color, combina el color actual con el que esta grabado en el colorbuffer, es decir hace un alpha blending a manopla.
Luego de este punto, en el dephmap esta grabado el SEGUNDO punto mas cercano al near plane, y en color buffer, la combinacion de ambos.
Este paso hay que volver a repetirlo una cierta cantidad de veces, hasta que finalmente no hay mas cambios en el depthmap y en el color buffer. En cada paso avanzamos "un lugar" hacia el farplane.

Para poder verlo graficamente, modifique el algoritmo para que borre (Clear) el colorbuffer en cada paso, asi se puede ver que puntos son visibles en cada paso, como el plano va avanzando desde el pto mas cercano hasta los mas lejamos.



Uniendo todos los colorBuffers:




Este es el PS que dibuja la escena en el ColorBuffer

void RenderScenePS( VS_OUTPUT In,float2 vpos:VPOS,out float4 Color:COLOR0,out float Depth:DEPTH)

{

   float prof = In.depth.x / In.depth.y; // prof del punto actual

   // pos = pos en el colorbuffer (0,1)

   float2 pos = float2(vpos.x/DX,vpos.y/DY);

   float prof_ant = tex2D(g_samDepthMap, pos).r;

   Color = 0;

   Depth = -1;

      if(prof>prof_ant+EPSILON )

      {

         // punto visible

         float4 scr_color = tex2D(g_samColorBuffer, pos);

         float4 dest_color = tex2D(MeshTextureSampler, In.TextureUV) * In.Diffuse;

         Color = scr_color + dest_color*kt;

      }

}




El ps que genera el depth map es muy similar, solo que en lugar del color devuelve la profundidad. Si se usaran MRT no haria falta 2 ps separados.



El algoritmo tiene 3 problemas importantes:
- El orden en que se obtienen los distintos mapas es inverso, primero el que esta mas cerca y por ultimo el que esta mas lejos. Eso es asi aproposito, primero probe de hacerlo al reves y me encontre con otro problema peor que no pude resolver. El problema que esten inversos se resuelve con un poco de algebra, al aplicar la ecuacion de alpha blending en cada paso, pero basicamente, el primer paso tiene mas peso que el segundo, y asi sucesivamente. Los ultimos pasos son los que menos contribuyen a la imagen final.
- No se sabe bien cuantos pasos hay que hacer, si bien "teoricamente" el algortimo se detiene cuando no hay cambios entre un paso y el siguiente, es muy costoso verificar eso en una textura de 1000 x 700. Por ese motivo decidi que el orden sea desde el near plane hacia el far plane, asi la cantidad de pasos la puedo dejar fija, y listo, total como los ultimos pasos son los que menos contribuyen, no seria tan grave perderlos. Si lo hiciera al reves, ahi si estaria en problemas...
- Hay serios problemas de redondeo, de momento hay que preguntar siempre con algun EPSILON puesto muy a manopla, con lo cual estoy trabajando para resolver estos problemas de inestabilidad del algoritmo.