Search code examples
c#openglglslshaderopentk

Projecting and offseting vertices in shaders in OpenTK 3.3


I am trying to get to work a polygon class for OpenGL using OpenTK. The goal is to make an instance of the class, pass it an array of vertices in pixel coordinates, and have it drawn to the screen properly.

The way I intend to achieve this is to use a projection matrix (defined as a public variable in the screen class) that the shader should use to scale everything from NDC to pixel coordinates. I also intend to pass an offset vector to the shader to add it to the position.

The projection is calculated using

Matrix4.CreateOrthographicOffCenter(0.0f, width, 0.0f, height, -100.0f, 100.0f);

The vertices used for the polygon are:

float[] vertices = new float[]
                {
                    0.0f, 100f, 0f,
                    0.0f, 0.0f, 0f,
                    100f, 0.0f, 0f,
                    100f, 100f, 0f,
                };

This is how my vertex shader looks:

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 projection;
uniform vec3 offset;

void main()
{
    gl_Position = projection * vec4(position + offset, 1.0);
}

This is the Polygon class:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using Shaders;
using Engine;

namespace Engine.Engine.Shape
{
    class Polygon
    {

        float[] vertices;
        List<uint> indexes;
        uint[] indices;
        int VBO, VAO, EBO;
        Shader shader;
        Matrix4 projection;
        Vector3 offset;

        public Polygon(float[] vertices)
        {
            this.vertices = vertices;
            this.projection = Screen.projection;

            offset = new Vector3(10, 10, 0);

            // This creates an index array for the EBO to use
            indexes = new List<uint>();
            for (uint curr_vert = 1; curr_vert < vertices.Length / 3 - 1; curr_vert++)
            {
                // For each triangle of the polygon, I select a fixed vertex (index 0), the current vertex and the next vertex
                uint[] triangle = new uint[] { 0, curr_vert, curr_vert + 1 };
                foreach (uint x in triangle)
                {
                    //I use this list to make life easier
                    indexes.Add(x);
                }
            }
            // Now that the list is complete I convert it into an array
            indices = indexes.ToArray();
        }

        public void Load()
        {
            string path = "../../../Shaders/";
            VBO = GL.GenBuffer();
            GL.BindBuffer(BufferTarget.ArrayBuffer, VBO);
            GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);

            VAO = GL.GenVertexArray();
            GL.BindVertexArray(VAO);

            GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
            GL.EnableVertexAttribArray(0);



            // We create/bind the Element Buffer Object EBO the same way as the VBO, except there is a major difference here which can be REALLY confusing.
            // The binding spot for ElementArrayBuffer is not actually a global binding spot like ArrayBuffer is. 
            // Instead it's actually a property of the currently bound VertexArrayObject, and binding an EBO with no VAO is undefined behaviour.
            // This also means that if you bind another VAO, the current ElementArrayBuffer is going to change with it.
            // Another sneaky part is that you don't need to unbind the buffer in ElementArrayBuffer as unbinding the VAO is going to do this,
            // and unbinding the EBO will remove it from the VAO instead of unbinding it like you would for VBOs or VAOs.
            EBO = GL.GenBuffer();
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, EBO);
            // We also upload data to the EBO the same way as we did with VBOs.
            GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Length * sizeof(uint), indices, BufferUsageHint.StaticDraw);
            // The EBO has now been properly setup. Go to the Render function to see how we draw our rectangle now!

            shader = new Shader(path + "shader.vert", path + "shader.frag");
            shader.Use();
            


        }

        public void Show()
        {

            shader.Use();
            shader.SetMatrix4("projection", projection);
            shader.SetVector3("offset", offset);
            GL.BindVertexArray(VAO);
            GL.DrawElements(PrimitiveType.Triangles, indices.Length, DrawElementsType.UnsignedInt, 0);
        }

    }
}

The expected result is a window with a gray background with a white polygon on the top left, offset from the corner by 10 pixels on each axis.

However, no polygon appears if I either try to apply the offset or the projection matrix to the shader. Since GLSL cannot print to a console, I have no way of debugging the shader itself, and I can only see the results and change the inputs. What am I doing wrong? This is the way I've seen everyone do it.

The Shader class used in this code is the following (can be found here):

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using OpenTK.Graphics.OpenGL;
using OpenTK;

namespace Shaders
{
    // A simple class meant to help create shaders.
    public class Shader
    {
        public readonly int Handle;

        private readonly Dictionary<string, int> _uniformLocations;

        // This is how you create a simple shader.
        // Shaders are written in GLSL, which is a language very similar to C in its semantics.
        // The GLSL source is compiled *at runtime*, so it can optimize itself for the graphics card it's currently being used on.
        // A commented example of GLSL can be found in shader.vert
        public Shader(string vertPath, string fragPath)
        {
            // There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders.
            // The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader.
            //   The vertex shader won't be too important here, but they'll be more important later.
            // The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel.
            //   The fragment shader is what we'll be using the most here.

            // Load vertex shader and compile
            var shaderSource = File.ReadAllText(vertPath);

            // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created.
            var vertexShader = GL.CreateShader(ShaderType.VertexShader);

            // Now, bind the GLSL source code
            GL.ShaderSource(vertexShader, shaderSource);

            // And then compile
            CompileShader(vertexShader);

            // We do the same for the fragment shader
            shaderSource = File.ReadAllText(fragPath);
            var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
            GL.ShaderSource(fragmentShader, shaderSource);
            CompileShader(fragmentShader);

            // These two shaders must then be merged into a shader program, which can then be used by OpenGL.
            // To do this, create a program...
            Handle = GL.CreateProgram();

            // Attach both shaders...
            GL.AttachShader(Handle, vertexShader);
            GL.AttachShader(Handle, fragmentShader);

            // And then link them together.
            LinkProgram(Handle);

            // When the shader program is linked, it no longer needs the individual shaders attacked to it; the compiled code is copied into the shader program.
            // Detach them, and then delete them.
            GL.DetachShader(Handle, vertexShader);
            GL.DetachShader(Handle, fragmentShader);
            GL.DeleteShader(fragmentShader);
            GL.DeleteShader(vertexShader);

            // The shader is now ready to go, but first, we're going to cache all the shader uniform locations.
            // Querying this from the shader is very slow, so we do it once on initialization and reuse those values
            // later.

            // First, we have to get the number of active uniforms in the shader.
            GL.GetProgram(Handle, GetProgramParameterName.ActiveUniforms, out var numberOfUniforms);

            // Next, allocate the dictionary to hold the locations.
            _uniformLocations = new Dictionary<string, int>();

            // Loop over all the uniforms,
            for (var i = 0; i < numberOfUniforms; i++)
            {
                // get the name of this uniform,
                var key = GL.GetActiveUniform(Handle, i, out _, out _);

                // get the location,
                var location = GL.GetUniformLocation(Handle, key);

                // and then add it to the dictionary.
                _uniformLocations.Add(key, location);
            }
        }

        private static void CompileShader(int shader)
        {
            // Try to compile the shader
            GL.CompileShader(shader);

            // Check for compilation errors
            GL.GetShader(shader, ShaderParameter.CompileStatus, out var code);
            if (code != (int)All.True)
            {
                // We can use `GL.GetShaderInfoLog(shader)` to get information about the error.
                var infoLog = GL.GetShaderInfoLog(shader);
                throw new Exception($"Error occurred whilst compiling Shader({shader}).\n\n{infoLog}");
            }
        }

        private static void LinkProgram(int program)
        {
            // We link the program
            GL.LinkProgram(program);

            // Check for linking errors
            GL.GetProgram(program, GetProgramParameterName.LinkStatus, out var code);
            if (code != (int)All.True)
            {
                // We can use `GL.GetProgramInfoLog(program)` to get information about the error.
                throw new Exception($"Error occurred whilst linking Program({program})");
            }
        }

        // A wrapper function that enables the shader program.
        public void Use()
        {
            GL.UseProgram(Handle);
        }

        // The shader sources provided with this project use hardcoded layout(location)-s. If you want to do it dynamically,
        // you can omit the layout(location=X) lines in the vertex shader, and use this in VertexAttribPointer instead of the hardcoded values.
        public int GetAttribLocation(string attribName)
        {
            return GL.GetAttribLocation(Handle, attribName);
        }

        // Uniform setters
        // Uniforms are variables that can be set by user code, instead of reading them from the VBO.
        // You use VBOs for vertex-related data, and uniforms for almost everything else.

        // Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method:
        //     1. Bind the program you want to set the uniform on
        //     2. Get a handle to the location of the uniform with GL.GetUniformLocation.
        //     3. Use the appropriate GL.Uniform* function to set the uniform.

        /// <summary>
        /// Set a uniform int on this shader.
        /// </summary>
        /// <param name="name">The name of the uniform</param>
        /// <param name="data">The data to set</param>
        public void SetInt(string name, int data)
        {
            GL.UseProgram(Handle);
            GL.Uniform1(_uniformLocations[name], data);
        }

        /// <summary>
        /// Set a uniform float on this shader.
        /// </summary>
        /// <param name="name">The name of the uniform</param>
        /// <param name="data">The data to set</param>
        public void SetFloat(string name, float data)
        {
            GL.UseProgram(Handle);
            GL.Uniform1(_uniformLocations[name], data);
        }

        /// <summary>
        /// Set a uniform Matrix4 on this shader
        /// </summary>
        /// <param name="name">The name of the uniform</param>
        /// <param name="data">The data to set</param>
        /// <remarks>
        ///   <para>
        ///   The matrix is transposed before being sent to the shader.
        ///   </para>
        /// </remarks>
        public void SetMatrix4(string name, Matrix4 data)
        {
            GL.UseProgram(Handle);
            GL.UniformMatrix4(_uniformLocations[name], true, ref data);
        }

        /// <summary>
        /// Set a uniform Vector3 on this shader.
        /// </summary>
        /// <param name="name">The name of the uniform</param>
        /// <param name="data">The data to set</param>
        public void SetVector3(string name, Vector3 data)
        {
            GL.UseProgram(Handle);
            GL.Uniform3(_uniformLocations[name], data);
        }
    }
}

The rest of the code can be found here


Solution

  • The projection matrix defines a viewing volume. Any geometry outside of this volume will be clipped. Your geometry is clipped because it is not between the near and far planes of the volume defined by the Orthographic projection. The geometry z coordinate is 0, but the distance to the near plane is 0.1 and the distance to the far plane is 100.

    Either change the z coordinate of the geometry and move the geometry along the negative z axis:

    offset = new Vector3(10, 10, 0);

    offset = new Vector3(10, 10, -10);
    

    Or change the near plane of the orthographic projection:

    Matrix4.CreateOrthographicOffCenter(0.0f, width, 0.0f, height, 0.1f, 100.0f);

    Matrix4.CreateOrthographicOffCenter(0.0f, width, 0.0f, height, -100.0f, 100.0f);
    

    In the shader code the matrix is multiplied to the vector from the left (which is common). Therefore you must not transpose the matrix:

    gl_Position = projection * vec4(position + offset, 1.0);
    

    GL.UniformMatrix4(_uniformLocations[name], true, ref data)

    GL.UniformMatrix4(_uniformLocations[name], false, ref data);