Shaders

So in the other guide we used Monogame's in-built spritebatch to draw our sprite/texture/image on to the screen. Its a perfectly good method, but sometimes you just want to do your own shaders.
In this tutorial we'll be using the Effect class, but before that we need to write a simple shader.

Create a new file in your Content folder and call it "shader.fx". (Monogame does have predefined shaders, but I'll cover those another time)

Matrices

I won't go into the nitty gritty about matrices here. All I'll say is we need 3 of them. They are used to calculate the point at which pixels are drawn on screen.

Define the matrices and a Texture2D as follows in your shader.fx.

 
				 
float4x4 World;
float4x4 View;
float4x4 Projection;  

Texture2D SpriteSheet;
				
			

The three matrices purposes:

  • World - Also known as the Model matrix, is used for determining the position of an object in the world e.g. (0,0,0) - origin
  • View - Think of this simply as where your eye/camera in the world is.
  • Projection - Usually orthographic or perspective. If perspective then the further away something is the smaller it will appear. Orthographic does not do this perspective divide and so no matter how "far" away something is from the View, it'll be appear the same size. Good for 2D.

Inputs, Outputs & Samplers

Next we need a texture sampler so we can colour our pixels correctly on screen (SpriteSheetSampler).

The VertextShaderInput is the data passed from our game through to the video card so we can calculate where things need to be drawn.

The VertexShaderOutput contains the final pixel position and that pixel's colour once our shader has finished its calculations.

 
				 
SamplerState SpriteSheetSampler
{
  Filter = None;
  AddressU = Wrap;
  AddressV = Wrap;
  Texture = (SpriteSheet);
};

struct VertexShaderInput
{
  float4 Position : POSITION0;
  float4 Normal : NORMAL0;
  float2 TextureUV : TEXCOORD0;

};

struct VertexShaderOutput
{
  float4 Position : POSITION0;
  float2 TextureUV : TEXCOORD0;
};
				
			

Vertex and Pixel Shader Functions

The VertexShaderFunction is where we do the calculations to determine where the pixel is on screen. These calulations take the data from the input and we store the results in the VertexShaderOutput object.


The PixelShaderFunction determines the colour of the pixel based on the position of texture's UV co-ordinates.

 
				 
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
  VertexShaderOutput output;

  float4 worldPosition = mul(input.Position, World);
  float4 viewPosition = mul(worldPosition, View);
  output.Position = mul(viewPosition, Projection);
  output.TextureUV = input.TextureUV;
  return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
  return tex2D(SpriteSheetSampler, input.TextureUV);
}
				
			



Finally we specify the technique. These enable us to use multiple shading functions with a single draw call. I.e pass1: draw scene , pass2: draw outlines. For this example, though, we'll just be using a single pass.

 
				 
technique basic 
{
  pass Pass1
  {
    VertexShader = compile vs_2_0 VertexShaderFunction();
    PixelShader = compile ps_2_0 PixelShaderFunction();
  }
}
				
			

With all that now defined in our shader.fx file, we can begin loading it into our program and drawing to the screen. As usual, here is the source.

Loading the shader into the Project

With the complete shader, it is time to import it first into the Pipeline Tool. This process is exactly the same as for images.

Press F6 to build it and check there are no error messages. The build is done when activated in Visual Studio, but the error messages are rather cryptic. It is best to build shaders in the pipeline tool to get understandable error messages. (If you think there is a chance that you made an error)

We'll now head back to our Visual studio project. You can either use the project created from the basics guide or just create a new one using the "Cross-platform" Monogame template.

In the Game class, some more members need to be added. Ensure your code looks as follows:

 
				 

namespace MyAwesomeGame
{

  public class Game1  : Game
  {

    GraphicsDeviceManager graphics;
    Texture2D       boxman;
    Vector3         boxmanPos = new Vector3(0,0,0);

   VertexBuffer    spriteVertexBuffer;
   Effect          shader;

    Matrix          World;
    Matrix          View;
    Matrix          Projection;

