jcanton

Code Line by Code Line
WP7 Problemas de rendimiento con Struct (II / II)

Realicemos un pequeño benchmark para poder evaluar cual es la pérdida de rendimiento obtenido, el cual se producirá por:

CPU: Tiempo empleado en gestionar las llamadas (realizar las copias de las estructuras)

Para el test vamos a crear un proyecto para Windows Phone 7.1 con Visual Studio, y luego lo analizaremos con el profiler de WP7 corriendo directamente en un dispositivo real.

La demo será muy simple, colocaremos un bucle en el método Draw que es donde más operaciones con struct se suelen hacer, y dentro del bucle realizaremos una llamada a un método al cual se le pasarán 50 matrices por parámetro (esto es para probar que realmente el problema viene por las copias de struct).

 

//Iteraciones será igual a 5000 para simular carga de trabajo
for (int i = 0; i < iterations; i++)
{
   //Llamada al método
}


Crearemos dos versiones del método que va dentro del bucle, en uno pasaremos las matrices por valor (lo normal) y en el otro lo haremos por referencia.

 

Por valor:

private void UpdateMatrices(Matrix world0, Matrix view0, Matrix projection0,
                                     Matrix world1, Matrix view1, Matrix projection1,
                                     Matrix world2, Matrix view2, Matrix projection2,
                                     Matrix world3, Matrix view3, Matrix projection3,
                                     Matrix world4, Matrix view4, Matrix projection4,
                                     Matrix world5, Matrix view5, Matrix projection5,
                                     Matrix world6, Matrix view6, Matrix projection6,
                                     Matrix world7, Matrix view7, Matrix projection7,
                                     Matrix world8, Matrix view8, Matrix projection8,
                                     Matrix world9, Matrix view9, Matrix projection9,
                                     Matrix world10, Matrix view10, Matrix projection10,
                                     Matrix world11, Matrix view11, Matrix projection11,
                                     Matrix world12, Matrix view12, Matrix projection12,
                                     Matrix world13, Matrix view13, Matrix projection13,
                                     Matrix world14, Matrix view14, Matrix projection14,
                                     Matrix world15, Matrix view15, Matrix projection15,
                                     Matrix world16, Matrix view16, Matrix projection16,
                                     Matrix world17, Matrix view17, Matrix projection17,
                                     Matrix world18, Matrix view18, Matrix projection18,
                                     Matrix world19, Matrix view19, Matrix projection19,
                                     Matrix world20, Matrix view20, Matrix projection20,
                                     Matrix world21, Matrix view21, Matrix projection21,
                                     Matrix world22, Matrix view22, Matrix projection22,
                                     Matrix world23, Matrix view23, Matrix projection23,
                                     Matrix world24, Matrix view24, Matrix projection24,
                                     Matrix world25, Matrix view25, Matrix projection25,
                                     Matrix world26, Matrix view26, Matrix projection26,
                                     Matrix world27, Matrix view27, Matrix projection27,
                                     Matrix world28, Matrix view28, Matrix projection28,
                                     Matrix world29, Matrix view29, Matrix projection29,
                                     Matrix world30, Matrix view30, Matrix projection30,
                                     Matrix world31, Matrix view31, Matrix projection31,
                                     Matrix world32, Matrix view32, Matrix projection32,
                                     Matrix world33, Matrix view33, Matrix projection33,
                                     Matrix world34, Matrix view34, Matrix projection34,
                                     Matrix world35, Matrix view35, Matrix projection35,
                                     Matrix world36, Matrix view36, Matrix projection36,
                                     Matrix world37, Matrix view37, Matrix projection37,
                                     Matrix world38, Matrix view38, Matrix projection38,
                                     Matrix world39, Matrix view39, Matrix projection39,
                                     Matrix world40, Matrix view40, Matrix projection40,
                                     Matrix world41, Matrix view41, Matrix projection41,
                                     Matrix world42, Matrix view42, Matrix projection42,
                                     Matrix world43, Matrix view43, Matrix projection43,
                                     Matrix world44, Matrix view44, Matrix projection44,
                                     Matrix world45, Matrix view45, Matrix projection45,
                                     Matrix world46, Matrix view46, Matrix projection46,
                                     Matrix world47, Matrix view47, Matrix projection47,
                                     Matrix world48, Matrix view48, Matrix projection48,
                                     Matrix world49, Matrix view49, Matrix projection49)
        {
                   //Dentro no hacemos nada
         }

Por referencia:

private void UpdateMatricesOptimized(ref Matrix world0, ref Matrix view0, ref  Matrix projection0,
                                             ref Matrix world1, ref Matrix view1, ref  Matrix projection1,
                                             ref Matrix world2, ref Matrix view2, ref  Matrix projection2,
                                             ref Matrix world3, ref Matrix view3, ref  Matrix projection3,
                                             ref Matrix world4, ref Matrix view4, ref  Matrix projection4,
                                             ref Matrix world5, ref Matrix view5, ref  Matrix projection5,
                                             ref Matrix world6, ref Matrix view6, ref  Matrix projection6,
                                             ref Matrix world7, ref Matrix view7, ref  Matrix projection7,
                                             ref Matrix world8, ref Matrix view8, ref  Matrix projection8,
                                             ref Matrix world9, ref Matrix view9, ref  Matrix projection9,
                                             ref Matrix world10, ref Matrix view10, ref Matrix projection10,
                                             ref Matrix world11, ref Matrix view11, ref Matrix projection11,
                                             ref Matrix world12, ref Matrix view12, ref Matrix projection12,
                                             ref Matrix world13, ref Matrix view13, ref Matrix projection13,
                                             ref Matrix world14, ref Matrix view14, ref Matrix projection14,
                                             ref Matrix world15, ref Matrix view15, ref Matrix projection15,
                                             ref Matrix world16, ref Matrix view16, ref Matrix projection16,
                                             ref Matrix world17, ref Matrix view17, ref Matrix projection17,
                                             ref Matrix world18, ref Matrix view18, ref Matrix projection18,
                                             ref Matrix world19, ref Matrix view19, ref Matrix projection19,
                                             ref Matrix world20, ref Matrix view20, ref Matrix projection20,
                                             ref Matrix world21, ref Matrix view21, ref Matrix projection21,
                                             ref Matrix world22, ref Matrix view22, ref Matrix projection22,
                                             ref Matrix world23, ref Matrix view23, ref Matrix projection23,
                                             ref Matrix world24, ref Matrix view24, ref Matrix projection24,
                                             ref Matrix world25, ref Matrix view25, ref Matrix projection25,
                                             ref Matrix world26, ref Matrix view26, ref Matrix projection26,
                                             ref Matrix world27, ref Matrix view27, ref Matrix projection27,
                                             ref Matrix world28, ref Matrix view28, ref Matrix projection28,
                                             ref Matrix world29, ref Matrix view29, ref Matrix projection29,
                                             ref Matrix world30, ref Matrix view30, ref Matrix projection30,
                                             ref Matrix world31, ref Matrix view31, ref Matrix projection31,
                                             ref Matrix world32, ref Matrix view32, ref Matrix projection32,
                                             ref Matrix world33, ref Matrix view33, ref Matrix projection33,
                                             ref Matrix world34, ref Matrix view34, ref Matrix projection34,
                                             ref Matrix world35, ref Matrix view35, ref Matrix projection35,
                                             ref Matrix world36, ref Matrix view36, ref Matrix projection36,
                                             ref Matrix world37, ref Matrix view37, ref Matrix projection37,
                                             ref Matrix world38, ref Matrix view38, ref Matrix projection38,
                                             ref Matrix world39, ref Matrix view39, ref Matrix projection39,
                                             ref Matrix world40, ref Matrix view40, ref Matrix projection40,
                                             ref Matrix world41, ref Matrix view41, ref Matrix projection41,
                                             ref Matrix world42, ref Matrix view42, ref Matrix projection42,
                                             ref Matrix world43, ref Matrix view43, ref Matrix projection43,
                                             ref Matrix world44, ref Matrix view44, ref Matrix projection44,
                                             ref Matrix world45, ref Matrix view45, ref Matrix projection45,
                                             ref Matrix world46, ref Matrix view46, ref Matrix projection46,
                                             ref Matrix world47, ref Matrix view47, ref Matrix projection47,
                                             ref Matrix world48, ref Matrix view48, ref Matrix projection48,
                                             ref Matrix world49, ref Matrix view49, ref Matrix projection49)
        {
            //No hacemos nada
        }

Resultados:

Una vez tenemos el benchmark montado, lo ejecutamos sobre un dispositivo y le conectamos el profiler, y este es el resultado:

WP_001061

 

 

WP_001062

 

 

 

benchmark

 

En la primera zona, estaba realizando las 5000 iteraciones llamando al método que hace el paso de parámetros por valor, y en la segunda etapa estaba usando el método que realiza el paso de parámetros por referencia.

 

Conclusiones

Como podemos ver, el paso de parámetros se está “comiendo” literalmente la CPU en la primera zona. Es decir, la CPU está trabajando al 100% solo en realizar las llamadas con las respectivas copias de structs, no hay tiempo de CPU para el código de nuestro juego. Sin embargo, en la segunda zona, realizando la misma operación, tenemos una CPU liberada para ejecutar nuestro código.

 

Este último dato asusta bastante, así que es importante que todo desarrollador de Windows Phone 7 lo conozca y tenga todo esto presente.

 

Proyecto de Visual Studio

Publicado 9/11/2011 21:14 por Javier | 4 comment(s)

WP7 Problemas de rendimiento con Struct (I / II)

Recientemente he pasado bastantes horas optimizando código para Windows Phone 7, y una de las primeras cosas que hay que tener en mente es que el CLR(Common Language Runtime) de WP7 no es el de Windows. Incluso en la última versión (Mango) en la que han hecho grandes mejoras, como por ejemplo que ahora el GC (Garbage Collector) ofrece un rendimiento más decente con 2 generaciones, o como al aprovechamiento de las instrucciones SIMD.

Otro factor a tener en cuenta para todos esos desarrolladores que se están acercando a esta plataforma para desarrollar juegos con XNA, es que a diferencia de la mayoría de APIs que existen en .NET, en XNA se aprovechan mucho las struct para aquellos tipos con tiempo de vida relativamente bajo, algo muy común en el desarrollo de videojuegos. Por ejemplo Vector3, Vector2, Color, Matrix, etc son tipos muy usados y se suelen crear y destruir estructuras de estos tipos muy rápidamente.

Si estos tipos estuviesen definidos como class, se perdería muchísimo tiempo en reservar espacio en el Heap para luego destruirlo, por este motivo se definen como struct que permite una creación y destrucción rápida cuando se almacenan en el Stack.

Podemos llegar a tener grandes problemas de rendimiento si no comprendemos bien cómo se comportan los struct en el .NET Framework. A diferencia de las instancias de class, las instancias de struct se suelen copiar cuando las pasamos como parámetros en métodos, y es importante saber detectar todos los escenarios en los que esto ocurre.

 

