Tuesday, March 11, 2014

OpenTK Tutorial 6 Part 1: Loading Shaders with a Class

This tutorial will cover basic texturing. To add a texture, we will first need to have a shader that handles it. Right now, we're using one shader, with no way to easily switch to another without requiring everything to use that shader. In a real game or program, we would probably want to be able to use multiple shaders (for example, UI elements probably don't need lighting applied to them, but game objects probably do), so we're going to handle this with a class that allows us to easily add more shaders if we need them, and switch between them at will.

This tutorial is split in parts, based on how long it is. Each step is rather substantial (and this groundwork will make things easier down the line), so I hope everyone understands why it needed to be split up like this.


Create a new class called ShaderProgram. This class will store information about shaders and handle loading them.

First, let's add the variables we need it to store in the ShaderProgram class:

 
        public int ProgramID = -1;
        public int VShaderID = -1;
        public int FShaderID = -1;
        public int AttributeCount = 0;
        public int UniformCount = 0;

        public Dictionary<String, AttributeInfo> Attributes = new Dictionary<string, AttributeInfo>();
        public Dictionary<String, UniformInfo> Uniforms = new Dictionary<string, UniformInfo>();
        public Dictionary<String, uint> Buffers = new Dictionary<string, uint>();

Each ShaderProgram will have a programID, a vertex shader, a fragment shader, and information about the inputs for both shaders.We're storing the inputs in Dictionaries for easier storage and lookup (proper optimization will come later, but for now it's left as an exercise for the reader).

In the Dictionary variables, you may notice some times that don't look familiar: AttributeInfo and UniformInfo. That's because we need to add these classes. They'll store the information about each shader input.


 
    public class AttributeInfo
    {
        public String name = "";
        public int address = -1;
        public int size = 0;
        public ActiveAttribType type;
    }

    public class UniformInfo
    {
        public String name = "";
        public int address = -1;
        public int size = 0;
        public ActiveUniformType type;
    }

To start using these variables, we'll first make a generic constructor in ShaderProgram:

 
        public ShaderProgram()
        {
            ProgramID = GL.CreateProgram();
        }

Next we want to create a function to load a shader. To make this method more flexible, we'll be making it take a string as input for a private method, while public methods for loading shaders will exist for both strings and file inputs. This method is very straightforward, using the same steps to loading a shader we used before:

 
        private void loadShader(String code, ShaderType type, out int address)
        {
            address = GL.CreateShader(type);
            GL.ShaderSource(address, code);
            GL.CompileShader(address);
            GL.AttachShader(ProgramID, address);
            Console.WriteLine(GL.GetShaderInfoLog(address));
        }

Now, to use this from outside the class, we'll make the public methods:

 
        public void LoadShaderFromString(String code, ShaderType type)
        {
            if (type == ShaderType.VertexShader)
            {
                loadShader(code, type, out VShaderID);
            }
            else if (type == ShaderType.FragmentShader)
            {
                loadShader(code, type, out FShaderID);
            }
        }

        public void LoadShaderFromFile(String filename, ShaderType type)
        {
            using (StreamReader sr = new StreamReader(filename))
            {
                if (type == ShaderType.VertexShader)
                {
                    loadShader(sr.ReadToEnd(), type, out VShaderID);
                }
                else if (type == ShaderType.FragmentShader)
                {
                    loadShader(sr.ReadToEnd(), type, out FShaderID);
                }
            }
        }

More very straightforward code. We take either the shader code or a filename and load it with the private method we created earlier.