    int screenWidth = 800;
    int screenHeight = 600;

    public Game1()
    {
      graphics = new GraphicsDeviceManager( this);
      graphics.PreferredBackBufferWidth = screenWidth;
      graphics.PreferredBackBufferHeight = screenHeight;

      Content.RootDirectory = "Content"; 
    }
				
			

So there are quite a few changes from the basic tutorial but there is nothing too complicated.

The first change is the position of boxman to a Vector3. This can be useful over a Vector2 if you are wanting to easily organise multiple layers. It will give easier control on the front-to-back order of the entities drawn on screen. The beauty is, it doesn't have to be used.

Initialise our matrices and vertexbuffer:

 
				 
protected override void Initialize()
{
  Matrix.CreateOrthographic(screenWidth, screenHeight, 1000.0f, -1000.0f, out Projection);
  View = Matrix.CreateLookAt( new Vector3(0,0,10),Vector3.Zero, Vector3.Up);

  float width = 0.5f;
  float height = 0.5f;
  float uv = 1f;

  var spriteVertices = new VertexPositionNormalTexture[6];

  spriteVertices[0] = new VertexPositionNormalTexture( new Vector3(-width,height,0f),  Vector3.Forward, new Vector2(0,0));
  spriteVertices[1] = new VertexPositionNormalTexture( new Vector3(-width,-height,0f), Vector3.Forward, new Vector2(0,uv));
  spriteVertices[2] = new VertexPositionNormalTexture( new Vector3(width,-height,0f),  Vector3.Forward, new Vector2(uv,uv));
  spriteVertices[3] = new VertexPositionNormalTexture( new Vector3(width,-height,0f),  Vector3.Forward, new Vector2(uv,uv));
  spriteVertices[4] = new VertexPositionNormalTexture( new Vector3(width,height,0f), Vector3.Forward, new Vector2(uv,0));
  spriteVertices[5] = new VertexPositionNormalTexture( new Vector3(-width,height,0f),  Vector3.Forward, new Vector2(0,0));

  spriteVertexBuffer = new VertexBuffer(graphics.GraphicsDevice,typeof(VertexPositionNormalTexture),6,BufferUsage.WriteOnly);
  spriteVertexBuffer.Name = "Sprite Vertex Buffer";
  spriteVertexBuffer.SetData<VertexPositionNormalTexture>(spriteVertices);

  GraphicsDevice.RasterizerState = RasterizerState.CullClockwise;
 base.Initialize();
}
				
			

Use the ContentManager to load the shader to our Effect member:

 
				 
protected override void LoadContent()
{
  boxman =  Content.Load<Texture2D>("boxman");
  shader =  Content.Load<Effect>("shader");
}
				
			

Drawing with the Shader

Finally, we use it in our draw method.

 
				 
protected override void Draw(GameTime gameTime)
{
      GraphicsDevice.Clear(Color.CornflowerBlue);

      World = Matrix.Identity;
      World *= Matrix.CreateScale(32);
      World *= Matrix.CreateTranslation(boxmanPos);

      shader.Parameters["World"].SetValue(World);
      shader.Parameters["View"].SetValue(View);
      shader.Parameters["Projection"].SetValue(Projection);
      shader.Parameters["SpriteSheet"].SetValue(boxman);

      GraphicsDevice.SetVertexBuffer(spriteVertexBuffer);

      foreach(EffectPass pass in shader.CurrentTechnique.Passes)
      {
        pass.Apply();
        GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList,0,4);
      }

     base.Draw(gameTime);
}
				
			

The World matrix is essentially the matrix for our sprite. It is translated using the position. A scale of 32 is used so that 1 unit is equal to 1 pixel (in this particular instance).
The shader.Parameters are the variables/data passed to the graphics card to do our drawing, and finally the scene is drawn. You should see the same as the image below. Source available here.

Email: magellanicDev@gmail.com
Last Updated: 16/02/2019
Copyright: Magellanic Games LTD

Generated with SpeedyHtml.