XNA Game Engine Tutorial #10: Post Processing

In this tutorial, we will be adding a postprocessing system to our engine. A postprocessor is an effect (.fx file) that only contains a pixel shader. It takes the current frame buffer value and applies the pixel shader to it, applying effects such as blurs, bloom, and special effects. The post processor class is called PostProcessor and has methods for getting the current frame buffer and drawing with the post processor.

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace Innovation
{
    // Applies a post processing effect to a texture or the frame buffer
    public class PostProcessor : Component, I2DComponent
    {
        // The post processor fx file
        public Effect Effect;

        // Width and height the the render target/frame buffer
        protected int Width;
        protected int Height;

        // Texture that will processed
        Texture2D input;

        public Texture2D Input
        {
            get { return input; }
            set
            {
                input = value;

                if (Effect.Parameters["InputTexture"] != null)
                    Effect.Parameters["InputTexture"].SetValue(value);
            }
        }

        // Draw rectangle
        public Rectangle Rectangle { get { return new Rectangle(0, 0, Width, Height); } set { this.Width = value.Width; this.Height = value.Height; } }

        public PostProcessor()
            : base()
        {
            Setup(null, Engine.GraphicsDevice.Viewport.Width, Engine.GraphicsDevice.Viewport.Height);
        }

        public PostProcessor(Effect Effect, int Width, int Height) : base()
        {
            Setup(Effect, Width, Height);
        }

        public PostProcessor(Effect Effect, int Width, int Height, GameScreen Parent)
            : base(Parent)
        {
            Setup(Effect, Width, Height);
        }

        // Set the effect, width, and height, and inititalize the Input texture
        void Setup(Effect Effect, int Width, int Height)
        {
            this.Effect = Effect;
            Input = new Texture2D(Engine.GraphicsDevice, 1, 1);
            Input.SetData<Color>(new Color[] { Color.White });
            this.Width = Width;
            this.Height = Height;
        }

        // Gets the current scene texture from the frame buffer
        public void GetInputFromFrameBuffer()
        {
            // Make sure we are working with a resolve texture
            if (!(Input is ResolveTexture2D))
                Input = GraphicsUtil.CreateResolveTexture();

            // Then resolve the frame buffer to the Input texture
            Engine.GraphicsDevice.ResolveBackBuffer((ResolveTexture2D)Input);
        }

        public override void Draw()
        {
            Engine.GraphicsDevice.Clear(Color.Black);

            // Begin in a mode that will overrite everything and draw immediately
            Engine.SpriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
            Effect.Begin();

            // For each pass...
            foreach (EffectPass pass in Effect.CurrentTechnique.Passes)
            {
                pass.Begin();
                // Draw the input texture with the effect applied
                Engine.SpriteBatch.Draw(Input, Rectangle, Color.White);
                pass.End();
            }

            Effect.End();
            Engine.SpriteBatch.End();

            base.Draw();
        }
    }
}

