domingo, 27 de junio de 2010

Simular un Object Shader

Con el Direct10, se introduce el concepto de object shader, que permite generar nuevas primitivas en el pipeline. Esto puede ser útil para el tesselado "on the fly", para efectos especiales de ropas, pelo, etc, donde se necesita un numero variable de polígonos que se puedan controlar durante la ejecución del pipeline.

Estoy trabajando con la idea de simular de alguna manera ese comportamiento en el DX9: es decir generar polígonos en tiempo real, sin usar el object shader. La motivación es que el VertexBuffer reside en la memoria física de la GPU, donde se ejecuta el Shader, entonces porque no acceder a él directamente?

Bueno, aca presento una manera de hacerlo, usando la posibilidad de acceder a texturas desde el VertexShader, (vertex texture), que se introdujo con la version 3.0 del modelo de shaders. (Anteriormente el VS no podia acceder a las texturas).

La instrucción que permite hacerlo es :

tex2Dlod(s, t)

(El algoritmo de Radiosity que presente en el post anterior hace uso intensivo de esto, ya que trabaja en el VS. )

Estrategia:

Se crea un VertexBuffer genérico que no tiene informacion de los vertices (ni posición, ni color, ni textura, nada). El vértice sólo tiene un ID o nro de vértice, que se implementa como una coordenada de texturas.
Este ID apunta a una texel dentro de una textura de "vértices" que se crea con tamaño suficiente para alojar a todos los vértices que se necesiten. Cada vértice tiene asociado un texel en dicha textura, que contiene las coordenadas x,y,z reales del vértice. Un valor de z =-1 indica que el vértice no esta siendo usado, ya que estamos hablando de un número dinámico de vertices o primitivas.
Para generar los vértices un shader especial renderiza directamente sobre la textura. Y el Vertex Shader que dibuja la escena, en lugar de tomar la posición como es habitual, del stream del VertexBuffer, solo usa el id, para ir a buscar la posición real a la textura.
Al estar el Vertex Buffer apuntando a una textura, y el VS dibujando sobre dicha textura, se esta simulando una especie de "dibujar sobre el Vertex Buffer", que es lo mismo que generar polígonos directamente desde la GPU.


Implementación:

Así definimos los vértices:



struct VERTEX_VIRTUAL

{

   D3DXVECTOR2 texcoord_id;

};

#define D3DFVF_VERTEX_VIRTUAL (D3DFVF_TEX1)





Lo único que tienen los vértices es un id. (Raro no?)

Para crear el VertexBuffer:



int cant_vertices = MAP_SIZE*MAP_SIZE;

g_pd3dDevice->CreateVertexBuffer( cant_vertices*sizeof(VERTEX_VIRTUAL),0,

D3DFVF_VERTEX_VIRTUAL,D3DPOOL_DEFAULT, &g_pVB, NULL);

VERTEX_VIRTUAL* pVertices;

g_pVB->Lock( 0, cant_vertices*sizeof(VERTEX_VIRTUAL), (void**)&pVertices, 0 );





Despues hay que generar los ids en forma correlativa:



int id_i = 0;

int id_j = 0;

for(int i =0;i<cant_vertices;++i)

{

  pVertices[i].texcoord_id.x = id_i++;

  pVertices[i].texcoord_id.y = id_j;

  if(id_i>=MAP_SIZE)

  {

    id_i = 0;

    ++id_j;

  }

}





Y hay que crear una textura para albergar los datos de los vertices pp dichos:



g_pd3dDevice->CreateTexture( MAP_SIZE, MAP_SIZE,1, D3DUSAGE_RENDERTARGET, D3DFMT_A32B32G32R32F,D3DPOOL_DEFAULT,&g_pTexVB,NULL);



Ojo: la textura tiene que ser del formato D3DFMT_A32B32G32R32F, porque el VS solo puede leer este tipo de textura, ademas necesitamos cierta precision para almacenar coordenadas.

Para generar los polígonos necesitamos que se llame un PS para cada texel de la textura. Para eso lo mismo de simpre, dibujamos 2 triángulos que ocupan toda la pantalla.
Ejemplo: Este PS simula un ObjectShader, y genera un único triangulo:



void PSGenerarTriangulos( float2 Tex : TEXCOORD0 , out float4 Color : COLOR0 )

