CXO2's picture

Simple Shader Tutorial

Here is it, my 2nd post blog :))
Today I am going to post about Simple Shader implementation.

Note that the example will only show you how fragment and / or vertex shader works with 2D Texture.
We will going to create a simple effect for 2D Texture and I would recommend you to write Texture class first.

Oh yea, almost forgot to tell, every GL call in this tutorial will use Renderer.Call(), it is a simple error checker for OpenGL Call that you can find at here: Simpler error checking.

If you don't want to use this, simply hit Ctrl + H, replace "Renderer.Call(() => " (without quotes) with "" (empty string, without quotes)
then replace "));" with ");" (this may cause mis-replace the code) in your IDE or use regular expression code (idk about those thing, search up on google)

Here a sample how to make a texture from a bitmap.

        private int Load(Bitmap bitmap, bool IsRepeated = false, bool IsSmooth = true)
        {
            try
            {
                int TextureID = 0;
                Renderer.Call(() => GL.GenTextures(1, out TextureID));
 
                Renderer.Call(() => GL.BindTexture(TextureTarget.Texture2D, TextureID));
 
                BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 
                Renderer.Call(() => GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
                    OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0));
 
                bitmap.UnlockBits(data);
 
                // Setup filtering
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, IsRepeated ? Convert.ToInt32(TextureWrapMode.Repeat) : Convert.ToInt32(TextureWrapMode.ClampToEdge)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, IsRepeated ? Convert.ToInt32(TextureWrapMode.Repeat) : Convert.ToInt32(TextureWrapMode.ClampToEdge)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, IsSmooth ? Convert.ToInt32(TextureMagFilter.Linear) : Convert.ToInt32(TextureMagFilter.Nearest)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, IsSmooth ? Convert.ToInt32(TextureMinFilter.Linear) : Convert.ToInt32(TextureMinFilter.Nearest)));
 
                return TextureID;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error creating new Texture:" + Environment.NewLine + ex.Message, "Error");
                return 0;
            }
        }
 
// Usage:
// int texture = Load(new Bitmap(Image.FromFile("texture.png")));
// Bind the texture:
// Renderer.Call(() => GL.BindTexture(TextureTarget.Texture2D, texture));
// Do Something with texture (drawing code goes here)
// Unbind it:
// Renderer.Call(() => GL.BindTexture(TextureTarget.Texture2D, 0));

Okayy, I dont think it necessary to explain because it was basic stuff.
Now, we are going to create Shader class.

there are 2 type of shader, Fragment Shader, Vertex Shader and we will going to create Shader class that can be created of either a vertex shader alone, a fragment shader alone, or both combined.

but before we go to the code, I will explain lil bit about Shader.

Shaders
Shaders are programs written in GLSL (which is a C-like language dedicated to OpenGL shaders), executed directly by the graphics card (GPU) and allowing to apply real-time operations to the rendered object / entities.

Its like a C/C++ program, a shader has its own variables that you can modify it later, our Shader class will able to handles 4 different types of variables:

  • Matrices
  • Floats
  • Vectors (that has 2 - 4 components)
  • Colors

Okayy, enough for the short explanation, lets get into the code!
Firstly, make a constructor, we need to detect if shader will be composed from fragment shader code or vertex shader code or both.
but, before we do anything with those code, we will check first whether the system is support shader or not

Here the class constructor:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
 
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
 
namespace Tutorial
{
    /// <summary>
    /// Shader Class (Vertex Shader and Fragment Shader)
    /// </summary>
    public class Shader : IDisposable
    {
 
        /// <summary>
        /// Type of Shader
        /// </summary>
        public enum Type
        {
            Vertex = 0x1,
            Fragment = 0x2
        }
 
        /// <summary>
        /// Get Whether the Shader function is Available on this Machine or not
        /// </summary>
        public static bool IsSupported
        {
            get
            {
                return new (Version(GL.GetString(StringName.Version).Substring(0, 3)) >= new Version(2, 0) ? true : false);
            }
        }
    }
}

If the Shader.IsSupported return false then we can't do anything about it.
Probably you are using old GPU, what you can do is upgrade your Graphic Cards.
Also there Type Enum which allow you to specify whether the Shader is Vertex Shader of Fragment Shader.