Lets create an example post processor, a gaussian blur. The gaussian blur first blurs the pixels horizontally, then vertically, using weights and offsets generated based on the width and height of the texture:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Innovation
{
    // Post processor that applies a Gaussian blur to an image or the frame buffer
    public class GaussianBlur : PostProcessor
    {
        // Offsets and wieghts for horizontal and vertical blurs
        Vector2[] sampleOffsetsH = new Vector2[15];
        float[] sampleWeightsH = new float[15];

        Vector2[] sampleOffsetsV = new Vector2[15];
        float[] sampleWeightsV = new float[15];

        // Constructors load Effect called GaussianBlur.fx
        public GaussianBlur(int Width, int Height)
            : base(Engine.Content.Load<Effect>("Content/GaussianBlur"), Width, Height)
        {
            Setup(Width, Height);
        }

        public GaussianBlur(int Width, int Height, GameScreen Parent)
            : base(Engine.Content.Load<Effect>("Content/GaussianBlur"), Width, Height, Parent)
        {
            Setup(Width, Height);
        }

        // Calculate the weights and offsets based on width and height
        void Setup(int Width, int Height)
        {
            Vector2 texelSize = new Vector2(1f / Width, 1f / Height);

            SetBlurParameters(texelSize.X, 0, ref sampleOffsetsH, ref sampleWeightsH);
            SetBlurParameters(0, texelSize.Y, ref sampleOffsetsV, ref sampleWeightsV);
        }

        // Borrowed from "Shadow Mapping" by Andrew Joli
        // http://www.ziggyware.com/readarticle.php?article_id=161
        void SetBlurParameters(float dx, float dy, ref Vector2[] vSampleOffsets, ref float[] fSampleWeights)
        {
            // The first sample always has a zero offset.
            fSampleWeights[0] = ComputeGaussian(0);
            vSampleOffsets[0] = new Vector2(0);

            // Maintain a sum of all the weighting values.
            float totalWeights = fSampleWeights[0];

            // Add pairs of additional sample taps, positioned
            // along a line in both directions from the center.
            for (int i = 0; i < 15 / 2; i++)
            {
                // Store weights for the positive and negative taps.
                float weight = ComputeGaussian(i + 1);

                fSampleWeights[i * 2 + 1] = weight;
                fSampleWeights[i * 2 + 2] = weight;

                totalWeights += weight * 2;

                // The 1.5 offset kicks things off by
                // positioning us nicely in between two texels.
                float sampleOffset = i * 2 + 1.5f;

                Vector2 delta = new Vector2(dx, dy) * sampleOffset;

                // Store texture coordinate offsets for the positive and negative taps.
                vSampleOffsets[i * 2 + 1] = delta;
                vSampleOffsets[i * 2 + 2] = -delta;
            }

            // Normalize the list of sample weightings, so they will always sum to one.
            for (int i = 0; i < fSampleWeights.Length; i++)
                fSampleWeights[i] /= totalWeights;
        }

        // Borrowed from "Shadow Mapping" by Andrew Joli
        // http://www.ziggyware.com/readarticle.php?article_id=161
        private float ComputeGaussian(float n)
        {
            float theta = 2.0f + float.Epsilon;
            return theta = (float)((1.0 / Math.Sqrt(2 * Math.PI * theta)) * Math.Exp(-(n * n) / (2 * theta * theta)));
        }

        // Applies the post processor to the texture in the specified direction
        public void Draw(GaussianBlurDirection Direction, Texture2D Input)
        {
            this.Input = Input;
            SetParameters(Direction);
            base.Draw();
        }

        // Applies Gaussian Blur to frame buffer
        public override void Draw()
        {
            GetInputFromFrameBuffer(); // Set Input texture
            Engine.GraphicsDevice.Clear(Color.Black);
            SetParameters(GaussianBlurDirection.Horizontal); // Set horizontal parameters
            base.Draw(); // Apply blur

            GetInputFromFrameBuffer(); // Set Input texture again
            Engine.GraphicsDevice.Clear(Color.Black);
            SetParameters(GaussianBlurDirection.Vertical); // Set vertical parameters
            base.Draw(); // Apply blur
        }

        // Set blur parameters to effect
        void SetParameters(GaussianBlurDirection Direction)
        {
            if (Direction == GaussianBlurDirection.Horizontal)
            {
                Effect.Parameters["sampleWeights"].SetValue(sampleWeightsH);
                Effect.Parameters["sampleOffsets"].SetValue(sampleOffsetsH);
            }
            else
            {
                Effect.Parameters["sampleWeights"].SetValue(sampleWeightsV);
                Effect.Parameters["sampleOffsets"].SetValue(sampleOffsetsV);
            }
        }
    }

    // Direction of Gaussian Blur: Horizontal or Vertical
    public enum GaussianBlurDirection { Horizontal, Vertical };
}

And here is the post processor effect file, GaussianBlur.fx:

float sampleWeights[15];
float2 sampleOffsets[15];

Texture InputTexture;

sampler inputTexture = sampler_state
{
    texture = <InputTexture>;
    magfilter = LINEAR;
    minfilter = LINEAR;
    mipfilter = LINEAR;
};

struct VS_OUTPUT
{
	float4 Position	: POSITION;
	float2 TexCoords : TEXCOORD0;
};

float4 GaussianBlur_PS (VS_OUTPUT Input) : COLOR0
{
	float4 color = float4(0, 0, 0, 1);

	for(int i = 0; i < 15; i++ )
		color += tex2D(inputTexture, Input.TexCoords + sampleOffsets[i]) * sampleWeights[i];

	return color;
}

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

And thats all there is to it! Now to implement a Gaussian Blur:

// Top of Game1 class:

GaussianBlur blur;

// End of LoadContent() method:

blur = new GaussianBlur(Engine.GraphicsDevice.Viewport.Width, Engine.GraphicsDevice.Viewport.Height);
blur.Visible = false; // This will keep the engine from drawing it before we want it to

// End of Draw() method:

blur.Draw();

Now, if you run the game, you should see everything is all blurry:

Gaussian Blurred Scene

Thats all for now!

Download the Code for this Chapter
Back to Game Engine Tutorial Series

