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)
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:
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;
};
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.
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");
}
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.