and Here the constructor code:

       private int Program = 0;
       private Dictionary<string, int> Variables = new Dictionary<string, int>();
 
        /// <summary>
        /// Create a new Shader
        /// </summary>
        /// <param name="source">Vertex or Fragment Source</param>
        /// <param name="type">Type of Source Code</param>
        public Shader(string source, Type type)
        {
            if (!IsSupported)
            {
                Console.WriteLine("Failed to create Shader." +
                    Environment.NewLine + "Your system doesn't support Shader.", "Error");
                return;
            }
 
            if (type == Type.Vertex)
                Compile(source, "");
            else
                Compile("", source);
        }
 
        /// <summary>
        /// Create a new Shader
        /// </summary>
        /// <param name="source">Vertex or Fragment Source</param>
        /// <param name="type">Type of Source Code</param>
        public Shader(string vsource, string fsource)
        {
            if (!IsSupported)
            {
                Console.WriteLine("Failed to create Shader." +
                    Environment.NewLine + "Your system doesn't support Shader.", "Error");
                return;
            }
 
             Compile(vsource , fsource);
        }

Compile() function will be covered next.
You may also notice that there 2 variables, Program and Variables dictionary.

  1. Program used for Program ID, which something like Texture ID, VBOID and sort of that.
  2. Variables used for caching variables that stored on Shader.

and the rest.. I dont think I need to explain this constructor, it just do simple operation.

