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], "y =", vertices[i])
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
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
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
It displays (4, 2).
If you only need the size of one dimension, use the brackets:
vertexCount = vertices.shape vertexDim = vertices.shape
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.
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): for vertexIndex in faces[faceIndex]: color = colors[vertexIndex] glColor3f(color, color, color) vertex = vertices[vertexIndex] glVertex2f(vertex, vertex) 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])
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 # 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, color, color) vertex = vertices[vertexIndex] glVertex2f(vertex, vertex) 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”.