Next, we'll handle linking the shaders. In the same function, we'll also assemble lists of the attributes and uniforms of the shaders, so we can look them up easier later.

 
        public void Link()
        {
            GL.LinkProgram(ProgramID);

            Console.WriteLine(GL.GetProgramInfoLog(ProgramID));

            GL.GetProgram(ProgramID, ProgramParameter.ActiveAttributes, out AttributeCount);
            GL.GetProgram(ProgramID, ProgramParameter.ActiveUniforms, out UniformCount);

            for (int i = 0; i < AttributeCount; i++)
            {
                AttributeInfo info = new AttributeInfo();
                int length = 0;

                StringBuilder name = new StringBuilder();

                GL.GetActiveAttrib(ProgramID, i, 256, out length, out info.size, out info.type, name);

                info.name = name.ToString();
                info.address = GL.GetAttribLocation(ProgramID, info.name);
                Attributes.Add(name.ToString(), info);
            }

            for (int i = 0; i < UniformCount; i++)
            {
                UniformInfo info = new UniformInfo();
                int length = 0;

                StringBuilder name = new StringBuilder();

                GL.GetActiveUniform(ProgramID, i, 256, out length, out info.size, out info.type, name);

                info.name = name.ToString();
                Uniforms.Add(name.ToString(), info);
                info.address = GL.GetUniformLocation(ProgramID, info.name);
            }
        }

Now that the shaders are loaded, we're much closer to being done with this class. Next, we'll want to generate buffer objects for us to use, through another function in ShaderProgram:

 
        public void GenBuffers()
        {
            for (int i = 0; i < Attributes.Count; i++)
            {
                uint buffer = 0;
                GL.GenBuffers(1, out buffer);

                Buffers.Add(Attributes.Values.ElementAt(i).name, buffer);
            }

            for (int i = 0; i < Uniforms.Count; i++)
            {
                uint buffer = 0;
                GL.GenBuffers(1, out buffer);

                Buffers.Add(Uniforms.Values.ElementAt(i).name, buffer);
            }
        }

Next, we'll want to make it easy to enable and disable vertex attribute arrays, since we can't have them hardcoded anymore:

 
        public void EnableVertexAttribArrays()
        {
            for (int i = 0; i < Attributes.Count; i++)
            {
                GL.EnableVertexAttribArray(Attributes.Values.ElementAt(i).address);
            }
        }

        public void DisableVertexAttribArrays()
        {
            for (int i = 0; i < Attributes.Count; i++)
            {
                GL.DisableVertexAttribArray(Attributes.Values.ElementAt(i).address);
            }
        }

Now we need methods to allow us to easily retrieve the addresses of our attributes and uniforms (and their buffers), based on names:

 
        public int GetAttribute(string name)
        {
            if (Attributes.ContainsKey(name))
            {
                return Attributes[name].address;
            }
            else
            {
                return -1;
            }
        }

        public int GetUniform(string name)
        {
            if (Uniforms.ContainsKey(name))
            {
                return Uniforms[name].address;
            }
            else
            {
                return -1;
            }
        }

        public uint GetBuffer(string name)
        {
            if (Buffers.ContainsKey(name))
            {
                return Buffers[name];
            }
            else
            {
                return 0;
            }
        }

Finally, we'll want to create a second constructor for the ShaderProgram class, which lets us load our shaders, link them, generate buffers, and store a list of the parameters all in one line:

 
        public ShaderProgram(String vshader, String fshader, bool fromFile = false)
        {
            ProgramID = GL.CreateProgram();

            if (fromFile)
            {
                LoadShaderFromFile(vshader, ShaderType.VertexShader);
                LoadShaderFromFile(fshader, ShaderType.FragmentShader);
            }
            else
            {
                LoadShaderFromString(vshader, ShaderType.VertexShader);
                LoadShaderFromString(fshader, ShaderType.FragmentShader);
            }

            Link();
            GenBuffers();
        }

With this, we can initialize the class with both shaders, from either strings or files, and have them totally set up for us all at once. Now, we just need to make the old code use this class instead of having a single hard-coded shader.

The following modifications are made to the Game class.

First off, we'll need to get rid of all the old variables that were specific to our old shader set-up. To help, here's a list of them: pgmID, vsID, fsID, attribute_vcol, attribute_vpos, uniform_mview, vbo_position, vbo_color, and vbo_mview. However, we'll need to add two new variables to replace all of these: a Dictionary to store shaders in a convenient format, and a string to store the currently active shader's name.

 
        Dictionary<string, ShaderProgram> shaders = new Dictionary<string, ShaderProgram>();
        string activeShader = "default";

