Design Patterns and Video Games

OpenGL 2D Facade (3): Vertex Array Objects

Vertex Arrays Objects allow us to send large meshes to the GPU. It is a bit technical, but this is something we only have to do once.

This post is part of the OpenGL 2D Facade series

Mesh data

In the previous post, we created three Numpy arrays:

That's all we need to make our scene; all that follows presents the OpenGL API which creates the structures.

As I explained previously, I go straight to Vertex Array Objects (VAOs) because it is very efficient compared to drawing using OpenGL direct calls. It is more complicated, but we will "hide" this complexity behind a facade. This facade will have a simple API for 2D games while providing fast rendering.

Vertex Array Object

To embed all our data, we must first create a Vertex Array Object (VAO):

vaoId = glGenVertexArrays(1)

The argument of the glGenVertexArrays() is the number of VAO we want: in this example, only one.

We tell that we want to work on this new VAO:

glBindVertexArray(vaoId)

Remember that OpenGL is a state machine; every operation depends on the current context.

VAOs can contain one or more Vertex Buffer Object (VBO). In our example, we need three VBOs:

vboIds = glGenBuffers(3)
vertexVboId = vboIds[0]
colorVboId = vboIds[1]
indexVboId = vboIds[2]

The glGenBuffers() function with argument 3 returns an array with three VBO ids. I create three variables vertexVboId, colorVboId and indexVboId to store those ids.

Vertices VBO

Let's now define the VBO that contains all vertex coordinates:

glBindBuffer(GL_ARRAY_BUFFER, vertexVboId)

The glBindBuffer() function tells that the next VBO operations will be on vertexVboId. Furthermore, the first argument GL_ARRAY_BUFFERtells that this VBO contains vertex attributes. In this case, it includes the vertex coordinates, the most usual attribute.

The next lines send the data inside the Numpy array to the VBO:

vertices = np.ascontiguousarray(vertices.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(vertices), vertices, GL_STATIC_DRAW)

The first line builds a 1D array, so all values are contiguous, as in a buffer. There is a high chance that Numpy already stores data in this format, but we need to call these functions to be 100% sure of it.

The first argument of glBufferData() is consistent with the VBO format.

The second argument is the size of the data in bytes. Since float needs 4 bytes, the total size of the buffer is four times the number of values in the array.

The third argument is the Numpy array with all values.

The last argument is a hint for OpenGL. GL_STATIC_DRAW means that we don't plan to update the data. Then, the GPU can enable optimizations based on this assumption.

Finally, we set the attributes for this VBO:

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None)

The first argument defines an index for this VBO. A usual setup is to set the first VBO with index 0, the second one with index 1, and so on.

The second argument defines the number of values in each vertex. We are working with 2D vertices, so there are two coordinates per vertex.

The third argument defines the type of values. In our case, we use 32-bit float numbers.

The fourth argument tells if our values should be normalized. We don't need this feature in our example, so we set GL_FALSE.

The fifth argument defines the stride. The stride is the size of a "jump" between two vertices. A zero value means that data is contiguous.

The last argument is another advanced feature we don't need. A None value means that we don't need it.

Colors VBO

The Vertex Buffer Object is almost the same than the vertices one:

glBindBuffer(GL_ARRAY_BUFFER, colorVboId)
colors = np.ascontiguousarray(colors.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(colors), colors, GL_STATIC_DRAW)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)

Note the two first arguments of the glVertexAttribPointer(): the first one is 1 instead of 0, and the second one is 3 because there are 3 values per vertex (red, green and blue).

Faces VBO

The Vertex Buffer Object for faces is different from the two other ones:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVboId)
faces = np.ascontiguousarray(faces.flatten())
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * len(faces), faces, GL_STATIC_DRAW)

The type of this VBO is GL_ELEMENT_ARRAY_BUFFER. It tells OpenGL that it does not contain values but an index. You can only have one index per VAO, and OpenGL uses it for all other VBOs. In our case, OpenGL uses it for the vertex coordinates and the vertex colors. So, every time OpenGL needs the attributes of the ith vertex, it computes vertices[faces[i]] and colors[faces[i]].

Note that there is no call to glVertexAttribPointer() because you can only have one index buffer. Furthermore, the properties of this buffer are set during the drawing (see below).

Release the VAO

Once the VAO and all its VBOs are set, we must release them, using binding functions with a zero id:

glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)

Draw the content of the VAO

We only need to run the creation of VAO once (except if we want to change its content). Then, we can draw its content in the main loop using the following lines:

