Slithy home


building diagrams with OpenGL

This page documents how to incorporate your own C/OpenGL code into Slithy, which will allow you to build 3D diagrams, among other things.

In the opengl/skeleton/ directory of the Slithy distribution you'll find a simple, highly commented example called "skeleton.c" that draws a rotatable 3D cube. The files "slithygl.c" and "slithygl.h" have some utility functions you'll need to get your code to talk to Slithy. The other sample, in the opengl/polyhedron/ directory, has a fancier diagram and a little mini-presentation that shows off various ways to integrate it with other Slithy elements.

The basic idea is to build a parameterized diagram, only instead of writing a Python function that uses Slithy's drawing library, you write a C function that makes OpenGL calls directly. You'll need to compile your C function into a shared library (.so in Unix, .pyd in Windows), then it can be loaded into Python using the import statement, just like other stuff written in Python can.

skeleton.c has all the boilerplate you need for getting this working. We'll walk through it section by section here.

#ifdef _DEBUG
#undef _DEBUG
#include <Python.h>
#define _DEBUG
#else
#include <Python.h>
#endif

#ifdef WIN32
#include <windows.h>
#endif

#include <GL/gl.h>
#include <GL/glu.h>

#include "slithygl.h"

Standard header stuff. The nonsense with _DEBUG is necessary to let you use the debugger on Windows without installing the debug versions of the Python DLLs. (You'll be able to debug your own code, but not step into any of the Python library calls. This probably won't be too much of a limitation.)

slithygl.c and slithygl.h contain all the functions documented below. You'll want to use them.

static PyObject* cube_draw( PyObject* self, PyObject* args );

static PyMethodDef SkeletonMethods[] = {
    { "cube", cube_draw, METH_VARARGS },     
    { NULL, NULL } };

