viernes, 7 de octubre de 2011

Reflexiones planas usando el Stencil. (Espejos)




Como comentamos en otros post, las técnicas de environment map, (ya sea cube map o sphere maps) no se llevan bien con las reflexiones planas: los objetos del tipo espejos, o superficies planas brillantes, como pisos encerados, o mesadas, son muy sencibles a los artifacts de los enviroments maps. Para peor, el ojo humano esta muy acostumbrado a ver reflexiones en los espejos, y nota rapidamente cuando las mismas estan mal.

Uno de los métodos más usados para dibujar espejos consiste en re-dibujar la escena reflejada. Es decir dibujar todo otra vez, pero cambiando el look from y el look at, de tal manera de obtener la escena "reflejada".



El problema es que no queremos que el dibujo se "salga" de los limites del espejo o de la superficie en cuestion.
Muchas veces, esto se logra dibujando las cosas en un orden preciso: primero la escena reflejada, luego la escena real. En el post anterior vimos como el juego de tron usaba esa tecnica para la reflexion en el piso.

En el caso general esto no se puede hacer o no se puede asegurar, y la técnica mas comun para que el dibujo no se pase de los limites del espejo, es usar el stencil buffer, que funciona como un "máscara" que indica que pixeles se tienen que dibujar y cuales no.

Este metodo es el mas general, ya que por usar el stencil como mascara, soporta cualquier tipo de forma que tenga el "espejo": por ejemplo el caso de un piso de baldosas, donde solo algunas baldosas reflejan y otras no. O una mesada, donde la textura indica si hay o no reflexion. Esto gracias al que el stencil puede controlar que puntos se dibujan o no a nivel de pixel.

La técnica en si es muy sencilla, en verdad el desafio es reducir la cantidad de geometria que se renderiza en las reflexiones, con el uso de otras tecnicas de optimizacion que no vienen al caso.

Paso x paso seria algo asi: (nota: A veces conviene dibujar los espejos primero, aunque no es indispensable).

1- Antes de dibujar el espejo, hay que crear y preparar el stencil.

    // Guardo el zbuffer actuales
LPDIRECT3DSURFACE9 pDSOld = NULL;
GetDepthStencilSurface( &pDSOld);
// Creo el stencil
CreateDepthStencilSurface(d3dpp.BackBufferWidth,d3dpp.BackBufferHeight,D3DFMT_D24S8,D3DMULTISAMPLE_2_SAMPLES,0,TRUE,&g_pStencil,NULL);
SetDepthStencilSurface( g_pStencil);
Clear( 0, NULL, D3DCLEAR_ZBUFFERD3DCLEAR_STENCIL,0,1,0);


2- Hay que inicializar el stencil, poniendo "1" en los pixeles donde va el espejo, para ello hay que:

    // activar el stencil
SetRenderState( D3DRS_STENCILENABLE,TRUE );

// queremos que siempre escriba un 1
SetRenderState( D3DRS_STENCILFUNC,D3DCMP_ALWAYS);
SetRenderState( D3DRS_STENCILREF,1);
SetRenderState( D3DRS_STENCILPASS,D3DSTENCILOP_REPLACE);

// no queremos que escriba ningun color, no interesa el colorbuffer, solo queremos inicializar el stencil
SetRenderState( D3DRS_COLORWRITEENABLE,0x00000000);
SetRenderState( D3DRS_ZWRITEENABLE,FALSE);
SetRenderState( D3DRS_ZENABLE, FALSE);
// tampoco queremos texturas,
SetTexture( 0, NULL);

// dibujo el espejo pp dicho: (se supone que estas primitivas dibujan 2 triangulos que representan el espejo)
DrawPrimitive( D3DPT_TRIANGLEFAN,0, 2);

// luego de esto queda el zbuffer limpio, el color buffer limpio
// y el stencil con 1 en los pixels que tienen espejo
// dejo todo como estaba antes:
SetRenderState( D3DRS_ZWRITEENABLE,TRUE);
SetRenderState( D3DRS_ZENABLE, TRUE);
SetRenderState( D3DRS_STENCILPASS,D3DSTENCILOP_KEEP);
SetRenderState( D3DRS_COLORWRITEENABLE,0x0000000f);
SetRenderState( D3DRS_STENCILFUNC,D3DCMP_LESSEQUAL);



3- Ahora tengo que dibujar la escena, como se veria reflejada, afortunadamente, hay una funcion de utilidad del directx que devuelve la matriz de reflexión.
Solo necesita el plano sobre el que está el espejo y hay una estructura en directX para definir un plano:


    // Preparo el lano del espejo Pn = normal al plano del espejo, plano_D = distancia al plano
D3DXPLANE espejo;
espejo.a = Pn.x;
espejo.b = Pn.y;
espejo.c = Pn.z;
espejo.d = plano_D;

