We get the best performance when sending mesh data to the GPU in a single operation. In this post, I present the numpy library that can create such data.

*This post is part of the OpenGL 2D Facade series*

## Vertices and Python lists

In the previous post, we created rectangles thanks to the enumeration of 2D vertices:

glVertex2f(-0.5, -0.5) glVertex2f(-0.5, 0.5) glVertex2f(0.5, 0.5) glVertex2f(0.5, -0.5)

We could store all these vertices in a Python list of lists:

vertices = [ [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5] ]

Each item of the `vertices`

list is a list with two float numbers. We can read each one using brackets `[]`

. For instance, we can print all of them in the following way:

for i in range(4): print(vertices[i])

It leads to these messages in the console:

[-0.5, -0.5] [-0.5, 0.5] [0.5, 0.5] [0.5, -0.5]

To read one float value in each item of the `vertices`

list, we must use brackets a second time. For instance, we can print float values in the following way:

for i in range(4): print("x =", vertices[i][0], "y =", vertices[i][1])

We get this display:

x = -0.5 y = -0.5 x = -0.5 y = 0.5 x = 0.5 y = 0.5 x = 0.5 y = -0.5

## Numpy arrays

Unfortunately, we can’t send Python list to a GPU, but we can with Numpy arrays.

First import Numpy:

import numpy as np

Note that I use an alias `np`

. It means that, instead of writing `numpy.func()`

we only need to write `np.func()`

.

We can create Numpy arrays from Python list of lists:

vertices = np.array([ [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5] ], dtype=np.float32)

The second argument `dtype=np.float32`

tells Numpy that we want IEEE 754 32-bit float numbers, the one expected by OpenGL. There is also another option `order`

that defines the order of values: row-major or column-major. The default value is row-major, the one excepted by OpenGL.

We can access values in a Numpy array with only of pair of brackets. For a 2D-array, the syntax is `array[i, j]`

and is equivalent to `list[i][j]`

with Python lists. We can get the same display as in the previous example with this syntax:

for i in range(4): print("x =", vertices[i, 0], "y =", vertices[i, 1])

All Numpy arrays know their size. At any time, you can ask for it with the `shape`

attribute:

print(vertices.shape)

It displays (4, 2).

If you only need the size of one dimension, use the brackets:

vertexCount = vertices.shape[0] vertexDim = vertices.shape[1]

In our example, `vertexCount`

equals 4, and `vertexDim`

equals 2.

Numpy arrays have many features that allow complex computations. I’ll show examples throughout this series.

## Vertex indexing

Meshes are made of faces; in our case, since we focus on tileset-based 2D rendering, these faces are rectangles (or quads). Each face has four vertices, like the ones in the Numpy array we created in the previous section.

A first approach to create all these meshes is to define four vertices for each face. For instance, for two faces/quads/rectangles:

vertices = np.array([ [-0.5, -0.5], # Face 0 vertex 0 [-0.5, 0.5], # Face 0 vertex 1 [0.5, 0.5], # Face 0 vertex 2 [0.5, -0.5], # Face 0 vertex 3 [-0.3, -0.7], # Face 1 vertex 0 [-0.3, 0.3], # Face 1 vertex 1 [0.7, 0.3], # Face 1 vertex 2 [0.7, -0.7] # Face 1 vertex 3 ], dtype=np.float32)

It is fine as long as there is no shared vertex between faces. In the opposite case, we lose memory, and more importantly, some processing is more complicated to run.

For instance, if we want to create a grid of tiles:

You can see that there are three types of vertices:

- Vertices that belong to a single face (0, 2, 6, 8);
- Vertices that belong to two faces (1, 3, 5, 7);
- Vertices that belong to four faces (4);

With vertex indexing, we can describe such shares. It requires two arrays: the first one with vertex coordinates, and the second one with vertex indices.

Let’s take the two previous rectangles to create such an index. There is no shared vertex, so I hope it is easier to understand.

The vertex array with coordinates in the same, and the corresponding faces/vertex indices is the following one:

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

The first four indices (0, 1, 2, 3) point to the first four vertices. The last four indices (4, 5, 6, 7) point to the last four vertices.

## Render from vertex index

We can render the rectangles using the `vertices`

coordinates and the `faces`

vertex indices:

glBegin(GL_QUADS) for faceIndex in range(faces.shape[0]): for vertexIndex in faces[faceIndex]: color = colors[vertexIndex] glColor3f(color[0], color[1], color[2]) vertex = vertices[vertexIndex] glVertex2f(vertex[0], vertex[1]) glEnd()

Lines 1 and 8 start then end the rendering of rectangles.

Line 2 iterates through the faces. Since we have two faces in our example, `faceIndex`

gets value 0 then 1.

Line 3 iterates through vertex indices in the current face. During the first iteration, when `faceIndex`

equals 0, vertexIndex gets values 0, 1, 2, and then 3. During the second iteration, when `faceIndex`

equals 1, vertexIndex gets values 4, 5, 6, and then 7.

Lines 4-5 set the color of the next vertex. I created another Numpy array `colors`

with (red, green, blue) values for each vertex.

Lines 6-7 render a vertex. Note that could also write:

glVertex2f(vertices[vertexIndex, 0], vertices[vertexIndex, 1])

## Final program

This program renders the same rectangle as before but uses vertex indices. Note that you can play with the content of the Numpy arrays to change the rendering:

import os import pygame import numpy as np from OpenGL.GL import glClear, GL_COLOR_BUFFER_BIT, glBegin, GL_QUADS, glEnd, glColor3f, glVertex2f, glFlush, \ glClearColor 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] # 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) glBegin(GL_QUADS) for faceIndex in range(faceCount): for vertexIndex in faces[faceIndex]: color = colors[vertexIndex] glColor3f(color[0], color[1], color[2]) vertex = vertices[vertexIndex] glVertex2f(vertex[0], vertex[1]) glEnd() glFlush() pygame.display.flip() clock.tick(60) pygame.quit() quit()

In the next post, I’ll show how to send this data into OpenGL structures called “Vertex Array Objects”.