3 min. readOkay for this first post, I’m going to keep things simple. Here is a shader that does nothing. We’ll put that in a file called Nothing.fx and load it as content in a MonoGame OpenGL game.
#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0_level_9_1
#define PS_SHADERMODEL ps_4_0_level_9_1
#endif
sampler TextureSampler : register(s0);
float2 ViewportSize;
struct VertexToPixel {
float4 Position : SV_Position0;
float4 Color : COLOR0;
float4 TexCoord : TEXCOORD0;
};
VertexToPixel SpriteVertexShader(float4 color : COLOR0, float4 texCoord : TEXCOORD0, float4 position : POSITION0) {
VertexToPixel Output = (VertexToPixel)0;
// Half pixel offset for correct texel centering. - This is solved by DX10 and half pixel offset would actually mess it up
position.xy -= 0.5;
// Viewport adjustment.
position.xy /= ViewportSize;
position.xy *= float2(2, -2);
position.xy -= float2(1, -1);
//pass position and color to PS
Output.Position = position;
Output.Color = color;
Output.TexCoord = texCoord;
return Output;
}
float4 SpritePixelShader(VertexToPixel PSIn): COLOR0 {
float4 diffuse = tex2D(TextureSampler, PSIn.TexCoord);
return PSIn.Color * diffuse;
}
technique SpriteBatch {
pass {
VertexShader = compile VS_SHADERMODEL SpriteVertexShader();
PixelShader = compile PS_SHADERMODEL SpritePixelShader();
}
}
//In LoadContent():
effect = Content.Load<Effect>("Nothing");
//In Draw():
effect.Parameters["ViewportSize"].SetValue(new Vector2(Window.ClientBounds.Width, Window.ClientBounds.Height));
spriteBatch.Begin(effect: effect);
spriteBatch.Draw(image, new Vector2(200, 100), Color.White);
spriteBatch.End();
Note: Not seen but it is implied here that a Texture2D is loaded in the variable “image”.
As you can see, we pass a ViewportSize to the shader and draw the Texture2D using our Nothing shader at a location of 200, 100.
Some information about how shaders work.
sampler TextureSampler : register(s0);
This line receives the “image” Texture2D we gave to the draw call.
float2 ViewportSize;
This line receives the Vector2 passed in using the shader parameters.
SpriteVertexShader(float4 color : COLOR0, float4 texCoord : TEXCOORD0, float4 position : POSITION0)
Here we receive 3 variables in our vertex shader.
position with POSITION0 as the semantic. This means that position uses the screen’s coordinate system. Earlier, in our draw call we passed a Vector2(200, 100). That Vector is used to translate position by 200, 100.
color with COLOR0 as the semantic. This value comes from our draw call. Earlier we passed Color.White. Colors have a value between 0 and 1 in other words, White is (1, 1, 1, 1). The forth parameter is the alpha transparency channel. Note that colors have their alpha premultiplied by default in MonoGame. For example, the color (0.5, 0.5, 0.5, 0.5) is white with 0.5 as the alpha.
texCoord with TEXCOORD0 as the semantic. This one has a mapping of 0 to 1. We use texCoord to sample the texture we received above.
Okay, now we get to the interesting part, the meat of this vertex shader. Turns out this isn’t as complicated as it looks.
// Half pixel offset for correct texel centering. - This is solved by DX10 and half pixel offset would actually mess it up
position.xy -= 0.5;
// Viewport adjustment.
position.xy /= ViewportSize;
position.xy *= float2(2, -2);
position.xy -= float2(1, -1);
So we start with the position variable. This is in screen coordinates. We want to transform this in a different coordinate system. On the X axis, it will go from -1 to 1 and on the Y axis, it will go from 1 to -1. Here is an example for a screen that is 1000px by 500px.


After the transformation, coordinate (0, 0) becomes (-1, 1) and coordinate (1000, 500) becomes (1, -1). This coordinate system is called homogeneous coordinates and it’s what the pixel shader expects later. As you can notice, (0, 0) is the center of the screen.
After that, it’s just a matter of bundling the variables in the VertexToPixel struct and returning that.
float4 diffuse = tex2D(TextureSampler, PSIn.TexCoord);
tex2D is a function that samples the color of a texture at the point TexCoord. Here our texture is “image” which is stored in TextureSampler.
return PSIn.Color * diffuse ;
All this does is it multiplies our diffuse color which we just extracted from our “image” with the color we passed in. That color is White which is 1 therefore the result is simply diffuse. As an exercise, you can try passing in other colors in the draw call with new Color(100, 150, 200)
to see how the image’s colors get changed.
That’s it for now! If you see any errors, make sure to tell me in the comments below. Hopefully this can act as a template that can be used to build various shaders.
Further reading: https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl