Part 4: Vertex and Index Buffer Objects

In this part we are going to be covering what is essentially the opposite of immediate mode by delving into using Vertex and Index Buffer Objects (VBOs and IBOs). This is sometimes referred to as Deferred Mode and can be very useful for a variety of reasons. It will also be essential when we get into using custom vertex, geometry, and fragment shaders.

Since we are going to be drawing things completely different, feel free to delete the drawing code we implemented in Part 2 till you have a render function that is something like below.

Step 1: Creating a Simple Vertex Buffer

If you haven't ever heard of Vertex Buffers before don't worry. They are actually quite simple to understand and if you know the reason why we use them then they seem like a no brainer.

Up until this point we have been using Immediate Mode to send our vertices to OpenGL so that it can draw them for us. As I said before, this is really nice and easy to set up but it's actually really sub-optimal when to comes to performance. The main reason for this stems from the fact that there is actually two processors in your computer. One of the processors we call the CPU (Central Processing Unit) and the other we call the GPU (Graphical Processing Unit). You have probably heard these before if you are into any computer gaming. These two processors work on separate tasks and are optimized completely different. The CPU runs all of the computation that needs to happen in order to run programs and manage RAM and a few other things. The GPU mainly deals with rendering output data for the screen to display. These two processors are connected by a bus so that they can transfer data between each other. However this bus is not infinite in it's capability to transfer data and often times is the bottle-neck for unoptimized games.