Next we will going to write Compile code.
Here is it:

        // I prefer to return the bool rather than throwing an exception lol
        private bool Compile(string vertexSource = "", string fragmentSource = "")
        {
            int status_code = -1;
            string info = "";
 
            if (vertexSource == "" && fragmentSource == "")
            {
                Console.WriteLine("Failed to compile Shader." +
                    Environment.NewLine + "Nothing to Compile.", "Error");
                return false;
            }
 
            if (Program > 0)
                Renderer.Call(() => GL.DeleteProgram(Program));
 
            Variables.Clear();
 
            Program = GL.CreateProgram();
 
            if (vertexSource != "")
            {
                int vertexShader = GL.CreateShader(ShaderType.VertexShader);
                Renderer.Call(() => GL.ShaderSource(vertexShader, vertexSource));
                Renderer.Call(() => GL.CompileShader(vertexShader));
                Renderer.Call(() => GL.GetShaderInfoLog(vertexShader, out info));
                Renderer.Call(() => GL.GetShader(vertexShader, ShaderParameter.CompileStatus, out status_code));
 
                if (status_code != 1)
                {
                    Console.WriteLine("Failed to Compile Vertex Shader Source." +
                        Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                    Renderer.Call(() => GL.DeleteShader(vertexShader));
                    Renderer.Call(() => GL.DeleteProgram(Program));
                    Program = 0;
 
                    return false;
                }
 
                Renderer.Call(() => GL.AttachShader(Program, vertexShader));
                Renderer.Call(() => GL.DeleteShader(vertexShader));
            }
 
            if (fragmentSource != "")
            {
                int fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
                Renderer.Call(() => GL.ShaderSource(fragmentShader, fragmentSource));
                Renderer.Call(() => GL.CompileShader(fragmentShader));
                Renderer.Call(() => GL.GetShaderInfoLog(fragmentShader, out info));
                Renderer.Call(() => GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out status_code));
 
                if (status_code != 1)
                {
                    Console.WriteLine("Failed to Compile Fragment Shader Source." +
                        Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                    Renderer.Call(() => GL.DeleteShader(fragmentShader));
                    Renderer.Call(() => GL.DeleteProgram(Program));
                    Program = 0;
 
                    return false;
                }
 
                Renderer.Call(() => GL.AttachShader(Program, fragmentShader));
                Renderer.Call(() => GL.DeleteShader(fragmentShader));
            }
 
            Renderer.Call(() => GL.LinkProgram(Program));
            Renderer.Call(() => GL.GetProgramInfoLog(Program, out info));
            Renderer.Call(() => GL.GetProgram(Program, GetProgramParameterName.LinkStatus, out status_code));
 
            if (status_code != 1)
            {
                Console.WriteLine("Failed to Link Shader Program." +
                    Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                Renderer.Call(() => GL.DeleteProgram(Program));
                Program = 0;
 
                return false;
            }
 
            return true;
        }

Here the explaination:

  1. This part of code:

                int status_code = -1;
                string info = "";
     
                if (vertexSource == "" && fragmentSource == "")
                {
                    Console.WriteLine("Failed to compile Shader." +
                        Environment.NewLine + "Nothing to Compile.", "Error");
                    return false;
                }
     
                if (Program > 0)
                    Renderer.Call(() => GL.DeleteProgram(Program));
     
                Variables.Clear();
     
                Program = GL.CreateProgram();

    Is just the basic Initialization.
    status_code and info used for debugging purpose when Shader either vertex shader or fragment shader compilation get error.
    Then check if the Shader program id is exist or not, if current shader program id is exist, we must delete it first also delete all Variables cache that stored in current instance.

    Finally, create a new program id for Shader.

  2. the next code is handling Vertex Shader Source Code

                if (vertexSource != "")
                {
                    int vertexShader = GL.CreateShader(ShaderType.VertexShader);
                    Renderer.Call(() => GL.ShaderSource(vertexShader, vertexSource));
                    Renderer.Call(() => GL.CompileShader(vertexShader));
                    Renderer.Call(() => GL.GetShaderInfoLog(vertexShader, out info));
                    Renderer.Call(() => GL.GetShader(vertexShader, ShaderParameter.CompileStatus, out status_code));
     
                    if (status_code != 1)
                    {
                        Console.WriteLine("Failed to Compile Vertex Shader Source." +
                            Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
     
                        Renderer.Call(() => GL.DeleteShader(vertexShader));
                        Renderer.Call(() => GL.DeleteProgram(Program));
                        Program = 0;
     
                        return false;
                    }
     
                    Renderer.Call(() => GL.AttachShader(Program, vertexShader));
                    Renderer.Call(() => GL.DeleteShader(vertexShader));
                }

    As you can see, if the vertex source code is empty, don't compile it.
    Firstly, we create Vertex Shader ID (just like Program ID) and specify the Shader Source code with GL.ShaderSource and then we compile it.

    After the compilation finished (or at least hit an error) we check the Shader Info Log to rip out the compilation log info message.
    The error determined by status code, use GL.GetShader(vertexShader, ShaderParameter.CompileStatus, out status_code) to extract the latest shader status code.

    if the status_code return -1, it mean the compilation is error, It could be there are errors on Vertex Shader Source Code, Check the error message and Vertex Shader source code and Re-Compile it again.

    In this case, we are going to display the error code (status_code) and description, then delete the Vertex Shader ID and the Program, exiting without create anything..

    if the compilation run smoothly, we gonna attach it into the Program and then Delete the Vertex Shader ID since we no longer need it anymore.
    the compilation of Fragment Shader is the same, so I don't think to explain it again.

  3.             Renderer.Call(() => GL.LinkProgram(Program));
                Renderer.Call(() => GL.GetProgramInfoLog(Program, out info));
                Renderer.Call(() => GL.GetProgram(Program, GetProgramParameterName.LinkStatus, out status_code));
     
                if (status_code != 1)
                {
                    Console.WriteLine("Failed to Link Shader Program." +
                        Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
     
                    Renderer.Call(() => GL.DeleteProgram(Program));
                    Program = 0;
     
                    return false;
                }
     
                return true;

    Finally, after everything compiled successfully, we gonna link the program id.
    I am not really sure what it does but its like linking the object Program and When a program object has been successfully linked, the program object can be made part of current state by calling GL.UseProgram()

    We gonna check error first after linking the program.
    If there no error, function will return true which mean our Shader compiled successfully!!

Now we can compile the shader. What the next?
Like I said before, our Shader class will able to handle 4 types of variables.

We will gonna write the function to modify those variable in runtime.
The variable is stored in Shader is defined by Location ID.

So before we create a function to modify the variable, we will write a function to retrieve the Variable Location.
So here the code:

        private int GetVariableLocation(string name)
        {
            if (Variables.ContainsKey(name))
                return Variables[name];
 
            int location = GL.GetUniformLocation(Program, name);
 
            if (location != -1)
                Variables.Add(name, location);
            else
                Console.WriteLine("Failed to retrieve Variable Location." +
                    Environment.NewLine + "Variable Name not found.", "Error");
 
            return location;
        }

Okay, so the function will return the Location ID, and the Location ID will be cached into Variables dictionary.
first, check if the variable location is cached or not, so we just return the cached one.

If not, then get the location with GL.GetUniformLocation(Program, name); where the Program is the Program ID and name is the variable name.

next, we check if the variable is exist or not, if it exist, add it into cache, otherwise show an error that Failed to retrieve Variable Location.

Now we can read the variable Location, this will be used to modify the variable.
Here the code:

        /// <summary>
        /// Change a value Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">Value</param>
        public void SetVariable(string name, float x)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform1(location, x));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 2 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        public void SetVariable(string name, float x, float y)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform2(location, x, y));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 3 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        /// <param name="z">Third Vector Value</param>
        public void SetVariable(string name, float x, float y, float z)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform3(location, x, y, z));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 4 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        /// <param name="z">Third Vector Value</param>
        /// <param name="w">Fourth Vector Value</param>
        public void SetVariable(string name, float x, float y, float z, float w)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform4(location, x, y, z, w));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a Matrix4 Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="matrix">Matrix</param>
        public void SetVariable(string name, Matrix4 matrix)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                {
                    // Well cannot use ref on lambda expression Lol
                    // So we need to call Check error manually
                    GL.UniformMatrix4(location, false, ref matrix);
                    Renderer.CheckError();
                }
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 2 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="vector">Vector Value</param>
        public void SetVariable(string name, Vector2 vector)
        {
            SetVariable(name, vector.X, vector.Y);
        }
 
        /// <summary>
        /// Change a 3 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="vector">Vector Value</param>
        public void SetVariable(string name, Vector3 vector)
        {
            SetVariable(name, vector.X, vector.Y, vector.Z);
        }
 
        /// <summary>
        /// Change a Color Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="color">Color Value</param>
        public void SetVariable(string name, Color color)
        {
            SetVariable(name, color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
        }

So here the short explanation about these codes.
Firstly, we need to check whether the program id is valid or not by doing statement if (program > 0) and then, get the variable name with int location = GetVariableLocation(name); which the "location" will be stored on location variable, then check whether the location is exist or not.

if its exist then change the variable, depending how much component that you want to modify, for example you want to modify 2 components value (x and y) so what you need is GL.Uniform2. Its like GL.Uniform* where * is the number of component.
it also apply for matrix, the difference is GL.UniformMatrix*, the first parameter is location, 2nd parameter is transposed or not and the 3rd parameter is the new matrix value.

then after we are finish with everything, disable program by calling GL.UseProgram(0);

What we gonna do next is write Bind function
I prefer write it as static for Bind and Unbind the Shader.

        /// <summary>
        /// Bind a Shader for Rendering
        /// </summary>
        /// <param name="shader">Shader to bind</param>
        public static void Bind(Shader shader)
        {
            if (shader != null && shader.Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(shader.Program));
            }
            else
            {
                Renderer.Call(() => GL.UseProgram(0));
            }
        }