{

  Color.xy = 0;

  Color.w = 1;

  Color.z = -1; // x defecto el vert. no es visible

  int id_i = Tex.x*MAP_SIZE;

  int id_j = Tex.y*MAP_SIZE;

  int t = id_j*MAP_SIZE + id_i;

  int nro_triangulo = t/3;

  if(nro_triangulo==0)

  {

    int nro_vertice = round(fmod(t,3));

    if(nro_vertice==0)

    {

      Color.x = -1;

      Color.y =  1;

    }

    else

    if(nro_vertice==1)

    {

      Color.x = 0;

      Color.y = 0;

    }

    else

    {

      Color.x = 1;

      Color.y = 1;

    }

    Color.z = 1;

  }

}






Desde la CPU, en cada frame hacemos asi:
1- Hay que generar los triangulso, como habiamos dicho, dibujamos los 2 triangulos:



g_pEffect->SetTechnique( "GenerarTriangulos");

LPDIRECT3DSURFACE9 pOldRT = NULL;

g_pd3dDevice->GetRenderTarget( 0, &pOldRT );

LPDIRECT3DSURFACE9 pSurf;

g_pTexVB->GetSurfaceLevel( 0, &pSurf );

g_pd3dDevice->SetRenderTarget( 0, pSurf );

g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, 0, 1.0f, 0 );

UINT cPass;

g_pEffect->Begin( &cPass, 0);

g_pEffect->BeginPass( 0 );

g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );

g_pEffect->EndPass();

g_pEffect->End();

// Restauro el render Target

g_pd3dDevice->SetRenderTarget( 0, pOldRT );






2- Ahora que tengo los triangulos cargados en la textura, dibujo la escena pp dicha:




g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0 );

g_pEffect->SetTechnique( "Draw");

g_pd3dDevice->SetStreamSource( 0, g_pVB, 0, sizeof(VERTEX_VIRTUAL) );

g_pd3dDevice->SetFVF( D3DFVF_VERTEX_VIRTUAL );

g_pEffect->Begin( &cPass, 0);

g_pEffect->BeginPass( 0 );

// dibujo la cantidad maxima de polígonos, la mayoria no se va madar mucho mas adelante del VS, en el pipeline

// ya que el z=-1 hara que no sea visible.

g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, MAP_SIZE*MAP_SIZE/3);

g_pEffect->EndPass();

g_pEffect->End();




Ahora, desde el VS de dibujar la escena, hay que tomar la pos. del vertice, accediendo a la textura:
Notar que el VS NO recibe ninguna posicion, pero si la devuelve, aca esta la posta:



void VS_Draw(float2 iId: TEXCOORD0,out float4 oPos : POSITION)

{

  // voy a buscar la pos. del vertice a la textura de datos

  oPos = tex2Dlod( g_samVertexBuffer, float4(iId/MAP_SIZE,0,0));

}




Y asi sale el triangulito por pantalla :





Escribir el PS que genere los triangulos NO es nada trivial, y ahi esta toda la complicacion del implementar esta idea para algun caso concreto. Ya que no se puede poner un if(nro_triangulo==n) para todo n...
Y la logica queda inversa: usualmente uno parte del triangulo, genera sus 3 vertices, y luego guarda en memoria esos 3 vertices, correlativos, y un triangulo al lado de otro, en el stream del VertexBuffer.
El problema que aca estamos "al reves", en el PS tengo la posicion del byte o del vertice en el stream del VertexBuffer, y tengo que hacer el camino inverso para determinar que triangulo seria el que genero el vertice que va a ser guardado ahi. O sea un kilombo. Sin embargo se puede resolver. Aca muestro un ejemplo, de algo mas concreto, es un shader que toma una textura previamente grabada de un bmp, y genera un triangulo para cada pixel en la textura, cuya pos x,y corresponde al ubicacion dentro del bmp, y la profundidad corresponde al color. Esto es la base para implementar un heigh-map dinámico, pero eso lo voy a presentar en el próximo post: la idea es que la precision del height map se adapte a la distancia al punto de vista.



void PSHeightMap( float2 Tex : TEXCOORD0 , out float4 Color : COLOR0 )

