Let’s create a mobile game with Xamarin and OpenGL – Part 2 (Shaders)

In this part I’m going to show you how you can use simple shaders to draw objects. We will start by drawing simple rectangles which are filled with a color.

Creating the shaders

Create a new folder “Resources/Shaders” in the project “Engine.Shared”. Add a new empty file “ColorShader.fsh” to the folder with the following content:

#version 300 es out lowp vec4 fragColor; uniform lowp vec4 color; void main() { fragColor = color; }

This is the fragment (Pixel) shader. It basically tells your graphics card which color should be used when drawing the next object. For this we declare a parameter “color” which we can later set in our application.

Add another new file “ColorShader.vsh” with the following content:

#version 300 es in vec4 vertex; uniform mat4 modelViewProjection; void main() { gl_Position = modelViewProjection * vertex; }

This is the vertex shader. It will define where the object is drawn. We declare modelViewProjection as a parameter. This is a matrix we can pass to move the object around.

Make sure that for both files you set the “Build Action” to “Embedded resource”. Right click on the files and choose “Properties”:

You just created your own fragment and vertex shaders!

To represent this shader as an object in the code we will add two classes. Create a new folder “Shaders” in the project “Engine.Shared” and add a new class “Shader.cs”:

namespace Engine.Shared { public abstract class Shader { public int Program { get; set; } } }

And another class “ColorShader.cs”:

namespace Engine.Shared { public class ColorShader : Shader { public readonly int AttribVertex = 0; public int UniformModelViewProjection { get; set; } public int UniformColor { get; set; } } }

We create a base class “Shader” so we can later add other shaders. The “ColorShader” has some properties that I will explain later in this article.

Now we create a helper class to actually load these shaders into the graphics card. Create a new class “ShaderHelper”.cs directly in “Engine.Shared” project:

