Writing HLSL Shaders
Until now, you saw the building blocks that you will use from now on to write shaders. As a reminder, there are two types of shaders. The vertex shader is responsible for transforming vertices, while the pixel shader is responsible for determining the final color of a pixel on the screen. In HLSL, you can use several data types, which were created for the specific purpose of computer graphics. The language also offers a set of functions that provide useful functionality in many cases. Now you will learn how to put these building blocks together.
Declaring Variables
As in any programming language, to declare variables, you need to specify the type and name of the variables. The type can be any of the data types mentioned earlier. Arrays are defined using brackets. You can also initialize variables during its declaration. Below, you can see some examples of declarations.
float float_variable1; float float_variable2 = 3.4f; float3 position = float3(0,1,0); float4 color = 1.0f; float4x4 BoneMatrices[58];
A storage class and/or a type modifier may precede variable declarations. The storage class modifiers specify the scope and lifetime of the variables.
Storage class modifiers:
- extern – a global variable is an external input to the shader; global are considered extern by default
- noninterpolation – do not interpolate the output from a vertex shader, when passing it to a pixel shader
- shared – the variable is shared between several effects
- static – specified that a local variable is initialized once, and keeps its value between function calls
- uniform – the variable has a constant value through the execution of a shader; global variables are considered uniform by default
- volatile – a variable that changes value often; only for local variables
Type Modifiers:
- const – variable cannot be changed by a shader. Must be initialized in the declaration.
- row_major – components are stored 4 in a row; stored in a single constant register
- column_major – components are stored 4 in a column; optimize matrix math
Semantics and annotations can also follow variable names, but we will return to that a little later.
Shader Inputs
In order to generate some interesting output, shaders need some input data. This data can be of two forms: uniform inputs and varying inputs.
Uniform Inputs
Uniform inputs consist of data that is supposed to have a constant value throughout multiple executions of a shader. Typically, uniform inputs hold data like material colors, texture, world transformations.
There are two ways to specify uniform input. The first and most commonly used it through global variables. You declare these outside the shader functions, and use them inside the shader functions. The second way is to mark an input parameter of a function with the uniform storage class modifier.
float4x4 WorldMatrix : WORLD; //this is a global variable, with the WORLD semantic
float4 AFunction(PSIn input, uniform another_var)
{
//the values of WorldMatrix and another_var are constant through multiple executions
}
In the code above, we declared another_var with the uniform modifier. This makes it act like a global variable.
The uniform variables are stored in a constant table, which can be accessed from the application. We will usually access them from XNA using EffectParameters. The Effect class has a collection of parameters in its Parameters member. Thus, we can access the global variables by index, by name, or by semantic. Assuming we have an effect loaded inside a variable called effect, the code below shows the three ways of accessing the WorldMatrix variable (from the above example).
effect.Parameters[0] effect.Parameters[“WorldMatrix”] effect.Parameters.GetParameterBySemantic(“WORLD”)
To set the value of a parameter from an application, you need to use the SetValue function. The type of the parameter given to the function has to match the type of the shader variable.
//correct effect.Parameters[“WorldMatrix”].SetValue(Matrix.CreateTranslation(10,0,10)); //incorrect – runtime error effect.Parameters[“WorldMatrix”].SetValue(Vector3.Zero);
If you ever need to (though it is not recommended for performance reasons) you can read the value of a variable using the GetValueXXX functions, where XXX specified the data type that is to be read.
effect.Parameters[“WorldMatrix”].GetValueMatrix();
Varying Inputs
Varying inputs represent data that is unique to each execution of the shader. For example, each time a vertex shader is executed, is has as input a different position, different texture coordinates, etc.
Varying input parameters are declared as input parameters in the shader functions. In order for the shader to compile, each parameter has to be marked with a semantic. If a parameter does not have an attached semantic (to make it a varying input), or a uniform modifier (to make it a uniform input), the compilation of the shader will fail.
So what is a semantic? A semantic is a name used to specify how data is passed from a part of the graphics pipeline to another. For example, the POSITION0 semantic specifies that a variable should be filled with data specifying the position of a vertex. The exact process of how data is extracted from streams of bytes and put into variables with the corresponding semantics will be detailed later in this article, in the Vertex Declarations section. Vertex shaders use semantics to link data from the vertex buffers sent by the applications, while pixel shaders use semantics to link data from the outputs of a vertex shader.
Semantics need to be specified both for input variables and for output variables, to let the graphics pipeline know how to move the data around. To assign semantics to variables, two methods are used: adding the semantic after the variable declaration, using a colon, or defining a data structure where each member has an attached semantic. You can also specify an output semantic by attaching it to the shader function name.
float4 PixelShaderFunction(float2 TexCoords:TEXCOORD0) : COLOR0
{}
In the above code, the TEXCOORD0 semantic specifies that the variable TexCoords should contain the first channel of texture coordinates. The semantic COLOR0 is attached to the function, which means that the value returned by PixelShaderFunction will be assigned to the color of that pixel. If you want to specify an output variable in the parameter list of the function, you need to add the out modifier to it. You will often encounter input data specified as structures, like the ones seen below.
struct VertexShaderInput
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
float2 TexCoord : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float3 Normal : TEXCOORD1;
};
The VertexShaderInput structure holds three variables, each of them linked to a specific data in the input vertex stream: the position of a vertex, the normal, and the texture coordinates. If the vertex declaration and the vertex buffer do not contain all elements specified by the semantics, the corresponding variables are set to 0. The VertexShaderOutput structure outputs data from the shader, and puts it in place, so a pixel shader can read it. A vertex shader that uses these structures is declared below.
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
[...]//compute members of the output based on the input
return output;
}
The data outputted by a vertex shader is interpolated before being given as input to pixel shaders. The minimum output of a vertex shader is the position of the vertex, using the POSITION semantic.
To use the above data, a pixel shader can have the following declaration.
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
Inside the function you can use all members of the input structure, except the Position, which is hidden from the pixel shader, and its usage would generate a compilation error. A pixel shader must always output a COLOR0, of type float4.
You saw how data is passed from the application, to the vertex shader, and then to the pixel shader. However, when drawing objects on the screen, we also want to draw those using textures. Next, you will see how to use textures and samplers as inputs to shaders.
Textures and Samplers
To read data from a texture, it is not enough to have a member of type texture. You also need a sampler, which specifies from what texture to read, and how to do the reading, and a sampler instruction, which executes the actual reading, at some given coordinates.
A sampler declaration needs to contain a sampler state. You can see the most important elements of a sampler state below.
- AddressU Specifies the addressing mode for the U texture coordinate
- AddressV Specifies the addressing mode for the V texture coordinate
- MagFilter Specifies the magnifying filter to use when sampling from a texture
- MinFilter Specifies the minimizing filter to use when sampling from a texture
- MipFilter Specifies the mip filter to use when sampling from a texture
Below you can see an example of a sampler declaration.
Texture DiffuseTexture;
sampler TextureSampler = sampler_state
{
Texture = (DiffuseTexture);
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Wrap;
AddressV = Wrap;
};
This sampler will be used to read data from the DiffuseTexture texture, using Linear filtering, and wrapping the texture coordinates if they are outside the [0..1] range.
Now that you have a texture and a sampler, you want to read data from that texture, using the sampler. To do this, use the sampling functions (seen earlier in the Intrinsic Functions section). The most common are tex2D, tex3D and texCUBE. They take as parameters a sampler variable and texture coordinates, of type float2, for tex2D and float3 for tex3D and texCUBE. The example below reads from a 2D texture using the TextureSampler sampler, and the texture coordinates received from the vertex shader.
float4 PixelShaderFunction(float2 TexCoords : TEXCOORD0) : COLOR0
{
return tex2D(TextureSampler, TexCoords);
}
Flow Control
Depending on the shader model, HLSL also supports flow control instructions, like branching and looping.
The simplest form of branching for the video hardware is static branching. This allows for blocks of code to be executed or not, based on some shader constant. You can set the value of the corresponding constant between draw call to make the shader behave different for each model you draw. However, the block of code will be enabled / disable for the whole object.
For programmers, branching is more familiar as done on the CPU. The comparison condition is evaluated for each pixel or vertex at run time, so different code paths may be taken for different parts of the model. This is called dynamic branching. While more flexible and convenient, dynamic branching incurs some performance hits, and is not available on all hardware.