The SkeletonMethods array defines the mapping of Python functions to C functions. Here we see that our module provides a single function to Python called "cube", which is implemented by the C function "cube_draw". Your module can have as many functions (diagrams) as you want; you just need to make an entry for each one in the methods array. (Leave the "{ NULL, NULL }" entry as the last item to mark the end of the array.

void 
#ifdef WIN32
__declspec( dllexport )
#endif
initskeleton( void )
{
    PyObject* m;
    PyObject* p;

    m = Py_InitModule( "skeleton", SkeletonMethods );

    p = make_opengl_diagram( m, "cube" );
    add_scalar_parameter( p, "x", 0.0, 1.0, 0.0 );
    add_scalar_parameter( p, "theta", 0.0, 360.0, 0.0 );
    Py_XDECREF( p );
}

You have to have a function called "initfoo" (all lowercase), where "foo" is the name of the module (the final .so or .pyd file). In it, you should call Py_InitModule() with the name of the module an the method array. You'll get back a pointer to the module object.

Next you have to describe each of the diagrams to Slithy. For each diagram:

  1. Call make_opengl_diagram() to tell Slithy that it is a parameterized diagram. You'll pass in the module object and the name of the function (which must match the name in the methods array). You'll get back a "parameter list object".

  2. Declare parameters with the add_*_parameters() functions. You'll pass the parameter list object you got in the previous step, the parameter name, and its default value (and range, for some parameter types). There are six functions that correspond to the different parameter types:
    void add_scalar_parameter( PyObject* plist, char* pname, double min, double max, double def );
    void add_integer_parameter( PyObject* plist, char* pname, int min, int max, int def );
    void add_color_parameter( PyObject* plist, char* pname, double r, double g, double b, double a );
    void add_string_parameter( PyObject* plist, char* pname, char* def );
    void add_boolean_parameter( PyObject* plist, char* pname, int def );
    void add_object_parameter( PyObject* plist, char* pname, PyObject* def );
    

    Slithy makes a copy of the def parameter to add_string_parameter(); you don't have to keep the string around after you pass it to this function.

  3. After you've declared all the parameters, call "Py_XDECREF()" on the parameter list object.

You will follow these steps for each parameterized diagram you have in the module. In this example, there's just one.

static PyObject* cube_draw( PyObject* self, PyObject* args )
{
    PyObject* dict;
    double aspect, alpha;
    double x, theta;

    if ( !PyArg_ParseTuple( args, "Odd", &dict, &aspect, &alpha ) )
	return NULL;

Now we'll start writing the drawing function itself. It will always be called with three arguments: a dictionary of parameter values, the aspect ratio of the viewport, and a suggested alpha value. You are free to ignore the alpha value if you want, but using it will make your OpenGL function respond to the fade_in() and fade_out() animation commands like every other Slithy object.

x and theta are variables needed by our cube-drawing code.

    glMatrixMode( GL_PROJECTION );
    glScaled( aspect, 1.0, 1.0 );

When your function is called, the OpenGL projection will be set up so that the corners of the viewport are at (-aspect,-1) and (aspect,1). We'll stretch the projection so that the corners are (-1,-1) and (1,1) — this corresponds to the default projection when you open up a new OpenGL window.

You should not push or pop the projection matrix stack at any time in your function.

    glColor4d( 0.2, 0.2, 0.3, alpha );
    opengl_clear_color();

If you want to use the depth buffer, you must call one of the following two functions:

void opengl_clear_blank( void );
void opengl_clear_color( void );

Use these instead of glClear(). Both clear out the depth buffer so it is ready for use (Slithy normally keeps object id information in the depth buffer). The "color" version also clears the viewport to the current OpenGL color. In this case, we're clearing the viewport to dark blue. You can use opengl_clear_color() with an alpha less than one in order to have a semitransparent background.

Both these functions require the projection matrix to be scaled as above. (It's not done for you by the caller because you might want to do something in the original coordinate system.)

    gluPerspective( 25.0, aspect, 0.1, 30.0 );

    glMatrixMode( GL_MODELVIEW );
    glTranslated( 0.0, 0.0, -12.0 );

Now we'll use an ordinary gluPerspective() call to set up a 3D viewing projection. Since this is multiplied onto the existing projection, the result will be to set up the 3D projection within the diagram's viewport.

The model-view stack can be used normally, including pushing and popping. The model-view matrix will be initialized to the identity matrix when your function is called. Here we dolly back to see something drawn at the origin.

    glEnable( GL_DEPTH_TEST );

Depth testing is not on by default, so we have to enable it. You do not have to undo your changes to the OpenGL state; the caller will take care of it.

    x = get_scalar( dict, "x", 0.0 );
    theta = get_scalar( dict, "theta", 0.0 );

Here we will read values out of the parameter dictionary. There are six functions that do this, one for each parameter type:

double get_scalar( PyObject* dict, char* pname );
int get_integer( PyObject* dict, char* pname );
char* get_string( PyObject* dict, char* pname );
void get_color( PyObject* dict, char* pname, double color[4] );
int get_boolean( PyObject* dict, char* pname );
PyObject* get_object( PyObject* dict, char* pname );

The first argument is the dict object we obtained at the top of the function. The second is the parameter name.

A few notes: The string returned by get_string() is a pointer into Python-owned memory; it should not be freed, nor should the data it points to be changed. get_color() fills in the array you pass it with the RGBA values of the color object. get_object() returns a borrowed reference (if you're working with Python objects directly, you'll know what that means).

    glRotated( theta, 0.7, 0.4, -0.1 );
    
    glPushMatrix();
    glColor3d( 1.0, 0.0, 0.0 );
    draw_face( x );
    glColor3d( 0.0, 1.0, 1.0 );
    glRotated( 180.0, 1.0, 0.0, 0.0 );
    draw_face( x );
    glPopMatrix();

Now there's some perfectly ordinary OpenGL drawing code (some of which has been omitted; see skeleton.c for the full version). You can do pretty much anything you can in OpenGL, except:

Finally, at the bottom of the function we have some boilerplate to return the special Python value None:

    Py_INCREF( Py_None );
    return Py_None;
}

That's all there is to it. Once you've built it, your C parameterized diagram can be used just like any other. To test it, you'll need to use a simple little Python script:

from slithy.library import *
import skeleton
test_objects( skeleton.cube )

This imports your C diagram and passes it to test_objects(), which should give you something like this:

compiling

On Unix, use the included makefile as a guide. You need to link against the GL and GLU libraries, and find the GL and Python include files.

On Windows, you need to build the code as a DLL, linking against python22.lib, opengl32.lib, and glu32.lib. You also need to change the output filename to "foo.pyd" instead of "foo.dll".