{

  Color.xy = 0;

  Color.w = 1;

  Color.z = -1; // x defecto el vert. no es visible

  int id_i = Tex.x*MAP_SIZE;

  int id_j = Tex.y*MAP_SIZE;

  int t = id_j*MAP_SIZE + id_i; // pos en el VertexBuffer

  int offset = round(fmod(t,12));

  int nro_triangulo = offset/3;

  int nro_vertice = round(fmod(t,3));

  // pos. en el heightmap

  int pos = t/12;

  int i = pos/HMAP_SIZE;

  int j = round(fmod(pos,HMAP_SIZE));

  if(i<HMAP_SIZE && j<HMAP_SIZE)

  {

    float h = tex2Dlod( g_samDatos, float4((float)i/HMAP_SIZE,(float)j/HMAP_SIZE,0,0)).r;

    if(nro_triangulo==0)

    {

      if(nro_vertice==0)

      {

        Color.x = (float)i/HMAP_SIZE;

        Color.y = (float)j/HMAP_SIZE;

      }

      else

      if(nro_vertice==1)

      {

        Color.x = (float)(i+1)/HMAP_SIZE;

        Color.y = (float)(j+1)/HMAP_SIZE;

      }

      else

      {

        Color.x = (float)(i+1)/HMAP_SIZE;

        Color.y = (float)(j-1)/HMAP_SIZE;

      }

      Color.z = h;

    }

  }

  Color.x = 2*Color.x - 1;

  Color.y = 1 - 2*Color.y;

}




Y asi sale por pantalla (para los que quieren ver algun dibujito...mucho código para poca cosa parece no?) Lo bueno esta abajo de la superficie (en z<0? )


hacking depth-buffer

Me preguntaron como se podía acceder al depth-buffer directamente desde un shader. La verdad que siempre que necesite el valor de z, por ejemplo para un shadow map, lo tenia guardado en una textura, previamente generada.
Sin embargo, porque no poder acceder directamente al depth-buffer? aunque sea solo para lectura, si de alguna manera esta disponible.

Buscando, encontre este truco en el manual de programación de la placa. Desafortunadamente es especifico de la placa, en este caso de la nvidia serie 7, y no es algo del directx. El truco es asi, para poder hacerlo, el driver de la placa expone una textura especial que contiene los datos del depth-buffer, engañando asi al directX. Para poder hacerlo, el formato de la textura se crea con una macro FOURCC y el siguiente codigo "no documentado" :


pd3dDevice->CreateTexture(ancho, alto, 1,D3DUSAGE_DEPTHSTENCIL,
(D3DFORMAT)MAKEFOURCC('R','A','W','Z'),
D3DPOOL_DEFAULT, &m_DepthBufTexture, NULL);

El codigo RAWZ funciona para la GeForce serie 6 y 7. Una vez creada esta textura se comporta como cualquier otra textura. Por ejemplo desde un shader se puede acceder al valor del z asi:

(1)
float z = dot(tex2D(DepthBufSampler, Tex).arg,
float3(0.99609375,0.0038909912109375,0.000015199184417724609375));

De donde sale semejante formula?
El tema es asi, el rawz significa que el driver expone la memoria tal como esta, sin convertila a ningun valor, el depth buffer tiene que tener el formato de 24bits para el z-buffer y 8 para el stencil. El z-buffer se almacen en los primeros 3 bytes en el A,R y G, y el stencil se almacena en el B.
A,R y G van de 0 a 1, el A es el de mayor peso, lo que nos lleva a esta formula:
z = A*255/256 + R*255/256/256 + G*255/256/256/256
que es justamente la formula (1). (Ya se imaginan los problemas de redondeo que debe tener esta fórmula no?)

Y esto anda? Bueno me anduvo barbaro hasta que quise probar otra forma de hacerlo, que con el código mas nuevo: INTZ, que dice que expone directamente el valor de Z, en cualquier de los 3 canales, con lo cual no habría que hacer ninguna conversión, y chau problemas de redondeo. Tambien dice que requiere el nuevo driver de nvidia, asi que me lo baje.
A partir de eso no me anda ni el RAWT que si andaba y se me cuelga el panel de control de la nvidia (entre otras cosas....) (tendria que haber guardado el driver original no? )
Mas alla de eso, es interesante como usan una textura con un código mágico para exponer al directx una característica no standard. Buscando un poco por la red encontré que esto es comun en casi todas las placas y hay otras características que se pueden acceder de esta manera.

domingo, 20 de junio de 2010

Real Time Radiosity - Problemas y Soluciones

Algunos Problemas y como se fueron resolviendo.

Anti-Aliasing del Shadow Map.
Si acercamos la vista a la sombra de una de las piedras, vemos que la se puede notar un efecto "serrucho". Sin embargo este error no es introducido por el shadow map (cosa que se puede comprobar agrandando o achicando el tamaño del mapa sin ningun efecto en el mismo), si no que esta producido por el tesselado.