To bind the shader, simply call Shader.Bind(shader); to unbind any program, use Shader.Bind(null);
So we are finished!

Oooops, we forgot the Disposal method, here is it:

        public void Dispose()
        {
            if (Program != 0)
                Renderer.Call(() => GL.DeleteProgram(Program));
        }

Don't forget to call it once you are done with it!
Here the full codes:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
 
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
 
namespace Tutorial
{
    /// <summary>
    /// Shader Class (Vertex Shader and Fragment Shader)
    /// </summary>
    public class Shader : IDisposable
    {
 
        /// <summary>
        /// Type of Shader
        /// </summary>
        public enum Type
        {
            Vertex = 0x1,
            Fragment = 0x2
        }
 
        /// <summary>
        /// Get Whether the Shader function is Available on this Machine or not
        /// </summary>
        public static bool IsSupported
        {
            get
            {
                return (new Version(GL.GetString(StringName.Version).Substring(0, 3)) >= new Version(2, 0) ? true : false);
            }
        }
 
       private int Program = 0;
       private Dictionary<string, int> Variables = new Dictionary<string, int>();
 
        /// <summary>
        /// Create a new Shader
        /// </summary>
        /// <param name="source">Vertex or Fragment Source</param>
        /// <param name="type">Type of Source Code</param>
        public Shader(string source, Type type)
        {
            if (!IsSupported)
            {
                Console.WriteLine("Failed to create Shader." +
                    Environment.NewLine + "Your system doesn't support Shader.", "Error");
                return;
            }
 
            if (type == Type.Vertex)
                Compile(source, "");
            else
                Compile("", source);
        }
 
