Understanding C# 2D XNA HLSL

Introduction

Firstly, what is HLSL? What can we use it for? HLSL is an acronym for High-Level Shader Language, and because it is a shader language, it runs on the GPU of the computer. With HLSL you can perform many post-processing effects, such as: blur, point lights, black and white, bloom, and pretty much any filter you can think of that can be done in Adobe Photoshop.

When working with HLSL on the Xbox 360, you are limited to using Pixel Shader 2.0 (This way you can support all Xbox’s) rather than 3.0, which isn’t terribly bad, but it could be so much better. You can do a lot with pixel shader 2.0 with up to 64 mathematical operations per-pixel. Because it is a pixel shader, the code that you write will be executed on every single pixel that is on the texture you’re working with. If your game resolution is 720×360, totalling to 259200 pixels, and if you have 3 mathematical operations per pixel (for example you are using sin, cos, and +), totalling to 777600 mathematical operations done for every time this shader code is ran (typically the total amount of times you draw the effect to the screen).

 

Examples

Nearly any filter that can be done in photoshop can be done with a shader. So, check out an example (more to come):

Radial Blur Example
fig. 1.0: Radial Blur

Structure & Creation

A typical HLSL file has the extension .fx, and can be named pretty much anything, myFilter.fx, or blur.fx, etc.. An HLSL file is a lot like a C/C++ file in the idea that functions must be predefined, and cannot be defined before the main pixel shader function, it also shares similar syntax to C/C++. If you need to find see a code reference for some of the built-in methods, why don’t you check out the XNA Intrinsic Methods. I think the best way to learn to program a shader is to first see a sample, then start to break it down.

Before we start, I need to point you to a realtime HLSL editor that was essential to my shader development. Here’s a youtube video showing it in action:

And link to the apphub forum topic is here, and the you can download it here. Some of the code I show here won’t necessarily work perfectly with the editor, but it gives you good base to work from, and it isn’t exactly rocket science to get things working either.

Let’s get started. The first thing we are going to make is a shader that turns the whole screen black and white. First, we’ll see the code. Then I’ll go through it and explain what everything does.

uniform extern texture ScreenTexture;	

sampler screen = sampler_state
{
	Texture = <ScreenTexture>;
};

float4 PixelShaderFunction(float2 inCoord: TEXCOORD0) : COLOR
{
	float4 color = tex2D(screen, inCoord);

	color.rgb = (color.r+color.g+color.b)/3.0f;

	return color;
}

technique
{
	pass P0
	{
		PixelShader = compile ps_2_0 PixelShaderFunction();
	}
}

The Breakdown

Lines 1-6: Defining a texture that we will be using in our function. To note, you can have more that one texture in your shader, you aren’t limited to having only that one. For example, some people use multiple textures when creating a toon (cel) shader. The texture that is typically loaded has a few colors to choose from. Another example of multiple textures is used in lights, rather than calculating attenuation, they just reference the pixel position on the texture and adjust accordingly.

Lines 8-15: This is the good stuff. We’ve defined our actual shader function, PixelShaderFunction, we then gave a parameter that holds the position of the current pixel on the screen we are working with. From there we can do anything. I took this position, plugged it into a built in function that returns a color from any texture. Keeping in mind that we want all pixels to be in black and white, I added all the color channels together, then averaged them, returning a grey-scale value.

Lines 10-End: This is the technique that will be used in drawing our shader, and it only has one pass. Something to note about the technique and passes are that you can have as many as you want. So let’s say you have another function that pixelates the texture, and the current black and white function. You can create a second pass that has the pixelate function in it, and then run a loop doing all the passes, meaning that first the texture will be turned black and white, and then that black and white texture will then be pixelated.

HLSL Specifics

The coordinate that is passed into the PixelShaderFunction is not a pixel-based number, it is actually a position that is a relative percentage of the texture. IE, {0.5, 0.5} is the center of the texture. A couple of work arounds are to create 2 variables, one that holds the texture width, and one that holds the texture height, then mutliple the inputed coordinate by those values.

Positions aren’t the only thing that are 0 – 1, colors are also 0 – 1.

Properties of floats: If you have a float4, you can access all four values as if it was an array, myFloat[2] (this modifies the blue value because the index does start at 0), or you can use “r”, “g”, “b”, or “a”, myFloat.b = 0.5, If you want to modifiy the RGB values at the same time, you can do that too: myFloat.rgb *= 0.5;.

How to use shaders in XNA 4.0

Using shaders in XNA 4.0 is EXTREMELY easy. Look:

// Create our effect variable:
Effect mainEffect;

//In the LoadContent function, load your fx file
protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);

    mainEffect = Content.Load<Effect>("bnw");
}

// When you initialize spriteBatch.Draw(...) pass in your effect as a param
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin(0, null, null, null, null, mainEffect);
    // Draw things here.
    spriteBatch.End();

    base.Draw(gameTime);
}

Now, this is great and all, but what if you want a few textures to have the shader applied to them, and the rest to display regularly. Well, it’s easy enough:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin();
    // Draw things regularly here.
    spriteBatch.End();

    spriteBatch.Begin(0, null, null, null, null, mainEffect);
    // Draw things here that will have the shader applied to them.
    spriteBatch.End();

    base.Draw(gameTime);
}

You can’t only have one SpriteBatch and still apply effects to specific textures. Pretty much if you’re gonna use shaders, then you you have to draw it separately from other things being drawn.

 

More to come about doing specific passes and techniques to have full control over your shaders.

3 thoughts on “Understanding C# 2D XNA HLSL”

Leave a Reply

Your email address will not be published. Required fields are marked *