OpenGL 4 with OpenTK in C# Part 5: Buffers and Triangle
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.
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:
- OpenGL 4 with OpenTK in C# Part 1: Initialize the GameWindow
- OpenGL 4 with OpenTK in C# Part 2: Compiling shaders and linking them
- OpenGL 4 with OpenTK in C# Part 3: Passing data to shaders
- OpenGL 4 with OpenTK in C# Part 4: Refactoring and adding error handling
- OpenGL 4 with OpenTK in C# Part 5: Buffers and Triangle
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
.
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
.
_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:
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
:
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
:
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:
GL.VertexArrayVertexBuffer(_vertexArray, 0, _buffer, IntPtr.Zero, Vertex.Size);
Rendering this all with:
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.
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:
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.
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.
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
#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
#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.)
Until next time: Work to Live, Don't Live to Work