Para resolverlo seguimos 2 caminos.

- Primero cuanto mas tesselado este la escena menos visible sera el error. Pero como estamos muy limitados con la cantidad de poligonos y pacthes a evaluar, no es una buena solucion. La disposición del tesselado tambien repercute senciblemente en el error:




El tesselado de la izquierda es muy superior a la de la derecha, es preferible tener menos cantidad de polígonos totales pero distribuidos centralmente como en la izquierda, que un tessellado mas fino, pero con una distribución como el de la derecha. Con esa distribución si la linea de sombras fuera dialogonal en menos 45 grados, el efecto del error sería muy visible.

De contar con un el un object shader (directx10) se podria resolver el problema directamente en el shader generando un tesselado en el momento, de acuerdo a si hay diferencias de iluminacion marcadas entre los 3 vertices.

- Aplicar un algoritmo de anti-aliasing modificado:

// anti-aliasing del shadowmap
// I = intensidad de luz
float I2 = 0;
float r = 1;
float FP = 2;
for(int i=-r;i<=r;++i)
for(int j=-r;j<=r;++j)
I2 += (tex2Dlod( g_samShadow,
float4(auxCoord + float2((float)(i*FP)/SHADOWMAP_SIZE, (float)(j*FP)/SHADOWMAP_SIZE),0,0) )
+ EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;

I2 /= (2*r+1)*(2*r+1);
I = I*0.2 + I2*0.8;

El algoritmo toma un cierto radio r alreder del punto i,j del shadow map para promediar los distintos valores (que pueden ser cero o 1 si esta o no en la sombra). Sin embargo, como estamos trabajando con polígonos y no con pixels, hay que introducir un factor, (lo llamamos factor de penumbra = FP), que permite alejarse mas del punto inicial del shadow map. De no introducir este factor, habria que tomar un radio enorme para obtener algun efecto. El un gran factor FP produce un efecto de penumbra. De todas formas en algoritmo de radiosity usualmente no nos preocupamos por este error del shadow map, porque la sombra se va a suavizar por si sola, en la medida que sigamos evaluando mas rayos, y como consecuencia todas las sombras tienden a ser suaves en cualquier solución de radiosity. Sin embargo, como en nuestro algoritmo estamos tomando muy pocos rayos, se logra un buen efecto haciendo un anti-aliasing SOLO del primer rayo:




En estos ejemplos solo se toma el primer rayo para ver bien el efecto del anti-aliasing. En la práctica con todos los rebotes se llega a un buen balance con r = 1, y FP=2.
Ejemplo con r = 1, y evaluando todos los rayos, el error original ademas se ve suavizado con tonalidades rojizas que provienen de la pared.




- Intensidad de la luz en cada Rebote.

Si el algoritmo fuese físicamente correcto no habría ningun problema con la intensidad de luz. Pero al simplificar las ecuaciones, y evaluar solo cierto rayos etc etc, el algoritmo esta precisa ciertos valores que se introducen artificialmente y que si no estan corrrectos pueden producir errores extraños.
La fuente de luz inicial parte con una intensidad i0, los rebotes primarios i1, y los secundarios i2. Tiene que haber un balance entre la cantidad de rayos totales y las intensidades relativas.




Afortundamente estos valores se pueden aproximar ya que son directamente proporcionales a la cantidad de rayos. Y luego se pueden ajustar por cuestiones estéticas o subjetivas.

Velocidad y Discontinuidad en el tiempo.

El algoritmo estandard funciona aproximadamente a 2fps usando 40 rebotes promedio, y menos de 1fps usando 60 rebotes promedio. Para mejorar la performance se usa la siguiente técnica: en lugar de usar un solo mapa de radiosity, se usan 3 mapas, uno para cada paso del algoritmo y se divide el algoritmo en 4 pasos de carga similar. En cada frame solo se calcula uno de los pasos:
paso 1: se procesa la 1er luz con el antialiasing del shadow map.
paso 2: se calculan las luces secundarias.
paso 3: se procesan las luces secundarias.
paso 4: se generan las luces terciarias y se procesan.
En cada paso se procesa el input y se dibuja la escena sumando los valores de los 3 mapas:

Diffuse = tex2Dlod( g_samRMap, float4(iId/MAP_SIZE,0,0))
+ tex2Dlod( g_samRMap2, float4(iId/MAP_SIZE,0,0))
+ tex2Dlod( g_samRMap3, float4(iId/MAP_SIZE,0,0));

Los primeros frames el algoritmo dibuja resultados parciales, pero una vez que llenaron todos los mapas, en cada paso dibuja una solución que es una mezcla de la solución actual + 2 soluciones anteriores.
Esto ademas resolvió otro problema que tiene que ver con una discontinuidad en el tiempo al mover las fuentes luminosas o los objetos de la escena. Una mínima diferencia en la fuente luminosa puede producir un bruzco cambio de toda la solución, al caer por ejemplo en otro patch diferente. De esta manera al promediar siempre 3 soluciones se minimiza ese efecto a la vez que se llega a velocidades de entre 6 y 10 fps.



Rebotes con color.

Uno de los efectos del radiosity es que la luz al rebotar contra un objeto, sale de un color preciso, con lo cual los objetos son bañados por diferentes colores provenientes del la escena y no solo por la luz blanca original.
En la implementacion el color de cada objeto esta previamente almacenado (no esta tomando el color exacto donde rebota, pero no seria un impedimento generarlo offline).
Para visualizarlo más graficamente se modificó el algoritmo para la primera luz sea roja pura, las luces secundarias sean verdes, y las terciarias azules:




Se ve claramente como la pared donde pega la primer luz se ve en un tono rojo intenso, luego rebota la luz de color verde, que llena la pared izquierda y el piso de un tono verde. El efecto de las luces terciarias es mucho mas leve, pero se puede notar en la base de las columnas y en el techo.

Ejemplo con 30 rayos entre 13 y 14 fps y el mismo ejemplo con 60 rayos aprox 6 fps:

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));
}

