domingo, 20 de junio de 2010

Real Time Radiosity - Detalles de Implementación

IMPLEMENTACION.

La implementación del algoritmo es interesante porque representa un ejemplo de uso de la GPU para una aplicación distinta de dibujar. Si bien el resultado final es un dibujo, en todos los pasos intermedios hay que aplicar diferentes artilugios para usar la GPU como si fuese una CPU: dibujar a texturas que representan matrices, proyectar a dentro de una matriz, etc etc.

Algunas estructuras y definiciones.

La geometría de la escena tiene que estar fuertemente tessellada, ej. los muros lejos de ser 16 triangulos formando una caja, están compuestos de varios cientos de triángulos, y convenientemente distribuidos (*).

Ejemplo de la pared



El cálculo de la ecuación de radiosity se va a resolver en el vertex shader, por lo cual la implementación está orientada al vértice. Las superfices Bi o patchs estan relacionadas con un vértice. (NO CON EL POLIGONO COMPUESTO DE 3 VERTICES)
Entonces, a cada vértice de la escena se le asigna un ID único compuesto de 2 valores : ID = (id_i, id_j)
La estructura de vértices para las mallas, tendrá a su vez el id del polígono, incrustado en una coordenada de texturas:

// Vertex format para mesh
struct VERTEX
{
D3DXVECTOR3 position;
D3DXVECTOR3 normal;
D3DXVECTOR2 texcoord;
D3DXVECTOR2 texcoord_id;
};

Una textura llamada Radiosity Map, se utilizará para almacenar la cantidad de luz acumulada en cada vértice (o sea en cada patch). El texel i,j de la textura esta relacionada con el vértice cuyo ID = (i,j). Como la luz puede tener color, los valores de radiosity son vectores (float3). Y por este motivo el Radiosity Map es una textura D3DFMT_A32B32G32R32F.

Implementación del Primer Paso:

// Bi = Ei + ri Sum (Bj * Fij)
// Bi = Radiosidad de la superficie i
// Ei = Emision superficie i
// ri = reflectividad de la sup i
// Bj = Radiosidad de la superficie j
// Fij = Form factor de la superficie j relativa a la superficie i

// y el form factor se calcula asi:
// si vRayo_ij es el vector que une el pto en la superficie i con la superficie emisora j
// Fij = cos(an_i)*(an_j)/(pi*||vRayo_ij||)*Vij
// an_i = angulo entre la normal de la superficie i y el vector vRayo_ij (an_j = idem j)
// Vij es un factor de visibilidad, que indica si la superficie j que emite la luz
// es visible desde la punto i, y por lo tanto se definie como
// Vij = 1 si j es visible desde i, o 0 caso contrario

// en nuestro caso:
// la division por pi, surge de integrar la ecuacion en el area, y lo eliminamos directamente
// (no queremos un resultado exacto, eso se usa cuando se necesita el valor exacto de
// radicion en un cierto punto)
// de la misma forma la norma de vRayo_ij es muy cara de conseguir y la vamos a desestimar
// (responde a que la intesidad de la luz decrece con el cuadrado de la distancia)

// por otra parte, como no podemos calcular todas las luces se produce que en general
// la imagen queda mas oscura de lo que deberia, y por eso multiplico por 2 la ecuacion
// con lo cual nuestro form factor simplificado queda asi (sin el factor de visibilidad)
// Fij = 2*cos(an_i)*(an_j)
// Este ff depende de la orientacion relativa de ambas superficies


Como esto lo vamos a calcular en el vertex shader, necesitamos que se llame para cada vértice, para ello ponemos el Radiosity Map como RenderTarget y luego dibujamos como puntos TODOS los vertices de la escena. Esto es independiente del punto de vista, y todos los vértices tienen que pasar por el pipeline. Es importante dibujarlos como puntos, para que no se interpole el id, (el id interpolado no tiene ningun sentido)
Por otra parte en el Vertex Shader, tenemos que "redireccionar" la salida para que el resultado caiga en la posición id_i,id_j correspondiente al Id del vértice que se está procesando. Esto se hace generando la posición transformada de esta manera:

