Retrocade.net

Indie Game Dev

Post-processing shaders in Monogame

While working on Trans Neuronica I realized I don’t want to work on some things relating to the gameplay right now, so I figured “Hey Maurycy, it’s time to do all the cool shader effects you want to have in the final game.” I don’t really want to do that either, but I already wasted some time on research, so why not finish it and talk it over?

Two important things first:

  1. This is not a typical tutorial that takes you step-by-step explaining in-depth everything I do. I’m going to dump a lot of knowledge without much explanation but I think overall the steps are pretty simple.
  2. I use RetrocadeRenderBatch which is my own extension to SpriteBatch so the functions may not map 1:1. And there is some other code that you should understand how it works but won’t be able to find it in MonoGame.

Shaders?

Shaders are tiny functions/programs you upload to your GPU so it can do magic on things you render. Vertex shaders are concerned about the geometry, fragment/pixel shaders are concerned with manipulating the actual pixels before they are rendered.

Post-processing shaders are basically pixel shaders which take your whole, rendered scene as the input and manipulate it in some way, for example to add CRT simulation or add ripple.

Let’s start

So! I have my own C# framework built on top of MonoGame and MonoGame Extended called Retromono (I’ll talk about it some other day). I have RetrocadeGame class extending Mono’s Game which wraps around a couple of custom components of mine (like States and UI and Input Managers) and it also wraps around all of the rendering in the game.
What I did first was to inject a new PostProcessor class which has one method called directly before all Draws and another directly after:

Yes, there is a slight inconsistency with the naming there. I’ll refactor it later.

What it does, basically is call Game.GraphicsDevice.SetRenderTarget(RenderTarget); before anything draws and Game.GraphicsDevice.SetRenderTarget(null); after everything draws. Then it puts RenderTarget on the screen. Here is my PostProcessor class which I extend for the specific game/project/use:

public abstract class BasePostProcessor {
    protected readonly RenderTarget2D RenderTarget;
    protected readonly RetrocadeGame Game;

    protected BasePostProcessor(RetrocadeGame game, int width, int height) {
        Game = game;
        RenderTarget = new RenderTarget2D(game.GraphicsDevice, width, height);
    }

    public virtual void BeforeDraw(RetrocadeRenderBatch batch) {
        Game.GraphicsDevice.SetRenderTarget(RenderTarget);
    }

    public void AfterRender(RetrocadeRenderBatch batch) {
        AfterDraw(batch);
        DrawSelf(batch);
    }

    protected virtual void AfterDraw(RetrocadeRenderBatch batch) {
        Game.GraphicsDevice.SetRenderTarget(null);
    }

    protected virtual void DrawSelf(RetrocadeRenderBatch batch) {
        batch.Begin();
        batch.Draw(RenderTarget, Vector2.Zero, RenderTarget.Bounds);
        batch.End();
    }
}

Applying cool shaders

The above code is basically all that I need to get the post-processing working with a single exception – I don’t have the shader yet. I don’t have it physically and I don’t have it loaded and used, so technically that’s three exceptions.

Without further ado, here is the shader we’ll use:

#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0 // This was ps_4_0_level_9_1, do that for DirectX

Texture2D SpriteTexture;

sampler2D SpriteTextureSampler = sampler_state
{
    Texture = ;
};

struct VertexShaderOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR0;
    float2 TextureCoordinates : TEXCOORD0;
};

// I don't understand the stuff before here
float4 MainPS(VertexShaderOutput input) : COLOR
{
	// Applying our cool effect. What it does is: when drawing pixel X:Y, instead of taking the 
	// pixel from texture position X:Y, take it from (X+Y*0.2:Y) to create a slanted effected
	float2 tex2; // I am using a temp var because I don't know if we can/should modify input.TC
	tex2[0] = input.TextureCoordinates[0] - input.TextureCoordinates[1] * 0.2f;
	tex2[1] = input.TextureCoordinates[1];
	
    return tex2D(SpriteTextureSampler,tex2) * input.Color;
}
// Here comes the rest of the things I don't understand
technique SpriteDrawing
{
    pass P0
    {
        PixelShader = compile PS_SHADERMODEL MainPS();
    }
};

Before I tell you how to get this into your project, here is a gif showing it in action so that you don’t get too bored after that wall of textcode:

When I press Ctrl the shader is used.

Fancy! Save the shader above as shader.fx somewhere in your assets directory and open MonoGame’s Pipeline Tool. Create a new project if you haven’t already, drop your shader, make sure your project build platform is DesktopGL (if you’re making your project for other platform you might need to change the shader somehow but I don’t know how ¯\_(ツ)_/¯). Then load it and feed it to draw.Begin():

Code for importing the shader: Effect PostProcessShader = content.Load("shaders/shader");

Code for using the shader: batch.Begin(effect:PostProcessShader);

And voila!

tl;dr

  1. Wrap all your rendering in GraphicsDevice.SetRenderTarget(RenderTarget); and GraphicsDevice.SetRenderTarget(null); and I mean all of  it.
  2. Create the actual shader file
  3. Import it with MonoGame Pipeline Tool (alternatively use 2MGFX util to convert it to necessary format)
  4. Load it with content.Load<Effect>("shaders/shader");
  5. When rendering your RenderTarget, add the loaded effect in begin: batch.Begin(Gfx.PostProcessShader);