Real Time Radiosity - Intro

Dicen que una vez Sábato (que además de escritor, ensayista y pintor aficionado, es físico) estaba dando una charla de relatividad general a unos ingresantes a ingeniería. Luego de la charla les preguntó, si habian entendido, a lo cual se hizo el silencio... Bueno, dijo, no se hagan problema, vamos a simplificar la teoría original para que se comprenda mejor, y volvió a explicar, esta vez en términos mas simples. Al finalizar la exposición volvió a preguntar si habían entendido y de nuevo el mismo silencio.. Entonces, simplificó aún mas la teoría, y al finalizar y preguntarle a los alumnos si esta vez habían entendido, todos dijeron: si profe, ahora si entendimos. Les dijo: bueno, eso que vimos NO es la relatividad general.

No le hagán caso al título del post, lo que voy a presentar no es un algoritmo de radiosity, y lo real-time es discutible.
Pero si es un algoritmo de iluminación global y que funciona lo suficientemente rápido para usarse en un entorno interactivo. Como el muy largo, lo voy a meter en varios post, en la medida que tenga tiempo.




INTRODUCCION

Para una descripcion detallada del algoritmo de radiosity:
http://en.wikipedia.org/wiki/Radiosity_(3D_computer_graphics)

La radiosidad es la cantidad de energía que sale de una superficie x unidad de tiempo, que se puede expresar con esta ecuación :





Bi es la radiosidad de la superficie i. El primer término esta relacionado con la luz que emite la superficie. El segundo término representa la cantidad de luz que refleja proveniente de cualquier otra superficie y depende de la orientacion relativa de ambas superficies, se expresa con un factor llamado Form Factor o View Factor. Este factor es puramente geométrico y no varía si la escena y fuentes de luz son estáticas, lo que motivó a numerosos algoritmos para pre-calcular la radiosidad (Ver: PRT Engine = Pre-computed Radiance Transfer).

Ecuación del form factor
El Form Factor entre 2 superficies i,j representa la fracción de la energia total que sale de la superfice i y llega a la superfice j.

En la práctica el form factor esta multiplicado por un factor de visiblidad H(i,j), que es 1 si la sup. i es visible desde la sup j o cero si no les.

La implementación estandard de esta ecuación asume que la superficie i es lo suficientemente pequeña para tomarse como un punto y las integrales se aproximan como una sumatoria. Luego se introducen algunas restricciones, usualmente se asume que la energia se mantiene constante dentro de una habitación o caja.
La solución del algoritmo requiere calcular para cada par de superficies i, j (i!=j) el form factor Fi,j. Esto genera un sistema de ecuaciones lineales expresado como una matriz llamada "full matrix radiosity solution".

