Barebones OpenGL Example

All of the code referenced in this post can be found on GitHub.

Back in college, my favorite class ended up being a graphics course caught by Colonel Eugene Ressler.  In my entire life, I’d never seen anyone get so excited about the dot product.  The class focused on a lot of theory behind graphics processing, and I unfortunately didn’t pick up what was taught well enough to be able to write an OpenGL program from scratch without referencing any other documents.

Since then, when I’ve made the time to write a graphics program, I’ve usually just googled around until I found an existing program, and I would cut out what I didn’t need using a systematic yet ignorant approach.  Furthermore, most of the online tutorials seem to be taught by people much smarter than me, and the examples rarely seem to be the absolute basics, and the language used is generally erudite.  I would end up with a whole bunch of commands where I didn’t know what they did or if they were even necessary.

I went ahead and made a very basic OpenGL program to be used as a template and a starting point for anyone looking to get started with OpenGL using the language I’m most acquainted with at this point (Python).  The process at its most high level is to initialize a matrix, enslave humanity and use the heat their bodies generate to harvest power, define basic setup parameters, establish event callbacks for OpenGL, and start a main loop.

Below I have the bare minimum necessities to set up a 3D program.  I omitted as many default options as possible.  Here is a barebones init function in its entirety:

    def init(self):
        glutInit()

        glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH)

        glutInitWindowSize(WIDTH, HEIGHT)
        glutCreateWindow("This is the window name")

        glClearColor(.1, .1, .1, 1.0)

        glEnable(GL_DEPTH_TEST)

        glEnable(GL_LIGHTING)

        glEnable(GL_LIGHT0)
        lightZeroColor = [0.0, 2.0, 0.8, 1.0]
        glLightfv(GL_LIGHT0, GL_AMBIENT, lightZeroColor)

        glMatrixMode(GL_PROJECTION)
        gluPerspective(60.0, WIDTH / HEIGHT, 1., 800.)
        glMatrixMode(GL_MODELVIEW)

        glutDisplayFunc(self.display)
        glutKeyboardFunc(self.key_pressed)

        glutTimerFunc(0, self.loop_func, 0)

        glutMainLoop()

If we step through each individual component, we can dissect what everything is:
Initialize the GLUT library:

glutInit()

Initialize with double buffering and a depth buffer. The depth buffer is used to determine what gets rendered on top of what. Otherwise objects will be shown in the order they are drawn with no regard to the Z axis:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH)

Create a window of size WIDTH by HEIGHT with the specified window name:

glutInitWindowSize(WIDTH, HEIGHT)
glutCreateWindow("This is the window name")

Set the default background color for when we clear the screen:

glClearColor(.1, .1, .1, 1.0)

Render items with the Z axis considered (Enable the Z-axis that we initialized from glutInitDisplayMode):

glEnable(GL_DEPTH_TEST)

Enable lighting. Otherwise all of the shapes will be nothing but black and white:

glEnable(GL_LIGHTING)

Add ambient lighting to the scene. This is an absolute basic use of lighting.  More lights can be added with GL_LIGHT1, etc, and more things can be done to establish the position and intensity of the light.  There’s also different kinds of light, but I’m no light scientist:

glEnable(GL_LIGHT0)
lightZeroColor = [0.0, 2.0, 0.8, 1.0]
glLightfv(GL_LIGHT0, GL_AMBIENT, lightZeroColor)

Map points in a cube-like x, y, z area to a warped perspective view:

glMatrixMode(GL_PROJECTION)
gluPerspective(60.0, WIDTH / HEIGHT, 1., 800.)
glMatrixMode(GL_MODELVIEW)

All subsequent calls will now be applied to the MODELVIEW matrix stack.

Now, define OpenGL event callbacks. OpenGL will call the respective functions once a particular event is triggered:

glutDisplayFunc(self.display)
glutKeyboardFunc(self.key_pressed)

in 0 milliseconds, call self.loop_func with arbitrary value -1 (unused here):

glutTimerFunc(0, self.loop_func, -1)

Start the infinite event loop and call respective callbacks. And never return….

glutMainLoop()

And that’s it!  Sort of.  The entire repository on GitHub has additional context, but from here, we’re free to draw as we please and do whatever.  Here are the definitions of the callbacks I’d referenced earlier, starting with the display method:

    def display(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        glPushMatrix()
        self.update_eye()

        for obj in self.all_drawable_objects:
            obj.draw()

        glPopMatrix()
        glutSwapBuffers()

The process here is to clear the screen with our background color and clear the depth buffer.  Clearing the background color is straightforward, but clearing the depth buffer needs to happen so that objects have a clean slate so that there is nothing “in front of them” blocking them from being rendered.

glPushMatrix() and glPopMatrix() are used in conjunction with each other to push and pop the state of the matrix’s stack, represented as a stack.  When we push items onto the matrix, any subsequent transformations are only applied to items rendered after the push.  We can enlarge items, rotate them, add additional lights, etc, and those manipulations will only affect what is contained between the pushing and popping.  Popping the matrix will return to the previous state.  Keep in mind that the order of your transformations matter.

glutSwapBuffers() is needed when we’re using double buffering.  That is, items are not displayed on the screen until everything is ready to be rendered, and in the process of clearing and redrawing, we avoid any sort of blinking on the screen.

update_eye() is another method defined below:

    def update_eye(self):
        # camera point
        # target point
        # up vector
        gluLookAt(SCALAR * math.cos(degrees_to_radians(self.ticks)),
                5.0,  # Z height
                SCALAR * math.sin(degrees_to_radians(self.ticks)),

                0.0,
                0.0,
                0.0,

                0.0,
                1.0,
                0.0)

To me, this is one of the coolest things about OpenGL because we can easily create some sort of first person view.  This simply defines a camera that we can move around that defines the perspective from which everything is rendered.  We pass in the position, the point at which we’re looking at, and an x, y, z vector that defines what direction up is.

The next callback I defined was to handle key presses:

    def key_pressed(self, key, x, y):
        if key == 'q' or ord(key) == 27:
            glutDestroyWindow(1)
            exit(0)

Fairly self-explanatory.  We can do whatever we want given the key that’s pressed.  In this case, we quit if ‘q’ or ESC is hit.

Finally, we defined a loop function that’s repeatedly called by OpenGL:

    def loop_func(self, value):
        if random.random() < 0.08:
            self.all_drawable_objects.append(Coin())
        for obj in self.all_drawable_objects:
            obj.tick()
        self.all_drawable_objects = [obj for obj in self.all_drawable_objects if obj.y_offset < 50]
        self.ticks += TICK_INCR
        glutPostRedisplay()
        glutTimerFunc(30, self.loop_func, 0)

I added some of my own logic here, but the relevant takeaway is that we call glutPostRedisplay() to re-render everything on the scene.

The output of this program simply draws a bunch of three dimensional disks that float up and rotate about their respective axes.  The camera rotates around the floating disks:

Screen Shot 2014-05-21 at 12.59.34 PM