by Dennis Brandis, on 11.19.09 @ 12:22 am
On page 4:
float4 PixelShaderFunction(float2 TexCoords : TEXCOORD0) : COLOR0
{
return tex2D(TextureSampler, input.TexCoord);
}
may be it should be:
return tex2D(TextureSampler, TexCoord);
by Catalin Zima, on 11.19.09 @ 2:16 pm
I fixed it now. Thanks!
by Crash Course in HLSL « optic rust, on 01.02.10 @ 4:43 pm
[...] What does HLSL stand for? Why was it created? How does an HLSL effect file look like? What can you do with HLSL? Catalin Zima answers these questions and more in a brilliant article introducing HLSL over at her site. Check it out here [...]
by Hassan Aly Selim, on 01.31.10 @ 8:42 pm
Thanks alot for this HLSL Crash Course =)
Now I can start writing my own Custom Shaders in XNA !
by Catalin’s XNA Experiments » Re-awarded XNA/DirectX MVP for 2010, on 04.03.10 @ 6:10 pm
[...] Crash Course in HLSL (also published [...]
by Enio, on 05.01.10 @ 3:12 pm
Thanks for your helpful explanations. Which book would be more suitable for a beginner who needs to learn everything from scratch (shaders, algebra, algorithms, and physical)? Thanks!
by Enio, on 05.02.10 @ 12:41 am
Why not develop the second part of the course with several practical examples (Blur, DOF, Sepia, etc.)?
by Crash Course in HLSL, on 06.17.10 @ 8:54 am
[...] Can be found at: http://www.catalinzima.com/?page_id=575 [...]
by Devrunner, on 07.27.10 @ 11:50 am
You’re the best.