Using OpenGL in Python

This document describes how to get started using OpenGL from Python, particularly using the PyOpenGL 3.0.0 package from pyopengl.sourceforge.net.  This document assumes familiarity some familiarity with Python, OpenGL and Numpy (Numeric Python).

Installing the Package

The PyOpenGL package uses the "SetupTools" package, which is a new, common mechanism for distributing and installing Python packages.  It is quite likely that Python developers will already have setuptools installed on their system.  If you do not have it installed, download and run ez_setup.py.  If you want to install your Python egg files in a non-standard location be sure to setup your .pydistutils.cfg to support this before installing the packages.

Note that you may want to uninstall any PyOpenGL 2.x or OpenGL-3.0.0a4 installation before attempting to install. The 3.0.0a4 release used the undecorated name in a misguided attempt to make things "simpler".  The 3.0.0a5 and beyond packages use the same name as all previous PyOpenGL packages, "PyOpenGL".

Once you have setuptools installed (many Python developers already will have it), simply issue the command:

easy_install PyOpenGL

To have setuptools lookup the current version of the PyOpenGL package, download and install it.  If setuptools fails to install the package, you may need to update your setuptools.  Or you can try a direct download of the package with this command in the directory where you downloaded the package:

easy_install -f . PyOpenGL

If you do not have administrative permissions on your machine, you can create a .pydistutils.cfg file in your home directory to tell setuptools where to install new .egg files.

To update your install to the latest release of PyOpenGL:

easy_install -U PyOpenGL

Which will search for the latest registered version of the package and install that on your system.

If you use setuptools to package your application, you should declare a dependency on "PyOpenGL" to pull in the latest PyOpenGL 3.x release.

As of 3.0.0a3 PyOpenGL is dependant on the setuptools package.  You cannot run without the setuptools support, as it is used to provide the plugin mechanism used by array data-type plugin mechanism.  You will probably want to install numpy as well.  ctypes is a dependency for Python 2.4 and Python 2.3 but included with Python 2.5.

Bleeding Edge Development

If you are working in an area that's currently under active development you may prefer to use the CVS version of the PyOpenGL package.  OpenGL-ctypes (the code-name for the 3.0.0 version of PyOpenGL) is developed and maintained within the PyOpenGL CVS repository.  To check out the current version of OpenGL-ctypes:

cvs -z3 -d:pserver:anonymous@pyopengl.cvs.sourceforge.net:/cvsroot/pyopengl co -P OpenGL-ctypes

You can install the checkout to your path for further development as follows (from the OpenGL-ctypes checkout directory):

./setupegg.py develop --install-dir=~/YOUR-WORKING-DIRECTORY-ON-PYTHONPATH-HERE

When you make a change, run cvs diff on the OpenGL-ctypes directory to produce a patch file and send it to me as an attachment.  I prefer "context" diffs (cvs diff -c) for contributed code, as it makes it easier to see where the code fits in.  That said, I'm happy to get code in any readily integrated format.

Accessing OpenGL Functionality

The OpenGL library is a singleton instance for each process that is shared by all in-process code that uses OpenGL commands.  Normally you will want to use OpenGL from within a GUI framework, such as wxPython, PyGame, Tkinter, PyGTK or PyQt.  For stand-alone 3D programs, you may want to use the simple, but often quite effective,GLUT library, a wrapper for which is included in the PyOpenGL package.

Your GUI package will generally have a way to define an OpenGL "window" and set that window "active".  Once the window is active, PyOpenGL commands will render into the window (as will any commands in the process issued from another language, such as a C extension).  Normally this is handled by registering a handler for "events" from the GUI for such things are requests to display, resize, deal with mouse movements and the like.

To get access to basic OpenGL functionality, you should import the OpenGL.GL and OpenGL.GLU packages.  This is normally done with global imports [XXX show multi-version-install usage as well]:

from OpenGL.GL import *
from OpenGL.GLU import *

If you want to access functions in an extension module, you can load the extensions via a similar import:

from OpenGL.GL.ARB.shader_objects import *
from OpenGL.GL.ARB.fragment_shader import *
from OpenGL.GL.ARB.vertex_shader import *

It's a good idea to use the initialisation functions in the extension modules to check that the extension is available on the current machine before using any of the functions in the extension (note: calling the init functions was a requirement in PyOpenGL 2.x).  For example:

if not glInitShaderObjectsARB():
raise RuntimeError(
"""ARB Shader Objects extension is required """
"""but not supported on this machine!"""
)

or more reasonably, to trigger the use of fallback code for the missing functionality from the extension.

API Changes from C-level OpenGL

Generally speaking, PyOpenGL 3.x tries to be compatible with the PyOpenGL 2.x series, which tried to provide a "Pythonic" interface to OpenGL.  This includes providing support for implying various arguments, such as the dimensions of arrays from array's declaration of their dimensions.  The online pydoc documentationprovides the most accurate (because it is auto-generated), though somewhat confusing, reference to the API as provided by PyOpenGL.  We will be revising that documentation to be more friendly as work progresses.

If you are updating from PyOpenGL 2.x, see the Upgrading section below.

Error Handling

As with previous versions of PyOpenGL, PyOpenGL 3.x tries to follow Python's