Usualmente esta matriz se resuelve en forma progresiva, en lo que se llama: Progressive Radiosity Algorithm. En cada interacción se selecciona un patch i, se calculan los form factors Fij para todo j. Luego para cada superfice j se actualiza la cantidad de energia recibida del patch i (usando los Fij previamente calculados) y finalmente se pone en cero la cantidad de energia de la superficie i. Esto se repite para cada patch. Hay varios algoritmos que resuelven esto desde la GPU, utilizando una aproximacion por hemicubos para calcular los Fij. Para ello en cada paso se dibuja la escena desde el pto de vista de cada patch i utilizando una proyeccion sobre el hemicubo. Luego se suma la cantidad de energia que se almacena en alguna estructura auxiliar. Este proceso se repite para cada pach, y al finalizar se puede presentar la escena calculada hasta ese momento. En el primer paso el unico patch que tiene energia es la fuente luminosa. Al dibujar desde cada patch, solo los patch que "pueden" ver la fuente luminosa sumarán energia. En el segundo paso, la fuente luminosa ya habrá vaciado su energía, pero ahora varios patchs tendran energía acumulada, que ahora llegara a otros patch. En cada paso se refina la solución progresivamente.

-------------------------------------

Inspirados por el algoritmo de radiosity queremos implementar una versión (sumamente) simplificada para que se pueda utilizar en tiempos interactivos (entre 3 a 10 fps). El algoritmo está diseñado para aplicaciones usualmente off-line (diseño de interiores, arquitectura, etc) , en las que se requiere pre-visualizar el modelo de iluminación en el momento de diseño como paso previo, antes de obtener un render de calidad fotográfica.
La implementacion stadard parte de dibujar todo desde cada superficie, para poder presentar la primer escena primero hay que terminar de dibujar cada uno de los patch, con lo cual para los tiempos que queremos lograr no parece factible con el hardware actual y la cantidad de patchs que usualmente necesitamos.

-------------------------------------
ALGORITMO:

Paso 1: Fuente de Luz Principal.
Partimos de la fuente luminosa j y para cada superficie i que es alcanzada por j calculamos el Form Factor actualizando luego la cantidad de energia de j que va a ir a parar a i. En este primer paso ya podemos dibujar algo: el resultado corresponde a tomar solo la iluminación directa sin tener en cuenta ningún rebote. Las superfices que están en areas iluminadas tendrán una cierta intensidad de luz y las demás estarán totalmente oscuras.

Paso 2: Rebotes primarios.
En el segundo paso tenemos varios (cientos o miles) de superficies iluminadas, que por ende, se convierten en fuentes luminosas secundarias. Como no podemos repetir el mismo proceso para todas ellas, tenemos que seleccionar solo algunas de ellas, (con algún criterio específico). El proceso es idéntico (más alla del muestreo) al primer paso, y al finalizar tenemos una imagen que tiene en cuenta los rebotes primarios.

Paso 3: Rebotes secundarios.
El paso 2 se podría repetir un cierto numero de veces dependiendo de la cantidad de polígonos, la calidad requerida, la cantidad de fuentes secundarias (o terciarias, etc) que se tomen en cada paso. Teniendo en cuenta que en cada rebote solo una parte de la energia asociada al rayo es reflejada, y la otra es absorbida por la superfice, los rebotes primarios tienen mucho mas efecto que los siguientes, y asi sucesivamente, por lo cual en el rebote secundario solo tomamos un único rayo desde la fuente de luz secundaria: el rayo reflejado (sobre la normal de la superfice).

En síntesis:

En la luz principal se toman algunos de los rayos dentro del cono de luz para generar una pequeña cantidad de luces secundarias. Por su parte en las luces secundarias se toma un UNICO rayo en el centro del cono de luz para generar un UNICA luz terciaria. Y para las luces terciarias no se toma NINGUN otro rayo de luz, de forma que el algoritmo converge rápidamente.

En la próxima van detalles de implementación....

miércoles, 16 de junio de 2010

VISION 2.99 D

Alguien se acuerda de la revista billiken, cuando venía con los anteojos 3d?
En fin...cada tanto años la moda del 3d, como todas las modas, vuelve, esta vez renovada con las nuevas tecnologías, de la mano de películas como Avatar y de hardware especial que prometen una revolución en la forma en que vemos las imágenes.







Para no quedarse atras, y hacerle la competencia a Avatar (bueno mejor a la revista billiken de hace 20 años), queremos implementar el sistema de visión 3d, pero en un monitor común, con una placa de video común y con unos anteojos de cartón y plástico.

