This is part 1 of a series and it is about getting started with visualizing triangle meshes with Python 2.7 using the libraries PyOpenGL and PyQt41.
I will assume you know python, you will not need a lot of Qt or OpenGL experience, though I will also not go into the deeper details of how OpenGL works. For that I refer you to official documentation and the excellent (C++) tutorials at https://open.gl/. Although they are C++, there is a lot of explanation about OpenGL and why to do certain calls in a certain order.
On a final note: I will make generalizations and simplifications when explaining things. If you think something works different then I say it probably does, this is to try and convey ideas to beginners, not to explain low level openGL implementations.
Part 1: Drawing a mesh using buffers.
1.1. Setting up
Download & run Python with default settings:
Download & run PyQt4 with default settings:
Paste the following in a windows command window (windows key + R -> type “cmd.exe” -> hit enter):
C:/Python27/Scripts/pip install setuptools
C:/Python27/Scripts/pip install PyOpenGL
1.2. Creating an OpenGL enabled window in Qt.
The first thing to know about OpenGL is that any operation requires OpenGL to be initialized. OpenGL is not something you just “import”, it has to be attached to a (possibly hidden) window. This means that any file loading or global initialization has to be postponed until OpenGL is available.
The second thing to know about OpenGL is that it is a big state machine. Any setting you change is left until you manually set it back. This means in Python we may want to create some contexts (using contextlib) to manage the safe setting and unsetting of certain states. I will however not go this far.
Similar to this Qt also requires prior initialization. So here’s some relevant code:
# import the necessary modules from PyQt4.QtCore import * # QTimer from PyQt4.QtGui import * # QApplication from PyQt4.QtOpenGL import * # QGLWidget from OpenGL.GL import * # OpenGL functionality from OpenGL.GL import shaders # Utilities to compile shaders, we may not actually use this # this is the basic window class OpenGLView(QGLWidget): def initializeGL(self): # here openGL is initialized and we can do our real program initialization pass def resizeGL(self, width, height): # openGL remembers how many pixels it should draw, # so every resize we have to tell it what the new window size is it is supposed # to be drawing for pass def paintGL(self): # here we can start drawing, on show and on resize the window will redraw # automatically pass # this initializes Qt app = QApplication() # this creates the openGL window, but it isn't initialized yet window = OpenGLView() # this only schedules the window to be shown on the next Qt update window.show() # this starts the Qt main update loop, it avoids python from continuing beyond this # line and any Qt stuff we did above is now going to actually get executed, along with # any future events like mouse clicks and window resizes app.exec_()
Running this should get you a black window that is OpenGL enabled. So let’s fill in the view class to draw something in real-time. This will show you how to make your window update at 60-fps-ish, how to set a background color and how to handle resizes.
class OpenGLView(QGLWidget): def initializeGL(self): # set the RGBA values of the background glClearColor(0.1, 0.2, 0.3, 1.0) # set a timer to redraw every 1/60th of a second self.__timer = QTimer() self.__timer.timeout.connect(self.repaint) # make it repaint when triggered self.__timer.start(1000 / 60) # make it trigger every 1000/60 milliseconds def resizeGL(self, width, height): # this tells openGL how many pixels it should be drawing into glViewport(0, 0, width, height) def paintGL(self): # empty the screen, setting only the background color # the depth_buffer_bit also clears the Z-buffer, which is used to make sure # objects that are behind other objects actually are not shown drawing # a faraway object later than a nearby object naively implies that it will # just fill in the pixels with itself, but if there is already an object there # the depth buffer will handle checking if it is closer or not automatically glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # the openGL window has coordinates from (-1,-1) to (1,1), so this fills in # the top right corner with a rectangle. The default color is white. glRecti(0, 0, 1, 1)
Note that the QTimer forces the screen to redraw, but because we are not animating any data this will not be visible right now.
1.3. Creating a Vertex Array Object (VAO)
In OpenGL there is a lot we can put into a mesh, not only positions of vertices, but also triangulation patterns, vertex colors, texture coordinates, normals etcetera. Because OpenGL is a state machine (as described at the start of 2.) this means that when drawing 2 different models a lot of settings need to be swapped before we can draw it. This is why the VAO was created, as it is a way to group settings together and be able to draw a mesh (once set up properly) in only 2 calls. It is not less code, but it allows us to move more code to the initialization stage, winning performance and reducing risk of errors resulting in easier debugging.
Our mesh however will not be very complicated. We require 2 sets of data, the vertex positions and the triangulation (3 integers per triangle pointing to what vertex to use for this triangle).
As you can see this would result in the following data:
Positions = [0, 0, 1, 0, 0, 1, 1, 1]
Elements = [0, 1, 2, 1, 3, 2]
4 2D vertices and 2 triangles made of 3 indices each.
So let’s give this data to a VAO at the bottom of initializeGL.
# generate a model # set up the data positions = (0, 0, 1, 0, 0, 1, 1, 1) elements = (0, 1, 2, 1, 3, 2) # apply the data # generate a vertex array object so we can easily draw the resulting mesh later self.__vao = glGenVertexArrays(1) # enable the vertex array before doing anything else, so anything we do is captured in the VAO context glBindVertexArray(self.__vao) # generate 2 buffers, 1 for positions, 1 for elements. this is memory on the GPU that our model will be saved in. bufs = glGenBuffers(2) # set the first buffer for the main vertex data, that GL_ARRAY_BUFFER indicates that use case glBindBuffer(GL_ARRAY_BUFFER, bufs) # upload the position data to the GPU # some info about the arguments: # GL_ARRAY_BUFFER: this is the buffer we are uploading into, that is why we first had to bind the created buffer, else we'd be uploading to nothing # sizeof(ctypes.c_float) * len(positions): openGL wants our data as raw C pointer, and for that it needs to know the size in bytes. # the ctypes module helps us figure out the size in bytes of a single number, then we just multiply that by the array length # (ctypes.c_float * len(positions))(*positions): this is a way to convert a python list or tuple to a ctypes array of the right data type # internally this makes that data the right binary format # GL_STATIC_DRAW: in OpenGL you can specify what you will be doing with this buffer, static means draw it a lot but never access or alter the data once uploaded. # I suggest changing this only when hitting performance issues at a time you are doing way more complicated things. In general usage static is the fastest. glBufferData(GL_ARRAY_BUFFER, sizeof(ctypes.c_float) * len(positions), (ctypes.c_float * len(positions))(*positions), GL_STATIC_DRAW) # set the second buffer for the triangulation data, GL_ELEMENT_ARRAY_BUFFER indicates the use here glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufs) # upload the triangulation data glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(ctypes.c_uint) * len(elements), (ctypes.c_uint * len(elements))(*elements), GL_STATIC_DRAW) # because the data is now on the GPU, our python positions & elements can be safely garbage collected hereafter # turn on the position attribute so OpenGL starts using our array buffer to read vertex positions from glEnableVertexAttribArray(0) # set the dimensions of the position attribute, so it consumes 2 floats at a time (default is 4) glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None)
So that was quite some code, and it is quite simple because we only have positions to deal with right now. But first let’s try to draw it!
Replace the glRecti call with:
# enable the vertex array we initialized, it will bind the right buffers in the background again glBindVertexArray(self.__vao) # draw triangles based on the active GL_ELEMENT_ARRAY_BUFFER # that 6 is the element count, we can save the len(elements) in initializeGL in the future # that None is because openGL allows us to supply an offset for what element to start drawing at # (we could only draw the second triangle by offsetting by 3 indices for example) # problem is that the data type for this must be None or ctypes.c_void_p. # In many C++ example you will see just "0" being passed in # but in PyOpenGL this doesn't work and will result in nothing being drawn. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)
Now we should have an identical picture. Some more info about glVertexAttribPointer:
In OpenGL we can upload as many buffers as we want, but for now I’ll stick with the 2 we have. This means that if we want to (for example) add colors to our mesh, we have to set up multiple attrib pointers, that both point to different parts of the buffer. I like to keep all my vertex data concatenated, so that we could get (x,y,r,g,b,x,y,r,g,b…) etcetera in our buffer.
Now for OpenGL to render it not only wants to know what buffer to look at (the array_buffer), but it also wants to know how to interpret that data, and what data is provided. OpenGL understand this through attribute locations. Here we activate attribute location 0 (with glEnableVertexAttribArray) and then set our buffer to be 2 floats per vertex at attribute location 0.
The default openGL attribute locations are as follows:
To support multiple attributes in a single buffer we have to use the last 2 arguments of glVertexAttribPointer. The first of those is the size of all data per vertex, so imagine a 2D position and an RGB color that would be 5 * sizeof(float). The second of those is where this attribute location starts. Here’s an example to set up position and color:
vertex_data = (0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1) ## vertex_element_size = 5 ## elements = (0, 1, 2, 1, 3, 2) self.__vao = glGenVertexArrays(1) glBindVertexArray(self.__vao) bufs = glGenBuffers(2) glBindBuffer(GL_ARRAY_BUFFER, bufs) glBufferData(GL_ARRAY_BUFFER, sizeof(ctypes.c_float) * len(vertex_data), (ctypes.c_float * len(vertex_data))(*vertex_data), GL_STATIC_DRAW) ## glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufs) glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(ctypes.c_uint) * len(elements), (ctypes.c_uint * len(elements))(*elements), GL_STATIC_DRAW) glEnableVertexAttribArray(3) ## glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(ctypes.c_float) * vertex_element_size, ctypes.c_void_p(2 * sizeof(ctypes.c_float))) ## glEnableVertexAttribArray(0) glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ctypes.c_float) * vertex_element_size, None) ##
This is an update to initializeGL, new / changed code ends in ## (because I don’t know how to override the syntax highlighting), and your rectangle will immediately start showing colors2!
One last thing. add this to the top of paintGL:
import time glLoadIdentity() glScalef(self.height() / float(self.width()), 1.0, 1.0) glRotate((time.time() % 36.0) * 10, 0, 0, 1)
The first line (after the import) restores the transform state, the second line corrects aspect ratio (so a square is really square now), the last line rotates over time. We are using a startup time because Python’s time is a too large value, by subtracting the application start time from it we get a value OpenGL can actually work with.
That’s it for part 1!
1PyQt5, PySide, PySide2 are (apart from some class renames) also compatible with this.
2The color attribute binding only works on NVidia, there is no official default attribute location and most other drivers will ignore glVertexAttribPointer (or do something random) if you do not use a custom shader. So if you’re not seeing colors, don’t worry and try diving into shaders later!