Every time we've passed a primitive to OpenGL it in turn passes that data across this bus so that the GPU can render it. For a few primitives this works fine but once you start to get a full fledged game being rendered every frame you will most likely experience performance drops, and it's largely due to this bus bottleneck. (There's also some performance gained from optimizations that the GPU can do when you use VBO's and IBO's). So what we need to do is minimize this data transfer and speed it up by transferring all that data at once over to the GPU. We do this by making a Vertex Buffer Object that contains all our vertices and then pass that data together as a package over to the GPU. This can either happen each frame, if your primitives are going to be very dynamic, or it can happen only once in your program, like using a 3D model.

After that lengthy explanation I think we're ready to get started. The first thing we are going to want to do is create a list of Vector2s that will define our vertex data. Add this to the window_Load function, just after we load our texture in.

These six vertices should give us two triangles that will represent a square when rendered. Now we need to create our VBO before we can fill it with data. Just like when we passed the texture data, OpenGL is going to give us a reference to our VBO in the form of and integer. Now we can make a class if we want, to encapsulate our integer and call it a VBO. I often do this but since VBOs come in many different varieties my class often tends to be very complex and complicated. We're not going to do all that in this tutorial so this class will be completely optional. Either way you choose make a variable of the corresponding type at the top our our Game class.

Now back, after we create our vertices, let's add a line to create our new VBO.

As you'll see later, buffers come in all shapes and sizes so this function is intentionally vague. We will have to define how our buffer will be used a bit later when we go to use it for drawing. Now let's bind our buffer so we can work on it. We bind it as a generic ArrayBuffer since this is the type used for generic data that will be used for drawing.

Now that we have it created and bound we can pass our vertex data. We do this by calling GL.BufferData.

As you can see we told OpenGL to put this data in the ArrayBuffer that is currently bound. We also had to tell OpenGL the size in bytes of our data. Since our data is just an array of Vector2s we can simply multiply the size of a Vector2 by how many we have. We passed it a reference to our data. And then we also gave OpenGL what it calls a BufferUsageHint. This is actually aptly named since this basically is just telling OpenGL how to optimize this buffer for usage. They will all work for almost any usage but they just might have slightly different performances depending on what you are doing with the buffer. In our case we are simply passing the buffer once and then drawing it each frame. For that, we use StaticDraw. You can also use Dynamic if you are planning on recreating the buffer every now and then, or Stream if you are passing the data each frame.

Now, down in our RenderFrame function we can add some code to actually utilize this buffer to draw vertices to the screen. This code will be going just after we bind our texture. Before we tell OpenGL to draw using the array we need to tell it a bit more about what the array contains.

The first function simply tells OpenGL that we want to enable our buffer object to define the vertices to be drawn. We will later use this to enable color and texture coordinates in our buffer as well. The second function is quite important as well. This is where we tell OpenGL how to find and read our vertex position data from our array. We first tell it that each vertex has 2 floats to define a 2D position. Then it asks for a "stride" and an "offset". Since we only have one Vector2 per vertex in our array right now we simply need to step (or stride) the size of a Vector2 each index and the offset from that position will be 0.

With that set up correctly we can now bind and draw our buffer. Note that since we don't have texture coordinate or color data in our buffer yet we still need to define them manually, else we would simply get a transparent pixel being sampled from the top left corner.

We simply bind our VBO object like we did before, set the color and texture coordinate values, and then use the DrawArrays function to tell it to draw starting at index 0 and stepping through 6 vertices.

If you've done everything correctly, running this now should produce a result similar to this.

Note that this can be very finicky when you first start learning. If you typed one thing wrong in the last few pieces of code it's very likely that you will get an Access Violation exception thrown when you call DrawArrays that will tell you nothing about why it didn't work. Unfortunately there is no easy way that I know of to debug this other than to simply step through your code, step by step, and proof read it to make sure everything is correct. If you are still experiencing problems I recommend deleting the code and typing EXACTLY what I have here. Once you have something working you can then backtrack step by step changing it back into your own unique code to draw what you want.
Despite being very picky, drawing using this method works for almost any application you might need it for and is actually really stable. It's simply a little hard to debug when something goes wrong.

If you've gotten everything to work then give yourself a pat on the back. This can be quite annoying to get working your first time. (I say this from experience) However, now that you have something set up and running correctly, it should be relatively easy to step it up, piece by piece, till it works with all the functionality we want. And if some point along the way it starts throwing the error again, you should know exactly which line, or couple lines, of code you just added that made the error occur.

Step 2: Texture Coordinates and Color in our VBO

Now that we have vertices being drawn from our VBO, we want to add the ability to store texture coordinate and color data in our buffer as well. This is quite simple actually and can be done a couple of different ways. We could turn our array into an array of generic objects and interlace our texture coordinate Vector2s, color Vector4s, and position Vector2s all together in the array. However I would like to do this a little more nicely so instead we will create a Vertex struct that will hold these three pieces of data. It'll be a relatively small struct so you can put this in the same file as our game if you want but I'm going to put mine in a separate file. Either way you should have something like this.

I'm also going to add some overloads to make it easier to use our Vector4 color object with System.Drawing's Color class.

We also need to make a SizeInBytes parameter for our Vertex class since we will need it later. This is really easy to calculate since our struct only contains 2 Vector2s and 1 Vector4 and can be calculated outside the class. But it's always nice to keep this in the class so that if we add a property to our Vertex we can update it once here and have it be reflected everywhere.

Now that we have our Vertex struct set up we can go ahead and change our array from a Vector2 type to a Vertex type. At the same time let's go ahead and just move the declaration of it up to our class scope so that it will persist and we can get info about it's length and values and such.

Keeping our Vertex array in scope is not necessary and often is not desired since it will increase the memory load of your program. Once you have passed to vertex data to OpenGL you can go ahead and get rid our your original array. In this tutorial however since we are not working on a large scale I find that the ease of readability that comes with having access to the array size and type outweighs any performance issues. However I do not recommend doing this when loading in a high-poly 3D model or a large number of vertices of any type. If you need to know the number of vertices at draw time then you can save that in a separate variable.

We will still initialize it in our Load function but with a little bit different format and some extra data added.

We also need to make sure we change our BufferData call to reflect the new type and size.

Now that we have the data being created and passed correctly let's go update our draw call so that we can use the color and texture coordinate data we just added. We first need call EnableClientState a couple more times.

And then we need to update the pointer for the vertex data and add two more for texture coordinate and color.

Since each vertex has more than just one piece of data, our stride and offset become much more important. The stride will still be the size of each vertex, e.g. the size of our Vertex struct. But the offset now is different for each type of data. Since we define position first on our struct the offset is zero. Then the texture coordinate comes after it so the offset is the size of the position. And finally the color is going to be offset by the size of the both the position and texture coordinate.

With all of that in place we can also get rid of our calls to TexCoord2 and Color4 since we don't need them anymore. Let's go ahead and update our DrawArrays call a little so it's more versatile as well. Since we know the length of our array here we can dynamically choose how many vertices to draw dependent on how we initialize the array at the top.

And that's it. If everything went to plan you should get something similar to this.

Step 3: Creating An Index Buffer Object

Now that we have our vertices being drawn correctly there's only one more thing we need to figure out in this part and that's using and Index Buffer Object (IBO). An index buffer will allow us more control over which vertices will be drawn and in what order. Essentially all the buffer will hold is an array of integers that represent which index of our VBO should be drawn.

The reason this is so useful is it means we can use a vertex more than once, for multiple primitives. For example, in our setup here where we have 2 triangles forming a rectangle we had to define 6 vertices, even though the object really only has 4 unique vertices. If we implement a index buffer when can then just define 4 vertices and tell OpenGL in what order to use those vertices to make triangles. This seems like a small trade-off now, but if you start getting hundreds of triangles on the screen for a 3D model or 2D particles, you often have quite a few shared vertices between primitives.

Alright, so let's get started. Creating a IBO is really almost exactly the same as creating a VBO. We'll first start by making a uint array to hold our indices and a regular integer called IBO to hold the pointer to our Index Buffer.

Note that you can also use other types of numbers besides int and uint if you need a larger range. You simply have to change the types and then tell OpenGL that the buffer is made of whatever type you are using. In this case though we will be using unsigned-integers since we don't need negative numbers.

Now that those are defined we can go ahead and initialize them down next to our VBO. At the same time we can also get rid of our redundant vertices in our vertex array.

Notice that instead of binding the buffer to the ArrayBuffer target we use the ElementArrayBuffer target instead. I'm not really sure if this is needed here, but it will be important later when we bind it for drawing so might as well keep it consistent. Other than that though the creation is relatively the same.

Now that we have that data passed we can go back to our render function and update it to use our new IBO.

Again this is relatively straight forward. We enable the IndexArray ClientState like we did the others and then bind our IBO to the ElementArrayBuffer target before drawing. However, the key difference here is that instead of calling GL.DrawArrays we use GL.DrawElements which is the function that tells OpenGL to use the ElementArrayBuffer to know which vertices to use. Again we tell it to draw Triangles and how many vertices there are (indices.Length) but now we also tell it what type of numbers the indices are and then point it to the first index with 0.

If we run this now we should get exactly the same output as before.

That's it for this part. If you have any questions or suggestions feel free to leave them below or contact me from the About page. And if you want to keep learning more OpenTK then head on to the

next Part

Back to Table of Contents