Métodos:

Cuando hacemos llamadas como esta:

Matrix a, b, c;
c = Math.Multiply( a, b );

Debemos saber que Matrix está definida como struct y el paso por parámetro de estas en los métodos se hace por valor (por defecto). Dentro del cuerpo del método Multiply no se tiene la referencia a las matrices a y b, si no que dentro se trabaja con una copia de ellas, es decir, se crean dos nuevas matrices y se rellenan con los 16 (4*4) valores de las matrices fuentes. Además, la matriz que devuelve el método también se devuelve como copia, si hacemos un cálculo de cuánta memoria se ha tenido que reservar para hacer esto:

4 bytes / float

16 floats / Matrix

3 copias

3 * 16 * 4 = 192 Bytes

Esto puede llegar a ser un problema, por la basura generada, la cual pueda acelerar una pasada del GC y por el coste de CPU necesario para hacer las copias.

Properties:

Las propiedades no suelen ser muy recomendables para combinarlas con las struct, ya que su set y get tienen un comportamiento parecido al de los métodos y las estructuras se van a pasar por valor (copia).

Por ejemplo:

//Matrices en propiedades
public Matrix World {get; set;}
public Matrix View {get; set;}
public Matrix Projection {get; set;}
 
//Vectores como propiedad
public Vector3 Position {get; set;}

Es común usar estas propiedades como si fuesen campos, sin tener en cuenta que cada vez que se accede a ellas internamente es como si se ejecutara un “método get” que devuelve una copia de la struct:

Vector3: 3 elementos * 4 bytes / float = 12 bytes

Matrix: 16 elementos * 4 bytes / float = 64 bytes

Este comportamiento también es algo que conociéndolo se puede aprovechar. Por ejemplo, Microsoft en su API para Matrix ha colocado un atributo estático llamada identity. De forma que cada vez que queremos inicializar una matriz con la matriz identidad hacemos:

Matrix mat1 = Matrix.Identity;
mat1.M11 = 0;
 
Matrix mat2 = Matrix.Identity;

 

Cada vez que llamamos a Matrix.Identity nos devuelve una copia de la matriz identidad almacenada dentro de la clase Matrix de forma estática. Si no fuese así y lo que nos devolviera fuese una referencia, con la segunda línea estaríamos modificando la matriz identidad y al asignársela a mat2 esta ya no representaría a la matriz identidad.

La API de XNA está llena de esto, ejemplos:

Color.Red, Color.White, Color.Black

Vector3.Up, Vector3.Zero

Operadores:

Los operadores internamente también tienen un comportamiento similar y sufrimos copias para el paso de parámetros cuando usamos struct.

Por ejemplo:

//Operador multiplicación
Matrix worldViewProj = world * view * proj;

Internamente es como si estuviésemos haciendo llamadas a un método multiplicación al cual se le pasan por parámetro las matrices de dos en dos. Esto quiere decir que se ejecutaría una llamada al método pasándole por parámetro (copia) las matrices world y view, después el resultado se devolvería también como copia, y este se volvería a pasar al método multiplicación por valor junto con la matrix proj, y el resultado se devolverá por valor también, en total:

64 bytes / Matrix * 6 copias = 384 bytes

Si añadimos que las matrices fueron definidas como propiedades tenemos que añadir una copia más por cada acceso a matrix:

3 accesos para world, view, proj, y otra copia para realizar el set y worldviewproj es propiedad

4 copias * 64 bytes / Matrix = 256 bytes

256 bytes + 384 bytes = 640 bytes

 

 

¿Qué podemos hacer para evitar todo esto?

Métodos:

Usar los métodos en los que podamos pasar los parámetros de tipo struct como referencias:

Matrix a, b, c;
 
//Paso de matrices por referencia (no se copia la estructura)
Matrix.Multiply(ref a, ref b, out c);

Propiedades:

Usar preferiblemente campos para almacenar las struct, por ejemplo las matrices World, View, Projection de la típica clase cámara, es preferible definirlas como campos públicos ya que serán consumidas desde cada objeto que se vaya a dibujar en pantalla, ahorrándonos las copias de cada acceso.

Operadores:

Sustituir los operadores por las llamadas a métodos que permiten el paso por ref de los parámetros.

//Matrix worldViewProj = world * view * projection;
 
Matrix worldViewProj, temp;
Matrix.Multiply(ref world, ref view, out temp);
Matrix.Multiply(ref temp, ref projection, out worldViewProj);

(Importante: el código aplicando estas optimizaciones suele ser menos elegante y entendible, por lo que es recomendable solo para aquellas partes “críticas” de la aplicación)

Publicado 9/11/2011 21:11 por Javier | 1 comment(s)

Wave Engine

Buenas,

Hoy es un gran día para volver a escribir en el Blog, tras casi un año trabajando en Plain Concepts donde he conocido a gente muy grande y de la que he aprendido mucho, hoy hacemos público un vídeo en el que se puede ver Bye Bye Brain App-ocalypse corriendo sobre Wave Engine en el que hemos estado trabajando durante tantos meses, el cual permite desarrollar juegos de forma productiva para múltiples plataformas.

A pesar de que aún no nos dejan desvelar todos sus secretos, creo que el vídeo habla por si solo:

 

 

Publicado 16/9/2011 19:27 por Javier | con no comments

Occlusion Culling (IV de IV)

************************************************************************************************

- Back-face culling

- Frustum Culling

- Occlusion Culling

- Optimizaciones para el Occlusion Culling

************************************************************************************************

En el post anterior explicamos los principios básicos del occlusión culling, y este último post lo dedicaremos a la implementación de dicha técnica de forma eficiente. 

Recordemos rápidamente en que consistía el occlusión culling. El objectivo es no enviar a la tarjeta gráfica aquellos objetos que van a ser eclipsados y para ello renderizabamos los ObjectBounds de los objetos de nuestra escena ordenados de más cercanos a más lejanos. Tras cada dibujado le preguntamos a la tarjeta gráfica mediante occlusion query cuantos pixeles se han renderizado del ObjectBounds si este es igual 0 no enviaremos el “objeto complejo” a la GPU.

Un ejemplo con imágenes sería:

image

Renderizamos los ObjectBounds de delante hacia atrás.

image

En dicha iteración pintamos un ObjectBound que quedará por detrás de los ya dibujados

image

El resultado de la occlusion query para dicho ObjectBound serán 0 pixeles, de forma que el objeto “complejo” que tenía por Boundingbox este último cubo será descartado del render.

 

El primer problema que nos encontramos es que la CPU debe esperar a que la GPU le devuelva un resultado, y como ya sabemos la transferencia entre CPU y GPU es muy costosa. En el algoritmo original esta espera la realizamos para cada ObjectBound:

 

image

 

Como se puede apreciar en la imagen tanto la CPU como la GPU está ociosas durante ciertos periodos de tiempo, y mientras una trabaja la otra espera.

Sumado a esto tenemos el problema de la pérdida de paralelismo, es decir la CPU y GPU suelen trabajar en paralelo para conseguir un mayor rendimiento (cuando la CPU termina de preparar un frame se lo envía a la GPU para que empiece a renderizarlo y mientras tanto la CPU empieza a preparar el frame siguiente).

Afortunadamente las gráficas de hoy dan solución a este problema, permitiéndonos almacenar el resultado de varias occlusion query en la GPU. Esto significa que las occlusion queries pueden ser batched, podemos lanzar varias encadenadas y luego ir recogiendo los resultados de forma que los tiempos de espera quedan enmascarados. Es importante resaltar que algunas GPU tienen limitado el número de resultados que pueden almacenar, por lo que en dichos casos tendremos que lanzar las queries en grupos del número máximo soportado.

 

image

 

En este gráfico se puede observar visualmente la mejora de rendimiento tras usar el batching de queries.

 

En el gráfico anterior vimos una mejora notable en el paralelismo de la CPU y GPU, usando dicha técnica eliminamos (enmascaramos) la mayoría de la esperas producidas a nivel de GPU, pero aún tenemos un “parón” en GPU ya que tenemos que esperar el resultado de la última query lanzada.

Para evitar esta latencia podemos aplicar la siguiente idea, recoger el resultado de las últimas queries en el frame siguiente.

 

image

 

De esta forma eliminamos esa última espera que nos quedaba a nivel de GPU, (en la CPU es normal e importante que nos queden espacios ociosos ya que este se encarga también de actualizar la escena, lo que puede implicar IA, Física…)

 

Hay más temas a tener en cuenta, como por ejemplo:

- Los ObjectBounds deben ajustarse lo mejor posible a los objetos para no tener problemas de oclusiones no deseadas.

fig29-01

En este caso estaríamos aproximando el árbol mediante un boundingBox, y tras aplicar occlusion culling el resultado sería que el coche no se dibujaría. Una solución a este problema pasaría por mejorar nuestra aproximación usando por ejemplo varios boundingbox (uno para el tronco y otro para la copa). El ajuste optimo para nuestro tipo de escena será el objetivo a conseguir para que nunca se produzcan errores de este tipo.

 

- Algo a tener también en cuenta sería el aplicar algoritmo de visibilidad espacial para poder descartar gran cantidad de objetos más rápidamente y reducir al mínimo el número de queries a lanzar.

image

Como se puede ver en este gráfico el 90% del tiempo lo empleamos en comprobar objetos para los cuales el resultado del test será negativo. Para evitar esto podríamos usar una jerarquía o una agrupaciones de objetos para que tras descartar el boundingbox de una agrupación de objetos, todos los objetos que contiene pueden ser descartados directamente.

 

Por último comentar que esta técnica hace posible que los juegos AAA (triple A) actuales tengan esa gran cantidad de detalles, y todos los engines de las grandes empresas lo usan. Incluso existe una empresa con gran éxito llamada Umbra Software la cual tiene como producto estrella un middleware dedicado a realizar esta técnica de forma eficiente en varias plataformas. Aquí os dejo unos vídeos en los que podéis ver algunos ejemplos de como funciona el occlusion booster que tienen implementado.

 

GPU accelerated occlusion culling

 

Umbra Software GDC 2009

Publicado 12/3/2011 10:46 por Javier | 2 comment(s)

Occlusion Culling (III de IV)

************************************************************************************************

- Back-face culling

- Frustum Culling

- Occlusion Culling

- Optimizaciones para el Occlusion Culling

************************************************************************************************

Por fin toca escribir sobre la técnica que puso nombre a esta serie de artículos XD, Occlusión Culling también es una técnica que podemos incluir dentro de los algoritmos de visibilidad, esta técnica intenta acelerar el rendimiento gráfico evitando que los objetos que van a ser eclipsados por otros sean enviados a la gráfica. Desde la técnica del back-face culling lo que se intenta siempre es enviar solo lo imprescindible a la tarjeta gráfica, ya que el principal cuello de botella actual se encuentra en el intercambio de información entre la CPU y la GPU.