In initProgram, we'll want to remove most of the code and add our own. We'll be left with this (as a bare minimum, we'll add objects back in when we have our new TexturedCube class):

 
            GL.GenBuffers(1, out ibo_elements);
                        shaders.Add("default", new ShaderProgram("vs.glsl", "fs.glsl", true));

We can completely remove the loadShader method. We're doing that through the ShaderProgram class now, so we can keep our code clean by removing it from Game.

In OnRenderFrame, we'll need to change a few things. First, the GL.EnableVertexAttribArray calls should be replaced by the following:

 
            shaders[activeShader].EnableVertexAttribArrays();

The calls to GL.DisableVertexAttribArray should be replaced with a single line as well:

 
            shaders[activeShader].DisableVertexAttribArrays();

This makes it use the correct attribute arrays for the shader we want to use, no matter what arrays are present in it.

The call to GL.UniformMatrix4 needs to be changed to:

 
                GL.UniformMatrix4(shaders[activeShader].GetUniform("modelview"), false, ref v.ModelViewProjectionMatrix);

OnUpdateFrame has a lot of changes as well.

Where there are two sets of calls to GL.BindBuffer, GL.BufferData, and then GL.VertexAttribPointer (one set for position, one for color), we need to replace them with this:

 
            GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vPosition"));
            GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);             GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vPosition"), 3, VertexAttribPointerType.Float, false, 0, 0);             if (shaders[activeShader].GetAttribute("vColor") != -1)             {                 GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vColor"));                 GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(coldata.Length * Vector3.SizeInBytes), coldata, BufferUsageHint.StaticDraw);                 GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vColor"), 3, VertexAttribPointerType.Float, true, 0, 0);             }

This code still sends them, but uses our new class. It also only sends color data if the shader supports it. Our textured shader won't have a need (or a variable) for vertex colors, so we shouldn't send them.

Where we have code that says "GL.UseProgram(pgmID);", we'll need to replace it with this:

 
            GL.UseProgram(shaders[activeShader].ProgramID);

We're now at the end of Part 1. Part 2 will be done shortly, and will actually bring in the textures this tutorial is supposed to focus on.

11 comments:

  1. Nice tutorial!... why not make tutorial videos too?

    ReplyDelete
    Replies
    1. I'd really like to be able to use video, but it would be very difficult for a few reasons. The way I work right now isn't very conducive to a video format, and I'd need a different setup to record, edit, and render it.

      When the tutorials get into more complex topics (like lighting), I plan on having some videos as a visual aid, but the code will likely stay in text posts.

      Delete
  2. Good work, Kabuto.

    Just want to pop in and say I hope all is well. Still (patiently) hoping for that next part ;)

    Pete

    ReplyDelete
  3. For some reason the line:
    shaders.Add("default", new ShaderProgram("vs.glsl", "fs.glsl", true));

    throws a already existing error.
    Fixed it by changeing it too:
    ShaderProgram newshader = new ShaderProgram("vs.glsl", "fs.glsl", true);
    try
    {
    shaders.Add("default", newshader);
    }
    catch(Exception e)
    {

    }

    ReplyDelete
  4. Just out of curiosity, why aren't you using VAOs? You'd be saving yourself and everyone a lot of time, especially with things like enabling and disabling attributes.

    ReplyDelete
  5. I translated that by hand to VB2010
    http://hastebin.com/ebewecagab.vbs

    ReplyDelete
  6. I was wondering is you could in the future include the using statements, as it often spits out an error at me for these things. Is there a trick to it? or is it that i'm to new to this sort of stuff and just cant remember it. Anyway great tutorial.

    Thanks

    ReplyDelete
    Replies
    1. Hi Sven,

      There's sort of a trick if you're using Visual Studio. It should (I can't confirm if this is in the newest versions, since I'm in 2010 Ultimate on this computer) underline the class that needs a using directive with a red line. If you right click on them, there's an option for "Resolve", and then you can have it add them for you quickly. You should also be able to select the class name and press CTRL+. to get the same menu with the option to add the using directive.

      If you're using MonoDevelop, it should also have the right click menu to quickly add the directive, but I don't know if there's another shortcut for it.

      Delete
    2. Thanks for the reply, works fine now.

      -Sven

      Delete
  7. Finally then , nowhere understandable lessons , thank you!

    ReplyDelete