Overview

To get going with graphics programming, we have to introduce a number of somewhat disparate topics, so this presentation may seem disorganized. However, I hope you'll see by the end of it that all of these topics help to understand the example code of the barn demo. The topics we'll cover are:

Finally, after all that, we'll be ready to read the code for the barn.

What is an API?

An Application Programmer Interface is a set of programming stuff (types, variables, functions, and methods) that enable some new capability or allow the programmer to interact with a piece of hardware (such as a robot or a graphics card).

We have two APIs (well, three or four, really):

TW is written in Python and OpenGL. You are always welcome and encouraged to read the code in TW.py. It may change from time to time, since it's still under development, but that should be transparent to you.

The OpenGL Pipeline

The OpenGL API and the graphics card is implemented as a pipeline. (Those of you who have taken an computer architecture course will be familiar with this term.) What does that mean? It means that your calls to the API functions will often put data into one end of a pipeline, where they undergo transformations of various sorts, finally emerging at the other end. The other end of the OpenGL pipeline is the monitor (or some graphics output device).

Essentially, we put vertices into one end of the pipeline and get pixels out the other end.

Of course, that's not the only thing that happens. Some API functions modify the pipeline, so that subsequent data are transformed in different ways.

For efficiency, the pipeline hangs onto some information and other information just slips through, with only pixels to show for it. That is, some data is not retained. In particular, vertices are not retained. So, "the barn" does not exist. If we want to look at the barn from a different angle, we have to move the camera and build it again (by "build," I mean send all the vertices down the pipeline again).

