Luz ambiente
de Petri Wilhelmsen

Hola, y bienvenidos para mi primer tutorial sobre los shaders en XNA.
Me llamo Petri Wilhelmsen y estoy un miembro de los estudios Dark Codex. Solemos participar en varias competiciones en cuanto a los juegovideos, como The Gathering, Assembly, Solskogen, Dream-Build-Play, NGA, etc.
Esta serie de tutoriales sobre los shaders cubrirá varios aspectos de XNA, y escribir shaders en HLSL, utilizando XNA y su GPU. Empezaré con la teoría básica, y despues continuaré con un enfoque más práctico de la programación de los shaders.
La parte teoría no será muy detallada, pero hará lo suficiente para que puedan empezar con los shaders y para poder experimentar solos. Cubrirá lo básico de HLSL, y como funciona el lenguaje HLSL y unas palabras claves, que es mejor saber.
Ahora, cubriré XNA y HLSL, y también un algoritmo sencillo para hacer luz ambiente.
I Requisitos
Un conocimiento básico de XNA, porque no voy a explicar mucho el cargamiento de texturas, de modelos 3D, las matrices y unos conocimientos de matemáticas.
II Una pequeña historia de los shaders
Antes DirectX8, los GPUs tenían una manera fija de transformar los pixeles y vértices, llamado "The fixed pipeline". Esto hizo imposible para los desarolladores cambiar como los pixeles y vértices estaban transformados y tratados despues de haber sido pasados al GPU, y eso hizo que todos los juegovideos parecían prudentes en el plano gráfico.
DirectX8 introdujo el vertex shader y el pixel shader, que éran un método que los desarolladores podían utilizar para decidir como los vértices y los pixeles deberían ser tratados cuando íban por el pipeline, dandolos más flexibilidad.
Un lenguaje de montaje estaba utilizado para programar los shaders, eso hizo muy difícil ser un desarollador de shaders, y la sola versión soportada estaba el shader model 1.0. Pero, eso cambió cuando DirectX9 fue hecho público, dando a los desarolladores la oportunidad de desarollar shaders con un languaje de shaders alto nivel (High Level Shader Language = HLSL), reemplazando el languaje de montaje con algo que se parecía más al languaje C. Esto hizo que ahora los shaders son más fácil de empleo (escribirlos, leerlos, aprender,...)
DirectX10.0 introdujo un nuevo shader, el Geometry Shader, que estaba parte del Shader Model 4.0. Pero eso requisita una nueva tarjeta gráfica, muy eficaz, y Windows Vista.
XNA soporta desde el Shader Model 1.0 hacia 3.0, pero funciona con XP, Vista y XBox360!
III Shaders ?
Basta con historia.. ¿Entonces, que es un shader?
Como ya lo he dicho, los shaders se utilizan para personalizar las etapas en el pipeline para que sea el desarollador el que implementa la manera con que tratar pixeles y vértices.
Como se puede ver en la imagen de abajo, una aplicación se inicia y utiliza un shader cuando generando, el vertex buffer trabaja con el pixel shader enviandole los datos requeridos desde el vertex shader hacia el pixel shader, trabajando juntos para crear una imagen en el framebuffer.