oPos.x = 2*iId.x/MAP_SIZE-1;
oPos.y = 1-2*iId.y/MAP_SIZE;
oPos.z = 1;
oPos.w = 1;

Con estos artificios logramos simular en la GPU una matriz común y corriente de la CPU.

Ahora en el VS tenemos casi todos los datos necesarios para calcular el form factor, salvo el factor de visibilidad H(i,j). Aca se presenta el primer problema, ya que no es trivial determinar en este contexto si el vértice que estoy procesando es visible desde la fuente luminosa. Para resolverlo, antes de procesar los vértices vamos a generar un "shadow map".
Luego, en el VS, para evaluar el H(i,j) comparamos con el shadow map. Este cálculo de visibilidad es muy similar a un shadow map standard, solo que se evalúa por vértice en lugar de por pixel (De hecho estoy estudiando implementar este tipo de shadow map como un tema aparte, que sea más rapida y genere soft shadows, cuando se dispone de una escena que ya esta bien tesselada)

La escena luego del primer paso: asi se vería la escena si se corta en el algoritmo en el primer paso. Notar que la parte superior izquierda esta completamente oscura ya que la luz apunta para el otro lado.




Implementación del Segundo Paso:
Luego del primer paso el RMap tiene varios elementos distintos de cero (o sea varias superficies iluminadas), y deberíamos tomar algunos de ellos como nuestras fuentes de luz secundarias. El problema es como tomar una muestra representativa: la práctica parece indicar que hay que tomar los que más energía tienen y a la vez distribuidos uniformemente por todo el cono de luz. Para resolver ámbas cuestiones de un solo paso: se vuelven a dibujar los vértices desde el punto de vista de la luz con un shader especial que almacena en la sálida (otra vez una textura) el id del vértice + un promedio de la energia del mismo. Recordemos que el RMaP, como soporta colores tiene 3 canales de energia. Ahora eso lo resumimos en un mapa llamado RayTracingMap:

void PixRayT( float4 Diffuse : COLOR0,float2 iId : TEXCOORD1,out float4 Color : COLOR )
{
Color.rg = iId;
float4 E = tex2D( g_samRMap, iId/MAP_SIZE);
Color.b = (E.r + E.g + E.b)*0.333;
Color.a = 0;
}

El r,g representa el id del vértice, y el b un promedio de la energía asociada al mismo. Luego habría que dividir el mapa en celdas (tipo grilla) y tomar el máximo (el vértice con mas energía) en cada una de ellas, de esta manera quedarían patch bien representativos de cada región del cono de luz. Hacer eso desde la CPU requiere mucho tiempo, porque hay que copiar la textura a memoria de sistema y procesar gran cantidad de datos. Para resolverlo desde la GPU probamos un metodo similar a los usados en procesamiento de imagenes, que consiste en un pixel shader que toma 4 vecinos como entrada y genera el máximo como salida. En cada paso se reduce en 4 (2 el ancho y 2 el alto) el mapa original, con lo cual en muy pocos pasos nos queda un mapa de 4x4 o de 8x8 con la info de los ids de las luces secundarias listo para leer desde la CPU.
Este algoritmo es muy interesante, ya que usa el artificio de dibujar 2 triangulos que ocupan toda la pantalla, con el objeto de que se llame al pixel shader para procesar toda la imagen.


Estos 2 triangulos representan toda la pantalla:

CALC_MAX_VERTEX vertices[] =
{
{ -1, 1, 1, 0,0, },
{ 1, 1, 1, 1,0, },
{ -1, -1, 1, 0,1, },
{ 1,-1, 1, 1,1, },
};
Y con esta instrucción
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
Se fuerza a que se llame el ps para cada punto de la textura
En el Vertex Shader, usamos este artificio para hacer que la textura de salida quede arriba a la izquierda

double K = DS/RAYTMAP_SIZE;
oPos.x = Pos.x*K-1+K;
oPos.y = Pos.y*K+1-K;