using System; using OpenTK.Graphics.ES30; namespace Engine.Shared { public static class ShaderHelper { public static void LoadShader<TShader>(TShader shader, string vertexShaderSource, string fragmentShaderSource, Action<TShader> bindAttribLocations, Action<TShader> setUniformLocations) where TShader : Shader { shader.Program = GL.CreateProgram(); int vertexShader; int fragmentShader; // Create and compile shader. if (!CompileShader(ShaderType.VertexShader, vertexShaderSource, out vertexShader)) { throw new Exception("Failed to compile vertex shader."); } if (!CompileShader(ShaderType.FragmentShader, fragmentShaderSource, out fragmentShader)) { throw new Exception("Failed to compile fragment shader."); } // Attach shader to program. GL.AttachShader(shader.Program, vertexShader); GL.AttachShader(shader.Program, fragmentShader); // Bind attribute locations. // This needs to be done prior to linking. bindAttribLocations(shader); // Link program. if (!LinkProgram(shader.Program)) { if (vertexShader != 0) { GL.DeleteShader(vertexShader); } if (fragmentShader != 0) { GL.DeleteShader(fragmentShader); } if (shader.Program != 0) { GL.DeleteProgram(shader.Program); shader.Program = 0; } throw new Exception($"Failed to link program: {shader.Program}"); } // Get uniform locations. setUniformLocations(shader); // Release shader. if (vertexShader != 0) { GL.DetachShader(shader.Program, vertexShader); GL.DeleteShader(vertexShader); } if (fragmentShader != 0) { GL.DetachShader(shader.Program, fragmentShader); GL.DeleteShader(fragmentShader); } } private static bool CompileShader(ShaderType type, string src, out int shader) { shader = GL.CreateShader(type); GL.ShaderSource(shader, src); GL.CompileShader(shader); #if DEBUG || true int logLength = 0; GL.GetShader(shader, ShaderParameter.InfoLogLength, out logLength); if (logLength > 0) { Console.WriteLine("Shader compile log:\n{0}", GL.GetShaderInfoLog(shader)); } #endif int status = 0; GL.GetShader(shader, ShaderParameter.CompileStatus, out status); if (status == 0) { GL.DeleteShader(shader); return false; } return true; } internal static bool LinkProgram(int prog) { GL.LinkProgram(prog); #if DEBUG int logLength = 0; GL.GetProgram(prog, ProgramParameter.InfoLogLength, out logLength); if (logLength > 0) { Console.WriteLine("Program link log:\n{0}", GL.GetProgramInfoLog(prog)); } #endif int status = 0; GL.GetProgram(prog, ProgramParameter.LinkStatus, out status); if (status == 0) { return false; } return true; } } }

This class will help us load different shaders.

To load the shaders from the text file, we should create a “Managers/FileManager.cs” class file inside “Engine.Shared” project:

using System.IO; using System.Reflection; using System.Text; namespace Engine.Shared.Managers { public class FileManager { public string LoadText(Assembly assembly, string fileName, Encoding encoding = null) { return new StreamReader(GetResourceStream(assembly, fileName), encoding ?? Encoding.Default).ReadToEnd(); } public Stream GetResourceStream(Assembly assembly, string fileName) { return assembly.GetManifestResourceStream($"{assembly.GetName().Name}.{fileName}"); } } }

Implementing a renderer

Now we’re getting to the interesting part. We will create our first renderer. With this renderer we will draw simple colored rectangles to the screen!

But first we have to add another class which will manage buffer binding. This is a crucial part of OpenGL. All the data that is sent to the graphics card eventually has an id. This id is needed to draw the desired data when we want to, and also to release the data from the graphics card when we don’t need it anymore. Create a new class file “Managers/BufferManager.cs” in project “Engine.Shared”:

using System; using System.Collections.Generic; using System.Runtime.InteropServices; using OpenTK.Graphics.ES30; namespace Engine.Shared.Managers { public class BufferManager : IDisposable { private List<int> bufferIds = new List<int>(); public int Load<T>(BufferTarget bufferTarget, T[] data) where T : struct { int bufferId; GL.GenBuffers(1, out bufferId); bufferIds.Add(bufferId); GL.BindBuffer(bufferTarget, bufferId); GL.BufferData(bufferTarget, (IntPtr)(data.Length * Marshal.SizeOf(default(T))), data, BufferUsage.StaticDraw); GL.BindBuffer(bufferTarget, 0); return bufferId; } public void Dispose() { GL.DeleteBuffers(bufferIds.Count, bufferIds.ToArray()); bufferIds.Clear(); } public void Dispose(int bufferId) { GL.DeleteBuffers(1, new int[] { bufferId }); bufferIds.Remove(bufferId); } } }

Now we also have to define some data structure for the rectangles we want to draw. We start by adding a base class that represents anything we can draw. Add a new class “Drawables/DrawableRectangle.cs” in “Shared.Engine”:

namespace Engine.Shared.Drawables { public class DrawableRectangle { public DrawableRectangle(int vboId, float width, float height) { VboId = vboId; Width = width; Height = height; } public int VboId { get; private set; } public float Width { get; private set; } public float Height { get; private set; } } }

Now we can finally create our first Renderer:

using OpenTK; using System; using OpenTK.Graphics.ES30; using Engine.Shared.Drawables; namespace Engine.Shared.Renderers { public class RectangleRenderer { private int vbi = 0; // Is the same for every rectangle private static readonly ushort[] indices = { 0, 1, 2, 1, 3, 2 }; private readonly BufferManager bufferManager; private readonly ColorShader shader; public RectangleRenderer(BufferManager bufferManager, ColorShader shader) { this.bufferManager = bufferManager; this.shader = shader; } public DrawableRectangle Create(float width, float height) { // In OpenGL textures are rendered upside down so we need to flip coordinates // x, y, z float[] vertices = { 0f, 0f, 0f, width, 0f, 0f, 0f, height, 0f, width, height, 0f, }; var vbo = bufferManager.Load(BufferTarget.ArrayBuffer, vertices); if (vbi == 0) { vbi = bufferManager.Load(BufferTarget.ElementArrayBuffer, indices); } var rectangle = new DrawableRectangle(vbo, width, height); return rectangle; } public void Dispose(DrawableRectangle rectangle) { bufferManager.Dispose(rectangle.VboId); } public void BeforeRender() { // Use shader program. GL.UseProgram(shader.Program); // Enable transparency GL.Enable(EnableCap.Blend); GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha); GL.BindBuffer(BufferTarget.ElementArrayBuffer, vbi); } public void Render(DrawableRectangle rectangle, Vector4 color, Matrix4 modelViewProjection) { if (rectangle.VboId == 0) { return; } // Set color GL.Uniform4(shader.UniformColor, color); // Set model view projection GL.UniformMatrix4(shader.UniformModelViewProjection, false, ref modelViewProjection); // Update attribute value Position GL.BindBuffer(BufferTarget.ArrayBuffer, rectangle.VboId); GL.VertexAttribPointer(shader.AttribVertex, 3, VertexAttribPointerType.Float, false, sizeof(float) * 3, IntPtr.Zero); GL.EnableVertexAttribArray(shader.AttribVertex); // Draw one instance indexed GL.DrawElements(BeginMode.Triangles, indices.Length, DrawElementsType.UnsignedShort, IntPtr.Zero); } public void AfterRender() { // Unbind / Disable GL.BindBuffer(BufferTarget.ArrayBuffer, 0); GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0); GL.Disable(EnableCap.Blend); } } }

First we define the indices. These numbers represent the order of how the polygons are drawn. Each of the numbers 0 to 3 are related to a row in the “vertices” variable. “0,1,2” represents the first triangle and “1,3,2” the second triangle which together form a rectangle. The vertices variable contains all vectors of each “corner” of the rectangle. With indices we can save the amount of data that we need to send to the graphics card:

As you can see in this illustration both triangles share the vectors with index 1 and 2. These two vectors which contain of 3 floats can be accessed with single integer indexes.

The “BeforeRender” method will tell the graphics card to use the shader we uploaded initially for everything that we will draw. We enable transparency and also bind the vertex index, so the graphics card knows in what order to draw the vertices.

The “Render” method will set the uniform attribute “color” with the value we pass into the method. We also pass the projection matrix. Then the buffer with the actual vertices is bound. Unless the indices, the dimension of the rectangle can differ. That’s why this is called for every rectangle we want to draw separately. Next the “DrawElements” method is called. We pass the the BeginMode “Triangles” the length of our indices and their data type.

The “AfterRender” method will basically clean up everything we used. It unsets all the bound buffers and disables transparency again, since we might not want to always use it.

Bringing it all together

As a final step we have to update the “GameController” class where everything comes together:

using Engine.Shared.Drawables; using Engine.Shared.Renderers; using OpenTK; using OpenTK.Graphics.ES30; using System; using System.Reflection; namespace Engine.Shared { public class GameController : IDisposable { private readonly FileManager fileManager; private readonly RectangleRenderer rectangleRenderer; private readonly BufferManager bufferManager; private ColorShader colorShader = new ColorShader(); public bool IsLoaded { get; set; } = false; private DrawableRectangle rectangle1; private DrawableRectangle rectangle2; private Matrix4 viewProjection; public GameController() { this.fileManager = new FileManager(); this.bufferManager = new BufferManager(); this.rectangleRenderer = new RectangleRenderer(bufferManager, colorShader); } public void Load() { // Return since state is only cleared for OnStop, OnPause or low memory. // https://developer.android.com/reference/android/app/Activity#ActivityLifecycle if (IsLoaded) { return; } IsLoaded = true; GL.ClearColor(0f, 0f, 0f, 1f); // Use black as clear color GL.Enable(EnableCap.CullFace); // Activate differentiation which that only one side of faces is visible GL.CullFace(CullFaceMode.Back); // Don't draw backside of faces GL.Disable(EnableCap.DepthTest); // Enable for transparent textures GL.Disable(EnableCap.Dither); // Turn off color illusion GL.FrontFace(FrontFaceDirection.Ccw); // Polygons that are draw counter clock wise are pointing to the front var engineAssembly = Assembly.GetExecutingAssembly(); ShaderHelper.LoadShader(colorShader, fileManager.LoadText(engineAssembly, "Resources.Shaders.ColorShader.vsh"), fileManager.LoadText(engineAssembly, "Resources.Shaders.ColorShader.fsh"), (shader) => { GL.BindAttribLocation(shader.Program, shader.AttribVertex, "vertex"); }, (shader) => { shader.UniformModelViewProjection = GL.GetUniformLocation(shader.Program, "modelViewProjection"); shader.UniformColor = GL.GetUniformLocation(shader.Program, "color"); }); rectangle1 = rectangleRenderer.Create(512f, 512f); rectangle2 = rectangleRenderer.Create(256f, 256f); } public void Render() { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); rectangleRenderer.BeforeRender(); rectangleRenderer.Render(rectangle1, new Vector4(1f, 0f, 0f, 1f), viewProjection); rectangleRenderer.Render(rectangle2, new Vector4(0f, 1f, 0f, 1f), viewProjection); rectangleRenderer.AfterRender(); } public void Resize(int width, int height) { if(width > 0 && height > 0) { GL.Viewport(0, 0, width, height); viewProjection = CreateOrthographicProjection(width, height); } } private Matrix4 CreateOrthographicProjection(int width, int height) { // Shift projection so the axis is in the left bottom corner of the display return Matrix4.CreateTranslation(-width / 2f, -height / 2f, 0f) * Matrix4.CreateOrthographic(width, height, 1f, -1f); } public void Dispose() { if(bufferManager != null) { bufferManager.Dispose(); } } } }

The “LoadShader” method is loading the shader from the text files into the graphics card. In the first lambda expression we are binding the attribute variables and in the second one the uniform variables. These are important so we can pass arguments into the shader. In this case we pass the color and the position of the object.

The “CreateOrthographicProjection” method will first translate everything to the bottom left corner, because we don’t want the axes to be in the center of the screen. Then we multiply this with an orthographic projection matrix. An orthographic projection means for example that objects don’t get smaller, the further away they are. This is ideal for programming 2D games since we don’t want our objects to be rendered like in the real visual world.

The “Resize” method will now also update the viewProjection matrix whenever the screen size changes. This also applies for when you go from portrait to landscape mode.

The “Render” method now makes use of the “RectangleRenderer”. The “BeforeRender” and “AfterRender” methods make sense when you draw multiple objects of the same type between them. Without them you would always make additional calls to the graphics card which will slow down the rendering process. The “RectangleRenderer.Render” method takes our rectangle object, a 4-dimensional vector which represents an RGBA value and the projection matrix. Since we do not want to move the rectangle we just use the normal view projection matrix. This will result in the object being drawn at the 0 point of the axes.

Now you can run the application and you will see that we successfully draw a bigger green rectangle containing a smaller red rectangle onto the screen.

You can download the source code of this article here: