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? )


No hay comentarios:

Publicar un comentario