Introduction to Vertex Arrays and Vertex Buffer Objects (OpenGL)

Introduction to Vertex Arrays and Vertex Buffer Objects
Copy 'n Paste OpenGL

It is my suspicion that most aspiring developers these days prefer to copy snippets of code, paste them into their own project, see if it works, modify a few things, and work from there. Therefore I decided not to explain too much in this tutorial, because most is either self descriptive or properly explained elsewhere (see here).

Having said that, here goes:

Getting started…
First we initialize an OpenGL context through LWJGL:


import org.lwjgl.*;
import org.lwjgl.opengl.*;
import static org.lwjgl.opengl.ARBBufferObject.*;
import static org.lwjgl.opengl.ARBVertexBufferObject.*;
import static org.lwjgl.opengl.GL11.*;

public class JgoVbo
{
   static void initContext() throws LWJGLException
   {
      int w = 640;
      int h = 480;

      Display.setDisplayMode(new DisplayMode(w, h));
      Display.setFullscreen(false);
      Display.create();
      glViewport(0, 0, w, h);
   }

Render loop
Once we have the context, we need some loop that will prepare a frame, render some graphics and showing it on screen, until we close the window.


   static void renderLoop()
   {
      while (!Display.isCloseRequested())
      {
         preRender();

         render();

         Display.update();

         Display.sync(10 /* desired fps */);
      }

      Display.destroy();
   }

Clearing the screen
Before the render anything, we need to start with a clean slate by clearing the framebuffer and resetting the the OpenGL matrices.

   static void preRender()
   {
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();

      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
   }

Immediate mode
Now lets start simple, by drawing a triangle using the immediate mode of OpenGL:


   static void drawImmediateMode()
   {
      glBegin(GL_TRIANGLES);

      glColor3f(1, 0, 0);
      glVertex3f(-0.5f, -0.5f, 0.0f);

      glColor3f(0, 1, 0);
      glVertex3f(+0.5f, -0.5f, 0.0f);

      glColor3f(0, 0, 1);
      glVertex3f(+0.5f, +0.5f, 0.0f);

      glEnd();
   }

Output:

Vertex Arrays
We can draw exactly the same triangle using Vertex Arrays, like this:


   static void drawVertexArray()
   {
      // create geometry buffers
      FloatBuffer cBuffer = BufferUtils.createFloatBuffer(9);
      cBuffer.put(1).put(0).put(0);
      cBuffer.put(0).put(1).put(0);
      cBuffer.put(0).put(0).put(1);
      cBuffer.flip();

      FloatBuffer vBuffer = BufferUtils.createFloatBuffer(9);
      vBuffer.put(-0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(+0.5f).put(0.0f);
      vBuffer.flip();

      //

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glColorPointer(3, /* stride */3 << 2, cBuffer);
      glVertexPointer(3, /* stride */3 << 2, vBuffer);
      glDrawArrays(GL_TRIANGLES, 0, /* elements */3);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);
   }

Vertex Buffer Objects
With Vertex Buffer Objects (VBOs) we need to first generate resources on the GPU, and access them by their handles. The idea is to keep that geometry as long as possible on the GPU, so, as opposed to Vertex Arrays, we don’t have to upload them every frame.


   static void drawVertexBufferObject()
   {
      // create geometry buffers
      FloatBuffer cBuffer = BufferUtils.createFloatBuffer(9);
      cBuffer.put(1).put(0).put(0);
      cBuffer.put(0).put(1).put(0);
      cBuffer.put(0).put(0).put(1);
      cBuffer.flip();

      FloatBuffer vBuffer = BufferUtils.createFloatBuffer(9);
      vBuffer.put(-0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(+0.5f).put(0.0f);
      vBuffer.flip();

      //

      IntBuffer ib = BufferUtils.createIntBuffer(2);

      glGenBuffersARB(ib);
      int vHandle = ib.get(0);
      int cHandle = ib.get(1);

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, vHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, vBuffer, GL_STATIC_DRAW_ARB);
      glVertexPointer(3, GL_FLOAT, /* stride */3 << 2, 0L);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, cHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, cBuffer, GL_STATIC_DRAW_ARB);
      glColorPointer(3, GL_FLOAT, /* stride */3 << 2, 0L);

      glDrawArrays(GL_TRIANGLES, 0, 3 /* elements */);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);

      // cleanup VBO handles
      ib.put(0, vHandle);
      ib.put(1, cHandle);
      glDeleteBuffersARB(ib);
   }

