Python, Rabbyt and GTK GLExt

It’s been a while since I wrote something actually useful to anybody here. I’ve decided to take down my personal posts, they are of no interest to anyone except me. Life sometimes sucks for me big time, but I’m moving along… So. Enough of this crap.

Recently, due to almost completing my work project I had more spare time to shift focus onto game programming once again. Since it’s always been my dream to create a game I’d love to play, at the beginning of this year I decided to give it a go and try writing game engine and then based upon it, develop a 4X game called Galaxy Lords.

So for the last few days in my spare time I moved back to work on the SpaceX Engine. I have designed brief structure for the engine, much thinking and reading bits of information regarding game engines spread around the net. Then I moved to diagramming in UML parts of the engine. Since I had another work projects for the last few months, I have gained few more experience points in regard to Python programming. Two days ago I have designed engine scene graph components. The structure of rendered scene and much inclusions of the object hierarchy gave a clear view that I would need few editors, especially for textures, sprites, animations among other things. Since the internal scenegraph is hierarchical, it would be an excellent choice for a descriptive file format if I went with XML. Of course I could edit those things by hand, but honestly for animations and effects it would be too much wasting time. Hand editing of XML animation description files would be simply too much overhead, except for very simple things. So I have decided to write a full-blown editor for SpaceX engine.

Since I’m using Python 2.5, 64bit Linux and the engine relies heavily on OpenGL via Rabbyt library I evaluated few possibilities for OpenGL canvas widgets from different GUI toolkits available. I had few things in mind. The toolkit must be multiplatform, must be relatively stable, be open and free of charge and have widgets for OpenGL context. After evaluating few possibilities like wxPython, PyQT4, Fox Library I ended up with GTK2.

However GTK2 does not have an OpenGL canvas element out of the box. It used to have one called GLArea, but it’s deprecated for a long time now. I needed a widget that could be easily integrated with my existing SpaceX code to leverage it’s power, not needing to rewrite things especially for canvas widget. After all the widget should have OpenGL context and nothing else.

Rabbyt by default has support for PyGame and PyGlet libraries. They are fine when you are developing a fullscreen app like game, but for editor you ultimately need a very different thing. PyGame and PyGlet allows to create only window that has OpenGL context and they don’t allow embedding this window as widget in some kind of GUI toolkit. Last but not least, even if it was possible most editors need multiple OpenGL contexts to render the object from different perspectives.

So I’ve came across GTK GLExt which stands for GTK GL Extension. This extension allows you to create a canvas object with OpenGL context. I has also a nice Python wrapper. So I’ve began investigating possibility for Rabbyt compatibility with this extension. Suprisingly, browsing Rabbyt’s code it does not rely on PyGame or PyGlet for anything except texture loading. It’s almost pure OpenGL. So I’ve began examining the examples for PyGtkGlExt. After few minutes fiddling with it I had success with displaying Rabbyt through GTK! Actually yes, it’s pretty simple and very obvious.

I’ll try to explain how I did it.

PyGTKGLExt can be initialized in two different ways. I’m not sure if it matters or simply the second function is an alias to the first. You start by importing necessary things and setting all up.

import pygtk
pygtk.require('2.0')
import gtk
import gtk.gtkgl
import rabbyt
size = (200, 200) # Size in pixels

Additionally you can import OpenGL. Then you have to create a GL context configuration. This can be done like this:

try:
    glconfig = gtk.gdkgl.Config(mode = (gtk.gdkgl.MODE_RGB | gtk.gdkgl.MODE_DOUBLE | gtk.gdkgl.MODE_DEPTH))
except gtk.gdkgl.NoMatches:
    glconfig = gtk.gdkgl.Config(mode = (gtk.gdkgl.MODE_RGB | gtk.gdkgl.MODE_DEPTH))

If gtk.gdkgl.NoMatches error is raised it means that requested mode is not available, so we try without double buffering. For more information on the supported modes refer to GTK documentation. This is the GL context configuration which is then passed to widget.

After you have your imports you have to create a widget. It can be done like this:

glarea = gtk.gtkgl.DrawingArea(glconfig)
glarea.set_size_request(size[0], size[1])

Or like this:

glarea = gtk.DrawingArea()
glarea.set_size_request(size[0], size[1])
gtk.gtkgl.widget_set_gl_capability(glarea, glconfig)

This would create widget and initialize its OpenGL context capabilities. Ok. So we have our widget initialized. What’s next? Well we actually need to connect some events (or signals using PyQT terminology) to our newly initialized widgets. There are four events we are mostly interested in. They are as follows:

  • realize – used for widget initialization
  • configure_event – used when widget is resized
  • expose_event – used for actual drawing
  • map_event – used for mapping (widget is idle)

Ok. So now we know what those events are for. So what we do now? Actually we connect these events to functions. This is how it’s done:

glarea.connect_after('realize', init)
glarea.connect('configure_event', reshape)
glarea.connect('expose_event', draw)
glarea.connect('map_event', map)

After we did that we need to write actual functions. This is where our rabbyt integration occurs. Let’s start with init function.

def init(glarea):
    # Get OpenGL context
    glcontext = gtk.gtkgl.widget_get_gl_context(glarea)
    # Get actual OpenGL surface
    gldrawable = gtk.gtkgl.widget_get_gl_drawable(glarea)
    
    if not gldrawable.gl_begin(glcontext): return
    
    # Actual OpenGL initialization code goes here
    
    gldrawable.gl_end()
    
    return True

So this is our new init function. Actually it’s not used here at all, but can be used for setting some specific OpenGL attributes. In fact each of our functions will use the construct such as above. gldrawable.gl_begin(glcontext) allows to connect widget surface to OpenGL context. If you have two different OpenGL widgets I think it’s not a problem, because context switching will occur, but I’m not really sure if you wouldn’t need additional OpenGL context locking/realising mechanisms. I’m not also sure if it would be thread safe. But it’s up to you to find out.

Let’s get back to our functions. We create a reshape function. This is how it should look like:

def reshape(glarea, event):
    # Get OpenGL context
    glcontext = gtk.gtkgl.widget_get_gl_context(glarea)
    # Get actual OpenGL surface
    gldrawable = gtk.gtkgl.widget_get_gl_drawable(glarea)
    
    if not gldrawable.gl_begin(glcontext): return
    
    # Get viewport size
    x, y, width, height = glarea.get_allocation()
    
    # This is where Rabbyt initialization goes
    rabbyt.set_viewport((width, height))
    rabbyt.set_default_attributes()
    
    gldrawable.gl_end()
    
    return True

And that’s it really. Note that rabbyt.set_default_attributes() resets OpenGL state. Now we have setup our OpenGL context to work with Rabbyt. Widget may be resized and Rabbyt will adjust itself to new situation. So. How the actual drawing function looks like?

def draw(glarea, event):
    # Get OpenGL context
    glcontext = gtk.gtkgl.widget_get_gl_context(glarea)
    # Get actual OpenGL surface
    gldrawable = gtk.gtkgl.widget_get_gl_drawable(glarea)

    if not gldrawable.gl_begin(glcontext): return

    # Clear screen
    rabbyt.clear((0.0, 0.0, 0.0))

    # ACTUAL DRAWING GOES HERE!!!
    # ... code goes here ...

    if gldrawable.is_double_buffered():
        gldrawable.swap_buffers()
    else:
        # Remember this needs 'from OpenGL.GL import *'
        glFlush()

    gldrawable.gl_end()

    return True

So that’s it. Remember that if you actually want to draw something you need to place it above “if gldrawable.is_double_buffered():“. The last function we need is our map function. Actually there is also another function. So here it goes:

# First we start with importing gobject
import gobject

def map(glarea, event):
    gobject.idle_add(idle, glarea)

def idle(glarea):
    # ACTUAL COMPUTATION GOES HERE
    # ... code goes here ...

    # Invalidate window
    glarea.window.invalidate_rect(glarea.allocation, False)
    # Update window synchronously
    glarea.window.process_updates(False)

    return True

You update your computations (like for example calculating sprites rotation and so on) in idle() function. And that’s it!

That’s all you need to start your Python Rabbyt application to use GTK GL Extension. Fairly simple right? If you have any ideas on improvement or you noticed some quirks please feel free to post your views in comments. This would be greatly appreciated. Hope you found this tutorial useful. See you next time.

About Wolverine

If you are looking for IT consultant, let me know! karol at karoltomala dot REMOVE com Just remove the REMOVE word from the e-mail above!
This entry was posted in Coding, Galaxy Lords, Game Programming, Python, SpaceX Engine and tagged , , , , , , , , . Bookmark the permalink.

2 Responses to Python, Rabbyt and GTK GLExt

  1. Matthew Marshall says:

    Nice tutorial 🙂 I’ve started working on a gtk based map editor, and this helped me get started a little faster.

    I think it’s worth mentioning that gobject.idle_add causes the program to run at full speed, using 100% of the cpu (on a single core system.) To limit to 30 fps you can use “gobject.timeout_add(1000//30, idle, glarea)”

    MWM

  2. Wolverine says:

    Thanks for your comment Matthew. You are right that it will use all the cpu cycles available. This can be either good or bad, depending on what you want to achieve.

    BTW. AFAIK Python always uses only single core even on multicore processors. This has to do with Python implementation of threading. If you want multicore you use Parallel Python or you launch multiple instances of Python interpreter, possibly specifying which core to use via nice command under linux.

Leave a Reply

Your email address will not be published. Required fields are marked *