// Preparo la matriz de reflexion
// ------------------------------
D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );
D3DXMATRIX mReflectView;
D3DXMatrixReflect(&mReflectView,&espejo);
D3DXMatrixMultiply(&matView, &mReflectView, &matView);
SetTransform( D3DTS_VIEW, &matView );



4- Ahora, esto es un poco mas sutil, solo hay que dibujar las cosas que estan delante del espejo!!.




Afortunadamente, tambien hay una forma de decirle eso al directx con lo que se llaman Clip Planes, solo hay que pasarle el plano, que en principio es el plano del espejo.

SetClipPlane(0,espejo);

El problema es que a veces queda al reves!! , y dibuja lo que esta atrás del espejo. En ese caso hay que multiplicar por -1. Para calcular si hay que multiplicar por -1 o no, hay que ver de que lado del plano queda el punto de vista, (el vEyePt), para ello primero hay que transformarlo al espacio de la matriz de vista reflejada, y usar esta formula que si es negativa o positiva o cero, nos indica de que lado del plano esta, o si esta justo en el plano.

ndist = Pn.x*P.x + Pn.y*P.y + Pn.z*P.z + plano_D;

Esta fórmula se interpreta asi:
Si ndist<0 el punto P esta de un lado del plano, Si ndist>0 el punto P esta del otro lado,
y si ndist es cero, esta justo sobre el plano.


Aplicando esta fórmula entonces:
    D3DXVECTOR4 pOut;
D3DXVec3Transform(&pOut,&vEyePt,&mReflectView);
LF.y = pOut.x;
LF.x = pOut.y;
LF.z = pOut.z;
double K = ndist(TVector3d(LF.y,LF.x,LF.z))<0?1:-1;
float plano[] = {Pn.x*K,Pn.y*K,Pn.z*K,plano_D*K};
SetClipPlane(0,plano);
SetRenderState( D3DRS_CLIPPLANEENABLE,TRUE);




5- Dibujamos toda la escena pp dicha. (Menos el espejo!! logicamente) y una vez finalizado, dejamos todo como estaba antes de empezar la reflexion:
    // restauro el stencil global de la escena
SetDepthStencilSurface( pDSOld);
// deshabilito el stencil
SetRenderState( D3DRS_STENCILENABLE,FALSE);
// restauro el render state
SetRenderState( D3DRS_ZENABLE, TRUE);
SetRenderState( D3DRS_ZFUNC, D3DCMP_ALWAYS);




6- Ya "casi" estamos, ahora, un "snapshot" del color buffer, mostraría la escena dibujada sobre el espejo, y solamente sobre el espejo, ya que eso lo logramos con el stencil. Pero el zbuffer no esta correcto, ya que nosotros dibujamos en un depthbuffer auxiliar. Hay que tener en cuenta que el stencil buffer se almacena junto con el zbuffer, en lo que se llama DepthStencilBuffer.
Lo que necesitamos es que la profundidad grabada NO SEA la del dibujo, si no la del espejo. (Como se fuera opaco). El problema es que el espejo refleja los colores de un objeto pero el z tiene que seguir siendo el del espejo, no el del objeto reflejado. De esta forma cuando el resto de la escena se dibuje funcione correctamente el ztest.
Eso lo resolvemos simplemente dibujando el espejo, totalmente transparente, y con el ztest activado.


Por otra parte, el espejo se podria dibujar parcialmente transparente, inclusive con una textura para dar un efecto de superficie espejada, y no espejo al 100%. Es decir combinar ambas cosas, como una superficie pulida, como el piso de la siguiente escena:





Tambien, se pueden combinar varios espejos aplicando la misma tecnica varias veces




Como el stencil tambien se puede configurar para que sume, o reste cada vez que se dibuja algo, es posible, modificar esta misma técnica para que genere reflexiones "recursivas". Es decir que si mientras esta dibujando un espejo, se ve otro espejo, tiene que dibujar otra escena reflejada de la reflejada, y asi hasta una cierta profundidad en una especie de "arbol" de reflexiones. Para ello se saca provecho del "contador" que tiene el stencil, y cada vez que se dibuja un espejo, en lugar de escribir un "1", se le indica que "sume 1" al valor que tenia antes. De esta manera se van recortando los distintos espejos, de forma recursiva. Cuando termina de dibujar un espejo, para restaurar el stencil a como estaba antes, vuelve a dibujarlo pero le dice al stencil que "reste 1".
No me consta si esto es algo standard o no, porque si bien lo pude hacer funcionar (casi bien), no resultó práctico por la cantidad de veces que dibuja la escena, que crece en forma exponencial a la cantidad de espejos y de reflexiones.
Probablemente los motores que muestran muchas reflexiones recursivas esten usando una tecnica diferente.

No hay comentarios:

Publicar un comentario