        /// <summary>
        /// Create a new Shader
        /// </summary>
        /// <param name="source">Vertex or Fragment Source</param>
        /// <param name="type">Type of Source Code</param>
        public Shader(string vsource, string fsource)
        {
            if (!IsSupported)
            {
                Console.WriteLine("Failed to create Shader." +
                    Environment.NewLine + "Your system doesn't support Shader.", "Error");
                return;
            }
 
            Compile(vsource , fsource);
        }
 
        // I prefer to return the bool rather than throwing an exception lol
        private bool Compile(string vertexSource = "", string fragmentSource = "")
        {
            int status_code = -1;
            string info = "";
 
            if (vertexSource == "" && fragmentSource == "")
            {
                Console.WriteLine("Failed to compile Shader." +
                    Environment.NewLine + "Nothing to Compile.", "Error");
                return false;
            }
 
            if (Program > 0)
                Renderer.Call(() => GL.DeleteProgram(Program));
 
            Variables.Clear();
 
            Program = GL.CreateProgram();
 
            if (vertexSource != "")
            {
                int vertexShader = GL.CreateShader(ShaderType.VertexShader);
                Renderer.Call(() => GL.ShaderSource(vertexShader, vertexSource));
                Renderer.Call(() => GL.CompileShader(vertexShader));
                Renderer.Call(() => GL.GetShaderInfoLog(vertexShader, out info));
                Renderer.Call(() => GL.GetShader(vertexShader, ShaderParameter.CompileStatus, out status_code));
 
                if (status_code != 1)
                {
                    Console.WriteLine("Failed to Compile Vertex Shader Source." +
                        Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                    Renderer.Call(() => GL.DeleteShader(vertexShader));
                    Renderer.Call(() => GL.DeleteProgram(Program));
                    Program = 0;
 
                    return false;
                }
 
                Renderer.Call(() => GL.AttachShader(Program, vertexShader));
                Renderer.Call(() => GL.DeleteShader(vertexShader));
            }
 
            if (fragmentSource != "")
            {
                int fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
                Renderer.Call(() => GL.ShaderSource(fragmentShader, fragmentSource));
                Renderer.Call(() => GL.CompileShader(fragmentShader));
                Renderer.Call(() => GL.GetShaderInfoLog(fragmentShader, out info));
                Renderer.Call(() => GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out status_code));
 
                if (status_code != 1)
                {
                    Console.WriteLine("Failed to Compile Fragment Shader Source." +
                        Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                    Renderer.Call(() => GL.DeleteShader(fragmentShader));
                    Renderer.Call(() => GL.DeleteProgram(Program));
                    Program = 0;
 
                    return false;
                }
 
                Renderer.Call(() => GL.AttachShader(Program, fragmentShader));
                Renderer.Call(() => GL.DeleteShader(fragmentShader));
            }
 
            Renderer.Call(() => GL.LinkProgram(Program));
            Renderer.Call(() => GL.GetProgramInfoLog(Program, out info));
            Renderer.Call(() => GL.GetProgram(Program, GetProgramParameterName.LinkStatus, out status_code));
 
            if (status_code != 1)
            {
                Console.WriteLine("Failed to Link Shader Program." +
                    Environment.NewLine + info + Environment.NewLine + "Status Code: " + status_code.ToString());
 
                Renderer.Call(() => GL.DeleteProgram(Program));
                Program = 0;
 
                return false;
            }
 
            return true;
        }
 
        private int GetVariableLocation(string name)
        {
            if (Variables.ContainsKey(name))
                return Variables[name];
 
            int location = GL.GetUniformLocation(Program, name);
 
            if (location != -1)
                Variables.Add(name, location);
            else
                Console.WriteLine("Failed to retrieve Variable Location." +
                    Environment.NewLine + "Variable Name not found.", "Error");
 
            return location;
        }
 
        /// <summary>
        /// Change a value Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">Value</param>
        public void SetVariable(string name, float x)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform1(location, x));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 2 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        public void SetVariable(string name, float x, float y)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform2(location, x, y));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 3 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        /// <param name="z">Third Vector Value</param>
        public void SetVariable(string name, float x, float y, float z)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform3(location, x, y, z));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 4 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="x">First Vector Value</param>
        /// <param name="y">Second Vector Value</param>
        /// <param name="z">Third Vector Value</param>
        /// <param name="w">Fourth Vector Value</param>
        public void SetVariable(string name, float x, float y, float z, float w)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                    Renderer.Call(() => GL.Uniform4(location, x, y, z, w));
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a Matrix4 Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="matrix">Matrix</param>
        public void SetVariable(string name, Matrix4 matrix)
        {
            if (Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(Program));
 
                int location = GetVariableLocation(name);
                if (location != -1)
                {
                    // Well cannot use ref on lambda expression Lol
                    // So we need to call Check error manually
                    GL.UniformMatrix4(location, false, ref matrix);
                    Renderer.CheckError();
                }
 
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        /// <summary>
        /// Change a 2 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="vector">Vector Value</param>
        public void SetVariable(string name, Vector2 vector)
        {
            SetVariable(name, vector.X, vector.Y);
        }
 
        /// <summary>
        /// Change a 3 value Vector Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="vector">Vector Value</param>
        public void SetVariable(string name, Vector3 vector)
        {
            SetVariable(name, vector.X, vector.Y, vector.Z);
        }
 
        /// <summary>
        /// Change a Color Variable of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        /// <param name="color">Color Value</param>
        public void SetVariable(string name, Color color)
        {
            SetVariable(name, color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
        }
 
        /// <summary>
        /// Bind a Shader for Rendering
        /// </summary>
        /// <param name="shader">Shader to bind</param>
        public static void Bind(Shader shader)
        {
            if (shader != null && shader.Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(shader.Program));
            }
            else
            {
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        public void Dispose()
        {
            if (Program != 0)
                Renderer.Call(() => GL.DeleteProgram(Program));
        }
    }
}

so you are excited about the usage sample?
Lol here is it!

// Vertex Shader Source and Fragment Shader Source
string vs, fs;
 
// Loading the Texture
int texture = Load(new Bitmap(Image.FromFile("background.png")));
 
// Load Vertex Shader Source Code
vs = System.IO.File.ReadAllText("vertex.glsl");
 
// Load Vertex Shader Source Code
fs = System.IO.File.ReadAllText("fragment.glsl");
 
// Here, there are 3 method to loading the Shader.
// Load the Vertex and Fragment Shader Source
Shader shader = new Shader(vs, fs);
 
// Load the Vertex Shader Source only (there will be no fragment effect upon rendering)
Shader shader = new Shader(vs, Shader.Type.Vertex);
 
// Load the Fragment Shader Source only (same, there will be no vertex effect upon rendering)
Shader shader = new Shader(fs, Shader.Type.Fragment);
 
// Here the rendering code:
GL.BindTexture(TextureTarget.Texture2D, texture);
 
// Do any transformation matrix here, like rotating, scaling, translation, etc.
// Or possibly you could do that via Shader (SetVariable(string name, Matrix4 matrix)) if you good enough.
 
Shader.Bind(shader);
 
// Draw the vertices here, you could do it with VBO or Immediate mode
// here the example with immediate mode (CMIIW I am not good at Immediate mode, so deprecated one tho)
GL.Begin(PrimitiveType.Quads);
      GL.Color4(System.Drawing.Color.White);
      GL.TexCoord2(0.0f, 1.0f); GL.Vertex2(-1.0f, -1.0f);
      GL.TexCoord2(1.0f, 1.0f); GL.Vertex2(1.0f, -1.0f);
      GL.TexCoord2(1.0f, 0.0f); GL.Vertex2(1.0f, 1.0f);
      GL.TexCoord2(0.0f, 0.0f); GL.Vertex2(-1.0f, 1.0f);
GL.End();
 
// Unbind everything
Shader.Bind(null);
 
GL.BindTexture(TextureTarget.Texture2D, 0);

Here the example fragment source code that you can try (I took this from somewhere, idk i forgot):

uniform sampler2D texture;
uniform float pixel_threshold;
 
void main()
{
    float factor = 1.0 / (pixel_threshold + 0.001);
vec2 pos = floor(gl_TexCoord[0].xy * factor + 0.5) / factor;
gl_FragColor = texture2D(texture, pos) * gl_Color;	
}

to make the effect work, you need modify the pixel_threshold variable based on mouse movement (and window size).
in your game update function (OnUpdateFrame() if you are using GameWindow), change this variable with this calculation:

shader.SetVariable("pixel_threshold", (((float)Mouse.X / (float)this.Width) + ((float)Mouse.Y / (float)this.Height)) / 30);

here my example full code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
 
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
 
namespace Tutorial
{
    public class Game : GameWindow
    {
        Color ClearColor = Color.White;
 
        string fs = @"uniform sampler2D texture;
uniform float pixel_threshold;
 
void main()
{
    float factor = 1.0 / (pixel_threshold + 0.001);
vec2 pos = floor(gl_TexCoord[0].xy * factor + 0.5) / factor;
gl_FragColor = texture2D(texture, pos) * gl_Color;	
}";
        Shader shader;
        int texture;
        static void Main(string[] args)
        {
            using (Game game = new Game())
                game.Run();
        }
 
        public Game()
            : base(800, 600, GraphicsMode.Default, "CXO2", GameWindowFlags.Default)
        {
            this.VSync = VSyncMode.Off;
 
        }
 
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            this.ClearColor = Color.White;
            Renderer.Call(() => GL.ClearColor(ClearColor));
            Renderer.Call(() => GL.Enable(EnableCap.Texture2D));
 
            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);
 
            int[] viewPort = new int[4];
            Renderer.Call(() => GL.GetInteger(GetPName.Viewport, viewPort));
            Renderer.Call(() => GL.Enable(EnableCap.Blend));
 
            Renderer.Call(() => GL.MatrixMode(MatrixMode.Projection));
            Renderer.Call(() => GL.Viewport(viewPort[0], viewPort[1], viewPort[2], viewPort[3]));
            Renderer.Call(() => GL.Ortho(viewPort[0], viewPort[0] + viewPort[2], viewPort[1] + viewPort[3], viewPort[1], -1, 1));
 
            shader = new Shader(fs, Shader.Type.Fragment);
            texture = Load(new Bitmap(Image.FromFile("background.png")));
        }
 
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            Renderer.Call(() => GL.Viewport(0, 0, Width, Height));
        }
 
        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            base.OnUpdateFrame(e);
            shader.SetVariable("pixel_threshold", (((float)Mouse.X / (float)this.Width) + ((float)Mouse.Y / (float)this.Height)) / 30);
        }
 
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            base.OnRenderFrame(e);
 
            Renderer.Call(() => GL.ClearColor(ClearColor));
            Renderer.Call(() => GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit));
 
            Renderer.Call(() => GL.BindTexture(TextureTarget.Texture2D, texture));
            Shader.Bind(shader);
 
            GL.Begin(PrimitiveType.Quads);
                  GL.Color4(System.Drawing.Color.White);
                  GL.TexCoord2(0.0f, 1.0f); GL.Vertex2(-1.0f, -1.0f);
                  GL.TexCoord2(1.0f, 1.0f); GL.Vertex2(1.0f, -1.0f);
                  GL.TexCoord2(1.0f, 0.0f); GL.Vertex2(1.0f, 1.0f);
                  GL.TexCoord2(0.0f, 0.0f); GL.Vertex2(-1.0f, 1.0f);
            GL.End();
 
            SwapBuffers();
        }
 
        private int Load(Bitmap bitmap, bool IsRepeated = false, bool IsSmooth = true)
        {
            try
            {
                int TextureID = 0;
                Renderer.Call(() => GL.GenTextures(1, out TextureID));
 
                Renderer.Call(() => GL.BindTexture(TextureTarget.Texture2D, TextureID));
 
                BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 
                Renderer.Call(() => GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
                    OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0));
 
                bitmap.UnlockBits(data);
 
                // Setup filtering
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, IsRepeated ? Convert.ToInt32(TextureWrapMode.Repeat) : Convert.ToInt32(TextureWrapMode.ClampToEdge)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, IsRepeated ? Convert.ToInt32(TextureWrapMode.Repeat) : Convert.ToInt32(TextureWrapMode.ClampToEdge)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, IsSmooth ? Convert.ToInt32(TextureMagFilter.Linear) : Convert.ToInt32(TextureMagFilter.Nearest)));
                Renderer.Call(() => GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, IsSmooth ? Convert.ToInt32(TextureMinFilter.Linear) : Convert.ToInt32(TextureMinFilter.Nearest)));
 
                return TextureID;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error creating new Texture:" + Environment.NewLine + ex.Message, "Error");
                return 0;
            }
        }
    }
}