Primero hay que entender como funciona la sensación de profundidad en el sistema de visión humano (el fundamento de la vision estereoscópica).
Para ello: http://en.wikipedia.org/wiki/Stereoscopy

Resumiendo, la sensación 3D se produce porque el cerebro recibe una imágen distinta de cada ojo. Ambas imágenes levemente desfazadas se interpretan como información de profundidad. Para poder meter en la pantalla 2 imágenes distintas al mismo tiempo y que una vaya al ojo izquierdo y otra al derecho, se usan diferentes técnicas, una de las más comunes y baratas se llama "Complementary color anaglyphs", que consiste en generar 2 imágenes en colores complementarios, y eso combinado al filtro que tienen los lentes, permite que una imágen llegue a un ojo y la otra al otro ojo.

Implementación en DirectX

Nota: mis lentes de carton tienen el lado izquierdo rojo (R), y el otro cyan (GB = menos rojo). Hay otros que son azul y menos azul, etc..

El algoritmo es sumamente simple:


1- Hay que dibujar toda la escena desde el pto de vista del ojo izquierdo, eso se logra moviendo levemente la cámara unos "centímetros" para la izquierda, (se puede apuntar al mismo lugar o mover también el look at, yo estoy moviendo también el look at). Esta escena se dibuja a una textura, que la llamamos Imagen_Izq.
2- Despues, hay que hacer lo mismo pero para el ojo derecho, también en una textura que llamamos Imagen_Der
Hasta aca no dibujamos nada en la pantalla.
3- Por último, dibujamos 2 triángulos que ocupan toda la pantalla, a los efectos de que se llame un pixel shader especial, que ahora les muestro. Esta técnica de dibujar 2 triángulos tiene numerosas aplicaciones, especialmente en el procesamiento de imágenes 2d: detección de bordes, anti-aliasing, aplicar filtros etc. Los triángulos tienen que estar bien definidos de forma que cubran toda la pantalla, y que el shader se llame para cada punto. Como necesitamos la posición de pantalla, la podemos pasar como coordenadas uv de la textura.

// pixel shader para separar imagen ojo izquierdo / derecho
float4 LRSep(float2 screen_pos : TEXCOORD0) : COLOR
{
// separo en L/R
float4 color;
color.r = tex2D(Imagen_Izq, screen_pos).r;
color.gb = tex2D(Imagen_Der, screen_pos).gb;
color.a = 0;
return color;
}

La idea del PS es que en cada punto se combina el canal R de la imagen que va al ojo izquierdo con el canal GB de la otra imagen.

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)

lunes, 7 de junio de 2010

Currar de un solo lado es plagio, pero de varios es creatividad.

La mayor parte de los algoritmos, implementaciones, código y demas fruta que voy a publicar aca , estan basadas en cosas que encontre en la web. Sin embargo, tambien hay ideas que se me ocurrieron espontaneamente, pero estoy seguro que tambien están en internet, (o estan mal, o las dos cosas).

Al poco tiempo que empece a estudiar el directx llegue a la conclusión que la verdad de la milanesa esta en los shaders: basicamente son pequeños fragmentos de código que se ejecutan directamente en la placa de video. El vertex shader se ejecuta por cada vértice, lo mínimo que hace es recibir la posición (sin transformar) del vértice y genera como salida la posición transformada. El pixel shader por su parte se ejecuta por cada pixel, y recibe ciertos valores calculados por el vertex shader (por ejemplo la normal, el color, el uv de la textura, etc) interpolados en el polígono, generando en la salida usualmente el color final del pixel.

Aca hay un link donde esta todo explicado cortito y al pie
http://www.toymaker.info/Games/html/shaders.html

Para escribir estos fragmentos se puede usar una especie de asembler de la placa, o lo que es más habitual: usar HLSL, que es un lenguaje especialmente diseñado para escribir los shaders. Es de sintaxis similar al C, cuenta con decenas de funciones intrínsicas, operaciones vectoriales, etc etc. Viene bien un vistazo a la ayuda del directx, busquen "HLSL Language Basics" que está todo explicado.
Estos programas usualmente se graban en un archivo de efectos (.fx) y se pueden compilar en runtime con una función del DX: D3DXCreateEffectFromFile

