HLSL y Pixel Shaders – XNA


High Level Shader Language. Es un Lenguaje desarrollado por Microsoft que permite enviar instrucciones directamente a la tarjeta gráfica.

En HLSL, se escriben Shaders que son los que acceden a las características de la tarjeta gráfica, quitándole trabajo al procesador y dejándolo todo a la GPU, las tarjetas soportan dos tipos de Shaders: Vertex Shaders y Pixel Shaders.

El vertex Shader permite trabajar con los vértices de los modelos en 3D.

El Pixel Shader se utiliza para hacer cambios en texturas, haciendo los cambios directamente en los pixeles.

Como programar HLSL

Debemos conocer la sintaxis que se usa:

Las variables son declaradas de la misma forma que en C#, existen también palabras clave como: bool, float, string, false, true, return, struct, void entre otras.

Tipos de variables más usados:

float: representa un valor numérico con punto flotante

float2: es un vector con 2 valores,  se usa para guardar información de coordenadas. Ejemplo

float2(0.5,0.3)

float3: un vector con 3 valores, puede ser usado para coordenadas de 3 dimensiones X,Y y Z, o de los colores R,G y B.

float4: es un vector de 4 valores, en este caso los namespaces son X,Y,Z y W, y R,G,B y A, donde A es el color alfa o transparencia.

Los vectores pueden también ser declarados con la palabra clave vector, pero se usa más el float.

Los colores son representados por un float4 y pueden ser accedidos  por porciones de colores, por ejemplo si quieres obtener el color verde de un color hacemos lo siguiente:

float verde = color[1];
float verde = color.g;

También se puede acceder a los colores como si fueran coordenadas:

float verde = color.y;

Para obtener dos valores podemos combinar los elementos del vector y acceder a los valores en cualquier orden, por ejemplo queremos obtener un color en un orden diferente al original:

float4 colorDesorden = color.rabg;
float4 colorDesorden = { color[1], color[2], color[0], color[3]};

Esto es una técnica que se llama Swizzling, con esto podemos acceder a un valor desordenando de las propiedades de los namespaces rgba o xyzw, pero no podemos combinar los namespaces, el siguiente ejemplo arrojaría un error de compilación:

float4 = color.xyab; // Arroja error

Otro importante tipo de dato son las matrices, podemos declarar matrices usando la palabra clave matrix  o usando floatRXC, donde R son las filas y C las columnas, por ejemplo:

float4X4 matriz;
matrix <float, 4 , 4> matriz;

Para acceder a los valores de una matriz podemos hacerlo de las siguientes formas:

float4X4 matrizA;
float b = matrizA._m11;
float c = matrizA[0][0];

Las Estructuras son declaradas de la misma forma que en C#:

struct miEstructura
{
float4 Position;
}

PIXEL SHADER

Los pixel shaders modifican cada pixel de la textura, pixel por pixel.

Para continuar explicando la sintaxis y semántica de HSLS voy a mostrar un ejemplo de usar un Pixel Shader para cambiarlas propiedades de una imagen:

Primero debemos crear nuestro proyecto y adicionar un efecto:

 

Cuando lo abrimos, vemos que ya hay un código:

