Vertex Buffer Object
The primary way of rendering anything in OpenGL is using vertices. The easiest way of handling vertices in C# is by creating custom structures for them.
For this example we will use a simple vertex with a position and a colour.
To render our vertices, we will need a vertex buffer object (VBO). You can think of this as a piece of GPU memory that we can copy our vertices to, so that they can be rendered.
Below you can see a very simple implementation wrapping all the functionality we will need.
For reasons of brevity, I will not go into details for each of the used GL calls. Feel free however, to look up the documentation of the corresponding OpenGL function online, or ask questions in the comments, if anything is not clear.
To keep the code short, I am also completely ignoring the need for disposing OpenGL objects we are no longer using.
If you are interested in how to do so, you can take a look at how I implement the
IDisposable interface in my library’s VertexBuffer.
To render anything, we need shaders; little programs that we can run on the GPU to put our vertices onto the screen.
In our simple case we want to create a vertex shader to transform out vertices into screen space and prepare them for rasterization, and a fragment shader (also known as pixel shader, especially in Direct3D) to assign a colour to each of our rendered fragments. Fragments for us are the same as pixels, and we will drop the distinction for this post.
In OpenGL, we create these two shaders separately and then link them together into one shader program as follows:
Shaders in OpenGL are written in the OpenGL Shading language (GLSL), which looks very much like C.
For this example we will use very simple shaders. Our vertex shader will transform the vertex position by multiplying it with a projection matrix which we will set below.
It will also pass on the colour of the vertex, which will then be used in the fragment shader to colour the pixel.
Vertex Attributes and Vertex Array Objects
In principle we are almost done. However, when we pass our array of vertices to OpenGL, we will do so as a block of bytes, and it will not know how to interpret the data correctly.
To give it that information, specifically, to let it know what attributes our vertices have, and how they are laid out in memory, we will use a vertex array object (VAO).
VAOs are very different from vertex buffers. Despite their name, they do not store vertices. Instead they store information about how to access an array of vertices, which is exactly what we want.
Setting up a VAO is done as follows:
VertexAttribute type used is a simple container like this:
For information on how to define vertex attributes, I refer to the documentation of glVertexAttribPointer.
As example, this is how the definition looks for our vertex:
Defining this data – especially since a lot of it is redundant or can be inferred in most cases – can be largely abstracted and simplified (or even done completely automatically).
For an idea of how that can be achieved, feel free to check this helper class I wrote for my own library.
Something else we have to do before we can put everything together is set the value of the projection matrix.
Parameters like matrices, textures, and other user defined values that remain constant for the execution of a shader are stored in uniforms, which can in fact be treated like constants within shader code.
To avoid unpredictable or nonsensical behaviour, they should always be set before rendering as follows:
Attribute and Uniform Locations
Both attributes and uniforms have to be assigned to locations, which are specified by an integer. These locations can be specified in the shader’s source itself.
They can however also be extracted by their name, which I consider a better solution since detecting a wrong attribute name is much easier than finding inconsistencies between location assignments in the shader and the C# source.
To access these locations is easy, and can be done by adding the following two methods to our shader program type:
Note that these GL calls return -1 if a location was not found, which you may want to check for in a production setting. Further, it may be advantageous to keep a local copy of all known locations for easier and faster access.