It is possible to create XNA games without understanding HLSL, however, to get really unique lighting and texturing
effects, it is well worth the effort to learn HLSL. The syntax is very much like C. It is a procedural language and
therefore it is easy to follow. Chapter 12 in my book, Microsoft XNA Unleashed, covers the basics, including the syntax,
in detail. For this tutorial, we can see the highlights.
Variable Types
Just like C#, HLSL allows us to declare variables of different types. Most use the same keyword as their C# counterpart.
Examples of these are int, float, bool, struct, in, out, return, void, string, true, and false. There are more, of course, but those are the common ones we will be using.
Vectors can be defined several ways. Typically we see them listed as float3 for a vector with three components and float4 for a vector with four components.
We could also declare a vector with the vector keyword. We can access the different components of the vector in different ways. We can access them like an
array or through their component directly; for example, if we had a vector to hold a color value like:
float4 color;
We could access the red portion of the color by writing either of the next two lines:
float red = color[0];
float samered = color.r;
Vectors have two component namespaces: r,g,b,a and x,y,x,w. We could also get the red value from our color by writing the following valid line of code:
float anotherred = color.x;
If we wanted the red and green components of our color to store them in a vector with two components we could write the following code:
float2 something = color.xy;
float2 somethingelse = { color[0], color[1] };
When we access multiple components at the same time, like the first statement just shown, it is called swizzling. Although we can swizzle our components in the same namespace, the following is invalid code because we cannot combine component namespaces:
//bad code - we can't combine component namespaces
float2 wontcompile = color.xg;
We can set up matrices with the matrix keyword or by using floatRxC where R(ows) and C(olumns) can be any values. To define a 4 x 4 matrix we could use either of the following declarations:
float4x4 whatIsTheMatrix;
matrix <float, 4, 4> theMatrixHasYou;
The following code shows how we would access a particular row and column of a matrix:
matrix a = worldViewProjection;
float b = a._m11;
float c = a[0][0];
In HLSL we have storage class modifiers that we can associate with our variables. These determine how the variables are used. The storage class modifiers are as follows:
- extern
- shared
- static
- uniform
- volatile
The extern modifier allows our application to have access to our global variables. We can set and get those variables inside of our XNA code. The default for global variables is extern. The static modifier, on the other hand, does not allow us access to the global variable. Local variables can also have the static storage class modifier, which means that the value it has persists while each vertex or pixel is being shaded. There is also a shared modifier that allows multiple effect files to share the value. The last two modifiers are uniform and volatile. The volatile storage class modifier lets the HLSL compiler know that the global variable data will change frequently. The uniform modifier does the opposite and lets the compiler know that the data will not change. This is the default of our global variables. This is important because if we did not mark our variables as uniform (again, this is the default) then the shader would have many more instructions per vertex. Instead, the compiler does a preshader compilation and will execute those items to be executed once on the CPU and then do the per-vertex/pixel calculations on the GPU. This is very much the desired effect.
In practice you will probably never need to use these modifiers as the defaults generally do the right thing.
Setting up variables is a lot like C#. There is an additional step we need to take when setting up variables, though. HLSL has something known as semantics, which we discuss in the next section.
Semantics
Semantics are used to link our input with the output of the graphics pipeline function that was just executed. For example, there are semantics used to link our output from our application to the vertex shader input. These are called vertex input semantics. After the vertex shader is done processing the vertices, it has passed to the rasterization stage. These are called vertex output semantics.
The pixel shader receives data from both the vertex shader and the rasterization stage. These are called pixel input semantics. After the pixel shader is done, it sends the data to the correct render target and the pixel shader output color is linked to the alpha blend stage. These are called pixel output semantics. We discuss the syntax of semantics in the next section as we talk about structs.
Structs
We have to specify structs to hold our input and output data. For example, the vertex input struct that is populated from our XNA application could look something like this:
struct VertexInput //Application Data
{
float4 Position : POSITION0;
float3 Normal : NORMAL;
float4 TextureCoords : TEXCOORD0;
};
We set up our structs much in the same way as we do in C#. The only difference is the semantics. We discussed semantics in the last section but we did not actually get to see their syntax. Our effect is expecting our game to pass in the position of the vertex, the normal of the vertex, and the texture coordinates of the vertex. It then maps all of those types to a specific register so when it gets to the next stage in the pipeline it will be able to read in the appropriate values. A list of all the vertex input semantics can be found in the following table:
Vertex Shader Input Semantic |
Description |
BINORMAL[n] |
Binormal |
BLENDINDICES[n] |
Blend indices |
BLENDWEIGHT[n] |
Blend weights |
COLOR[n] |
Diffuse and specular color |
NORMAL[n] |
Normal vector |
POSITION[n] |
Vertex position in object space |
PSIZE[n] |
Point size |
TANGENT[n] |
Tangent |
TESSFACTOR[n] |
Tessellation factor |
TEXCOORD[n] |
Texture coordinates |
A struct that holds our vertex output data would look something like this:
struct VertexOutput
{
float4 Position : POSITION0;
float4 TexCoord : TEXCOORD0;
};
We can see it is set up the same as our input struct. The main thing to note about the vertex output is that it must return at least one vertex. All of the vertex output semantics can be found in the following table:
Vertex Shader Output Semantic |
Description |
COLOR[n] |
Diffuse or specular color. Any vertex shader prior to vs_3_0 should clamp a parameter that uses this semantic between 0 and 1, inclusive. A vs_3_0 vertex shader has no restriction on the data range. vs_1_1 through vs_1_3 only support two-color interpolators. vs_1_4 supports six and eight colors with subsequent versions. |
FOG |
Vertex fog. Not supported on Xbox 360 or with vs_3_0. |
POSITION |
Position of a vertex in homogenous space. Compute position in screen space by dividing (x,y,z) by w. Every vertex shader must write out a parameter with this semantic. |
PSIZE |
Point size. |
TEXCOORD[n] |
Texture coordinates. This is actually an all-purpose semantic meaning that we can pass any data (not just texture coordinates) with this semantic. |
The vertex output data is passed to the rasterization stage and then passed to the pixel shader. Our pixel shader also takes a struct as its input. An example of one follows:
struct PixelInput // Rasterization Data
{
float4 Color : COLOR0;
};
A list of pixel shader input semantics can be found in the following table:
Pixel Shader Input Semantic |
Description |
COLOR[n] |
Diffuse or specular color. For shaders prior to vs_3_0 and ps_3_0, this data ranges between 0 and 1, inclusive. Starting with ps_3_0, there is no restriction on the data range. |
TEXCOORD[n] |
Texture coordinates. |
VFACE |
Floating-point scalar that indicates a back-facing primitive. A negative value faces backward, whereas a positive value faces the camera. This is only valid with ps_3_0. |
VPOS |
Contains the current pixel (x,y) location. This is only valid with ps_3_0. |
Finally, we can see an example of a pixel shader output struct:
struct PixelOutput // Pixel Output Data
{
float4 Color : COLOR0;
float Depth : DEPTH;
};
The pixel shader must return the color of the pixel at the very least. All of the pixel shader output semantics can be found in the following table:
Pixel Shader Output Semantic |
Description |
COLOR[n] |
Output color. Any pixel shader prior to ps_3_0 should clamp a parameter that uses this semantic between 0 and 1, inclusive. For ps_3_0 shaders, the data range is dependent on the render target format. |
DEPTH[n] |
Output depth. |
We now have seen examples of the different structs we need to pass data around our shaders. We also saw the available semantics listed for each of those structs.
Intrinsic Functions
We can define our own functions in HLSL much like we do in C#, but there are also a lot of built-in functions, which are called intrinsic functions.
In general, these methods are used identical to how C# methods are used. They have a return value and take in parameters. There are a lot of functions, and that is a good thing. Some of the more frequently used functions are clamp, which clamps a value within the range specified; saturate, which is identical to clamp with an implied value range of 0 and 1; dot, which computes the dot product of two vectors; cross, which computes the cross-product of two vectors; normalize, which returns the normalized vector; and tex2D, which allows us to get a color from a vertex by passing in a sampler and the position of the texture we want the color of. Samplers are required whenever we need to use textures. A sampler has a direct relation to the part of the hardware where the texture is stored. The complete list of instrisic functions can be found in the documentation and my book.
Loops and Conditions
We are almost done hitting the highlights of the syntax but we need to discuss loops and conditions, the shader’s flow control. HLSL has many of the same flow controls that C# includes: if, while, do, and for. They really are identical to their C# counterparts. When the if statement only has one line, the curly braces are optional just like C#. Fortunately, there is nothing new in regard to our loop and conditions inside of HLSL compared to how they are structured in C#.
Wrap Up
Due to compete issues, no more of this chapter can be displayed, however we will wrap up this web tutorial by showing a complete example of some HLSL code.
float4x4 World : WORLD;
float4x4 View;
float4x4 Projection;
float4 AmbientColor : COLOR0;
float4x4 WorldViewProjection : WORLDVIEWPROJECTION;
texture Texture;
sampler TextureSampler = sampler_state
{
texture = <Texture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = mirror;
AddressV = mirror;
};
struct VertexInput
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
struct VertexOutput
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
VertexOutput vertexShader(VertexInput input)
{
VertexOutput output;
WorldViewProjection = mul(mul(World, View), Projection);
output.Position = mul(input.Position, WorldViewProjection);
output.TexCoord = input.TexCoord;
return( output );
}
struct PixelInput
{
float2 TexCoord : TEXCOORD0;
};
float4 pixelShader(PixelInput input) : COLOR
{
return( tex2D(TextureSampler, input.TexCoord) * AmbientColor);
}
technique Default
{
pass P0
{
VertexShader = compile vs_1_1 vertexShader();
PixelShader = compile ps_1_1 pixelShader();
}
}