float4x4 World;
float4x4 View;
float4x4 Projection;
// TODO: add effect parameters here.
struct VertexShaderInput
{
    float4 Position : POSITION0;
    // TODO: add input channels such as texture
    // coordinates and vertex colors here.
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    // TODO: add vertex shader outputs such as colors and texture
    // coordinates here. These values will automatically be interpolated
    // over the triangle, and provided as input to your pixel shader.
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    // TODO: add your vertex shader code here.
    return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // TODO: add your pixel shader code here.
    return float4(1, 0, 0, 1);
}
technique Technique1
{
    pass Pass1
    {
        // TODO: set renderstates here.

        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

Las primeras 3 líneas son variables globales y representan matrices de 4X4, cada una corresponde  al mundo, la cámara y la proyección de la cámara, estas variables son usadas en proyectos 3D.

Para entender bien el flujo, empecemos de abajo hacia arriba, lo que vemos es una technique, los archivos de efectos pueden tener 1 o más técnicas y desde el código de XNA especificamos la técnica que vamos a usar dando el nombre de la técnica, en este ejemplo solo hay una técnica y se llama Technique1.

Dentro de las técnicas hay una sección llamada pass, cada técnica puede tener una o más pass, los pass son usados para ejecutar un pixel shader o un vertex shader, veamos las líneas:

VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();

 
Lo que se hace en las líneas anteriores, es compilar las funciones y asignarlas al PixelShader o al VertexShader,  las palabras claves vs_1_1 y ps_1_1 equivalen a la versión  del pixel shader  y del Vertex shader, podemos cambiarla dependiendo de la versión que tiene nuestra tarjeta gráfica.

Las funciones PixelShaderFunction() y VertexShaderFuntion() son declaradas en el archivo, veamos la de PixelShaderFunction:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // TODO: add your pixel shader code here.
    return float4(1, 0, 0, 1);
}

La función recibe como parámetro una estructura VertexShaderOutput que es declarada anteriormente, también podemos ver que la función retorna un color, un float4.

La función VertexShaderFunction recibe como parámetro una estructura de tipo VertexShaderInput y devuelve otra estructura del tipo VertexShaderOutput.

struct VertexShaderInput
{
    float4 Position : POSITION0;
    // TODO: add input channels such as texture
    // coordinates and vertex colors here.
};

La estructura VertexShaderInput tiene una variable de tipo float4, la palabra clave POSITION0 sirve para vincular datos del juego, son llamadas Semánticas o Semantics. Cuando se especifica una semántica al frente de una variable, HLSL asigna automáticamente un valor a dicha variable, el valor es basado en la semántica dada, después cuando se aplique el efecto, se va a modificar cada vértice y modificar la propiedad Posición.

Las semánticas más usadas son: COLOR,  POSITION, PSIZE y TEXTCOORD.

Ahora lo que hacemos es crear el Efecto y cargarlo en el código c#:

GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Texture2D sprite;
        Effect efecto1;
protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            sprite = Content.Load<Texture2D>("Thundercats");
            efecto1 = Content.Load<Effect>("Efecto1");
        }

Thundercats es el nombre de la imagen que vamos a aplicarle los efectos.

Ahora dibujamos:

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate,SaveStateMode.SaveState);
efecto1.Begin();
efecto1.CurrentTechnique.Passes[0].Begin();
spriteBatch.Draw(sprite, new Vector2(0, 0), Color.White);
efecto1.CurrentTechnique.Passes[0].End();
efecto1.End();
spriteBatch.End();

Como el efecto tiene una sola técnica y un solo pass, no es necesario indicarle la técnica y para el pass indicamos que es el de la posición 0.

Creando nuestros Propios efectos:

Modificamos el archivo efecto1 para tener solo pixel shader:

sampler TextureSampler;
struct PixelInput
{
 float2 TexCoord : TEXCOORD0;
};

float4 changeColors(PixelInput input) : COLOR0
{
    float4 color = tex2D( TextureSampler, input.TexCoord);
    color.b = color.b + color.b * .25;
    color.rg *= .15;
    return color;
}

technique Technique1
{
    pass Pass1
    {
       PixelShader = compile ps_2_0 changeColors();
    }
}

Al ejecutar la imagen queda así:

 

Lo que hicimos fue obtener todos los colores de la textura, que en este caso es TextureSampler, y obtenemos los colores con la función text2D() que recibe como parámetro la textura y un float2 que son las coordenadas de la textura, luego comenzamos a hacer algunas modificaciones a  los colores de la textura, como se dieron cuenta podemos modificar cada color a nuestro antojo, haciendo otras cosas como esta:

float4 changeColors(PixelInput input) : COLOR0
{
      float4 color = tex2D( TextureSampler, input.TexCoord);
      color.r = color.r*sin(input.TexCoord.y*100)*2;
      color.g = color.g*cos(input.TexCoord.y*200)*2;
      color.b = color.b*sin(input.TexCoord.y*300)*2;
      return color;
}

Pasar parámetros a los shaders:

Para pasar parámetros, primero debemos declararlos en el efecto, por ejemplo declaramos la variable Intensidad, que puede servir para modificar algún efecto:

