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

« »