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:

[View:http://www.youtube.com/watch?v=8adBoxzdmXM:650:400] 

 

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

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.

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:

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

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.

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.

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.

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.

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)

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