float Intensidad;
sampler TextureSampler;

 
Luego en el código c#, asignamos valores a los parámetros de la siguiente forma:

efecto.Parameters["Intensidad"].SetValue(_intensidad);

_intensidad es una variable de c# que vamos a modificar con el teclado o con algún valor aleatorio.

Más Efectos:

Efecto Negativo:

float4 negativeImage(PixelInput input) : COLOR0
{
    float4 color = 1-tex2D(TextureSampler, input.TexCoord);
    color.a = 1.0f;
     return color;
}

 

Efecto de Olas

float4 wavyImage(PixelInput input) : COLOR0
{
      input.TexCoord.y = input.TexCoord.y + (sin(input.TexCoord.x*200)*0.01);
      float4 color = tex2D( TextureSampler, input.TexCoord);
      return color;
}

 

Efecto de Gradiente:

float4 gradientImage(PixelInput input) : COLOR0
{
      float4 color = tex2D( TextureSampler, input.TexCoord);
      float4 gs = dot(color.rgb, float3(0.9, 0.9, 0.11));
      if (input.TexCoord.x > 0.1f)
            color = lerp(gs, color, (1 - input.TexCoord.x) * 2);
     else
            color = lerp(gs, color, input.TexCoord.x * 2);
      return color;
}

 

Efecto de Invertir Colores:

 float4 InvertColorsImage(PixelInput input) : COLOR0
{
      // Intercambiar colores
      float4 color = tex2D( TextureSampler, input.TexCoord);
      color = color.brga;
      return color;
}

 

Efecto de Difuminado o imagen borrosa:

 float4 BlurImage(PixelInput input) : COLOR0
{
      // Efecto Blur
      float4 color;
      color = tex2D(TextureSampler, input.TexCoord.xy);
      color += tex2D(TextureSampler, input.TexCoord.xy + (0.01));
      color += tex2D(TextureSampler, input.TexCoord.xy - (0.01));
      color = color / 3;

      return color;
}

 

Efecto de Relieve:

 float4 sharpImage(PixelInput input) : COLOR0
{
      // Sharpening the image
      float sharpAmount = 15.0f;
      float4 color = tex2D( TextureSampler, input.TexCoord);
      color += tex2D( TextureSampler, input.TexCoord - 0.0001) * sharpAmount;
      color -= tex2D( TextureSampler, input.TexCoord + 0.0001) * sharpAmount;

      return color;
}

 

Efecto de Pintura en Tiza:

 float4 chalkImage(PixelInput input) : COLOR0
{
      // chalk image
      float sharpAmount = 100.0f;
      float4 color = tex2D( TextureSampler, input.TexCoord);
      color += tex2D( TextureSampler, input.TexCoord - 0.001) * sharpAmount;
      color -= tex2D( TextureSampler, input.TexCoord + 0.001) * sharpAmount;
      return color;
}

 

Efecto de Realce

float4 embossImage(PixelInput input) : COLOR0
{
      float sharpAmount = 15.0f;
      float4 color;
      color.rgb = 0.5f;
      color.a = 1.0f;
      color -= tex2D( TextureSampler, input.TexCoord - 0.0001) * sharpAmount;
      color += tex2D( TextureSampler, input.TexCoord + 0.0001) * sharpAmount;
      color = (color.r+color.g+color.b) / 3.0f;

      return color;
}

 

Efecto de Escala de Grises o blanco y negro:

float4 GrayScal(float2 texCoord:TEXCOORD0) : COLOR0
{
      float4 color =  tex2D(TextureSampler , texCoord.xy);
      float _grayColor = (color.r + color.g + color.b)/3;
      color.r += (_grayColor - color.r) ;
      color.g += (_grayColor - color.g) ;
      color.b += (_grayColor - color.b) ;
      return color;
}

En el siguiente Video podrán ver cada efecto:

 En conclusión, con los shaders podemos programar desde la tarjeta gráfica, dejandole los procesos a la GPU de la tarjeta gráfica.

La mayoría de Efectos, los ví en el Libro de Aaron Reed, Learning XNA 3.0

Código Fuente.

Deseas comentar o sugerir algo?

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s