18 Responses to “XNA Game Engine Tutorial #10: Post Processing” »

  1. Comment by Murali — December 30, 2008 @ 4:17 pm

    Thanks a lot Sean.

  2. Comment by Sami — December 31, 2008 @ 5:34 am

    Awesome, Thank you.

  3. Comment by James — December 31, 2008 @ 7:03 am

    Very nice Sean loving the speed with which you are pumping out these articles. Seeing as the post processor inherits from Component it has an automatic call to Draw() so you are getting Blur called twice which as well as making it quite blurry is killing your frame rate. Any reason for this that I am overlooking?

  4. Comment by Sean — December 31, 2008 @ 10:25 am

    Right, I forgot about that. Set blur.Visible = false; in the game class and it should only draw when told to.

  5. Comment by AndrĂ© — January 1, 2009 @ 8:58 am

    I’m deeply impressed :) ))

    I have downloaded the XNA tools from Microsoft just two days ago and haven’t had any experience with them yet. Your tutorial is great getting into the XNA stuff even if you just started doing some 3D work like me. Thank you!

  6. Comment by Jake — January 2, 2009 @ 1:10 pm

    This is me probably me being silly but couldnt the the virtical and horizontal blur be done as multiple passes in the shader rather than clearing the backbuffer then redrawing with another setting?

  7. Comment by Sean — January 2, 2009 @ 3:35 pm

    Yes, but its effectively the same thing as we already have. Plus, this way we save extra code in the shader by writing the same code for different directions or using ‘if’ statements.

  8. Comment by Eldon — January 4, 2009 @ 2:05 pm

    This is amazing. I have spent at lease $300 on books for XNA (starting with GS 1.0) and have learned more from you in these articles than all of the books.

    Not taking away from the books, I just seem to get what you are teaching here better than reading the books.

    Thank You, again. :)

  9. Comment by Cleve — January 23, 2009 @ 4:20 pm

    this tutorial rocks. Bring on the next chapter!

  10. Comment by Philip — March 21, 2009 @ 2:31 am

    Hey there,

    Just to say, a wonderful series of tutorials, good speed and the best way of structuring one’s project.

    What I have experienced as I enabled the blur effect, there suddenly was a problem with the depthbuffer of the terrain.
    As I searched really long for a solution I wanted to give it to people here:

    In the terrain class after

    Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;

    add

    Engine.GraphicsDevice.RenderState.DepthBufferEnable = true;
    Engine.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;

    This should work

    Bye and another time, thank you for the tutorials

  11. Comment by Z3nder — May 30, 2009 @ 3:51 am

    Does it support VertexShader too?

  12. Comment by Natrion — September 29, 2009 @ 4:28 am

    I want to apply a post process distortion effect to an area that is larger then the viewport. (distortion reduces the quality of the image so i want to make it bigger, apply the effect then scale it down)

    So basically, I want to render everything pre-process to a large framebuffer, sample that, take a rectangle from that, downscale it a bit and pass that to the viewport. Can you show me how I do that?

  13. Comment by seo packeg — March 12, 2011 @ 11:01 pm

    this is nice blogs and i’m interested about that. so it is so wonderful blogsite

  14. Trackback by Targeted Facebook Fans — December 14, 2011 @ 11:53 pm

    Website Trackback Link…

    [...]the time to read or visit the content or sites we have linked to below the[...]…

  15. Comment by Pascal — February 6, 2012 @ 9:14 am

    I am converting the Engine to XNA 4. Thus I had to change the draw mehtod of the PostProcessor class to this:

    public override void Draw()
    {
    Engine.GraphicsDevice.Clear(Color.Black);

    // Begin in a mode that will overrite everything and draw immediately
    //Engine.SpriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
    Engine.SpriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    //Effect.Begin();

    // For each pass…
    foreach (EffectPass pass in Effect.CurrentTechnique.Passes)
    {
    pass.Apply();
    // Draw the input texture with the effect applied
    Engine.SpriteBatch.Draw(Input, Rectangle, Color.White);
    }

    //Effect.End();
    Engine.SpriteBatch.End();

    base.Draw();
    }

    But now I get an Error in the Engine.SpriteBatch.Draw line I cant resolve by myself:

    InvalidOperationException:
    The render target must not be set on the device when it is used as a texture.

    I hope anybody is still requesting to this and may help me.

  16. Comment by Khorum — March 18, 2012 @ 4:41 pm

    If anyone is still having trouble with the depth buffer not seeming to work right, mine was a simple matter of the RenderTarget2D needing the appropriate settings. One of the RenderTarget2D overloads calls for a SurfaceFormat. I set this to Color, which finally fixed the depth issue.

    new RenderTarget2D(Engine.GraphicsDevice,
    Engine.GraphicsDevice.Viewport.Width,
    Engine.GraphicsDevice.Viewport.Height,
    false,
    SurfaceFormat.Color,
    DepthFormat.Depth24);

    This was located in the GraphicsUtil class, which I renamed to RenderCaptureUtil since Resolves were removed in 4.0.

    Cheers!

  17. Comment by Khorum — March 18, 2012 @ 4:45 pm

    I meant DepthFormat.Depth24. Silly me. You guys get the picture.

  18. Comment by sharonmc12 — April 27, 2012 @ 2:46 am

    I completely agree with this above comment, the internet is with a doubt growing into your most essential medium of communication across the globe and also its due to sites just like this that ideas are spreading so quickly.
    Quality Used Engines

RSS feed for comments on this post. TrackBack URI

Leave a comment