
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:
- 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.
- I use
RetrocadeRenderBatch
which is my own extension toSpriteBatch
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:
It calls 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.
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 = <SpriteTexture>;
};
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:
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
- Wrap all your rendering in
GraphicsDevice.SetRenderTarget(RenderTarget);
andGraphicsDevice.SetRenderTarget(null);
and I mean all of it. - Create the actual shader file
- Import it with MonoGame Pipeline Tool (alternatively use 2MGFX util to convert it to necessary format)
- Load it with
content.Load<Effect>("shaders/shader");
- When rendering your
RenderTarget
, add the loaded effect in begin:batch.Begin(Gfx.PostProcessShader);
[mc4wp_form id="444"]