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)

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