So that's it!
Wow such a long post haha

Thought I am still learning too, don't expect something too much from this 17 years old noob-kid ;) explore the shader more by yourself.
You can even interact texture with it, I mean with GL.ActivateTexture but you will need the full Texture class to make it neat and work properly.

Here the code (if you have the Texture class)

        // You will need a new dictionary<int, Texture> to save texture cache, where the int is location
        Dictionary<int, Texture> Textures = new Dictionary<int, Texture>();
 
       // This is the Current Texture ID
       int CurrentTextureID
 
       // The bind code will be like this instead
       /// <summary>
        /// Bind a Shader for Rendering
        /// </summary>
        /// <param name="shader">Shader to bind</param>
        public static void Bind(Shader shader)
        {
            if (shader != null && shader.Program > 0)
            {
                Renderer.Call(() => GL.UseProgram(shader.Program));
                shader.BindTextures();
 
                if (shader.CurrentTextureID != -1)
                    Renderer.Call(() => GL.Uniform1(shader.CurrentTextureID, 0));
            }
            else
            {
                Renderer.Call(() => GL.UseProgram(0));
            }
        }
 
        // this will be called after GL.UseProgram(Program) on Shader.Bind(Shader) function
        private void BindTextures()
        {
            int i = 1;
            foreach (KeyValuePair<int, Texture> tex in Textures)
            {
                Renderer.Call(() => GL.Uniform1(tex.Key, i));
                Renderer.Call(() => GL.ActiveTexture(TextureUnit.Texture0 + i));
 
                tex.Value.Bind();
                i++;
            }
 
            Renderer.Call(() => GL.ActiveTexture(TextureUnit.Texture0));
        }
 
        /// <summary>
        /// Change a Texture Variable of the Shader
        /// </summary>
        /// <param name="name">Name of Variable</param>
        /// <param name="texture">Texture</param>
        /// <returns></returns>
        public void SetVariable(string name, Texture texture)
        {
            if (Program > 0)
                return;
 
            if (texture == null)
                return;
 
            int location = GetVariableLocation(name);
 
            if (location != -1)
            {
                if (!Textures.ContainsKey(location))
                {
                    if (Textures.Count + 1 >= Renderer.GetMaxTextureUnits())
                    {
                        Log.Instance().WriteLine("Failed to add " + name + " Texture to Shader." +
                            Environment.NewLine + "Texture Unuts are exceeded.", "Error");
 
                        return;
                    }
 
                    Textures.Add(location, texture);
                }
                else
                {
                    Textures[location] = texture;
                }
 
                Textures[location] = texture;
            }
 
 
        }
 
        /// <summary>
        /// Set Current Texture of the Shader
        /// </summary>
        /// <param name="name">Variable Name</param>
        public void SetVariable(string name)
        {
            if (Program > 0)
                return;
 
            CurrentTextureID = GetVariableLocation(name);
        }

The usage somewhat like:

shader.SetVariable("offset", 1f);
shader.SetVariable("point", 0.4f, 0.5f, 0.6f);
shader.SetVariable("color", System.Drawing.Color.FromArgb(255, 128, 50, 255));
shader.SetVariable("matrix", matrix); // transform is a Matrix4
shader.SetVariable("layout", texture); // texture is a Texture
shader.SetVariable("texture");

But well, I am too lazy to make Texture Class in this post LOL, it will be very very long post.
anyway that's it!

Hope this effect will useful later for you.
Good luck ;)


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
elhombremono's picture

Hi! I´m acctualy reading your post jet, but i just wanted you to know how helpfull it is to me right now!
just to say, Thanks you!

btw, check the line return (new Version(GL.GetString(StringName.Version).Substring(0, 3)) >= new Version(2, 0) ? true : false); , i think you missed the new ;)