Post-Processing Shaders

Introduction

By now, you have probably done quite a bit of work with shaders. But up until this point, all of the tutorials have involved 3D shaders. It is possible to create a shader that works in 2D only. In fact, these kinds of shaders are much simpler to work with. Usually shaders like this are used when you want to do a post-processing effect. A post-processing effect is an effect that you want to apply to the entire window after you have drawn the entire scene. For instance, a common post-processing effect is to make your scene black and white, give it a sepia tone to look like an old photograph, or blur the screen or to create a bloom effect. In this tutorial, we are going to create a few different pixel shaders that perform a post-processing effect, first with just the regular coloring, then in black and white, and then finally, in a sepia tone.

I would recommend that you go through the tutorial on rendering to a texture, which shows you how to render your entire scene to a texture which you can then draw. If you don't want to go through that tutorial, feel free to just go grab the code at the end of that tutorial, or just use another texture. (These effects can be applied to any texture, so you could just apply it to a texture you load through the content pipeline, instead of one that you created in-game.)

A 2D Texture Shader

We will start with our very simple pixel shader that just takes the texture color and uses it. This won't have a very fancy effect, but we will be able to tell if it is working or not. Like with the previous tutorial, I am just going to simply give you the code for the shader up front, and we'll discuss it. This one's pretty basic:

//------------------------------ TEXTURE PROPERTIES ----------------------------
// This is the texture that SpriteBatch will try to set before drawing
texture ScreenTexture;
 
// Our sampler for the texture, which is just going to be pretty simple
sampler TextureSampler = sampler_state
{
    Texture = <ScreenTexture>;
};
 
//------------------------ PIXEL SHADER ----------------------------------------
// This pixel shader will simply look up the color of the texture at the
// requested point
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(TextureSampler, TextureCoordinate);    
    return color;
}
 
//-------------------------- TECHNIQUES ----------------------------------------
// This technique is pretty simple - only one pass, and only a pixel shader
technique Plain
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

You can see that this shader is extremely simple compared to the ones that we've been working on. We create a variable called ScreenTexture, which is the name that the SpriteBatch class will be looking for. We also have a basic texture sampler.

Our pixel shader simply returns the value it found in the texture.

The technique is extremely simple as well, since it only deals with a single pass that only has a pixel shader.

Copy this code into an effect file. I've called mine "Plain.fx".

Using a 2D Texture Shader in XNA

With a simple effect file ready, you are prepared to use it in XNA. The process of applying an effect to a 2D sprite is a little different than it is when we apply effects to a 3D scene. I have created an instance variable to store my effect (just like we did in 3D):

private Effect effect;

Then I load in the effect from the file in the LoadContent() method:

effect = Content.Load<Effect>("Effects/Plain");

After that, all we really need to do is draw our texture with the effect applied. In my Draw() method, I have the following code which will draw the texture with the effect:

spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, 
    SamplerState.LinearClamp, DepthStencilState.Default, 
    RasterizerState.CullNone, effect);
 
spriteBatch.Draw(texture, new Rectangle(0, 0, 800, 480), Color.White);
 
spriteBatch.End();

The key here, is that we set up the spriteBatch with our effect when we call spriteBatch.Begin. There are only a couple of overloads for the Begin method that allow us to specify an effect, and the one we're using here is the simplest. Even then, we are still required to specify all sorts of information, like the SpriteSortMode, and the BlendState. The things I've chosen here are just good defaults.

At this point, you should be able to run your game and see your game and your texture exactly like it was originally.

screenshot1.png

My code for this is below.

A Black and White Texture Shader

Well now that we've done that basic shader, let's try something a little more complicated. Let's take our texture and make it black and white (grayscale). This will be pretty simple and will only require a small change to your shader. (I made a copy of my plain shader and called it "BlackAndWhite.fx", just to keep them separate.) To determine the grayscale equivalent of a color, we will simply average the red, green, and blue values, and then set the red, green, and blue components to this average value. Below is my code for the black and white shader:

//------------------------------ TEXTURE PROPERTIES ----------------------------
// This is the texture that SpriteBatch will try to set before drawing
texture ScreenTexture;
 
// Our sampler for the texture, which is just going to be pretty simple
sampler TextureSampler = sampler_state
{
    Texture = <ScreenTexture>;
};
 
//------------------------ PIXEL SHADER ----------------------------------------
// This pixel shader will simply look up the color of the texture at the
// requested point, and turns it into a shade of gray
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(TextureSampler, TextureCoordinate);
 
    float value = (color.r + color.g + color.b) / 3; 
    color.r = value;
    color.g = value;
    color.b = value;
 
    return color;
}
 
//-------------------------- TECHNIQUES ----------------------------------------
// This technique is pretty simple - only one pass, and only a pixel shader
technique BlackAndWhite
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

The only real change was in the pixel shader, where we calculate the average value of the red, green, and blue components of the pixel and then assign that value to each of the color components. In your XNA game, you won't need to make any changes, unless you are using a new shader file, in which case you may need to change the name of the effect file that you read in. (For instance, I've changed my code from saying Content.Load<Effect>("Effect/Plain"); to say Content.Load<Effect>("Effect/BlackAndWhite"); because that is the name of the file that contains my black and white shader. You should be able to run your game again with the changes and get something like the following:

screenshot2.png

A Sepia Tone Shader

As a final shader, here, we will make one that does a sepia tone, which makes the texture look kind of like an old photograph. This shader, too is pretty simple, and once again, we will only need to make changes to the pixel shader itself. Once again, I have made a copy of my effect and called the new one "Sepia.fx". The math for this is a little more complicated, but essentially, we will take each color component and calculate its new value by combining the old color values in a specific way.

//------------------------------ TEXTURE PROPERTIES ----------------------------
// This is the texture that SpriteBatch will try to set before drawing
texture ScreenTexture;
 
// Our sampler for the texture, which is just going to be pretty simple
sampler TextureSampler = sampler_state
{
    Texture = <ScreenTexture>;
};
 
//------------------------ PIXEL SHADER ----------------------------------------
// This pixel shader will simply look up the color of the texture at the
// requested point, and turns it into a sepia tone
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(TextureSampler, TextureCoordinate);
 
    float4 outputColor = color;
    outputColor.r = (color.r * 0.393) + (color.g * 0.769) + (color.b * 0.189);
    outputColor.g = (color.r * 0.349) + (color.g * 0.686) + (color.b * 0.168);    
    outputColor.b = (color.r * 0.272) + (color.g * 0.534) + (color.b * 0.131);
 
    return outputColor;
}
 
//-------------------------- TECHNIQUES ----------------------------------------
// This technique is pretty simple - only one pass, and only a pixel shader
technique Sepia
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

So here we just take the three color components and refactor them together to get a sepia tone. You can play around with these numbers, which will give different colorations.

Once again, you should be able to run your game and see your scene rendered with a sepia tone.

screenshot3.png

My full source code for this project is contained in the link below:

PostProcessing.zip

What's Next?

This tutorial covers how to apply a shader directly to a texture. There are still a lot of other things that you can do with this, including bloom, and edge detection to get a nicer toon shader. I don't have any tutorials that do this at the moment, but hopefully in the future I will.


Troubleshooting.png Having problems with this tutorial? Try the troubleshooting page!