(Note that there are graphics APIs in which this is not the case. There are also ways in which OpenGL can be made to remember an object in the model, which we'll get to that much later.)

When we talk about state, we means that information is retained. For example, the current drawing color is part of the OpenGL state: if you're drawing in red, everything is red until you change colors. Changing colors doesn't display anything (that is, no pixels emerge from the pipeline), but subsequent operations is affected by the changed state.

Geometry Data Structures

The simplest things that OpenGL can draw are objects that you're familiar with from geometry. However, some of them are a little different in OpenGL. Also, how they're drawn is, perhaps, not what you'd think.

Representation and Definition

If we choose a suitable origin and coordinate system (more about origins and coordinate systems later), we can define a point in 3D space with just three numbers. We can also define vectors with just three numbers. Consider the following four objects:

P = (1, 5, 3)
Q = (4, 2, 8)
v = P-Q = (-3, 3, -5)
w = Q-P = (3, -3, 5)

P and Q are points in space. (Picture it any way that works for you).

A vector can be thought of as an arrow between two points or even as a movement from one to the other. Therefore, the vector v from Q to P is just P-Q (subtract the components, respectively). Notice that the vector w, from P to Q, is just the negative of vector v.

In 3D, both are represented as a triple of numbers. Mathematicians get all exercised about whether they are "row vectors" or "column vectors," but in this course, we don't care.

How do we specify points and vectors to OpenGL? Some functions will let you use three arguments; others take a single argument that is an array of three numbers. (The C programming language treats arrays differently than Java does, so we'll be careful about this.)

How do we represent line segments and polygons and such to OpenGL? The things OpenGL knows how to draw are defined by collections of points, so the way it's done is to specify what you want to draw and then supply the points, and then say you're done. Here's the basic idea:

glBegin(GL_LINES)
glVertex3f(1,5,3)   # P
glVertex3f(4,2,8)   # Q
glEnd();

Here we have four API calls. If you think about this in terms of the pipeline model, the glBegin() call changes the state of the pipeline so that it knows to draw lines. [Note that it is common in technical literature like this to put empty parentheses after the name of a function or method, merely to indicate that it's a function or method.] The first glVertex() call produces no pixels at all, because the pipeline doesn't know where to draw the line, so again, we've just modified the state of the pipeline. The second glVertex() call causes some, possibly large, number of pixels to be drawn by the pipeline. This call also causes the pipeline to completely forget about both P and Q, so if you were to try to ask the pipeline to draw the line again, it would say "what line?" However, the pipeline has not forgotten that we are in GL_LINES mode, so if you send two more vertices down the pipeline, it would draw another line. Finally, the glEnd() causes the pipeline to tear down all the infrastructure for drawing lines, and forgets entirely about it.

OpenGL Geometry Primitives

Now that we've thought about line segments and polygons and such, let's look at how to do them in OpenGL.

First, please read the "man" page for "glBegin()." Man pages are not easy reading, because they're intended to be concise and precise, but they are usually the most up-to-date and accurate description of an API function. It's worth getting to know them.

Here are some 2D pictures of the options.

Those examples were all in 2D. To see examples in 3D with real, working OpenGL code, look in ~cs307/pub/demos/early:

X11 and Callbacks

An OpenGL program has to interact with the windowing system in order to open a drawing area, draw objects, find out where the mouse is, react to the keyboard, and a host of other things.

On Linux machines, OpenGL interacts with a windowing system called "the X Window System." Actually, I quote: "The X Consortium requests that the following names be used when referring to this software:"

X
X Window System
X Version 11
X Window System, Version 11
X11
We'll call it "X11." X11 is pretty cool because it can work across networks, something that we'll take advantage of for demos in class.

Because of the interaction with the windowing system, most graphics programs are written with heavy use of callbacks.

In most programs, the calls of functions/methods produce a tree starting at "main." For example, suppose you have a program like the following:

def pebbles():
   print "goo goo\n"


def wilma():
   pebbles()
   
def fred():
   wilma()
   pebbles()

def main():
   fred()
   wilma()

The call tree for a program like that might look like:

main
   fred
      wilma
         pebbles
      pebbles
  wilma
      pebbles

The nice thing about the call tree is that you can always trace a single thread of control from main through intermediate functions or methods to the one you're interested in.

By contrast, with callbacks, the call tree becomes a forest. Certain functions get called (directly or indirectly) from main, but others (the callbacks) are called by the windowing system, such as X11.

How does X11 know what functions to call and when? It does because the graphics program has registered the function that it wants called.

Let's be concrete. For now, suppose there are three events that we might be interested in:

Here's how the code might look:

def buffy():
   ...


def angel():
   ...

def firefly():
   ...

def main():
    ...
    glutDisplayFunc(buffy);
    glutKeyboardFunc(angel);
    glutMouseFunc(firefly);
    ...

Notice that there are no calls to buffy, angel, or firefly! (There might not be any in the entire program, even the part I've elided.) The main function is only registering them. If they are called, they will be called by X11, at a time that is outside our control. These callback functions can also be termed "entry points."

X11 allows you to set up callback functions that it will call when certain events occur:

Okay, read the code? Sure. It's in Python, so if you don't know Python, you'll have to do some guesswork, but here goes:

''' Demo of my classic barn object.  This uses two helper functions that
improve the abstraction and brevity of the code.

Implemented from the C++ predecessor, Fall 2009
Scott D. Anderson
scott.anderson@acm.org
'''

import sys

from TW import *

def makeBarnVertexArray( w, h, len ):
    '''Creates and returns an array of vertices for the barn.
w is the width, h is the height, len is the length'''
    front = [ [ 0, 0, 0 ],       # left bottom 
              [ w, 0, 0 ],       # right bottom
              [ w, h, 0 ],       # right top 
              [ 0, h, 0 ],       # left top
              [ w*0.5, h+w*0.5, 0 ] # ridge 
              ]
    # list comprehension to construct back just like front except for Z
    back = [ [v[0], v[1], -len] for v in front ]
    front.extend(back)          # NOT "append," which only adds one item, even given a list
    return front

# Global variable to hold the vertex array, initialized when the module loads
# The following values are only used to set up the barn vertices

BarnVertices = makeBarnVertexArray(30,40,50)

# =====================================================================
# Two useful helper functions.  Probably general enough to move to a
# library like TW, but that hides too much for this early demo.

def drawTri(verts, a, b, c):
    '''Draw a triangle, given an vertex array and three indices into it, in CCW order'''
    glBegin(GL_TRIANGLES)
    glVertex3fv(verts[a])
    glVertex3fv(verts[b])
    glVertex3fv(verts[c])
    glEnd()

def drawQuad(verts, a, b, c, d):
    '''Draw a quad, given an vertex array and four indices into it, in CCW order'''
    glBegin(GL_QUADS)
    glVertex3fv(verts[a])
    glVertex3fv(verts[b])
    glVertex3fv(verts[c])
    glVertex3fv(verts[d])
    glEnd()

# ================================================================

def drawBarn(b):
    '''draws the barn, given an array of its vertices'''
    twColorName(TW_RED)
    drawQuad(b, 0, 1, 2, 3)     # front
    drawTri(b, 3, 2, 4)
    twColorName(TW_GREEN)
    drawQuad(b, 5, 6, 7, 8)
    drawTri(b, 7, 8, 9)
    twColorName(TW_PURPLE)
    drawQuad(b, 0, 3, 8, 5)     # left side
    twColorName(TW_MAROON)
    drawQuad(b, 1, 2, 7, 6)     # right side
    twColorName(TW_OLIVE)
    drawQuad(b, 3, 4, 9, 8)     # left roof
    drawQuad(b, 2, 4, 9, 7)     # right roof

# ================================================================
# a callback function, to draw the scene, as necessary

def display():
    twDisplayInit(0.7, 0.7, 0.7) # clear background to 70% gray
    twCamera()                   # set up the camera

    drawBarn(BarnVertices)      # draw the barn

    glFlush()                   # clear the graphics pipeline
    glutSwapBuffers()           # make this the active framebuffer

# ================================================================

def main():
    glutInit(sys.argv)
    glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
    twInitWindowSize(500,500)
    twVertexArray(BarnVertices) # set up the bounding box
    glutCreateWindow(sys.argv[0])
    glutDisplayFunc(display)    # register the callback
    ## twSetMessages(TW_ALL_MESSAGES)
    twMainInit()
    glutMainLoop()

if __name__ == '__main__':
    main()

Written by Scott D. Anderson
scott.anderson@acm.org
Creative Commons License
This work is licensed under a Creative Commons License.

Valid HTML 4.01!