Occlusion Culling-0

Se puede apreciar en la imagen superior que tanto a la izquierda como a la derecha, el resultado del render de la escena es el mismo (parte inferior de la imagen), sin embargo si miramos en la parte superior de la imagen, en el lado izquierdo solo se está enviando a la gráfica la habitación donde se encuentra la cámara mientras que en la parte derecha vemos que a la gráfica se envía todo.

(esta imagen de ejemplo no es demasiado buena ya que quien la reconozca sabe que es de unity 3D y que ellos tienen implementado el occlusion culling mediante un algoritimo de visibilidad espacial y no como vamos a explicar en este artículo, pero es que no he encontrado una imagen mejor XD).

¿Cómo funciona el occlusion culling?, bueno pues hasta hace tiempo se intentaba conseguir mediante algoritmos espaciales tipo BSP, QuadTree, kdtree. Pero actualmente tenemos soporte a nivel de hardware de nuestras GPUs para implementar la técnica de forma más eficiente. Las tarjetas gráficas actuales tiene un conjunto de instrucciones llamadas queries, las cuales nos informan del estado actual de la GPU, como por ejemplo cuanta memoria estamos consumiendo, que test tenemos activados en nuestro pipeline etc.

Estas instrucciones son algo muy potente ya que es como si tuviésemos una base de datos rellena tras cada render de la gráfica y mediante queries podemos conocer datos internos que solo están en la GPU.

La query en la que nos apoyaremos para implementar la técnica de Occlusion Culling se llama Occlusion query, mediante esta instrucción podemos saber cuantos pixeles a nivel de raster de la imagen final han sido coloreados de un objeto concreto.

675px-Raster_graphic_fish_40X46squares_hdtv-example

Esto lo utilizaremos para determinar si un objeto ha sido eclipsado que ocurrirá cuando el número de pixeles devuelto por la query sea 0. Pero la query es algo que podemos lanzar a posteriori por lo que ¿cómo nos ayudará esto a acelerar el render? ya que nosotros necesitamos saberlo a priori. El siguiente seudocódigo nos ayudará a aclararnos un poco:

1- Ordenamos los objetos de delante hacia atrás con respecto a la posición de la cámara
2- Por cada objeto de la escena
    2.1- Occlusion Culling
        2.1.1 Iniciamos la query
        2.1.2 Desactivamos la escritura en el zbuffer y en el frame buffer, esto hace que el raster de la gráfica sea muy rápido.
        2.1.3 Renderizamos un volumen que represente una aproximación más simple del objeto (normalmente boundingbox).
        2.1.4 Preguntamos por el número de pixeles dibujados y esperamos la respuesta.
    2.2- Si el número de pixeles es mayor que 0, renderizamos el objeto.

El secreto está en que lo que renderizamos para determinar si un objeto será visible o no, es un volumen simplificado de nuestro objeto (un boundingbox), y que al haber desactivado la escritura en el zbuffer y el frame buffer el test será muy rápido, de todas formas a mayor complejidad de los modelos mayores serán los beneficios obtenidos. Si el test determina que un objeto hay que renderizarlo, activaremos la escritura en el zbuffer y en el frame buffer, de manera que esos datos quedará guardados en el zbuffer y los utilizaremos para las siguientes pasadas del test.

¿Cómo se desactiva la escritura del zbuffer y el frame buffer en XNA 4.0?

//Para desactivar la escritura en Zbuffer, permitiendo la lectura 
GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
 
//Para desactivar la escritura en el framebuffer no existe un estado predeterminado dentro del BlendState
//Por lo que nos crearemos nuestro propio objeto blendState
BlendState blendState = new BlendState();
blendState.ColorWriteChannels = ColorWriteChannels.None;
 
//Posteriormente solo tendremos que asignarlo donde lo deseemos
GraphicsDevice.BlendState = blendState;

¿Cómo restaurar la escritura en el zbuffer y frame buffer?

GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;

¿Cómo se realiza una occlusion query en XNA?

//Creamos el objeto query
OcclusionQuery query = new OcclusionQuery(GraphicsDevice);
 
//Inicializamos la query
query.Begin();
 
    //Draw BoundingBox
 
query.End();
 
//Esperamos a que se realize la query
while (!query.IsComplete);
 
if(query.PixelCount > 0)
 
    //Draw object

 

Aquí os dejo un video de una implementación que realizé en XNA para XNACommunity sobre esta técnica, el código lo analizaremos en el siguiente post ya que en el se aplican varias técnicas para optimizar esta técnica.

Publicado 27/2/2011 20:08 por Javier | 5 comment(s)

Occlusion Culling (II de IV)

************************************************************************************************

- Back-face culling

- Frustum Culling

- Occlusion Culling

- Optimizaciones para el Occlusion Culling

************************************************************************************************

Frustum Culling: Es otra de las técnicas de visibilidad más usadas ya que es muy simple de implementar, consiste en no enviar a la tarjeta gráfica aquellos objetos que queden fuera del volumen de render de la cámara llamado Frustum.ViewingFrustum

Como podemos ver en la imagen de arriba, se le llama frustum al trozo de pirámide encerrado entre dos planos, near plane y far plane. Todo lo que está dentro de este volumen será dibujado por la tarjeta gráfica, y todo lo que quede fuera será descartado. La mejora de rendimiento que obtendremos con esta técnica esta en no esperar a que la tarjeta gráfica descarte los objetos que estén fuera del frustum perdiendo un tiempo valioso en transferir la información de estos objetos a la GPU, sino detectarlo en CPU.

El frustum de la cámara queda determinado por las matrices de view y projection de una cámara, es muy importante para el futuro rendimiento de nuestro juego que el frustum de la cámara tenga el tamaño optimo. Es decir, el near plane tiene que estar cerca de la cámara y el far plane tiene que estar a una distancia optima, si el far plane se coloca demasiado lejos estaremos dibujando más de lo necesario y si lo situamos demasiado cerca nuestros jugadores verán como van apareciendo polígonos en el horizonte. Antiguamente esto era un serio problema ya que no tenían las GPU de hoy en día entonces siempre si intentaba afinar mucho con el far plane, para evitar que los jugadores viesen como se iba generando el escenario por ejemplo en juegos de rally, el circuito estaba lleno de curvas que impedían ver más allá de la siguiente curva, en otros juegos utilizaban el fog para disimular esto.

Bueno una vez que tenemos el frustum solo nos queda calcular la intersección entre los objetos del escenario con dicho volumen.

view.frustum.culling

 

Pero para hacer más eficiente el test no se realiza una comprobación a nivel de polígono, sino que envolvemos cada uno de nuestros modelos en BoundingBox o BoundingSphere y realizamos el test con estos volumenes simplificados.

220px-BoundingBox

Por lo que a mayor poligonación de los elementos de nuestro juego mayor será la optimización realizada, para que esta técnica nos dé un buen resultado los elementos de nuestro juego de ven ser de un tamaño mediano. Por lo que si tenemos algún elementos muy grande lo partiremos en trozos y a cada trozo le asignaremos un BoundingBox o BoundingSphere.

El código en XNA podría ser algo así:

Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, -10), Vector3.Zero, Vector3.Up);
float aspectRatio = (float)graphics.PreferredBackBufferWidth / graphics.PreferredBackBufferHeight;
Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 1, 1000);
 
BoundingFrustum frustum = new BoundingFrustum(view * projection);
 
foreach (Model model in models)
{
    foreach (ModelMesh mesh in model.Meshes)
    {
        if (frustum.Contains(mesh.BoundingSphere) != ContainmentType.Disjoint)
        {
            //Draw
        }
    }
}      

Podemos mejorar el rendimiento de esta técnica utilizando estructuras de subdivisión de espacio como  QuadTree, OcTree ó KdTree, ya que sino el 90% del test estaremos comprobando intersecciones entre objetos que no serán dibujados.

Octree2

 

Esta mejora consiste en hacer agrupaciones jerárquicas de los elementos de la escena, de manera primero haremos comprobaciones de los boundingbox que representan a conjuntos de objetos, si descartamos alguno nos habremos ahorrado calcular la intersección de todos los elementos que contiene.

Aquí os dejo un pequeño video que muestra el resultado de esta técnica y como podemos optimizarla realizando agrupaciones jerárquicas:

Publicado 13/2/2011 20:31 por Javier | 1 comment(s)

Occlusion Culling (I de IV)

************************************************************************************************

- Back-face culling

- Frustum Culling

- Occlusion Culling

- Optimizaciones para el Occlusion Culling

************************************************************************************************

Hacía tiempo que no escribía y me alegra que el regreso sea con un tema tan interesante como el Occlusion Culling. Esta técnica podríamos clasificarla dentro de las técnicas de visibilidad para mejorar el rendimiento del render, y se suele usar junto a muchas otras técnicas de visibilidad conocidas, como el frustum culling o el back-face culling.

Back-face Culling: En esta técnica el objetivo es detectar cuales son las caras o polígonos de un objeto que no serán vistos desde la posición actual de la cámara (no se usará con objetos transparentes). De forma que dichas caras no serán dibujadas y por lo tanto reduciremos el coste en tiempo del render, sobre todo para objetos complejos donde podamos descartar gran cantidad de polígonos.

¿Y cómo funciona?, se obtiene la normal de una cara la cual puede tenerse ya almacenada tras la carga del modelo o se puede calcular usando el producto vectorial de dos aristas de la cara, después estudiaremos el signo del producto escalar entre dicha normal ( N ) y el vector de vista del observador ( L ), si el signo es “=” ó “> 0” significa que el ángulo formado por estos vectores es mayor o igual a 90º, lo que indicará que no será visible al observador y podremos descartar el polígono.fres-01

Esta técnica a día de hoy viene ya integrada dentro de la funcionalidades de las tarjetas gráficas y se realizan en uno de los estados del Pipeline llamado Geometry Processing, por lo que se realizar a nivel del hardware y por lo tanto es muy rápido, el ahorro se produce para las caras que no pasen el test ya que no llegarán al estado de Pixel Processing ahorrandonos por tanto el tiempo en rasterizar, y texturizar el polígono. El rendimiento obtenido crece con la complejidad de la escena y se acentúa cuando los shaders usados son muy complejos.

En XNA 4.0 esta técnica viene activada por defecto pero podemos configurarla a nuestro antojo, su funcionalidad viene encapsulada dentro de RasterizerState:

RasterizerState raster = new RasterizerState();
raster.CullMode = CullMode.CullCounterClockwiseFace;
CullMode tiene 3 posibles valores, estos tienen que ver con el sentido de las normales de las caras de un modelo, (CullCounterClickwiseFace es el valor que XNA asigna por defecto).
normal
Pongamos un ejemplo, esta cara ha sido expresada con los vértices en un orden determinado v1,v2,v3, es decir en sentido contrario a las agujas del reloj, si un triángulo lo expresamos de esta forma su normal queda hacia arriba siguiendo la regla de la mano derecha.
 