Errors should never pass silently.

philosophy, rather than OpenGL's philosophy of always requiring explicit checks for error conditions.  PyOpenGL functions run the function OpenGL.error.glCheckError after each function call.  This function is glBegin/glEnd aware, that is, the glBegin and glEnd functions enable and disable the checking of errors (because error checking doesn't work between those calls).

If you call C-level glBegin/glEnd the error checking may become confused.  You should explicitly disable error checking (described below) in such a case.

You can override the error-handler, either to provide your own custom functionality, or to disable checking entirely.  For instance, if you will always have a valid context, you could register the raw glGetError function as the error checker to avoid the overhead of the context-validity checks:

from OpenGL import error
error.ErrorChecker.registerChecker( myAlternateFunction )

OpenGL-ctypes has a set of errors defined in the OpenGL.error module.  It can also raise standard Python exceptions, such as ValueError or TypeError.  Finally, it can raise ctypes errors when argument conversion fails. (XXX that's sub-optimal, it has implementation details poking out to user code).

Wrapper objects catch OpenGL errors and annotate the error with extra information to make it easier to debug failures during the wrapping process.

Tkinter (Legacy GUI) Togl Support

We have included the Python wrapper for the Tk Togl widget in the OpenGL-ctypes package.  We do not, however, currently include the Togl widget itself.  If you would like to use Togl in your package, please use your system's package manager to install the Togl package (or compile from source).  You may have to recompile Python with Tk support as well.

Performance Tips for Python + OpenGL

Python is (currently) a fairly slow language due to the incredible generality of it's execution model.  This means that certain approaches to coding OpenGL that would work well in a language such a C or C++ will be painfully slow in Python.  The key idea to remember is that, as much as possible, you want to push the work of iteration and repetition into the OpenGL implementation (which is implemented in C and/or Hardware).

There are two major approaches taken to accomplishing this:

Array based geometry uses OpenGL 1.1 features (supported almost everywhere) that allow for passing large arrays of data to be processed with a single call.  Using numpy (or Numeric) arrays, you can readily pass your data into those functions without any need for Python-level iteration.  This is the more flexible of the two approaches, as it allows for readily mutating the data being rendered without heavy recompilation costs and allows for special effects such as translucency.  For static geometry, however, the display-list approach may be faster.

Display-list geometry normally uses slower individual-element rendering functions to render a piece of geometry in a special OpenGL mode which records the commands used to do the rendering.  In future rendering passes the geometry can be rendered with a single call.  You can create trees of display lists (where a root display list calls dozens of other display-lists) or call multiple display lists at once from an array.

Avoiding Array-data Copying

One of the biggest slowdowns you will see in array-based PyOpenGL code is where you are passing:

In these cases PyOpenGL implementation will cause an extra copy of the data-set every time it is passed into any function.  That copying, though done at the C level, can cause a signficant unnecessary overhead.

The copying is done by default to prevent new OpenGL coders from being confronted by mystifying errors about differences between "floats" and "double floats".  It also makes PyOpenGL 3.x compatible with the PyOpenGL 2.x series.

PyOpenGL 3.0.0 provides a flag that allows you to raise Errors in situations where numpy data-arrays are being copied.  You can use these error messages to optimise your application data-paths to eliminate the copying.To enable the checking:

from OpenGL.arrays import numpymodule
numpymodule.NumpyHandler.ERROR_ON_COPY = True

This will raise errors of the type:

OpenGL.error.CopyError

With a description of the condition and why the copy was to be done.

Context-specific Data

Because of the way OpenGL and ctypes handle, for instance, pointers, to array data, it is often necessary to ensure that a Python data-structure is retained (i.e. not garbage collected).  This is done by storing the data in an array of data-values that are indexed by a context-specific key.  The functions to provide this functionality are provided by the OpenGL.contextdata module.

The key that is used to index the storage array is provided by the platform module's GetCurrentContext() function.  The current context is used if the context argument is passed in as None.

If you are creating and destroying rendering contexts, you need to be aware of the Context-specific storage.  If you do not explicitly clean up the storage, you will produce a memory leak of all of the objects passed in as array data-sources and the like.  You can either explicitly set the values to None (the standard OpenGL approach), or use code like the following:

from OpenGL import contextdata
def cleanupCallback( context=None ):
"""Create a cleanup callback to clear context-specific storage for the current context"""
def callback( context = contextdata.getContext( context ) ):
"""Clean up the context, assumes that the context will *not* render again!"""
contextdata.cleanupContext( context )
return callback

registering the callback to be called after the context is destroyed.

It is very important that you call the cleanup operation after the context is destroyed, as any attempt to render the context after the cleanup call will almost certainly result in memory-access errors and potentially core dumps!

Get Involved

PyOpenGL is an Open Source project, and PyOpenGL 3.x is written in Python to allow Python coders to enhance the package.  If you see something that's wrong, or something that you'd like to fix, let people know, or fix it and send a patch.  We have documentation for those who are interested in contributing that should help you get started hacking on the code-base.

Upgrading

PyOpenGL 3.x tries to be API-compatible with PyOpenGL 2.x as much as possible, but there are some differences that need to be considered when porting code: