Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / OpenGL

OpenGL 4 with OpenTK in C# Part 5: Buffers and Triangle

5.00/5 (1 vote)
1 Feb 2017CPOL3 min read 12.6K  
In this post, we will look at how to access buffers in OpenGL from your application code to provide your shaders with vertex data and to draw our first triangle.
Image 1

In this post, we will look at how to access buffers in OpenGL from your application code to provide your shaders with vertex data and to draw our first triangle.

This is part 5 of my series on OpenGL4 with OpenTK.

For other posts in this series:

As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful, then all the better. :)
If you think that the progress is slow, then know that I am a slow learner. :P
This part will build upon the game window and shaders from the previous post.

Initialize a Buffer

First off, let's define a struct to hold our vertex position and color.

C#
public struct Vertex
{
    public const int Size = (4 + 4) * 4; // size of struct in bytes

    private readonly Vector4 _position;
    private readonly Color4 _color;

    public Vertex(Vector4 position, Color4 color)
    {
        _position = position;
        _color = color;
    }
}

The above struct matches the Vertex Shader, i.e., that it has a Vec4 for Position and Vec4 for Color. The Vector4 and Color4 are provided by OpenTK.

In the previous posts, we have used a pretty static VertexArray just because it is needed to be able to draw anything. So let's create a vertex array and a buffer.

C#
_vertexArray = GL.GenVertexArray();
_buffer = GL.GenBuffer();

GL.BindVertexArray(_vertexArray);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexArray);

Next step is to tell OpenGL how we will be using this new buffer:

C#
GL.NamedBufferStorage(
    _buffer,
    Vertex.Size*vertices.Length,        // the size needed by this buffer
    vertices,                           // data to initialize with
    BufferStorageFlags.MapWriteBit);    // at this point we will only write to the buffer

Now, we need to provide information on where to find the attribute data for the shader, i.e., position and color. Starting with Position:

C#
    GL.VertexArrayAttribBinding(_vertexArray, 0, 0);
    GL.EnableVertexArrayAttrib(_vertexArray, 0);
    GL.VertexArrayAttribFormat(
        _vertexArray,
        0,                      // attribute index, from the shader location = 0
        4,                      // size of attribute, vec4
        VertexAttribType.Float, // contains floats
        false,                  // does not need to be normalized as it is already, 
                                // floats ignore this flag anyway
        0);                     // relative offset, first item

And then the color:

C#
    GL.VertexArrayAttribBinding(_vertexArray, 1, 0);
    GL.EnableVertexArrayAttrib(_vertexArray, 1);
    GL.VertexArrayAttribFormat(
        _vertexArray,
        1,                      // attribute index, from the shader location = 1
        4,                      // size of attribute, vec4
        VertexAttribType.Float, // contains floats
        false,                  // does not need to be normalized as it is already, 
                                // floats ignore this flag anyway
        16);                    // relative offset after a vec4

Finally, we link this together using the following command:

C#
GL.VertexArrayVertexBuffer(_vertexArray, 0, _buffer, IntPtr.Zero, Vertex.Size);


Rendering this all with:

C#
GL.BindVertexArray(_vertexArray);
GL.DrawArrays(PrimitiveType.Triangles, 0, 3);

Refactoring to a RenderObject Class

As this is quite a lot of code, I decided to create a class for this called RenderObject. The above code for initiating goes into the constructor and the render code into the Render method.

C#
public class RenderObject : IDisposable
{
    private bool _initialized;
    private readonly int _vertexArray;
    private readonly int _buffer;
    private readonly int _verticeCount;
    public RenderObject(Vertex[] vertices)
    {
        _verticeCount = vertices.Length;
 
  // create vertex array and buffer here
  
        _initialized = true;
    }
    public void Render()
    {
        GL.BindVertexArray(_vertexArray);
        GL.DrawArrays(PrimitiveType.Triangles, 0, _verticeCount);
    }
        
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_initialized)
            {
                GL.DeleteVertexArray(_vertexArray);
                GL.DeleteBuffer(_buffer);
                _initialized = false;
            }
        }
    }
}

This will hopefully help us further down the road when we want to add more objects to the scene.
This gives us the following changes in our GameWindow starting the OnLoad method:

C#
private List<RenderObject> _renderObjects = new List<RenderObject>(); 
protected override void OnLoad(EventArgs e)
{
 Vertex[] vertices =
 {
  new Vertex(new Vector4(-0.25f, 0.25f, 0.5f, 1-0f), Color4.HotPink),
  new Vertex(new Vector4( 0.0f, -0.25f, 0.5f, 1-0f), Color4.HotPink),
  new Vertex(new Vector4( 0.25f, 0.25f, 0.5f, 1-0f), Color4.HotPink), 
 };
 _renderObjects.Add(new RenderObject(vertices));

 CursorVisible = true;

 _program = CreateProgram();
 GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);
 GL.PatchParameter(PatchParameterInt.PatchVertices, 3);
 Closed += OnClosed;
}

So we replaced the VertexBuffer initialization with initialization of a render object list instead. The example vertice array should result in a pink triangle on the screen similar to the title picture of this post.

The OnExit method is changed to dispose the render objects that have been initialized by our code.

C#
public override void Exit()
{
 Debug.WriteLine("Exit called");
 foreach(var obj in _renderObjects)
  obj.Dispose();
 GL.DeleteProgram(_program);
 base.Exit();
}

And finally our OnRenderFrame code loops over the render objects and calls their independent Render methods to get them on the screen.

C#
protected override void OnRenderFrame(FrameEventArgs e)
{
 _time += e.Time;
 Title = $"{_title}: (Vsync: {VSync}) FPS: {1f / e.Time:0}";
 Color4 backColor;
 backColor.A = 1.0f;
 backColor.R = 0.1f;
 backColor.G = 0.1f;
 backColor.B = 0.3f;
 GL.ClearColor(backColor);
 GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

 GL.UseProgram(_program);
 foreach(var renderObject in _renderObjects)
  renderObject.Render();
 SwapBuffers();
}

Shaders Used

The shaders used in this post are the following:

Vertex Shader

C#
#version 450 core

layout (location = 0) in vec4 position;
layout(location = 1) in vec4 color;
out vec4 vs_color;

void main(void)
{
 gl_Position = position;
 vs_color = color;
}

Fragment Shader

C#
#version 450 core
in vec4 vs_color;
out vec4 color;

void main(void)
{
 color = vs_color;
}

Hope this helps someone out there. :)

Thanks for reading. Here's another GIF of one of our cats playing to lighten up your day. (Full video at: https://youtu.be/EfE2v4x24vY.)

Image 2

Until next time: Work to Live, Don't Live to Work

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)