Right_hand_rule_simple
Si los vértices los expresamos en otro orden por ejemplo v1, v3, v2, la normal apuntaría hacia abajo siguiendo también la regla de la mano derecha, de esta manera también se determina desde que parte del espacio se verá un triangulo, esto tan simple puede provocarnos grandes quebraderos de cabeza.
 
- CullClockwiseFace : Los vértices de un polígonos han sido expresados siguiendo él sentido de las agujas del reloj.
- CullCounterClockwiseFace : Los vértices de un polígono han sido expresados siguiendo el sentido contrario de las agujas del reloj.
- None: Queremos que nuestros polígonos sean dibujados por ambos lados por lo que desactivamos el back-face culling.
 
En la siguiente imagen podemos ver el back-face culling en acción, a la izq tendríamos CullMode = None y en la derecha alguna de las otras dos posibilidades dependiendo de como hayan sido expresados los polígonos del objeto.
backfaceculling

Publicado 6/2/2011 19:25 por Javier | con no comments

Simular el acelerómetro de Windows Phone 7

Con la llegada de XNA 4.0 y el soporte para los nuevos teléfonos la API nos da acceso a una parte muy usada para el desarrollo de juegos en los teléfonos, el acelerómetro, el problema es que al no tener aún los dispositivos físicos (ya que saldrán en Septiembre-Octubre) y que el emulador que se ha proporcionado para Visual Studio 2010 tampoco nos permite emularlo, muchos desarrolladores nos encontramos limitados a la hora de crear proyectos que usen el acelerómetro.

Hace unos meses estuve desarrollando un Emulador del acelerómetro de Windows Phone 7, el cual podía ser usado con el stick del GamePad ó con el Mando de la Wii, y que nos permitía probar juegos que usaran el acelerómetro en el emulador de Visual Studio 2010.

Aquí podéis verlo en acción:

Imágenes

Capture1 Capture

 

Video

 

Este emulador lo fuimos mostrando en varios eventos de Windows Phone 7 que hicimos durante el mes de Julio, Eduardo Ortega, Isabel Gomez y yo, y ahora lo he publicado en XNACommunity.

Junto al proyecto del acelerómetro también hay una demo que lo usa, para que podáis ver lo sencillo que es usarlo.

Espero que os resulte útil.

Publicado 11/8/2010 20:10 por Javier | 4 comment(s)

Evento XNA 4.0 & Windows Phone 7

Actualmente me encuentro colaborando con personas de Microsoft como Isabel Gomez Miragaya en las presentaciones de Windows Phone 7 en España. El 28 de Mayo estaremos en Barcelona en un evento sobre Windows Phone 7 pero que lo centraremos en el desarrollo de videojuegos con XNA 4.0, dejo aquí más información:

Windows Phone 7 Developer Hub: Desarrolla juegos para Windows Phone 7 con XNA

28 de Mayo, Hotel Barcelo Sants, Barcelona

Durante este evento descubriréis la gran oportunidad que existe en Windows Phone 7 a la hora de comercializar juegos a través de su Marketplace y cómo pueden desarrollarse con XNA y Visual Studio 2010.

Registro al evento: https://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032451652&Culture=es-ES

Existe también la opción de seguir este evento mediante Webcasts Online:

9:30-10:30   Windows Phone 7: Una propuesta diferente y una gran oportunidad
10:15-11:45 Arquitectura de la Plataforma de Desarrollo de Windows Phone 7
12:30-14:30 Desarrollo de un juego para Windows Phone con XNA

 

Además estamos preparando una quedada en el evento para todos los desarrolladores XNA de España, allí estaremos muchos de XNACommunity entre ellos Rubén Lopez de Novarama, Jesús Bosch de la UOC ó Vicente Cartas MVP XNA, también vendrán muchos de grupos Indie españoles, por lo que será una buena oportunidad para conocer a muchos desarrolladores de XNA.

Yo participo en la última sesión del evento y actualmente me encuentro preparándola , estoy pidiendo feedback a desarrolladores XNA sobre que cosas os gustaría que mostráramos en el evento (sobre XNA 4.0 ó desarrollo de juegos XNA sobre Windows Phone 7), podéis dejar vuestras sugerencias como comentarios, gracias por adelantado.

Publicado 6/5/2010 9:17 por Javier | 1 comment(s)

XNA Graphics Pipeline

Si queremos extraerle el máximo partido gráfico a XNA usando HLSL es importante conocer el pipeline gráfico usado por este framework:

simple_pipeline

Cada uno de estos “módulos” cumplen un papel especial hasta conseguir pintar un objeto en pantalla, ya sea un modelo 3D ó una imagen 2D (la cual es pintada sobre un quad “anclado” a la pantalla).

Veamos que se hace en cada uno de estos “módulos”:

Vertex Data: Contiene un buffer de vértices sin transformar indexado o no indexado, es posible indicar mediante VertexDeclaration que información (position, color, texture coordinates, normals, …) viene definida por cada vértice en este buffer.

Primitive Data: Contiene las primitivas de geometría como points, lines, triangles y polygons leídos del index buffer que referencian a los vértices contenidos en el Vertex Data. En el index buffer aparecen cada una de estas primitivas y está asociada a una lista ordenada de vértices que la componen (hay que recordar que el orden es importante).

Textures: Aquí se almacenan el conjunto de texturas que son usadas por el modelo que se va a pintar, como máximo se pueden usar 8 (texture0 – texture7).

Texture Samplers: Todas las texturas que se vayan a usar en Vertex Processing ó Pixel Processing tienen que ser usadas a través de Samplers en los cuales se indica el modo de direccionamiento de la textura (TextureAddressModel) y como será filtrada (TextureFilter).

Tesselation: (No disponible bajo XNA) En esta unidad se trabaja con N-Patches ó Displacement maps para generar nuevos vértices y dar mayor nivel de detalle a los modelos evitando enviar previamente todos esos vértices a la tarjeta gráfica (lo cual produciría un cuello de botella). Estos vértices se añaden a los que ya existen en el vertex buffer y sólo existen en el pipeline de la tarjeta gráfica. Esta cualidad ha sido rediseñada para DirectX 11 para conseguir que se le pueda sacar mucho más partido y ya se han publicado algunos ejemplos increíbles sobre las nuevas posibilidades.

Vertex Processing: Aquí es donde se ejecuta el código HLSL del Vertex Shader de nuestro Effect, para cada uno de los vértices almacenados en el vertex buffer. La principal tarea que se debe realizar en el Vertex Shader es pasar todos estos vértices de Model Space a Projection Space que viene dada por la configuración de la cámara.

Greometry Processing: En este “módulo” tienen lugar varias tareas:

GeometryProcessing

  • Face Culling: Los triángulos que no miran hacia la cámara (el ángulo formado por el vector normal del triángulo y el vector lookAt de la cámara es mayor de 90º) son borrados de la escena.
  • User Clip Planes: Si el usuario a definido planos de corte, aquí es donde se realiza el test y todos los triángulos que están por detrás de estos planos son borrados de la escena.
  • Frustum Clipping: Clipping es el proceso por el cual los triángulos que no aparecen dentro de la visión de la cámara son borrados de la escena.
  • Homobeneous Divide: La información x,y,z de cada vértice es transformada a non-homogeneous space antes de ser enviada al rasterizer.
  • Viewport Mapping: Se lleva cabo el proceso de asignación de pixeles de pantalla a cada triángulo.

 

Pixel Processing: Es donde se ejecuta el código HLSL del Pixel Shader de nuestro Effect por cada uno de los pixeles que forman el modelo, desde la función de Pixel Shader se suele hacer uso de los Texture Samplers para aplicar las texturas al modelo y alguna información de la geometría (WorldPosition, Normal,…) para realizar ecuaciones de iluminación (Lambert, Phong) a nivel de pixel (PerPixel Lighting), ó técnicas más avanzadas como Normal Mapping, Parallax Mapping, etc.

Pixel Rendering: En este “módulo” tienen lugar los siguientes test configurables en el Device:

PixelRendering

  • Flog Blend: La API nos proporciona la funcionalidad de aplicar niebla a la escena y es aquí donde se realiza el test.
  • Scissor Test: También podemos definir un rectángulo de la pantalla (Scissor Rect) he indicar al Device que sólo se renderize esa porción del buffer. El test en el que se determina si los pixeles están dentro o fuera de dicho rectángulo se realiza aquí.
  • Alpha Test: Se realiza una comprobación para evaluar mediante los valores del canal alpha si cumplen o no una condición marcada por el Alpha Ref, más información.
  • Depth/Stencil Test: Se llevan a cabo los test de Depth (profundidad), se actualiza el depth buffer con la profundidad del pixel si este será visible. También se aplica el Stencil Test (plantilla) para ver si el pixel afectará al color final del pixel en pantalla.
  • Alpha Blend: En él se realizan las mezclas de colores que hacen posible el dibujado de objetos semitransparentes, más información.
  • Dither: Utiliza un algoritmo de interpolación de colores para combinar los pixeles adyacentes y así conseguir una gama de colores más consistente.
  • Channel mask: Se puede escribir solo en los canales que deseemos (RenderState.ColorWriteChannels).
  • RenderTarget: Se colocan los pixeles en el Render Target.
  • Presentation: El RenderTarget es presentado por pantalla en el monitor.

 

Conocer este pipeline nos puede ayudar a comprender mejor algunas técnicas de optimización muy usadas como Frustum Culling, ó Z-Prepass.

Publicado 1/4/2010 1:20 por Javier | 7 comment(s)

Archivado en: ,,,

HLSL, Un poco de historia

HLSL (High Level Shader Language) es el lenguaje desarrollado por Microsoft con el que podemos programar efectos gráficos mediante DirectX para que sean ejecutados por la GPU (Graphics Processor Unit) situada en nuestras actuales tarjetas gráficas, con el se pueden aplicar efectos gráficos tan impresionantes como:

Faces

Pero como se ha llegado hasta todo esto que hoy conocemos y nos es tan común, pues todo empezó en 1995 con la salida de la primera tarjeta aceleradora gráfica 3D para consumo doméstico lanzada por la en aquellos tiempos empresa llamada 3Dfx, estoy convencido que todos recordaréis los inicios y lo que suponía arrancar un juego con y sin que apareciese este logo al arrancar:

 

La 3Dfx Voodoo Card fue la primera por aquellos tiempos uno necesitaba una tarjeta gráfica 2D y una tarjeta aceleradora de gráficos 3D, finalmente esto se fue unificando en una sola tarjeta y las llamadas tarjetas aceleradoras pasaron a ser nuestras tarjetas gráficas 2D y 3D. Estas tarjetas provocaron una revolución en el mundo de los videojuegos aunque sus posibilidades eran muy limitadas, se empezó a pensar en trasladar gran parte del cálculo especifico en los videojuegos de la CPU hacia la GPU.

