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
.
public struct Vertex
{
public const int Size = (4 + 4) * 4;
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,
vertices,
BufferStorageFlags.MapWriteBit);
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,
4,
VertexAttribType.Float,
false,
0);
And then the color
:
GL.VertexArrayAttribBinding(_vertexArray, 1, 0);
GL.EnableVertexArrayAttrib(_vertexArray, 1);
GL.VertexArrayAttribFormat(
_vertexArray,
1,
4,
VertexAttribType.Float,
false,
16);
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;
_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