Creating your first shader in Monogame with Nez

What do I need to get started?

A Monogame project set up and ready with Nez referenced. That's it!

Why make a shader for 2D?

Well, shaders can do some really awesome stuff! Typically people think about shaders in regards to lighting in 3D engines but they can do a TON for you in 2D as well. Reflection, bloom, fades, and a slew of other effects can be created via shaders. Nez even comes with a handful of these effects pre-made for you.

A shader based screen wipe from a game I'm working on

It's also nice to handle things that can be accomplished other ways via shaders sometimes. I'm thinking in this case of a screen wipe . You could probably just move a black sprite across the screen in the foreground and cover everything that way, but this has a few problems:

  1. As soon as you want to do anything slightly complex with the wipe, creating these overlay objects will get exponentially more cumbersome
  2. This already feels awkward when you can simple update some values on a shader and have that wipe moving!

How do I make a shader in Monogame?

In Monogame shaders are handled via something called Effects (carried over from XNA) they even have a class called BasicEffect which let you dodge some shader code and write simple shaders in C#. We won't be covering these today however, we want to dive into some (simple) shader code.

Effects in monogame are loaded from .fx files. These are files containing the HLSL (High Level Shading Language) code. This is the type of shader code we will be writing. Don't worry that HLSL was made for DirectX, monogame handles the heavy lifting in getting it running elsewhere.

How can I use my shader in Nez

Nez allows you to use shaders in a few ways. The one we really want to talk about here are PostProcessor effects. Creating a PostProcessor & loading an effect into it, we can apply a shader to the entire screen. This will allow us to wipe over everything regardless of layers or how it is being rendered!

Adding a PostProcessor gives me a black screen!

This is an unfortunate bug atm, but luckily we have a fix! Referenced in this github issue rafaelalmeidatk offers a simple hacky fix until the problem is handled. In your game's initialize add this code (replacing Basic Scene with whatever scene you would like to load)

protected override void Initialize()  
{
    base.Initialize();
    scene = Scene.createWithDefaultRenderer();
    base.Update(new GameTime());
    base.Draw(new GameTime());
    scene = new BasicScene();
}

Can we please write a shader now ?!

Okay, okay, I hear you. Sometimes it's most fun to just get something going and play with the code yourself. So let's dive right in.

The Shader itself

First we will to create the .fx file.

sampler _mainTex;  
float _cutoff;  

float4 frag(float2 coords : TEXCOORD0) : COLOR0  
{   
    if (_cutoff > coords.x)
        return float4 (0, 0, 0, 1);
    return tex2D(_mainTex, coords);
}

technique Technique1  
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 frag();
    }
}

Not a ton of lines, but a lot to unpack:

technique Technique1  
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 frag();
    }
}

Last thing's first, this bit of code just defines our PixelShader to be the function frag() (a reference to the pixel shader often being referred to as the fragment shader in other shader languages). A PixelShader will essentially loop over each and every pixel of whatever sample you send through to the shader and apply whatever effect you return. In our case we will be returning COLOR0 (the color of the pixel in question). You can have more than one Pass but for our use case we just want 1 technique and pass.

sampler _mainTex;  
float _cutoff;  

Next our global variables. These can be set outside the shader and are very important. The sampler _mainTex; is the texture the pixel shader will be using to, well, sample from. With a PostProcessor this will be the entire screen. So our PixelShader will be running over each pixel of the entire screen! The other variable is a float for the cutoff (how far across the screen should the wipe be).

float4 frag(float2 coords : TEXCOORD0) : COLOR0  
{   
    if (_cutoff > coords.x)
        return float4 (0, 0, 0, 1);
    return tex2D(_mainTex, coords);
}

Ahh now onto the good stuff. This is the function we have assigned to the pixel shader. You can see it returns float4 and after the : is COLOR0. All this means is we are returning a float4 that we want applied to the pixel color for the pixel we are sampling. As a parameter of this function we have a float2 for the coordinates of the current pixel, you can think of this like a Vector2. As for the logic, pretty simple. If the cutoff (value 0-1) is greater than the current pixel X coordinate (also a value 0-1, where 0 is the far left of the sample and 1 is the far right), then we will return black (represented by float4(r,g,b,a)) otherwise we return what our sample had at this coordinate (basically make no change from the original).

Great so now our .fx file is ready. There are lots of fun things you can do to spice up this wipe later. Make the color editable or have it change as the cutoff does, change the shape of the wipe (as I did in the above gif), or maybe make it wipe in the opposite direction. These will all be fun things to play with after we get it working in our game.

We have to treat our .fx files the same way we treat all our content and load it into the content manager. Then to use it in game we load it like any other content from the manager!

            effect = scene.content.Load<Effect>("Effects/wipe");

The PostProcessor

We now have an effect file, but don't know how to use it! Luckily, like so many other things, Nez makes this easy. We just have to create a class that inherits from Nez.PostProcessor and have that class set our global variables.

public class WipePostProcessor : PostProcessor  
    {
        float _cutoff = 0f;
        private float _maxCutoff = 1f;
        public float Cutoff
        {
            get { return _cutoff; }
            set
            {
                if (value < 0f) _cutoff = 0f;
                else if (value > _maxCutoff) _cutoff = _maxCutoff;
                else _cutoff = value;
                cutoffParam?.SetValue(_cutoff);
            }
        }

        EffectParameter cutoffParam;

        public WipePostProcessor(int executionOrder, Effect effect = null) : base(executionOrder, effect) {}

        public override void onAddedToScene()
        {
            effect = scene.content.Load<Effect>("Effects/wipe");
            cutoffParam = effect.Parameters["_cutoff"];
            Cutoff = 0;
        }
    }

This one is actually pretty simple. Cutoff is a float 0-1 that we lock to 0-1 behind a public property and on being added to the scene we load our wipe.fx file into the effect property of the processor. The only confusing bit is the EffectParameter. This is a class which lets you assign a parameter of the effect once then apply changes to it without having to type the name of the variable as a string over and over. You can see we load it via cutoffParam = effect.Parameters["_cutoff"]; as it is added to the scene and we update it as Cutoff is updated cutoffParam?.SetValue(_cutoff);.

Finally using it in your scene

Now that is this loaded you can use it in your scene simply by adding it with var wipe = addPostProcessor<WipePostProcessor>(new WipePostProcessor(0)); and changing the cutoff with wipe.Cutoff += 0.1f; or something of the like.

For my game I ended up calling an update function on the WipePostProcessor in my scenes update and just having a function to turn it on and off. The update function would just crank the wipe up to 1f, pause for a short time (something like 0.3f) and then go back to 0.

More Shaders Please

If you insist. I think as I learn more about shaders myself I'll continue to write some simple posts about them.

In the meantime check out the case studies on Making Stuff Look Good. The Pokemon Battle Transitions episode was the inspiration for this and it's not impossible to convert the shader code written there (a form of HLSL with some Unity3D specific stuff) to HLSL for Monogame. If you want a non-Nez specific shader intro for Monogame then check out My First 2D Pixel Shader

Keep up with me @neipo13 on twitter

Andrew Napierkowski

Professional Software Engineer | Hobbyist Game Developer