viernes, 19 de noviembre de 2010

GPU-RayTracing

Alla por mediados de los 90, hice mi primer intento de escribir un ray-tracing. Estaba tratando de obtener imagenes “realistas” de muebles, para un software de cálculo. Algunos años antes de tener acceso a internet, habia leido sobre el algoritmo en un libro de trucos graficos 3d en GWBasic.
El raytracing es uno de esos algoritmos que en la teoria son tan fáciles que dan miedo:

http://es.wikipedia.org/wiki/Raytracing

y cuando uno los quiere implementar en la practica....si no se trabaja mucho sobre el tema, o la calidad es pobre o tarda 2 horas en dibujar una imagen. (o las 2 cosas)

El tema me hace acordar a esos algortimos para calcular numeros primos, que pueden ir desde unos cuantos for anidados y que tarde lo que tarde, hasta series de Rieman que no las entiende ni Riemann.


El raytracer que escribi se basaba en soportar la mayor cantidad de primitivas: esferas, elipses, conos, toros, y otras superficies cuarticas. En ese momento los triangulos no me parecian figuras muy atractivas, y le dedique mas esfuerzo a solucionar las ecuaciones de intereccion entre el rayo y superficies mas complicadas como toros.

LOAD TEXTURE piso4
LOAD TEXTURE piso3
LOAD TEXTURE marmol5
LOAD TEXTURE hela
WORLD <27000,27000,5000>
SHADOWS OFF
LOOK FROM = <3000,4200,2100>
LOOK AT = <0,0,500>
VUP = <0,0,1>
FIELD OF VIEW = 0.9
LIGHT = <3000,3200,2000>
SCREEN = 600,400
DEFINE OBJECT COPA <100,100,100>
SURFACE = 0.5,0.9,0
COLOR = 128,128,192
TEXTURE = -1,0,0
OBJECT CIRCLE <-50,-50,0> , <50,-50,0>, <-50,50,0>
OBJECT CILINDER <0,0,0> , <0,0,50> , 10
OBJECT ELIPSOID <0,0,100> , <50,50,75> , <-75,0>
END OBJECT

............
............

OBJECT SILLA <1200,1500,0> , <300,300,1000> , <0,0,0>
OBJECT APARADOR <1600,0,1300>, <300,1000,700>, <90,0,0>
OBJECT APARADOR <0,1500,1300>, <300,1000,700>
OBJECT APARADOR <0,0,1300>, <300,900,700>
OBJECT EXTRACTOR <1600,10,1300> , <500,400,800>
OBJECT MICROONDAS <300,10,800>, <500,400,400> , <0,0,0>
END OBJECT

OBJECT COCINA <0,0,0>, <5000,5000,2300>


Ray-tracing viejo:






Desafortunadamente en el mundo real (especialemente el de los muebles) no hay muchos toros, y si hay triangulos (o rectangulos) por todos lados.

Unos cuantos años mas tarde quiero retomar el tema pero desde otro punto de vista. La idea es implementar el ray tracing en la GPU, en lo que seria un ejemplo de usar la GPU para proposito general. (Vean CUDA en la pagina de nvidia)

Si bien todavia el raytracing esta lejos de competir con un rasterizer en aplicaciones de real time, hay algunas ventajas interesantes, especialmente con algunas geometrías.
Por ejemplo el caso de una esfera: para representarla en un raytracing se puede usar sola la posicion y el radio. Y luego, la precision de la misma es “ilimitada”, en el sentido que por mas que nos acerquemos la precision siempre va a ser la misma.
Para lograr lo mismo con un rasterizador, habria que transformar la esfera en triangulos, cientos de ellos. Y por mas triangulos que usemos, siempre que nos acerquemos lo suficiente se veran los triangulos.


Paradojicamente lo que queremos hacer es implementar el ray-tracing en la GPU. Que ventaja pueda tener? Mientras que no se si va a ser mejor o peor que el mismo algoritmo en la CPU hasta que no lo pruebe efectivamente (y dudo que lo haga) si hay 2 cuestiones por las cuales la GPU puede llegar a ser convieniente:
- Por el alto grado de paralismo que se puede lograr, siendo que cada rayo es completamente independiente de todos los demas, es un proceso ideal para correr como un shader en la gpu que los va a ejecutar varios al mismo tiempo.
- Una vez que se tiene el pto de intereccion se puede usar la funcion del shader tex2d para texturizar y evitarse programar los filtros de texturas por software.



Estrategia.