Una cosa muy importante, de que deberían acordarse, es que muchos GPUs no soportan todos los shader models; eso debe de estar tenido en cuenta, cuando desarollando shaders. Un shader debe tener métodos alternativos para hacer efectos similares o más sencillos. Así la aplicación puede funcionar en computadoras más antiguas.
IV Vertex Shaders
Los Vertex shaders están utilizados para manipular datos sobre los vértices, por vértice. Eso puede ser, por ejemplo, un shader haciendo un objeto más "engordo" cuando está generado, moviendo los vértices siguiendo sus vectores normales hacia una nueva posición, para cada vértice del modelo (se llaman shaders deformando).
Los Vertex shaders tienen sus entrada de datos desde una vertex structure definida en el código de la aplicación, y carga eso desde el vertex buffer, pasando por el shader. Eso describe cuales son las propiedades que cada vértice tendrá : Posición, Color, Normal, Tangente, etc.
El vertex shader envia su salida de datos para ser utilizado más tarde en el pixel shader. Definir que datos del vertex shader será enviado la próxima etapa puede hacerse definiendo una estructura en el shader, conteniendo los datos que quieren guardar, y hacer que el vertex shader vuelva esta instancia, o definiendo parámetros en el shader, utilizando la palabra clave : out. La salida puede ser : Posición, Color, coordenadas de texturas, Tangentes, posición de la luz, etc.
struct VS_OUTPUT
{
float4 Pos: POSITION;
};
VS_OUTPUT VS( float4 Pos: POSITION )
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
...
return Out;
}
// or
float3 VS(out float2 tex : TEXCOORD0) : POSITION
{
tex = float2(1.0, 1.0);
return float3(0.0, 1.0, 0.0);
}
V Pixel Shaders
El pixel shader manipula todos los pixeles (por pixel) en dado modelo/objeto/colección de vértices. Eso puede verse como una caja metálica, en que queremos personalizar el algoritmo de iluminación, colores, etc. El pixel shader coge datos de las valores de salida del vertex shader, como la posición, las normales y las coordenadas de textura :
float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR
{
...
return float4(1.0f, 0.3f, 0.7f, 1.0f);
}
El píxel shader puede tener dos valores de salida, color y profundidad.
VI HLSL
High Level Shading Language se utiliza para desarollar shaders. En HLSL, puden declarar variables, funciones, tipos de datos, pruebas( if/else/for/do/while+) y mucho más, para crear una lógica para los vértices y los pixeles. Abajo es un resumen de las palabras claves que existen en HLSL. Esta lista no es completa, sino que presenta los más importantes.
Ejemplos de tipos de datos en HSLS
bool true or false
int 32-bit integer
half 16bit integer
float 32bit float
double 64bit double
Ejemplos de vectores en HSLS
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
Matrices en HSLS
float3x3: a 3x3 matrix, type float
float2x2: a 2x2 matrix, type float
También hay muchas funciones en HLSL, que nos ayudan para almacenar expresiones matemáticas complejas.
cos( x ) Returns cosine of x
sin( x) Returns sinus of x
cross( a, b ) Returns the cross product of two vectors a and b
dot( a,b ) Returns the dot product of two vectors a and b
normalize( v ) Returns a normalized vector v ( v / |v| )
Con HLSL les esperan muchas funciones. Aprenderlas les permitirá resolver diferents problemas.
VII Archivos de efectos
Los archivos de efectos ( .fx ) facilitan el desarollo de los shaders, y casí pueden almacenar todo lo que quieren en un archivo .fx. Eso include variables globales, funciones, estructuras, vertex shader, pixel shader, diferentes técnicas/pases, texturas, etc.
Ya sabemos como declarar variables globales y estructuras en un shader, pero, no sabemos lo que es una técnica o un pase. Es muy sencillo. Un Shader puede tener una o más técnicas. Cada una tiene un nombre único y se puede eligir la técnica que queremos utilizar desde la aplicación, el juegovideo, precisando la propiedad CurrentTechnique de la clase Effect.
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
Aquí, decimos a "effect" de utilizar nuestra técnica "AmbientLight". Una técnica puede tener un o más pases, y tenemos que acordarse de tratar todos los pases, para almcenar el resultado que queremos.
Eso es un ejemplo de un shader que tiene una técnica y un pase :
technique Shader
{
pass P0
{
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_1_1 PS();
}
}
Eso es un ejemplo de un shader que tiene una técnica y dos pase :
technique Shader
{
pass P0
{
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_1_1 PS();
}
pass P1
{
VertexShader = compile vs_1_1 VS_Other();
PixelShader = compile ps_1_1 PS_Other();
}
}
Eso es un ejemplo de un shader que tiene dos técnica y un pase :
technique Shader_11
{
pass P0
{
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_1_1 PS();
}
}
technique Shader_2a
{
pass P0
{
VertexShader = compile vs_1_1 VS2();
PixelShader = compile ps_2_a PS2();
}
}
Se puede notar que una técnica tiene dos funciones, una para el pixel shader y una para el vertex shader.
VertexShader = compile vs_1_1 VS2();
PixelShader = compile ps_1_1 PS2();
Eso nos dice que la técnica utilizará VS2() como el vertex shader, PS2() como el pixel shader, y que soportará un shader model 1.1 o más alto. Esto permite tener un shader diferente y más completo para los GPUs que soportan versiones de shader model más altos.
VIII Implementando los shaders en XNA
Es muy fácil implementar shaders en XNA. En efecto, solo se necesitan unas líneas para cargar y utilizar un shader. Aquí está una lista de etapas que pueden seguir, cuando hacen un shader:
1. Hacer el shader
2. Poner el archivo del shader ( .fx ) en “Content”
3. Hacer una instancia de la clase Effect.
4. Iniciar la instancia de la clase Effect.
5. Eligir la técnica que quieren utilizar.
6. Lanzar el efecto
7. Dar los diferentes parámetros to the shader
8. Dibujar el objeto
9. Terminar el shader
Las etapas un poco más detalladas:
2. Cuando el shader está creado, arrastren el archivo y soltenlo en el archivo ”Content”. Así tendrá un "asset name":

Shader es el "asset name" del shader que hemos añadido al Content.
3. XNA Framework include una clase Effect, utilizada para cargar y compilar los shaders. Para hacer una instancia de esta clase, escriban este código :
Effect effect;
Effect forma parte de la biblioteca “Microsoft.Xna.Framework.Graphics” library, entonces, no olvidan añadir :
using Microsoft.Xna.Framework.Graphics
4. Para iniciar el shader, se puede utilizar Content para cargarlo :
effect = Content.Load<Effect>("Shader");
5. Eligen la técnica que quieren utilizar :
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
6. Para empezar effect, utiliza la función Begin() :
effect.Begin();
También deben empezar todos los pases de la técnica.
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Begin current pass
pass.Begin();
NB: Esto no es la manera más sencilla de hacer eso. Volveré más tarde en esto
7. Hay varias maneras de ajustar los parámetros del shader, pero eso es suficiente para este tutorial :
effect.Parameters["matWorldViewProj"].SetValue( worldMatrix * viewMatrix * projMatrix);
donde "matWorldViewProj" es definido en el shader: float4x4 matWorldViewProj; y worldMatrix * viewMatrix * projMatrix es una matrice que da su valor a matWorldViewProj.
SetValue da una valor al parámetro y envia esta valor al shader, y GetValue<Type> coge una valor del shader, donde Type es el tipo de datos. Por ejemplo, GetValueInt32() coge un entero del shader.
8. Genera la ecena que quieren tratar/trnasformar con el shader.
9. Para acabar con el pase, añaden pass.End() y para terminan el shader, llaman el método End() de la clase Effect :
pass.End();
effect.End();
Para entender mejor, utiliza el código del proyecto :