El próximo salto lo dio Nvidia con el lanzamiento de la GeForce 256 la cual liberaba a la CPU del cálculo de transformación e iluminación (Transform & Lighting) y permitía por tanto tener muchísimos más objetos en pantalla iluminados al realizar esta operación por hardware:

T&L

Hubo una segunda generación basada en esto introducida en el mercado por Nvidia a partir de su GeForce 2 y ATI con su gama Radeon, pero en aquellos entonces todas las capacidades gráficas estaba implementadas mediante fixed-functions, lo cual significaba que solo podías aplicar a los objetos las transformaciones e iluminación que permitía el hardware de la tarjeta gráfica, esto provocó que muchos juegos de la época tuviesen un aspecto muy parecido.

Cabe destacar también como 3Dfx lanzó con su tarjeta Voodoo 2 la tecnología SLI (Scan-Line Interleave) con la que se podían conectar dos tarjetas gráficas en paralelo, lo cual además de una buena estrategia de ventas fueron los inicios de la computación gráfica en multiples procesadores, 3Dfx innovaba mucho y finalmente fue comprada por Nvidia.

Más tarde la salida de DirectX 8 intentó mejorar esto incorporando la posibilidad de poder escribir pequeños programas que serían ejecutados por la GPU para cada vértice y cada pixel, esto aportó gran flexibilidad al desarrollo de efectos gráficos. Estos pequeños programas se llamaban Shaders y al conjunto de especificaciones se le llamó Shader Model el cual podía dividirse en dos conjuntos las especificaciones para trabajar con vértices llamada Vertex Shader y las especificaciones para trabajar a nivel de pixel llamadas Pixel Shader.

Microsoft lanzaba las especificaciones de cada versión de Shader Model pero las primeras tarjetas en incorporar soporte para shaders implementaban parcialmente dichas especificaciones por ello no se hablaba casi nunca de Shader Model y se especificaba hasta que versión de Pixel Shader y Vertex Shader soportaban que no tenían porque ser la misma. Sin embargo a patir de la versión 2.0 de Shader Model los fabricantes empezaron ha hablar ya de Shader Model lo cual indicaba que las tarjetas soportaban tanto Vertex Shader 2.0 como Pixel Shader 2.0.

Por aquellos tiempos la programación de shader era muy similar a la programación en emsamblador:

Vertex Shader

vs_1_1 // version instruction
#define fogStart c9.x
#define fogEnd c9.z
def c9, 2, 2.33, 2.66, 3 // fog start values
def c10, 3, 4.5, 6, 10 // fog end values
def c11, 0, 0, 1, 1 // clamping values
def c13, 0.66, 1.51, 0, 0
 
dcl_position v0 
dcl_texcoord v7
m4x4 r0, v0, c0  // transform vertices by world-view-projection matrix
mov oPos, r0
mov oT0, v7
m4x4 r1, v0, c4  // transform vertices by world-view matrix
// fog constants calculated in the application (6 instructions)
mov r2.x, c13.y  // 1 / (fog end - fog start)
sub r2.y, fogEnd, r1.z // (fog end - distance)
mul r2.z, r2.y, r2.x  // (fog end - distance)/(fog end - fog start)
max r2.w, c11.x, r2.z  // clamp above 0
min r2.w, c11.z, r2.z  // clamp below 1
mov oFog, r2.w  // output per-vertex fog factor in r2.x

 

Pixel Shader

ps_1_1 // version instruction
def c0, 0,0,0,0
def c1, 1,1,1,1
def c2, 1.0,0.5,0,0
def c3, 0,-0.5,-0.25,0
tex t0 // sample texture at stage 0,
// with texture coordinate set 0
mov r0, t0 // output texture color
// mov r0, 1 - t0 // output inverted texture color
// add r0, t0, c2 // add more reds and greens
// add r0, t0, c3 // subtract greens and blues
// mov r0, c2 // output solid pixel color

Lo cual hacía muy complicada su escritura, mantenimiento y reutilización, por esta razón se decidió crear lenguajes de alto nivel para la programación de Shader los cuales fuese compilados.

Microsoft desarrollo el lenguaje HLSL (High Level Shading Language) similar a C el cual lanzó con la versión de DirectX 9, esto provocó un impulso en en el mundo de los gráficos 3D, ahora era mucho más fácil escribir Shaders y por lo tanto el tamaño de estos pequeños programas fue aumentando al igual que su API.

También se desarrolló un lenguaje de alto nivel para trabajar bajo OpenGL llamado GLSL y posteriormente Nvidia lanzó un lenguaje más, llamado CG el cual permite compilar fácilmente un Shader a CG o HLSL.

Aquí podéis ver un ejemplo de un Shader básico que contiene su función para Vertex y Pixel Shader:

float4x4 WorldViewProjection;
 
float4 VertexShaderFunction(float4 Position : POSITION) : POSITION
{
    return mul(Position, WorldViewProjection);
}
 
float4 PixelShaderFunction() : COLOR
{
    return float4(1, 0, 0, 1);
}
 
technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

 

Tras programar en HLSL nunca más quieres volver a saber nada sobre lo anterior pero siempre es importante conocer algo de lo que había antes, la evolución de la API con cada versión de DirectX ha sido la siguiente:

  • DirectX 8.0 y 8.1 Shader Model 1.0
  • DirectX 9.0 Shader Model 2.0
  • DirectX 9.0c Shader Model 3.0
  • DirectX 10 y 10.1 Shader Model 4.0
  • DirectX 11 Shader Model 5.0

Cada una de estas versiones ha venido acompañada de mejoras, tanto en el número de instrucciones que se podían introduccir en cada shader (sin restricción alguna en las últimas versiones), como al número de intrisic functions disponibles. Cabe destar que XNA (hasta la versión actual XNA 3.1) está desarrollada sobre DirectX 9.0c por lo que desde este framework solo tenemos acceso a las especificaciones de Shader Model 3.0.

En la actualidad se ha lanzado ya la versión 11 de DirectX la cual da incluso un paso más allá con el DirectCompute del cual hablaremos otro día.

Publicado 11/3/2010 9:30 por Javier | 5 comment(s)

Alpha blending en XNA (IV de IV)

Vayamos ahora a por el último bloque de instrucciones presentado en el primer post:

 

GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.BlendFactor = new Color(255, 255, 255, 255);
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = false;
GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
GraphicsDevice.RenderState.AlphaSourceBlend = Blend.One;
GraphicsDevice.RenderState.AlphaDestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.AlphaTestEnable = false;
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Always;
GraphicsDevice.RenderState.ReferenceAlpha = 0;

 

Este bloque es independiente de los dos anteriores, mientras los anteriores se referían al Alpha Blending este hace referencia al Alpha Test, lo he escrito dentro del mismo artículo ya que mucha gente los suele confundir.

GraphicsDevice.RenderState.AlphaTestEnable = false;
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Always;
GraphicsDevice.RenderState.ReferenceAlpha = 0;

Estas tres instrucciones están ligadas de forma que mientras no pongamos AlphaTestEnable = true los valores asignados a AlphaFunction y ReferenceAlpha no tendrán efecto, (por defecto AlphaTestEnable = false).

Estas instrucciones configuran un test basado en el canal alpha que es mucho más rápido que el test de Alpha Blending, su funcionamiento es el siguiente, asignamos un valor de referencia (ReferenceAlpha) con un valor entre 0 y 255 y luego seleccionamos una operación de comparación la cual descartará todos los pixeles que no la cumplan.

Las posibles funciones que podemos seleccionar son:

Always
Todos los pixeles pasan el test.

Equal
Acepta todos los pixeles que tienen en su canal alpha un valor igual que el de referencia.

Greater
Acepta todos los pixeles que tienen en su canal alpha un valor mayor que el valor de referencia.

GreaterEqual
Acepta todos los pixeles que tienen en su canal alpha un valor igual o mayor que el valor de referencia.

Less
Acepta todos los pixeles que tienen en su canal alpha un valor menor que el valor de referencia.

LessEqual
Acepta todos los pixeles que tienen un su canal alpha un valor menor o igual que el valor de referencia.

Never
Ningún pixel pasa el test.

NotEqual
Acepta todos los pixeles que tienen en su canal alpha un valor distinto del valor de referencia.

Veamos algunos ejemplos:

Canales RGB

grass

Canal Alpha

Alpha

Code 1

 

//Desactivado test de alpha blending
GraphicsDevice.RenderState.AlphaBlendEnable = false;
 
//Activamos el Alpha Test
GraphicsDevice.RenderState.AlphaTestEnable = true;
//Pasan todos los pixeles cuyo valor en el canal alpha super 200
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Greater;
//El valor de referencia será 200
GraphicsDevice.RenderState.ReferenceAlpha = 200;

 

Capture

Code 2

//Desactivado test de alpha blending
GraphicsDevice.RenderState.AlphaBlendEnable = false;
 
//Activamos el Alpha Test
GraphicsDevice.RenderState.AlphaTestEnable = true;
//Pasan todos los pixeles cuyo valor en el canal alpha sea inferior a 200
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Less;
//El valor de referencia será 200
GraphicsDevice.RenderState.ReferenceAlpha = 200;

Capture1

Este test es interesante ya que es mucho más rápido que el Alpha Blending, el cual además presenta un problema bastante grande junto al ZBuffer, y es que este algoritmo no funciona muy bien con objetos transparentes ya que recordemos que en este algoritmo íbamos marcando en un buffer la profundidad de los objetos para no pintar aquellos que estuviese eclipsados, pero en este caso cuando tenemos objetos transparentes si nos interesa que se pinten todos aquellos objetos que están por detrás de estos, mucha gente decide simplemente desactivar la escritura en el Zbuffer pero esto es un error, lo correcto sería:

  1. Realizar un render de todos los objetos opacos de la escena.
  2. Ordenar por profundidad de atrás hacia delante los objetos transparentes de la escena.
  3. Pintar los objetos transparentes en orden (desactivando la escritura del zbuffer).

Como se puede intuir esta solución es bastante lenta si pretendemos pintar muchos planos con transparencias pero casualmente una de las situaciones que más se presentan en los videojuegos es el pintado de vegetación usando billboards (que son planos con texturas de hierba).

SpeedTree

 

Para mejorar el rendimiento de esto el equipo de SpeedTree fue el primero en combinar el Alpha Blending con el Alpha test, de forma que los billboard más cercanos se pintaban usando AlphaBlending +  pre-ordenado y el resto usando Alpha Test sin ordenar pero usando un truco, modificando los canales alpha de las texturas de hierba con otras texturas de ruido para ocultar la apreciación de errores de profundidad en el dibujado.FractalTexture

Por último comentar que todos estos atributos relacionados con Alpha del RenderState también pueden ser configurados directamente desde el código HLSL, por ejemplo:

