Making Sense of Modern OpenGL

So recently I’ve been dabbling around with “Modern” OpenGL – you know, where you don’t directly interact with the so-called “fixed pipeline” which is the old style (and apparently has been for a while). Instead, you have complete access to a vast majority of the pipeline via “Shaders”, of which there are many types (Vertex, Fragment, Geometry).

The main thing that took me a while to wrap my head around were the concepts of Vertex Buffers Objects, Vertex Array Objects, Vertex Attributes, etc. They are certainly a mouthful, and I imagine that I’m not the only person who had to spend some time sorting through the official OpenGL specs to figure out exactly what these are.

A very simplified view of the OpenGL pipeline.

The first thing to know about these mechanisms is that, in the grand scheme of things (aka the pipeline, represented very simply above), they all take place before the Vertex Shader; specifically, we use them to control how the Vertex Stream interacts with the Vertex Shader.

Vertex Stream

At the mouth of the pipeline is the Vertex Stream. Here we dump in all of the vertices and associated attributes of our models, our world, our hearts and our souls. Think of it like a relational database, where the attributes (columns) are the vertex attributes, and each tuple (row) is a single vertex:

Vertex Vertex Position Vertex Normal Vertex Color
0 -1.0f -1.0f -1.0f -0.58f -0.58f -0.58f 0.5f 0.5f 0.5f
1 -1.0f -1.0f 1.0f -0.58f -0.58f 0.58f 0.0f 0.0f 1.0f
2 -1.0f 1.0f -1.0f -0.58f 0.58f -0.58f 0.0f 1.0f 0.0f
3 -1.0f 1.0f 1.0f -0.58f 0.58f 0.58f 0.0f 1.0f 1.0f
4 1.0f -1.0f -1.0f 0.58f -0.58f -0.58f 1.0f 0.0f 0.0f
5 1.0f -1.0f 1.0f 0.58f -0.58f 0.58f 1.0f 0.0f 1.0f
6 1.0f 1.0f -1.0f 0.58f 0.58f -0.58f 1.0f 1.0f 0.0f
7 1.0f 1.0f 1.0f 0.58f 0.58f 0.58f 1.0f 1.0f 1.0f

This is purely a logical view of the data; when we get right down to it, however, we need to know a thing or two about how the vertices are actually stored before we can use them

Vertex Attributes

You might be asking, what constitutes a Vertex Attribute? A Vertex Attribute is simply a property that we define at the same position as the vertex is defined to be at – examples include (but are not limited to):

  • Normal & Tangent Vectors, used for lighting calculations
  • Texture Coordinates, for setting color by texture lookup
  • Color, for explicitly setting the color of a primitive

Vertex Storage

In the past, you could upload vertices to OpenGL as you needed them – however, this is the primitive ‘fixed’ pipeline that we avoid today. An upgrade to the method was to upload a batch of vertices at a time when you needed them, but this is still more primitive than what we want. Today, we have Vertex Buffer Objects, which is really a fancy way of saying that we get a block of memory on the graphics host to store the vertex data, and when we need it, we just make a reference to the VBO instead of spending time uploading data to the host.

A VBO is acquired with the glGenBuffers command, and is destroyed with the glDeleteBuffers command. To interact with a buffer, we first have to bind it using glBindBuffer. To upload data to a VBO, we first bind it to the GL_ARRAY_BUFFER target, and then use the glBufferData or glBufferSubData commands to populate it with data.

VBO per Attribute Layout

There are many ways to store vertex data (and associated attributes) inside of a vertex buffer; the first, and simplest, is to store each attribute of the vertex in a different VBO. However, you will rarely (if ever) see this in the wild. Every time a vertex is submitted to the vertex shader, its attributes have to be looked up from many VBOs; it’s much more preferential to pack all of the attributes into a single VBO. There are two ways to do this – interleaved attributes and serialized order. In practice, the interleaved ordering seems to be the most preferable.

Now that we know where our vertices and associated data are, we can pipe them into the Vertex Shader.

Vertex Shader

Each vertex “record” starts its journey at the Vertex Shader – where we can apply transformations to the vertices (generally, this will be the Model-View-Projection matrix), and optionally, affect the vertex attributes in some way before passing them down the pipeline. Here’s an example of a vertex shader:

#version 330 core

uniform mat4 transform;

in vec3 vCoord;
in vec3 vColor;
in vec3 vNorm;

// out variables

void main(void) {
    gl_Position = transform * vec4(vCoord, 1.0);
    // Here we might change other variables
}

In order to bind the arguments for the Vertex Shader to certain Vertex Attributes in the Vertex Stream, we’ll need to know how to address each attribute in the shader program; this is done through an unsigned integer retrieved using the glGetAttribLocation function. This function takes, as parameters, a shader program and an attribute name, and returns the index of the attribute in the shader.

So, now that we know what Vertex Attributes are, where they’re used, and where they’re stored, our next question should be how we go about assigning portions of the VBO to act as a Vertex Attribute for a specific parameter of the Vertex Shader.

Attribute Mapping

In order to assign values to an attribute in the Vertex Shader, we’ll need to know a few things (which we have discussed above):

  • The attribute ID (via glGetAttribLocation)
  • The source VBO ID
  • How the Vertex Attribute is laid out in the VBO
    • How many components comprise the attribute (3 floats for a Vec3, 4 floats for RGBA, etc)
    • How many bytes there are between the start of the nth record and the (n+1)th record
    • The offset (in bytes) from the start of the buffer to the first record

We’ll need to keep track of this information for every vertex attribute that is currently active – or rather, we would, if we didn’t have access to Vertex Array Objects. A VAO is simply a data structure that stores this information, and when we want to render an object (a process in which all of this information is involved), we can simply invoke the VAO instead of repeatedly making calls to glVertexAttribPointer.

VAOs are created using the glGenVertexArrays function and are destroyed with the glDeleteVertexArrays function. We enable a specific Vertex Attribute using the glEnableVertexAttribArray function, and we define the mapping that we defined above using the glVertexAttribPointer function. It is also possible to not enable a specific Vertex Attribute and instead supply a default value instead using the glVertexAttrib function.

Summary

  • A Vertex Stream is a list of vertices (composed of at least a position and a variable number of Vertex Attributes, such as color, normal, texture coordinate, etc).
  • When using the stream, we need to store it in a server-side location of memory, referred to as a Vertex Buffer Object. We can layout the Vertex Attributes in the memory however we want, though the interleaved format is particularly effective.
  • When we want to pipe the stream to the Vertex Shader, we need to know the IDs of the attributes for the currently active shader.
  • We need to know from what VBO to pull the attributes from and how they’re laid out in the VBO; we can use a Vertex Array Object to keep track of this state for us.
Advertisement

1 thought on “Making Sense of Modern OpenGL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s