glBindVertexArray(vaoId)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glDrawElements(GL_QUADS, 4 * faceCount, GL_UNSIGNED_INT, None)
glDisableVertexAttribArray(0)
glDisableVertexAttribArray(1)
glBindVertexArray(0)

Line 1 tells that we want to work on our vaoId.

Lines 2-3 enable the two VBO with vertex coordinates and colors, telling OpenGL that we want to use them for drawing. The first argument in each case is the index we defined during the creating of those VBOs.

Line 4 is the drawing of all rectangles thanks to the glDrawElements() function.

The first argument GL_QUADS indicates that faces have four vertices (replace by GL_TRIANGLES if you want to render triangles).

The second argument is the total number of vertices to draw.

The third argument indicates that the index values are unsigned integers.

The last argument is an advanced options we don't need now, None means that we don't use it.

Delete the buffers

At the end of the program, after the main loop, we delete the VBOs and the VAO:

glDeleteBuffers(3, vboIds)
glDeleteVertexArrays(1, vaoId)

It is not mandatory when you leave the program. Anyway, it shows you how to do it if you need to delete/create buffers.

Final program

Here is the final program:

import os

import numpy as np
import pygame
from OpenGL.GL import glClear, GL_COLOR_BUFFER_BIT, GL_QUADS, glFlush, \
    glClearColor, glGenVertexArrays, glBindVertexArray, glGenBuffers, GL_ARRAY_BUFFER, glBindBuffer, glBufferData, \
    ctypes, GL_STATIC_DRAW, glVertexAttribPointer, GL_FLOAT, GL_FALSE, GL_ELEMENT_ARRAY_BUFFER, \
    glEnableVertexAttribArray, glDrawElements, GL_UNSIGNED_INT, glDisableVertexAttribArray, glDeleteBuffers, \
    glDeleteVertexArrays

os.environ['SDL_VIDEO_CENTERED'] = '1'
windowSize = (1280, 800)
pygame.display.set_mode(windowSize, pygame.DOUBLEBUF | pygame.OPENGL)
pygame.display.set_caption("OpenGL 2D Facade - https://www.patternsgameprog.com/")

glClearColor(0.0, 0.0, 0.0, 1.0)

# All components of our scene
vertices = np.array([
    [-0.5, -0.5],
    [-0.5, 0.5],
    [0.5, 0.5],
    [0.5, -0.5],
    [-0.3, -0.7],
    [-0.3, 0.3],
    [0.7, 0.3],
    [0.7, -0.7]
], dtype=np.float32)

colors = np.array([
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 1.0, 0.0],
    [0.0, 1.0, 0.0],
    [1.0, 0.0, 0.0],
    [1.0, 0.0, 0.0]
], dtype=np.float32)

faces = np.array([
    [0, 1, 2, 3],
    [4, 5, 6, 7]
], dtype=np.uint)
faceCount = faces.shape[0]

# Create one Vertex Array Object (VAO)
vaoId = glGenVertexArrays(1)
# We will be working on this VAO
glBindVertexArray(vaoId)

# Create three Vertex Buffer Objects (VBO)
vboIds = glGenBuffers(3)
# Commodity variables to memorize the id of each VBO
vertexVboId = vboIds[0]
colorVboId = vboIds[1]
indexVboId = vboIds[2]

# Copy vertices data to GPU
glBindBuffer(GL_ARRAY_BUFFER, vertexVboId)
vertices = np.ascontiguousarray(vertices.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(vertices), vertices, GL_STATIC_DRAW)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None)

# Copy colors data to GPU
glBindBuffer(GL_ARRAY_BUFFER, colorVboId)
colors = np.ascontiguousarray(colors.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(colors), colors, GL_STATIC_DRAW)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)

# Copy index data to GPU
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVboId)
faces = np.ascontiguousarray(faces.flatten())
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * len(faces), faces, GL_STATIC_DRAW)

# Release focus on VBO and VAO
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)

# Main loop
clock = pygame.time.Clock()
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
                break

    glClear(GL_COLOR_BUFFER_BIT)

    glBindVertexArray(vaoId)
    glEnableVertexAttribArray(0)
    glEnableVertexAttribArray(1)
    glDrawElements(GL_QUADS, 4 * faceCount, GL_UNSIGNED_INT, None)
    glDisableVertexAttribArray(0)
    glDisableVertexAttribArray(1)
    glBindVertexArray(0)

    glFlush()

    pygame.display.flip()
    clock.tick(60)

glDeleteBuffers(3, vboIds)
glDeleteVertexArrays(1, vaoId)

pygame.quit()
quit()

If you run this program, you get white rectangles:

OpenGL rendering with VAO but without shader

It is as expected, we need shaders to color vertices, I'll show that in the next post.