La idea es meter el algoritmo de interseccion entre el rayo y todos los objetos de la escena en un Pixel Shader. El PS recibira como parametro la direccion del rayo, y tendra que devolver el objeto mas cercano con el cual intersecta.

Como almacenamos los datos de los objetos.
El PS tendra que acceder a todos los objetos para ir calculando intersecciones, con lo cual vamos a usar una (o muchas) texturas para guardar la info de los mismos. Para la esfera necistamos 3 float para la pos y un float para el radio. Pero tambien necesitamos el tipo de objeto, y algunos objetos tienen mas datos, paradojicamente los triangulos tienen 3 puntos, lo que da 9 float.

Ejemplo,
creo una textura de un cierto tamaño suficientemente grande, de formato float.

g_pd3dDevice->CreateTexture( OBJ_SIZE,OBJ_SIZE,
1, D3DUSAGE_DYNAMIC,
D3DFMT_A32B32G32R32F,
D3DPOOL_DEFAULT,
&g_pTexObj,NULL);
g_pEffect->SetTexture( "g_txObj", g_pTexObj);




Luego, tengo que inicializarla con los datos de los objetos pp dichos:

D3DLOCKED_RECT lr;
g_pTexObj->LockRect( 0, &lr, NULL, 0);
BYTE *bytes = (BYTE *)lr.pBits;
FLOAT *texel = (FLOAT *)(bytes+t);
// formato del texel (4 bytes por canal)
// | R | G | B | A | = 128 bits
texel[0] = 3; // pos X
texel[1] = 2; // Pos y
texel[2] = 0; // Pos Z
texel[3] = 0.5; // Radio
// etc etc etc




Como hacemos que se ejecute el PS.
Como hay que trazar un rayo desde el punto de vista hacia cada pixel de pantalla, eso nos sugiere dibujar 2 triangulos que ocupen toda la pantalla, y usar la pos. en pantalla para calcular la direccion del rayo, y asi se llamara el PS para cada uno de ellos.


El PS (resumido) queda asi:

void PS_RayTracing( float4 Diffuse:COLOR0, float2 Tex : TEXCOORD0,
out float4 Color : COLOR0)
{
// Calculo la direccion del rayo u Obtengo la direccion del rayo
float3 D = normalize(g_vViewDir + g_vDx*(2*Tex.x-1) + g_vDy*(1-*Tex.y)).rgb;
float R = 100000;
float3 Ip = 0;
int objsel = -1;

// recorro todos los objetos
for(int t=0;t<cant_obj;++t)
{
// tomo los datos de la esfera
float4 obj = tex2D( g_samObj, loat2(j+0.5,i+0.5)/OBJ_SIZE);
float3 esfera = obj.xyz;
float radio = obj.w;

// verifico la interseccion entre el rayo y la esfera
// si intersecta, y en una distancia menor a la actual (R)
// calculo el pto de interseccion
Ip = LF+D*t1; // punto de interseccion
R = tl;
objsel = t;

// paso al siguiente objeto
}

// si intersecto con algun objeto: tomo el color correspondiente
// al punto donde intersecto:
if(objsel!=-1)
// intersecto en la esfera objsel
// calculo mapping u-v de la esfera etc etc
// tomo el color de la textura del objeto
color_obj = tex2D( g_samDef, float2(u,v));
else
color_obj = color_fondo; // no toca contra nada


// devuelve el color del pixel
Color.rgb = color_obj;
Color.a = 1;
}







Como se puede ver, el algoritmo es bastante sencillo. Sin embargo la logica esta “forzada” para adaptarse a la GPU (como siempre que se quiere programar algo en la GPU que no sea dibujar triangulos).

Algunos de los problemas:
Si bien eso puede variar de GPU en GPU, (lo importante es el concepto), haciendo pruebas hay un cierto limite en la cantidad de objetos que se pueden evaluar en el PS. Por ejemplo con unos 100 objetos no hay problema, pero si fuesen 1000 objetos no soporta esa cantidad de lineas el PS (Los for....se explotan en varias lineas, no es como la CPU)
Eso nos sugiere que, al igual que en la CPU, hay que implementar una estructura jerarquica para organizar los objetos en el espacio, de tal forma de mantener la cantidad de intersecciones que se evaluan en cada PS en no mas de 100.
A diferencia de la CPU, aca tenemos que ver meter esta estructura en texturas, y dividir el proceso en pasos intermedios e ir almacenando los resultados parciales en texturas auxiliares.
Pero eso, para la proxima....


Poniendo todo junto:
En la primera prueba, vamos a usar solo 100 objetos, todos esferas, a modo de prueba.





No hay comentarios:

Publicar un comentario