Hay que tener en cuenta que estos fragmentos de código se ejecutan en forma paralela de a varios pixeles o vértices a la vez dependiendo de la arquitectura de la placa. Este es uno de los motivos por los cuales hay toda un área de investigación orientada a resolver problemas NO gráficos con la GPU. Generalmente son problemas donde hay un alto grado de paralelismo, por ejemplo la dinámica de fluidos, sistemas de partículas, gases, etc. Aca hay un link de CUDA (que es de nvidea) http://www.nvidia.es/object/cuda_home_new_es.html

Dejando de lado esto del CUDA, para aplicaciones gráficas comunes y corrientes, tambien hay que pensar en el problema desde otro punto de vista. La forma de resolver un problema con la GPU es bastante diferente a como se haría habitualmente.
Es común usar las texturas como matrices o vectores para almacenar datos. Para esto se utiliza la técnica de renderizar en una textura: es decir que la salida en vez de ir a la pantalla como es habitual, se almacena en una textura. Tambien es habitual que la salida en lugar de ser el color pp dicho (un RGBA) sea algún otro valor numérico que necesitamos calcular y que se alamacerá en dicha textura. Por este motivo el DX soporta varios formatos de texturas además del RGBA que se usa para dibujar en pantalla, por ejemplo hay varios formatos de texturas en punto flotante, como el D3DFMT_R32F que almacena en cada pixel un float de 32 bits en el canal Red.
El pixel shader (y con ciertas limitaciones el vertex shader tambien), pueden acceder a los datos de la textura. Usualmente lo hacen para obtener el color del pixel (texture mapping) y a esto aplicarle algun factor de iluminación. Pero tambien puede ser para usar datos previamente grabados por el pixel shader o escritos por el programa de la CPU. En estos casos se renderiza varias veces, antes de hacer el render a pantalla final.

De esta manera, con ciertas limitaciones, las texturas actuan como la memoria en el modelo standard de CPU, pero se trata de una memoria de sólo lectura. Para escribir a la textura la única forma es ponerla como RenderTarget, es decir redireccionar la salida de pantalla a esa textura.

La próxima un ejemplo concreto.

domingo, 6 de junio de 2010

Bienvenidos

La primer pc que tuve fue una AT 286. Recuerdo que lo primero que quise hacer fue re-escribir un jueguito de autos que habia hecho para la commodore 64. Cargue el GWBASIC, y ahi me encontre con la primera sorpresa, no habia pokes. Despues de varias horas de investigar (y no hablo de internet que todavia no existia), me di cuenta que tampoco habia sprites. Para los que no tienen idea de lo que estoy hablando, la c64 fue unas de las computadoras hogareñas mas populares de los 80, con suficiente memoria (64k), capacidad de procesamiento y una serie de caracteristicas que en esa epoca la hacian una maravilla de la tecnologia a la que podiamos acceder a relativamente bajo costo. Entre esas caracteristicas tenia un chip grafico (el VIC-II) que podria tranquilamente ser el antepasado de las placas de video. Este chip grafico soportaba hastas 8 "sprites" de 24x21 pixels, esos "sprites" se dibujaban en forma independiente del resto de la pantalla: al mover el sprite no se interfiere con el fondo, con lo cual no hay que tomar ninguna medida adicional para repintar o salvar la pantalla. Ademas se podia configurar la "profundidad" de cada sprite, y tenia deteccion de colisiones, todo via hardware.

Gracias a estas caracteristicas se escribieron miles de jueguitos para la c64, e incluso algunos programas utilitarios y hasta una especie de sistema operativo grafico, llamado GEOS que venia con editor de texto, hoja de calculo y un programa para dibujar.



Con la desilusion de no tener mas sprites y pokes, deje mi ambicion de escribir un juego para PC.
Unos cuantos años mas tarde, ya con el windows (95) en casi todas las maquinas, volvi a intentar, esta vez, una plataforma grafica que soporte modelos en 3d, y basada enteramente en los GDI de windows. Asi llegue a un motor 3d muy sencillo, que soportaba una cuantas primitivas graficas (triangulos, rectangulos, cajas, conos, cilindros y hasta toros y otras superfices cuarticas), pero sin texturizado. Para resolver el problema del texturizado escribi un ray-tracing, que soportaba un modelo sencillo de iluminacion, sombra directa, reflexion y transparencia. El motor3d al estar basado en gdi era sumamente ineficiente, y quizas el hecho que soportara primitivas graficas mas complejas que un triangulo fue el peor error de diseño. (La retrospeccion es una ciencia exacta).