Vertex Buffer Objects (indexed)
Now we might want to render the trangle using indexed geometry. That means you have a buffer that holds the indices of the triangles we want to render. This way you can reuse geometry (vertices) for more than one triangle (shared vertices). The only difference is that we need to create an index buffer, which also is a VBO, and fill it. In most cases 16 bit indices are a good choice:



   static void drawVertexBufferObjectIndexed()
   {
      // create geometry buffers
      FloatBuffer cBuffer = BufferUtils.createFloatBuffer(9);
      cBuffer.put(1).put(0).put(0);
      cBuffer.put(0).put(1).put(0);
      cBuffer.put(0).put(0).put(1);
      cBuffer.flip();

      FloatBuffer vBuffer = BufferUtils.createFloatBuffer(9);
      vBuffer.put(-0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(-0.5f).put(0.0f);
      vBuffer.put(+0.5f).put(+0.5f).put(0.0f);
      vBuffer.flip();

      // create index buffer
      ShortBuffer iBuffer = BufferUtils.createShortBuffer(3);
      iBuffer.put((short) 0);
      iBuffer.put((short) 1);
      iBuffer.put((short) 2);
      iBuffer.flip();

      //

      IntBuffer ib = BufferUtils.createIntBuffer(3);

      glGenBuffersARB(ib);
      int vHandle = ib.get(0);
      int cHandle = ib.get(1);
      int iHandle = ib.get(2);

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, vHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, vBuffer, GL_STATIC_DRAW_ARB);
      glVertexPointer(3, GL_FLOAT, /* stride */3 << 2, 0L);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, cHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, cBuffer, GL_STATIC_DRAW_ARB);
      glColorPointer(3, GL_FLOAT, /* stride */3 << 2, 0L);

      glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, iHandle);
      glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, iBuffer, GL_STATIC_DRAW_ARB);

      glDrawElements(GL_TRIANGLES, /* elements */3, GL_UNSIGNED_SHORT, 0L);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
      glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);

      // cleanup VBO handles
      ib.put(0, vHandle);
      ib.put(1, cHandle);
      ib.put(2, iHandle);
      glDeleteBuffersARB(ib);
   }

Vertex Buffer Objects (interleaved)
Until now we have created separate VBOs for vertices and colors. We can interleave them in two ways:

  1. VCVCVC (V stands for vertex element, C stands for color element)

   static void drawVertexBufferObjectInterleaved1()
   {
      // create geometry buffer (both vertex and color)
      FloatBuffer vcBuffer = BufferUtils.createFloatBuffer(9 + 9);

      vcBuffer.put(-0.5f).put(-0.5f).put(0.0f); // v
      vcBuffer.put(1).put(0).put(0); // c

      vcBuffer.put(+0.5f).put(-0.5f).put(0.0f); // v
      vcBuffer.put(0).put(1).put(0); // c

      vcBuffer.put(+0.5f).put(+0.5f).put(0.0f); // v
      vcBuffer.put(0).put(0).put(1); // c

      vcBuffer.flip();

      //

      IntBuffer ib = BufferUtils.createIntBuffer(1);

      glGenBuffersARB(ib);
      int vcHandle = ib.get(0);

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, vcHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, vcBuffer, GL_STATIC_DRAW_ARB);
      glVertexPointer(3, GL_FLOAT, /* stride **/(3 * 2) << 2, /* offset **/0 << 2); // float at index 0
      glColorPointer(3, GL_FLOAT, /* stride **/(3 * 2) << 2, /* offset **/(3*1) << 2); // float at index 3

      glDrawArrays(GL_TRIANGLES, 0, 3 /* elements */);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);

      // cleanup VBO handles
      ib.put(0, vcHandle);
      glDeleteBuffersARB(ib);
   }

  1. VVVCCC (V stands for vertex element, C stands for color element)

   static void drawVertexBufferObjectInterleaved2()
   {
      // create geometry buffer (both vertex and color)
      FloatBuffer vcBuffer = BufferUtils.createFloatBuffer(9 + 9);

      vcBuffer.put(-0.5f).put(-0.5f).put(0.0f); // v
      vcBuffer.put(+0.5f).put(-0.5f).put(0.0f); // v
      vcBuffer.put(+0.5f).put(+0.5f).put(0.0f); // v
      vcBuffer.put(1).put(0).put(0); // c      
      vcBuffer.put(0).put(1).put(0); // c      
      vcBuffer.put(0).put(0).put(1); // c

      vcBuffer.flip();

      //

      IntBuffer ib = BufferUtils.createIntBuffer(1);

      glGenBuffersARB(ib);
      int vcHandle = ib.get(0);

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, vcHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, vcBuffer, GL_STATIC_DRAW_ARB);
      glVertexPointer(3, GL_FLOAT, /* stride **/(3 * 1) << 2, /* offset */0 << 2); // float at index 0
      glColorPointer(3, GL_FLOAT, /* stride **/(3 * 1) << 2, /* offset */(3 * 3) << 2); // float at index 9

      glDrawArrays(GL_TRIANGLES, 0, 3 /* elements */);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);

      // cleanup VBO handles
      ib.put(0, vcHandle);
      glDeleteBuffersARB(ib);
   }

Pay close attention to the values specified as stride and offset in both examples of interleaved rendering.