Pixel Shader para calcular el maximo:
//-----------------------------------------------------------------------------
// calcula el maximo de los 4 pixeles vecinos
float DS = RAYTMAP_SIZE/2.0; // este DS se divide por 2 en cada paso sucesivo.
void PixMax( float2 Tex : TEXCOORD0 , out float4 Color : COLOR0 )
{
Color = tex2D( g_samDatos, Tex);
float4 r1 = tex2D( g_samDatos, Tex + float2(1/DS,0));
float4 r2 = tex2D( g_samDatos, Tex + float2(0,1/DS));
float4 r3 = tex2D( g_samDatos, Tex + float2(1/DS,1/DS));
if(r1.b>Color.b)
Color = r1;
if(r2.b>Color.b)
Color = r2;
if(r3.b>Color.b)
Color = r3;
}

Para verlo gráficamente, usamos un RMAP de 256 inicial:



El ultimo mapa tiene 8x8 texels, y corresponde a un maximo de 64 luces secundarias. En la práctica partimos de un rayt de 64 original, y en 2 pasos, llegamos a al de 16, pero eliminamos los bordes, con lo que nos quedan 6 x 6 = 36 posibles patches para analizar.

Nota: Si se tratara de hacer en el pixel shader un par de ciclos for.. anidados, rapidamente se queda sin registros.


Con los ids de las luces secundarias se calcula para cada luz la dirección del rayo reflejado, para poder repetir el primer paso y emitir la energía correspondiente a estos rebotes.
Nota: el nombre de raytracing tiene que ver con que estamos simulando cientos de rayos que salen del pto de vista de luz, no tiene nada que ver con el algoritmo de ray-tracing para generar imágenes, ni con los algoritmos de Gpu-Ray-tracing.

Escena luego de tomar los rebotes primarios. Ya se producen efectos se sombras suaves, y areas de penumbra. Ademas como los rebotes cargan con el color de las paredes, la el techo se iluminado con una leve tonalidad proveniente de las paredes.




Implementación del Tercer Paso:
Para calcular el siguiente rebote, como sólo vamos a generar un único rayo, no conviene hacer un raytracing map, si no que simplemente vamos usar la CPU para calcular la intersección entre el rayo y la escena, usando funciones del directx:

D3DXIntersect(g_pMesh,&pRayPos,&pRayDir[t],&pHit,&face,&pU,&pV,&pDist,NULL,NULL);

Para cada fuente de luz secundaria hay que realizar un par de transformaciones, ya que la función D3DXIntersect trabaja sobre el espacio del objeto y no de la escena. Hay que calcularla para todas las mallas de la escena y luego volver a transformar el resultado al espacio de escena. Sin embargo dado que son pocos rayos (entre 16-64), en esta parte del algoritmo no se genera el cuello de botella.


Representación gráfica de los rebotes. La linea roja representa la dirección de la luz primaria. Las flechas verdes las luces secundarias. Y las azules las terciarias.



Dibujamos algo....?

Hasta ahora todas las salidas fueron a parar a diferentes texturas en memoria, pero todavía no dibujamos nada en pantalla pp dicha. Para dibujar la escena hay que usar un vertex shader que haga un lookup al RMAP para calcular el valor de radiance asociado a dicho vértice. Pero ahora estamos dibujando polígonos pp dichos, reales, que saldrán por pantalla. En el VS el id del polígono se pasa como un TextCoord1, y con ese id se hace el lookup en el RMAP, para calcular el color Difuso. El pipeline standard de la GPU se encarga de interpolar este valor en todo el polígono, con lo cual el pixel shader es trivial.


//-----------------------------------------------------------------------------
// Vertex Shader para dibujar la escena pp dicha
//-----------------------------------------------------------------------------
void VertScene( float4 iPos : POSITION,float2 iId : TEXCOORD1,out float4 oPos : POSITION,out float4 Diffuse : COLOR0)
{
// transformo a screenspace
float4 vPos = mul( iPos, g_mWorldViewProj );
// obtengo el valor de radiance asociado al vertice
Diffuse = tex2Dlod( g_samRMap, float4(iId/MAP_SIZE,0,0));
}

No hay comentarios:

Publicar un comentario