technique Feathering
{
    pass Pass0
    {
      VertexShader = compile vs_1_1 vs11();
      PixelShader  = compile ps_1_1 ps11();
 
      AlphaBlendEnable = true;
      SrcBlend = SrcAlpha;
      DestBlend = InvSrcAlpha;
      ZWriteEnable = false;
    }
    
    pass Pass1
    {
      VertexShader = compile vs_1_1 vs11();
      PixelShader  = compile ps_1_1 ps11();
      
      AlphaTestEnable = true;
      AlphaRef = 0x00000040;
      AlphaFunc = GreaterEqual;
      ZWriteEnable = true;
    } 
}

************************************************************************************************

- Alpha Blending en XNA (I de IV)

- Alpha Blending en XNA (II de IV)

- Alpha Blending en XNA (III de IV)

- Alpha Blending en XNA (IV de IV)

************************************************************************************************

Publicado 4/3/2010 20:23 por Javier | 7 comment(s)

Alpha blending en XNA (III de IV)

Recordemos que al principio del todo presentamos un gran conjunto de parámetros que están relacionados con el Alpha dentro de la API de XNA:

GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.BlendFactor = new Color(255, 255, 255, 255);
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = false;
GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
GraphicsDevice.RenderState.AlphaSourceBlend = Blend.One;
GraphicsDevice.RenderState.AlphaDestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.AlphaTestEnable = false;
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Always;
GraphicsDevice.RenderState.ReferenceAlpha = 0;

En esta ocasión nos centraremos en el segundo bloque, este está relacionado con el canal alpha del backbuffer y mucha gente los suele confundir con los del primer grupo. Lo más importante es que todos estos parámetros vienen desactivados por defecto, y seguirán así mientras no pongamos SeparateAlphaBlendEnable a true. En muchos sitios podemos ver como se asignan valores a AlphaSourceBlend y AlphaDestinationBlend sin haber activado SeparateAlphaBlendEnable por lo que esos cambios no tendrán ningún efecto. También hay que tener en cuenta que ciertas instrucciones de XNA modifican el Device.RenderState, como pueden ser la instrucción de SpriteBacth ó la de pass de un effect.

Pasemos a explicar cual es la finalidad de este grupo de instrucciones, cuando queremos pintar objetos translucidos en la escena y estamos realizando un render sobre el framebuffer, este a su vez es también una imagen y por lo tanto tiene sus canales R, G, B, y A. El canal Alpha del framebuffer suele usarse para guardar información extra sobre todo cuando se aplican efectos de postprocessing en los que el render final es una composición de varios renders previos.

Bien con SeparateAlphaBlendEnable = false a la hora de realizar el test de Alpha durante el render se usa la fórmula:

(Source * SourceBlend) (blendFunction) (Destination * DestinationBlend)

esta operación se realiza tanto para los canales RGB como para el canal alpha del framebuffer, sin embargo cuando configuramos SeparateAlphaBlendEnable = true podemos cambiar esto y hacer que se apliquen dos operaciones (una para los canales RGB y otra para el canal alpha).

Operación1 sobre canales RGB

(Source.RGB * SourceBlend) (blendFunction) (Destination.RGB * DestinationBlend)

Operación2 sobre el canal Alpha

(Source.A * AlphaSourceBlend) (AlphaBlendOperation) (Destination.A * AlphaDestinationBlend)

Veamos un ejemplo, utilizaremos la siguiente textura en PNG que contendrá tanto los canales RGB como el canal Alpha, aunque veámoslos por separado:

Canales RGB

Glass

Canal Alpha

Alpha

Vamos a aplicar una operación a los canales RGB con BlendFactor para hacer la vidriera semitranslucida, y por otra parte vamos a usar el canal Alpha de la textura y vamos a mezclarlo con el canal alpha de FrameBuffer. Para poder apreciar la diferencia en todos los canales del FrameBuffer usaremos el siguiente procedimiento,  renderizamos sobre un RenderTarget2D y posteriormente pintaremos la textura de este RenderTarget2D usando un SpriteBatch al cual le aplicaremos un Shader:

XNA Code:

//Pintamos sobre un RenderTarget2D
GraphicsDevice.SetRenderTarget(0, rt);
 
graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer,
        Color.Black, 1.0f, 0);
 
//Background
spritebatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
spritebatch.Draw(background, Vector2.Zero, Color.White);
spritebatch.End();
 
GraphicsDevice.VertexDeclaration = vertexDeclaration;
 
GraphicsDevice.RenderState.CullMode = CullMode.None;
 
//Configuración del RenderState
GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.BlendFactor = new Color(127, 127, 127);
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.SourceBlend = Blend.BlendFactor;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseBlendFactor;
 
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = true;
GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
GraphicsDevice.RenderState.AlphaSourceBlend = Blend.SourceAlpha;
GraphicsDevice.RenderState.AlphaDestinationBlend = Blend.Zero;
 
//Render
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
    pass.Begin();
 
    effect.Parameters["World"].SetValue(Matrix.CreateRotationY(rotation));
    effect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(0, 0, -4), Vector3.Zero, Vector3.Up));
    effect.Parameters["Projection"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 1, 10000));
    effect.Parameters["DiffuseTexture"].SetValue(textures[index]);
 
    graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(
            PrimitiveType.TriangleList, boxData, 0, 2);
 
    pass.End();
}
effect.End();
 
//Ahora pintamos la imágen almacenada en el RenderTarget2D sobre un SpriteBatch
GraphicsDevice.SetRenderTarget(0, null);
 
spritebatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
 
alphaEffect.Begin();
alphaEffect.CurrentTechnique.Passes[0].Begin();
alphaEffect.Parameters["Alpha"].SetValue(alpha);
 
spritebatch.Draw(rt.GetTexture(), Vector2.Zero, Color.White);
 
alphaEffect.CurrentTechnique.Passes[0].End();
alphaEffect.End();
 
spritebatch.End();

Shader

sampler ColorMapSampler;
bool Alpha;
 
float4 PixelShaderFunctionAlpha(float2 TexCoord : TEXCOORD0) : COLOR0
{
    if(Alpha)
    {
        float a = tex2D(ColorMapSampler, TexCoord).a;
        return float4(a, a, a, 1);
    }
    else
    {
        return tex2D(ColorMapSampler, TexCoord);
    }
}
 
technique Technique1
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunctionAlpha();
    }
}

Este Shader nos permitirá mediante el valor de un parámetro “Alpha”, ver los canales RGB ó el canal alpha del resultado final.

 

Resultado en los canales RGB del FrameBuffer:

Capture2

 

Resultado en el canal alpha del FrameBuffer

Capture1

 

Bueno como veis si antes teníamos muchas posibilidades, ahora con SeparateAlphaBlendEnable estas se incrementan aún más, ya que AlphaSourceBlend y AlphaDestinationBlend pueden ser configurados con cualquiera de los valores Blend vistos anteriormente para SourceBlend y DestinationBlend, de igual modo AlphaBlendOperation puede tomar cualquiera de los valores que vimos para BlendFunction.

************************************************************************************************

- Alpha Blending en XNA (I de IV)

- Alpha Blending en XNA (II de IV)

- Alpha Blending en XNA (III de IV)

- Alpha Blending en XNA (IV de IV)

************************************************************************************************

Publicado 25/2/2010 8:16 por Javier | con no comments

Alpha blending en XNA (II de IV)

Recordemos la formula general usada para las transparecias:

(Source * SourceBlend) (blendFunction) (Destination * DestinationBlend)

 

En los dos ejemplos anteriores usamos como BlendFunction la operación suma que suele ser la más común, pero se pueden usar otras, la lista completa es:

Add     result = (Source * SourceBlend) + (Destination * DestinationBlend)