Mapped Vertex Buffer Object
We can also request a memory region from the driver, to push our vertex data into. We obtain this memory region as a ByteBuffer, in which we can solely write, using the glMapBuffer(…). When we’ve defined the data, we call glUnmapBuffer(…) to notify the driver we are done, allowing the driver exclusive access to the data.

  static void drawVertexBufferObjectInterleavedMapped()
   {
      IntBuffer ib = BufferUtils.createIntBuffer(1);

      glGenBuffersARB(ib);
      int vcHandle = ib.get(0);

      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, vcHandle);
      glBufferDataARB(GL_ARRAY_BUFFER_ARB, (9 + 9) << 2, GL_STATIC_DRAW_ARB);

      {
         ByteBuffer dataBuffer = glMapBufferARB(GL_ARRAY_BUFFER_ARB, ARBBufferObject.GL_WRITE_ONLY_ARB, (9 + 9) << 2, null);

         // create geometry buffer (both vertex and color)
         FloatBuffer vcBuffer = dataBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();

         vcBuffer.put(-0.5f).put(-0.5f).put(0.0f); // v
         vcBuffer.put(1).put(0).put(0); // c

         vcBuffer.put(+0.5f).put(-0.5f).put(0.0f); // v
         vcBuffer.put(0).put(1).put(0); // c

         vcBuffer.put(+0.5f).put(+0.5f).put(0.0f); // v
         vcBuffer.put(0).put(0).put(1); // c

         vcBuffer.flip();         

         glUnmapBufferARB(GL_ARRAY_BUFFER_ARB);
      }

      glVertexPointer(3, GL_FLOAT, /* stride */(3 * 2) << 2, /* offset */0L << 2); // float at index 0
      glColorPointer(3, GL_FLOAT, /* stride */(3 * 2) << 2, /* offset */(3 * 1) << 2); // float at index 3

      glDrawArrays(GL_TRIANGLES, 0, 3 /* elements */);

      glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

      glDisableClientState(GL_COLOR_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);


      // cleanup VBO handles
      ib.put(0, vcHandle);
      glDeleteBuffersARB(ib);
   }

Done!
Finally we can render our triangle, using the 7 strategies mentioned above:


   static void render()
   {
      glClearColor(1, 1, 1, 1);

      // start drawing a triangle with the different strategies

      drawImmediateMode();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexArray();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexBufferObject();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexBufferObjectIndexed();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexBufferObjectInterleaved1();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexBufferObjectInterleaved2();

      glTranslatef(+0.05f, -0.05f, 0);
      drawVertexBufferObjectInterleavedMapped();
   }

To prevent the triangles to be rendered on top of eachother, a slight offset/translation is added after drawing each triangle.

Output:

Although I agree the output is far from entertaining, it should help you implementing your own geometry rendering strategy.

Great stuff, you could give courses with these notes. Succinct and symetrical in the way the strategies are laid out so only the differences can be seen.
Thanks for sharing

Thanks for posting this tutorial. Unlike far too many examples online for LWJGL, this tutorial includes all the syntax required to actually use these features.

Ofcourse, I needed it to work to create the screenshots :slight_smile:

Although it’s easy to figure out, you’re missing main():


    public static void main(String[] args) throws LWJGLException {
        System.out.println("Hello world from OpenGL!");

        initContext();
        renderLoop();
    }

Thanks. Added glMapBuffer(…) variant.

Shouldn’t the stride be 0 for all of these examples?

It could also be pretty intresting for me to see a “port” of this code to how one would do it with your “MappedObject” library…
:slight_smile:

@ra4king setting the stride to zero means you force the driver to make an attempt at calculating it. First of all, that only works in trivial use cases (as it would fail in some of these trivial examples). Second, if you don’t see how the stride should be calculated, how are you ever going to understand them? This is a tutorial, which should explain it properly.

Wait…stride is simply the offset between each color. If it is zero then that means the colors are specified in a row, or tightly packed. What is there to calculate? The stride should only be present for interleaved arrays.

You misunderstood strides. Strides define the number of bytes between offsets of data, not the bytes between data. A stride of zero doesn’t mean it’s tightly packed, it means that all data (for every vertex) is at exactly the same byte offset (same memory address), which makes no sense whatsoever - and therefore is used as a special value, to instruct the driver to calculate it itself.

vertexAttributeData[vertexIndex] = vertexIndex * stride + vertexAttributeOffset

You can see that a real zero-stride would create a series of zero-surface-area triangles (all vertex-attributes are read from the same memory address).

Riven, I’ve used 0 as stride for all my VBO’s and they render just fine…

You’re also saying the Arcsynthesis explanation is wrong too :S

Who cares what some other tutorials tells you. Read the specs. You can also choose to be blissfully unaware of what strides really mean, and use 0 forever, until it turns out you’ve indeed misunderstood them all that time.

The specs agree with me! 0 means tightly-packed! :point:

EDIT: More specifically:

You, my dear, need some spoon feeding.

Well I got it now…thanks for your help <3