Here I'll try to teach you the basics of HLSL shader programming with .fx files.
The great thing about this technique is you don't need to have a compiler or anything -
just edit the file and launch the game to see your results.
I've made a test level in
Facewound called shader.

So here's our level without any post processing.
Our level is called shader so I'm going to create a file in the maps folder called 'shader.fx'.
Facewound will automatically look for this file when you load the map - if it doesn't exist it won't do any post processing.
Paste this into the .fx file using notepad or something.
sampler2D g_samSrcColor;
float4 MyShader( float2 Tex : TEXCOORD0 ) : COLOR0
{
float4 Color;
Color = tex2D( g_samSrcColor, Tex.xy);
return Color;
}
technique PostProcess
{
pass p1
{
VertexShader = null;
PixelShader = compile ps_2_0 MyShader();
}
}
So what the hell is all this.
- sampler2D g_samSrcColor; - this is the screen - where we're pulling the colour information from
- float4 MyShader - this is our shader routine. The float4 means we're returning 4 floats (floats are numbers with decimals). The floats are red, green, blue and alpha (this is a COLOR).
- float4 Color; - this creates a new float4 called Color for us to send off to the screen
- Color = tex2D( g_samSrcColor, Tex.xy); - this gets the pixel at Tex.xy and sets Color to the same colour. (better explanation later on)
- return Color; - this returns the Color (sends it to the screen)
- technique PostProcess - This is the technique - It tells the code what version of vertex/pixel shader we're using. In this example we're not using a vertex shader and we're using pixel shader 2.0 (and calling function MyShader)
I'm simplyfying a lot of this stuff so you can understand it - so don't take my explanations as gospel.
So as you should have worked out by now this shader does nothing more than return each pixel to the screen (or backbuffer).
It creates a Color - Sets it to whatever color the pixel is using TextureCoordinate Tex.xy - then returns it.
Texture Coordinate
Tex in our example is a float2. This means it has 2 floats in it. One is x and one is y. x is the across and y is down. They range from 0 to 1. This means that:-
- Tex.x = 0 means the far left of the screen (or texture)
- Tex.x = 1 means the far right of the screen (or texture)
- Tex.x = 0.5 means the exact center of the screen (or texture)
- Tex.y = 0 means the very top of the screen (or texture)
- Tex.y = 1 means the very bottom of the screen (or texture)
- Tex.y = 0.5 means the exact center of the screen (or texture)
We don't use pixels because it's using a texture (The whole screen is drawn onto a texture and passed then drawn onto the backbuffer (the screen) with the shader applied).
Colours
The colours are very similar to the Texture Coordinates. The colours have r, g, b and a. Each one of these has a range of 0-1 (when they're outputted). a or alpha is how transparent it is. This means that if r=0, g=0, b=1, a=1 then the outputted pixel will be pure blue.
Assigning Values
This is a pretty cool feature of HLSL. There are lots of ways you can apply (or retrieve) values, examples:
- Color.r = 1; - Sets r (red) to 1
- Color.rgb = 1; - Sets r,g and b to 1
- Color.rb = 1; - Sets r and b to 1
- Color = 1; - Sets r,g,b and a to 1
This makes it a lot similar to mess around with it.
A Change
Ok, lets change our code. Make this change:
Color = tex2D( g_samSrcColor, Tex.xy)*3;
We have added *3 to the end (before the ;). This multiplies the new colour by 3.

If you've done this right it should look like the above. A simple change but with results. You can multiply it by anything you like.
Color = tex2D( g_samSrcColor, Tex.xy)*Tex.y;

This isn't really portrayed well in the image. This multiplies it by it's vertical position. So the top pixel is multiplied by 0 and the bottom by 1. Note that this changes the alpha value too - so you get a visual feedback/motion blur kind of effect.
Changing Colours
It's possible to change each colour individually.
Color = tex2D( g_samSrcColor, Tex.xy);
Color.r = Color.r*2;

Let's go crazy.
Color = tex2D( g_samSrcColor, Tex.xy);
Color.r = Color.r*sin(Tex.y*100)*2;
Color.g = Color.g*cos(Tex.y*200)*2;
Color.b = Color.b*sin(Tex.y*300)*2;
Sampling Coordinates
You can edit the sample coordinates to stretch and manipulate the image.
Tex.y = Tex.y * 0.5;
Color = tex2D( g_samSrcColor, Tex.xy);

You're probably thinking "0.5! It should have made it smaller". It's easy to get confused with this stuff.
Think of it like this, it's saying "What's the color at x=0, y=0.5" and you're saying "You mean y=0.25". So it grabs the pixel colour at half the height - thus making it stretch. You're really manipulating what it samples from the original texture.
Tex.y = Tex.y + (sin(Tex.x*200)*0.01);
Color = tex2D( g_samSrcColor, Tex.xy);

So sin returns a range between -1 and 1, so we have to make it smaller because if we add 1 then we would be off the screen (coords range from 0 to 1 remember).
In this example you can change 200 to change the length of the waves vertically and 0.01 to change the size of the waves.
Variables from outside
You can pass variables to the shader from inside the game.
Facewound currently sets a few variables - one of them being 'timeraslow'. timeraslow is just a variable that grows slowly. It slows down with bullettime - which means any effects you do with a shader stays in sync (Like water).
Here's an example in full.
sampler2D g_samSrcColor;
float timeraslow=1.0;
float4 MyShader( float2 Tex : TEXCOORD0 ) : COLOR0
{
float4 Color;
Tex.y = Tex.y + (sin(timeraslow)*0.01);
Tex.x = Tex.x + (cos(timeraslow)*0.01);
Color = tex2D( g_samSrcColor, Tex.xy);
return Color;
}
technique PostProcess
{
pass p1
{
VertexShader = null;
PixelShader = compile ps_2_0 MyShader();
}
}

This gives you an animated under water feeling. The screenshot above is edited to try show that it's moving. We've declared our new variable at the top using 'float timeraslow=1.0;'. The rest should be pretty self explanatory.
If you explore
Facewound's maps folder you will find out how we use this technique to make water. To do this we pass a variable called 'waterline' - then if Tex.y is over 'waterline' we draw the water. Simple!
Multiple Samples
You can create a lot of interesting effects by sampling different positions and adding them together. Here's an example.
Color = tex2D( g_samSrcColor, Tex.xy);
Color += tex2D( g_samSrcColor, Tex.xy+0.001);
Color += tex2D( g_samSrcColor, Tex.xy+0.002);
Color += tex2D( g_samSrcColor, Tex.xy+0.003);

This should create a diagonal blur. It's really bright though! This is because we've added 4 samples together. We need to divide it by 4 to get back to normal.
Color = tex2D( g_samSrcColor, Tex.xy);
Color += tex2D( g_samSrcColor, Tex.xy+0.001);
Color += tex2D( g_samSrcColor, Tex.xy+0.002);
Color += tex2D( g_samSrcColor, Tex.xy+0.003);
Color = Color / 4;

Much better.
Example Effects
Here's a few example effects to get you started..
Color = tex2D( g_samSrcColor, Tex.xy);
Color -= tex2D( g_samSrcColor, Tex.xy+0.0001)*10.0f;
Color += tex2D( g_samSrcColor, Tex.xy-0.0001)*10.0f;

This makes stuff a lot sharper. You can use this same technique to do a couple more effects..
Color.a = 1.0f;
Color.rgb = 0.5f;
Color -= tex2D( g_samSrcColor, Tex.xy-0.001)*2.0f;
Color += tex2D( g_samSrcColor, Tex.xy+0.001)*2.0f;
Color.rgb = (Color.r+Color.g+Color.b)/3.0f;

Embossed
Color.a = 1.0f;
Color = tex2D( g_samSrcColor, Tex.xy);
Color.rgb = (Color.r+Color.g+Color.b)/3.0f;
if (Color.r<0.2 || Color.r>0.8) Color.r = 0.0f; else Color.r = 1.0f;
if (Color.g<0.2 || Color.g>0.8) Color.g = 0.0f; else Color.g = 1.0f;
if (Color.b<0.2 || Color.b>0.8) Color.b = 0.0f; else Color.b = 1.0f;

Black and white. You should be able to work out how to do a simple grey scale from this.
Color = 1-tex2D( g_samSrcColor, Tex.xy);
Color.a = 1.0f;

Inverted
This should get you started editing the fx files. I'm really interested in what you achieve with this knowledge.