Max     result = max( (Source * SourceBlend), (Destination * DestinationBlend)

Min     result = min( (Source * SourceBlend), (Destination * DestinationBlend)

ReverseSubtract     result = (Destination * DestinationBlend) - (Source * SourceBlend)

Subtract     result = (Source * SourceBlend) - (Destination * DestionationBlend)

Para los parámetros SourceBlend y DestionationBlend que como vimos anteriormente nos indica el nivel de aportación a la mezcla del Source (lo que queremos pintar con transparencia) y del Destination (lo que ya está se ha pintado en el framebuffer, el fondo), y los configuramos para que usaran un valor constante de alpha para toda la textura usando el BlendFactor ó que usaran el canal alpha de la textura, pero también existen muchos otros valores, aquí os dejo la lista completa también:

Zero
Each component of the color is multiplied by (0, 0, 0, 0).

One
Each component of the color is multiplied by (1, 1, 1, 1).

SourceColor
Each component of the color is multiplied by the source color. This can be represented as (Rs, Gs, Bs, As), where R, G, B, and A respectively stand for the red, green, blue, and alpha source values.

InverseSourceColor
Each component of the color is multiplied by the inverse of the source color. This can be represented as (1 − Rs, 1 − Gs, 1 − Bs, 1 − As) where R, G, B, and A respectively stand for the red, green, blue, and alpha destination values.

SourceAlpha
Each component of the color is multiplied by the alpha value of the source. This can be represented as (As, As, As, As), where As is the alpha source value.

InverseSourceAlpha
Each component of the color is multiplied by the inverse of the alpha value of the source. This can be represented as (1 − As, 1 − As, 1 − As, 1 − As), where As is the alpha destination value.

DestinationAlpha
Each component of the color is multiplied by the alpha value of the destination. This can be represented as (Ad, Ad, Ad, Ad), where Ad is the destination alpha value.

InverseDestinationAlpha
Each component of the color is multiplied by the inverse of the alpha value of the destination. This can be represented as (1 − Ad, 1 − Ad, 1 − Ad, 1 − Ad), where Ad is the alpha destination value.

DestinationColor
Each component color is multiplied by the destination color. This can be represented as (Rd, Gd, Bd, Ad), where R, G, B, and A respectively stand for red, green, blue, and alpha destination values.

InverseDestinationColor
Each component of the color is multiplied by the inverse of the destination color. This can be represented as (1 − Rd, 1 − Gd, 1 − Bd, 1 − Ad), where Rd, Gd, Bd, and Ad respectively stand for the red, green, blue, and alpha destination values.

SourceAlphaSaturation
Each component of the color is multiplied by either the alpha of the source color, or the inverse of the alpha of the source color, whichever is greater. This can be represented as (f, f, f, 1), where f = min(A, 1 − Ad).

BothInverseSourceAlpha
(Win32 only) Each component of the source color is multiplied by the inverse of the alpha of the source color, and each component of the destination color is multiplied by the alpha of the source color. This can be represented as (1 − As, 1 − As, 1 − As, 1 − As), with a destination blend factor of (As, As, As, As); the destination blend selection is overridden. This blend mode is supported only for the SourceBlend render state.

BlendFactor
Each component of the color is multiplied by a constant set in BlendFactor.

InverseBlendFactor
Each component of the color is multiplied by the inverse of a constant set in BlendFactor. This blend mode is supported only if SupportsBlendFactor is true in the SourceBlendCapabilities or DestinationBlendCapabilities properties.

BothSourceAlpha
This mode is obsolete. The same effect can be achieved by setting the source and destination blend factors to SourceAlpha and InverseSourceAlpha in separate calls.

Ahora es cuando muchos os estaréis preguntando que para que tantos valores, pues como veis las posibles combinaciones son muchas y con todas ellas el resultado es algo diferente, que sean más o menos útiles ya es otra cosa XD.

Las combinaciones más usadas y conocidas son:

Alpha Blending     (source * Blend.SourceAlpha) + (destination * Blend.InvSourceAlpha)

Additive Blending     (source * Blend.One) + (destination * Blend.One)

Multiplicative Blending     (source * Blend.Zero) + (destination * Blend.SourceColor)

2X Multiplicative Blending     (source * Blend.DestinationColor) + (destination * Blend.SourceColor)

 

BlendModes

1. Alpha Blending, 2. Additive Blending, 3. Multiplicative Blending, 4. 2X Multiplicative Blending

También cabe destacar que existen diferentes formas de pensar en la transparencia esta que acabamos de ver podemos llamar la forma convencional en la mayoría de APIs.

Conventional Alpha Blending

Idea:

blend(source, destination) = (source.rgb * source.a) + (destination.rgb * (1 – source.a))

En XNA:

GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

Pero como podemos observar en esta situación los valores RGB y el alpha son independientes, de manera que sería posible tener un objeto totalmente transparente con valores de color en RGB, esto a un informático no le preocupa en exceso, pero no es un comportamiento muy real. Por ello existen otras formas de pensar en la transparencia como:

Premultiplied Alpha Blending

Idea:

blend(source, destination) = source.rgb + ( destination.rgb * (1 – source.a) )

En XNA:

GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

En este caso el alpha y el RGB están ligados y para que un objeto sea transparente debe tener el máximo de transparencia, y el color de RGB debe ser nulo (0,0,0). Si os fijáis la formula es la misma que la anterior solo que el source.rgb lo multiplicamos por 1, esto implica que si el objeto es translucido tendremos que realizar una conversión previa de las texturas para pasarlas a premultiplied format:

color.rgb *= color.a

Uno de los grandes motivos por el que se tiende a usar está técnica es porque se adapta mejor con DTX compression.

************************************************************************************************

- Alpha Blending en XNA (I de IV)

- Alpha Blending en XNA (II de IV)

- Alpha Blending en XNA (III de IV)

- Alpha Blending en XNA (IV de IV)

************************************************************************************************

Publicado 18/2/2010 23:03 por Javier | con no comments

Alpha blending en XNA (I de IV)

Alpha blending es la técnica con la cual podemos trabajar con transparencias tanto en 2D como en 3D, nos centraremos en pintar un plano con una textura y veremos diferentes formas de aplicar esta técnica.

Para el primer ejemplo usaremos una textura de vidriera:

Glass

la cual nos gustaría aplicar a un plano en 3D y que se viese translucido. Si simplemente la textura el resultado sería algo como:

Capture1

Aquí entran en juego todos los parámetros con los que podemos trabajar, estos los podemos dividir en 3 grupos y sus valores por defecto son:

 

GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.BlendFactor = new Color(255, 255, 255, 255);
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = false;
GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
GraphicsDevice.RenderState.AlphaSourceBlend = Blend.One;
GraphicsDevice.RenderState.AlphaDestinationBlend = Blend.Zero;
 
GraphicsDevice.RenderState.AlphaTestEnable = false;
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Always;
GraphicsDevice.RenderState.ReferenceAlpha = 0;

 

Todos estos son muchos parámetros y mucha gente termina confundiéndolos, para empezar nos centraremos solo en el primer grupo:

AlphaBlendEnable deberemos ponerlo a true si queremos activar el test de AlphaBlending.

BlendFunction, SourceBlend y DestinationBlend serán usados para determinar el color de cada pixel en el framebuffer tras la mezcla mediante la siguiente fórmula:

(Source * SourceBlend) (blendFunction) (Destination * DestinationBlend)

En este ejemplo concreto:

- Source representará los colores almacenados en la textura de vidriera.

- SourceBlend será el valor por el que multiplicaremos Source e indicará su contribución a la mezcla.

- BlendFunction representa una operación que puede ser suma, recta….

- Destination es el color que ya está en el framebaffer en nuestro caso la imagen de fondo con los árboles.

- DestinationBlend indicará la contribución del fondo a la mezcla.

Para irnos familiarizando con todos estos parámetros seguiremos con el ejemplo de la vidriera, nosotros lo que queríamos era que se viese translucida (con el mismo valor de translucidez por toda la superficie) para ello usaremos el parámetro BlendFactor que os ayudará a aplicar un valor constante de translucidez a toda la superficie, la manera de usarlo será configurar el valor de BlendFactor y posteriormente usar este desde SourceBlend y DestinationBlend, por ejemplo:

 

//Activamos el AlphaBlending
GraphicsDevice.RenderState.AlphaBlendEnable = true;
 
//Los valores de color están entre (0-255) con 127 le estamos indicando que queremos que sea un 50% translucida
GraphicsDevice.RenderState.BlendFactor = new Color(127, 127, 127);
 
//Utilizamores como operador la suma para la mezcla
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
 
//Multiplica los valores RGB de la vidriera por el valor de BlendFactor 
GraphicsDevice.RenderState.SourceBlend = Blend.BlendFactor;
 
//Multiplica los valores RGB del fondo por el valor inverso de Blend factor (1 - BlendFactor)
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseBlendFactor;

 

El resultado:

Capture2

 

En este caso estábamos trabajando con una textura que no traía información en el canal Alpha ó este veían vacio (todo a 1), las texturas suelen almacenar 4 canales Red, Green, Blue, Alpha (RGBA).

Probemos ahora con una textura con canal alpha para lo cual necesitaremos un formato que sea capaz de almacenarlo por ejemplo PNG:

Canales RGBGlass 

Canal AlphacanalAlpha

 

El canal alpha al ser un solo canal, si lo presentamos en una imagen esta será en blanco y negro. En este caso podemos ver como la vidriera no tiene los mismos niveles de translucidez en toda su superficie, todo lo que esté más cercano al blanco será opaco, por lo que tanto el marco como el centro serán totalmente opacos (resultado de multiplicar RGB * 1).

Ahora ya el BlendFactor no nos sirve necesitamos que tanto SourceBlend como DestinationBlend reciban información del canal alpha de la textura, esto lo haremos de la siguiente forma:

//Activamos el AlphaBlending
GraphicsDevice.RenderState.AlphaBlendEnable = true;
 
//Utilizamores como operador la suma para la mezcla
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
 
//Multiplica los valores RGB de la vidriera por la información almacenada en el canal alpha de la textura
GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
 
//Multiplica los valores RGB del fondo por el inverso de los valores multiplicados en el canal alpha de la textura (1 - alpha)
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

 

El resultado es el siguiente:

Capture3

************************************************************************************************

- Alpha Blending en XNA (I de IV)

- Alpha Blending en XNA (II de IV)

- Alpha Blending en XNA (III de IV)

- Alpha Blending en XNA (IV de IV)

************************************************************************************************

Publicado 14/2/2010 14:37 por Javier | 1 comment(s)

Problemas con el ZBuffer en XNA

El zbuffer es el algoritmo que se utiliza a la hora de dibujar los objetos en pantalla para decidir que objetos están delante de otros, recibe este nombre ya que se realiza un test de las coordenadas z de los objetos para comparar la profundidad a la que se encuentran en el mundo 3D. Esto a muchos matemáticos o físicos les puede resultar extraño ya que están acostumbrados a que en sus sistema de referencia la z es el vector que apunta hacia arriba, en los gráficos 3D el eje z es el que “entra hacia dentro de la pantalla” para sus valores positivos. Mucha gente piensa que esto se debe a discusiones entre informáticos y matemáticos o físicos XD, pero no es cierto, simplemente se debe a que se intentan facilitar las operaciones que se tienen que realizar con la cámara para representar los modelos y por ellos la cámara en un mundo 3D siempre se pone “como mirando al suelo” perpendicular al plano XY.

Los problemas de Zbuffer pueden ser confundidos con problemas de flip de normales, suelen tener un aspecto como:

 

Capture

Render Correcto

Capture1Render con problema de Zbuffer

Como se puede apreciar en la segunda imagen, el algoritmo que decide que se debe pintar delante y que detrás está fallando, de todas formas para detectar este tipo de problemas también os podréis fijar en que si rotáis el modelo puede que desde algunos ángulos se vea correctamente y desde otros no.

Este es uno de los primeros problemas con los que se encuentran muchas de las personas que empiezan a trabajar con XNA, esto se debe generalmente a problemas en el código del render. XNA facilita muchas tareas y los que descubren esta tecnología después de haber trabajado con muchas otras librerías anteriores ven el “cielo abierto” y este tipo de problemas los saben detectar rápidamente y buscarle soluciones, el problema normalmente lo tienen los desarrolladores con menos experiencia que se adaptan rápidamente a la facilidad de trabajar con XNA pero cuando les surge un problema de este tipo no entienden el porque ocurre, muchos incluso piensan que el problema se debe al formato de modelos que usan ó a que no se ha exportado bien desde alguno de los programas de diseño con los que trabajan 3D Studio Max, Maya, SoftImage, blender, …

El problema suele aparecer cuando se intenta mezclar un render2D y un render3D al mismo tiempo usando para dibujar esto las clases SpriteBatch y BasicEffect que vienen por defecto con XNA, un posible ejemplo:

Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms); 
 
localWorld = Matrix.Identity                
* Matrix.CreateRotationY(rotation.Y)                
* Matrix.CreateRotationX(rotation.X)                
* Matrix.CreateRotationZ(rotation.Z)                
* Matrix.CreateTranslation(position)                
* Matrix.CreateScale(scale); 
 
foreach (ModelMesh mesh in model.Meshes)
{    
    foreach (BasicEffect effect in mesh.Effects)    
    {        
        effect.EnableDefaultLighting();        
        effect.PreferPerPixelLighting = true;         
        effect.World = transforms[mesh.ParentBone.Index] * localWorld * camera.World;        
        effect.View = camera.View;        
        effect.Projection = camera.Projection;    
    }     
    mesh.Draw();
} 
 
spritebatch.Begin();
 
spritebatch.DrawString(font, "Test", Vector2.Zero, Color.White);
spritebatch.Draw(Texture, position, Color.White);
 
spritebatch.End(); 
 
base.Draw(gameTime);

Este es el render básico 3D que podemos encontrar en los tutoriales de XNA, en el que pintamos un modelo 3D usando BasicEffect, y en una segunda parte pintamos un texto y una textura en 2D con spritebatch, este código probará estos errores de ZBuffer de los que hemos hablado, una posible solución sería:

Cambiar la línea del spritebatch.Begin(); por:

spritebatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

Con esto le estamos indicando que realice el test de alfa para la textura y el texto, que envíe los elementos 2D directamente sin ordenarlos previamente y que salve el estado del Device y lo restaura justo después de realizar el dibujado 2D. La clave está en lo último en salvar el Device y el motivo es que spritebatch modifica la configuración del Device actual (la configuración con la que hemos pintado el modelo 3D y que viene por defecto) desactivando el Zbuffer y no restaurándolo después.

Lo importante es que aunque uséis esta solución entendáis porque soluciona el problema, guardar el estado del Device y restaurarlo en cada frame es algo más lento que si configuráramos directamente el Device con una configuración completa valida para todo aquello que queremos dibujar, o si solo cambiáramos ciertos parámetros del Device y no todos, de todas formas tampoco estamos hablando de unas grandes pérdidas de tiempo, aunque para obtener resultado más profesionales esto si que sería algo a estudiar.

Además del Zbuffer ¿qué otras cosas modifica el spritebatch de la configuración del Device que puedan darme problemas?, pues aquí os dejo una lista completa de las configuraciones que realiza el spritebatch sobre el Device:

GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
GraphicsDevice.RenderState.DepthBufferEnable = false; 
 
GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = false; 
 
GraphicsDevice.RenderState.AlphaTestEnable = true;
GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Greater;
GraphicsDevice.RenderState.ReferenceAlpha = 0; 
 
GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Clamp;
GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Clamp; 
GraphicsDevice.SamplerStates[0].MagFilter = TextureFilter.Linear;
GraphicsDevice.SamplerStates[0].MinFilter = TextureFilter.Linear;
GraphicsDevice.SamplerStates[0].MipFilter = TextureFilter.Linear; 
GraphicsDevice.SamplerStates[0].MipMapLevelOfDetailBias = 0.0f;
GraphicsDevice.SamplerStates[0].MaxMipLevel = 0; 

Además de esto también modifica los vértices, los índices, la declaración de vértices, e incluso las propiedades del Device para pixel and vertex shader.

Una solución más eficiente sería simplemente reactivar el zbuffer después del SpriteBatch.End(), de la siguiente forma:

GraphicsDevice.RenderState.DepthBufferEnable = true;
GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.RenderState.AlphaTestEnable = false;

Y eso fue todo XD.

Publicado 24/1/2010 18:51 por Javier | 6 comment(s)

Archivado en: ,,

Shaders en XNA

Hace unas semanas Jesús Bosch me lió para dar un webcast online (si Jesús fue así ahora no lo niegues XD), y la idea principal es que iba a ser un webcast orientado a desarrolladores de XNA por lo que todos tendrían ya unos ciertos conocimientos previos sobre esta tecnología.

La charla entonces la centré en una introducción al mundo de los shaders y a como poder hacer uso de ellos dentro de XNA, los shaders para que el que no sepa de que estoy hablando son mini programas que se escriben para ser ejecutados en la GPU (Graphics Processing Unit, el procesador de las tarjetas gráficas de nuestros ordenadores).

Estas tarjetas están especializadas en cálculos en coma flotante y matriciales a diferencia de las CPU que son procesadores de propósito general, por lo que el rendimiento de nuestros juegos y aplicaciones con XNA y Shaders puede verse incrementado enormemente. Es más para el que lo desconozca en XNA todo se pinta mediante Shaders, por eso para poder ejecutar XNA en cualquier ordenador se requiere una tarjeta que soporte Pixel and Vertex Shader 1.1 al menos, esto tampoco es una restricción muy grande para los días que corren, ya cualquier chip gráfico de los que llevan varios años integrando en las placas base como las típicas de Intel 950 ya soportan esto.

Este es el principal motivo por el cual el rendimiento 3D de aplicaciones XNA está tan cercano al de aplicaciones DirectX escritas en C++. Como actualmente el cálculo intensivo se desplaza a la GPU, para la CPU quedan otro tipo de cálculos como puede ser la lógica del juego ó la IA, y es esto lo que se escribe en C#.

Para conseguir esta transparencia dentro de XNA y que los usuarios que prueban esta tecnología puedan desarrollar juegos con un alto rendimiento gráfico sin tener que preocuparse de aprender un lenguaje de Shaders como el HLSL, el equipo de desarrollo de XNA a creado dos clases básicas que se usan para pintar en 2D y en 3D.

Para 2D tenemos el spriteBatch:

Imagen:

spriteBatch.Begin();
spriteBatch.Draw(Texture, Rectangle, Color.White);
spriteBatch.End();
Texto:
spriteBatch.Begin();
spriteBatch.DrawString(font, "Hola Mundo", Position, Color.Red);
spriteBatch.End();

 

al cual le indicamos si queremos pintar un texto ó una imagen y el se encarga de dibujarlo en pantalla sobre un plano que coloca frente a la cámara, todo esto lo hace mediante un shader y de forma transparente para nosotros, el código de dicho shader está liberado y podéis verlo aquí.

Para pintar modelos 3D han desarrollado el objeto BasicEffect:

foreach (ModelMesh mesh in MyModel)
{
    foreach (BasicEffect effect in mesh.Effects)
    {
        effect.EnableDefaultLighting();
        effect.World = world;
        effect.View = camera.view;
        effect.Projection = camera.projection;
    }
    mesh.Draw();
}

el cual también usa un shader internamente y del cual también podemos bichear su código fuente aquí.

A pesar de estas facilidades si realmente queremos sacarle partido a nuestras tarjetas gráficas lo mejor es escribir nuestros propios shaders, y este webcast sobre shaders es un punto por el que empezar a conocer todo ese mundo, (aviso que son 2 horas a un buen ritmo XD).

 

Ahora está disponible en Channel 9 Spain gracias a Alejandro Hidalgo, espero que os sea útil.

Publicado 10/1/2010 19:21 por Javier | 1 comment(s)

Archivado en: ,,

Multitouch

Bueno, año nuevo y estrenando blog gracias a Rodrigo Corral XD, haciendo un repaso del trabajo realizado en 2009 he decidido escribir un post sobre la tecnología multitouch la cual me ha consumido bastante tiempo del 2009.

Para mi todo empezo tras ver algunos vídeos de las primeras surperficies multitouch en internet, vídeos de Microsoft, Intel, Google, quedé encantado y alucinado con la riqueza visual de las aplicaciones, siempre pensé que algún día terminaría realizando algún trabajo con este tipo de tecnología.

Esta es una tecnología apasionante que está apunto de estallar y que generará mucho negocio, un día mi hermano me estuvo enseñando algunos vídeos de pequeños proyectos hechos por algunos geeks de internet y aquello volvió despertó mi interés sobre esta tecnología.

El primer contacto fue en 2008 con un proyecto para la universidad realizado junto a Juliet Moreiro y Sergio Escalada (mi novia y un compañero), en el cual intentamos realizar nuestra propia superficie multitactil creando tanto el software como el hardware (aunque este con elementos caseros).

Hardware:

Construir el hardware con elementos caseros fue lo que más costó, la idea básica queda aclarada en el siguiente gráfico:

Basado en la tecnología llamada "Diffused Illumnation" en la cual la idea principal es iluminar una superficie traslucida desde abajo con luz IR y usar una cámara IR para detectar solo la luz con dicha logitud de onda, al acercar por ejemplo una mano desde arriba a dicha superficie todo aquello que se situe más cerca reflejará dicha luz IR con mayor intensidad que lo que se encuentre a mayor distacia, es decir la llema de nuestros dedos será diferenciada del resto de la mano gracias a los niveles de intensidad reflejada.

Bien para construir esto lo primero que ibamos a necesitar es una caja, para lo que usamos cartón pulma que compramos en una papelería:

Después necesitamos una cámara IR, no cual parecía una tarea dificil hasta que descubrimos que cualquier webcam es capaz de detectar el IR solo que viene de fábrica con un filtro de IR para que la luz de dicha logitud de onda no llegue al sensor, tan simple como quitarle dicho filtro y ya llegaba la información IR al sensor, pero también la visible y nosotros queríamos que solo llegara la IR por lo que necesitamos un filtro de luz visible, la sorpresa fue descubrir que los negativos de fotos podían ser usados como filtros visibles, ya que dejan pasar el IR pero no el visible, bueno esto no es un filtro ideal pero para lo que ibamos a construir nos servía.

Tras trucar la webcam, ya solo faltaba iluminar la superfice con led infrarroja para lo que usamos un pequeño circuito de 20 Led alimentados por un transformador a 12 V:

Y el resultado final era el siguiente, visto sin la superficie traslucida, para lo cual compramos acetato también en una papelería:

(Una auténtica fricada XD)

Software:

Lo primero que necesitabamos era crear el "driver" para poder usar dicho dispositivo desde código, este "driver" consistía en un sistema que fuese capaz de ir capturando las imágenes de la cámara, aplicarle algunos filtros y realizar tracking sobre los puntos detectados, esa lista de puntos sería usada por nuestras aplicaciones. Para este sistema decidimos basarnos en una solución open source ya existen llamada TBeta y desarrollada por NUI Group, esta aplicación ya realiza dicha tarea y actua como servidor, enviando por un socket UDP dicha lista de puntos, aquí podéis ver una captura de la aplicación:

 

Bien pues ya solo necesitamos crear una aplicación multitouch que mediante un thread idependiente fuese obteniendo dicha información del socket abierto por TBeta y lo usara para simular las pulsaciones sobre los elementos de la interfaz. Para que dichas aplicaciones fuese muy vistosas decidemos usar XNA, aquí dejo un enlace del proyecto completo realizado con XNA y Visual Studio.

Estaba claro que con dicho hardware se obtenía la mitad de la sensación multitouch porque a pesar de que podías escalar y rotar fotografias, estas solo se veían desde la pantalla del ordenador ya que la superficie creada no mostraba ninguna imagen. Meses más tarde pude probar dicho software en un prototipo más elaborado y aquí tenéis un vídeo del resultado.

Este trabajo me permitió entrar a colobar en el departamento de I+D+I de Syderis Technologies, en donde mi trabajo ha estado centrado en el diseño y creación de un SDK profesional para el desarrollo de aplicaciones multitactiles sobre un hardware propietario de la empresa, creando un framework multitactil basado en DirectX (aunque los desarrolladores no tienen que conocer nada de DirectX, sino que sus desarrollos se pueden apoyar sobre una librería de controles propia desarrollada ó sobre XNA si necesitan mayor control a nivel de gráficos), herramientas de reconocimiento de gestos, plantillas para visual studio 2008, lanzador de aplicaciones y un largo etc que está sujeto a un NDA por lo que tampoco puedo contar demasiado, pero si que os puedo mostrar un vídeo de prueba que he realizado sobre un prototipo profesional.

 

Saludos

Publicado 2/1/2010 10:16 por Javier | 10 comment(s)

Archivado en: ,