You are on page 1of 10

Introducing OpenGL

OpenGL is public, standard C language 3D graphics API based on an earlier, proprietary standard developed by SGI. OpenGL is a state machine of sorts, that takes in requests for graphics primitives, processes them through a rendering pipeline, and produces 2D pixel images as output. OpenGL actually consists of two libraries (GL and GLU), and to use it you must either write window system code yourself (X11 or Win32) or use a third party library such as "glut" to handle window creation and user input. Here is a sample Makefile for compiling an OpenGL program (simple.c) on our local linux environment:
LDLIBS = -L/local/Mesa4.01/lib -lglut -lGLU -lGL -L/usr/X11R6/lib -lXmu -lX11 -lm INCL = -I/local/Mesa4.01/include -I/usr/X11R6/include simple: simple.c cc -o simple $(INCL) simple.c $(LDLIBS)

In addition to this makefile, it turns out that you will need a LD_LIBRARY_PATH environment variable in order for the system to find your glut shared library. This is set in your .cshrc file with something like:
setenv LD_LIBRARY_PATH /local/Mesa4.01/lib

Note that you may already have an LD_LIBRARY_PATH, and you should just add the Mesa directory to your existing path if you have one. Compared with the earlier Icon 2D interface we have seen, OpenGL has many more features, and more complexity, for 3D programming. The glut library which interfaces OpenGL to the host window system is simple and easy to use, but limited and restrictive in its capabilities.

Callbacks
Most graphics API's are "event-driven", meaning that an application is written as a set of functions which are called by the window system in response to user input or when other services are required. glut follows this model in a strong sense; callback functions are "registered" with glut, after which glut's main loop owns the control flow of your program. You do not draw your graphics output in your main() procedure, you draw it in a "display callback" function that is invoked whenever the window needs to be redrawn.

Composing graphics primitives


Graphics primitives are generally composed using a series of functions expressed by the pattern:
glBegin glVertex+ glEnd

The glBegin(type_of_object) function specifies what primitive is being depicted, and the glVertex family of functions allow any number of (x,y,z) coordinates in a variety of types and dimensions. lecture #13 (virtual lecture) began here

This lecture is presenting selected material from the OpenGL Primer, chapter 2.

A Bevy of OpenGL and GLUT Functions


In addition to the core graphics functions, there are a lot of helper functions in the OpenGL API; we will present a few more details of these helper functions that you may find useful. int glutCreateWindow(char *title) Regarding window creation with glutCreateWindow(), I need to emphasize the point that you can create multiple windows, each call will return a separate integer "identifier". The example simple.c is slightly misleading since it uses old-style K&R C, implying glutCreateWindow() returns void. Since the OpenGL functions don't take a window argument, you can expect to find another helper function down the road which sets which window subsequent calls are directed to, stored in some hidden global variable. void glutDisplayFunc(void (*func)(void)) As the book emphasizes, your callback takes no parameters, so expect to use a lot of global variables in your opengl programs. glVertex* There are 3 X 4 X 2 = 24 versions of this function! It may be moderately inefficient to call this function 100 times in order to specify a single, 100-vertex polygon. You should be looking for whether there is any mechanism to streamline this. glFlush() Most graphics systems buffer output even heavily, just as standard file I/O systems do. In particular X11 buffers output to reduce the number of network packets it uses. glutInitWindowPosition(x,y), glutInitWindowSize(width, height) Most applications will use these typical convenience functions. They store values in global variables for later use. Call them before glutCreateWindow().

Color in OpenGL
OpenGL supports "RGB" (true color) and "color index" (color map) modes; writing an application that will run on either is a bit awkward, and color index display hardware is vanishing, so it is reasonable to consider only the RGB color mode. Colors are specified in real numbers between 0.0 and 1.0. Some displays (notably, current generation Macs) support degrees of transparency for superimposing multiple images on screen, and use a fourth color component (alpha) where 1.0 means the color is solid and 0.0 means the color is invisible. Consider now the task of setting the foreground color with which objects are drawn. Although colors are real numbers between 0 and 1, you can use almost any numeric type to supply RGB values; integer values are converted into fractions of the maximum value, so for example many programmers who are used to working with 8 bits each of R, G, and B, can call a function that takes values like (255, 127, 0) and internally this is converted to the equivalent (1.0, 0.5, 0). So, like the glVertex family, there are many (28) functions in the families that set the foreground color, glColor*. An example call would be: glColor3f(1.0, 0.5, 0.0). Apparently they didn't bother to make 28 functions for setting the background color with glClearColor(), because that operation is far less common.

This discussion of color is well and good, but tragically it all becomes meaningless as you transition from "toy" programs to more realistic ones, because once you introduce lighting into your application, glColor*() has no effect! When lighting is introduced, the color of objects becomes a function of the color of light and the reflective properties of the objects, specified by "materials". We will see lighting in detail a little later.

Cameras, take one


OpenGL allows you good control over what "window on the world coordinates" will be visible in your scene. In the general case, you will be able to specify viewing from an arbitrary (x,y,z) location, in an arbitary (x,y,z) direction, as well as how wide and high your field of view is. Section 2.8 mentions the 2D special case of this, gluOrtho2D(x1,x2,y1,y2); if you ever use it, beware the surprising parameter order, and note that OpenGL's world coordinates are based on classic cartesian coordinates, not the typical physical coordinates in which y grows going down.

OpenGL Transformation Matrices


The translation, scaling, and rotation used in converting objects' world coordinates to their onscreen pixel locations is done automatically by OpenGL. Two transformation matrices are maintained by OpenGL (in global variables) and combined to perform rendering: the model-view matrix and the projection matrix. The same set of functions are used for manipulating both matrices, so you start by specifying which matrix you are working on, by calling glMatrixMode(GL_PROJECTION) (or GL_MODELVIEW). When you use gluOrtho2D() you are manipulating the projection matrix. Functions like gluOtho2D modify (i.e. to a matrix multiply into) whatever is in the matrix already, and if you want to start from a known position, you need to reset the matrix to the identity matrix with glLoadIdentity(), so the complete code to specify a 2D window on the world in OpenGL looks like
glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(x1,x2,y1,y2);

Later on, we will see that you frequently compose these matrices, especially the model view matrix, while drawing a complex scene. From one transformation cooresponding to a given "parent" coordinate system, if you have several "child" coordinate systems whose coordinates are relative to the parent, each child will need to save the parent transformation, make changes and then draw their objects, and then restore the parent. When object hierarchies are multiple levels deep, a special type of stack is natural. OpenGL provides a built-in stack mechanism, in which the "push" operation copies the top stack element, to which the child may then apply transformations for their coordinate system relative to the parent coordinates. glPushMatrix() and glPopMatrix() are used in this way, and they operate on whichever current matrix you have specified using glMatrixMode().

More OpenGL Graphic Primitives


Points

GL_POINTS are not usually very interesting in 3D graphics, but opengl supports them and even has a "point size" attribute (set by function glPointSize()) that says how large (in pixels) plotted points should appear. Are points round, or square? Are they really spheres in a 3D scene? The fact that their "size" is given in pixels is at odds with the world coordinates used to specify position, so you can be suspicious that points are a "special case" hack in OpenGL. Lines There are three opengl primitives for depicting lines, similar to the two primitives used in Icon's 2D graphics. GL_LINES draws disconnected segments between pairs of points, similar to Icon's DrawSegment(), while GL_LINE_STRIP draws lines between every adjacent pair of points, forming a single connected object. GL_LINE_LOOP also the first and last points specified, for people too lazy to repeat the first point at the end. Lines have a color, a thickness (glLineWidth()), and a so-called stipple pattern that let's you specify dashes or dots in the lines you draw. But, you can only use these line styles if you call glEnable(GL_LINE_STIPPLE). Triangles, Triangle Strips, Triangle Fans Triangles get a lot of attention in 3D graphics because they are the smallest number of points necessary to specify a portion of a solid object's surface. 3D graphics hardware supports the rendering of triangles directly, while more complex surfaces are usually composed of many many (possibly millions) triangles. In addition to GL_TRIANGLES, are are cleverly optimized ways to specify sequences of physically connected triangles without having to specify the vertices they have in common multiple times. The net effect is only having to specify one new vertex per triangle. GL_TRIANGLE_STRIP connects each vertex with the previous two, while GL_TRIANGLE_FAN connects each vertex with the previous one, and the first vertex. Quads and Quad Strips Four vertex objects are also common, and may be rendered in hardware or broken up into triangles for rendering. GL_QUAD_STRIP defines each new quad using the last two vertices and two new vertices, costing new vertices per quad. As an afterthought, OpenGL also supports conventional 2D rectangles with the glRect* family. Polygons GL_POLYGON is the most general of these primitives, and is typically broken down into triangles during rendering. It is analogous to FillPolygon() in Icon's graphics. It is worth mentioning a few extra items relating to polygons. If their vertices cross, OpenGL's semantics are not guaranteed, implementations can handle them however they please. And convex polygons are MUCH easier and faster to render. If you use multiple colors in the middle of a glBegin/glEnd pair, opengl will use interpolation to gradually change the colors. You can specify glShadeModel(GL_FLAT) if you want each primitive to be a single color instead of an interpolation of the varying colors of its vertices.

Viewports
Within your window, you can select subregions and use them for different purposes; for example, to render simultaneous views looking forward and backward to provide "eyeballs in the back of your head".

glViewport(x, y, w, h) maps the current projection onto a specific region within the selected window. lecture #14 began here.

Reshape and Idle callbacks


One typical way to do animation is to modify some global variables inside the idle callback function, and then call glutPostRedisplay() to cause your display callback to be called. The book's example rotates a square by two degrees each time the idle callback runs. If you want rotation to achieve a constant velocity independent of hardware capabilities, say 30 degrees per second, you'd need to make the angle increment a variable, and run some timings to see how many frames per second your computer is achieving. From our mass media, we know 24 (movies) or 30 (tv) frames per second appears "smooth" to most viewers.

Keyboard and Mouse


Many separate callbacks are used to handle keyboard and mouse input. Both callbacks supply (x,y) mouse location at the time of the input. Keyboard callback has a single integer to say what key is pressed, and causes to you use auxiliary functions to try and detect special key combinations such as the ALT key. The main mouse callback has a separate integer codes to indicate which button and whether it was a press or release. Separate callbacks are used to request mouse motion when a button is pressed (a "drag") and when no button is pressed ("passive motion"). lecture #15 began here

Display Lists
A display list is a collection of off-screen invisible OpenGL objects, stored on the server in a format that can be rapidly (re)displayed. They are created and accessed by integer handles. Display lists may be called from within other display lists.
glNewList(myhandle, GL_COMPILE); glPushAttrib(CL_CURRENT_BIT); glColor3f(1.0,0.0,0.0); glRectf(-1.0,0.0,0.0); glPopAttrib(); glEndList(); ... glCallList(myhandle);

Selection Mode
To select an object in a 3D scene, you rerender the scene in "selection mode", not writing to the frame buffer but labeling each object being rendered with an integer code, with the clipping set closely around the mouse click so that only objects on/near the mouse are rendered. Most of this work is done in the mouse callback function.
void mouse(int button, int state, int x, int y) { ... glInitNames(); glPushName(0);

glSelectBuffer(SIZE, nameBuffer); glGetIntegerv(GL_VIEWPORT, viewport); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); gluPickMatrix((GLdouble)x,(GLdouble)(viewport[3]-y),N,N,viewport); gluOrtho2D(xmin,xmax,ymin,ymax); draw_objects(GL_SELECT); glMatrixMode(GL_PROJECTION); glPopMatrix(); glFlush(); hits = glRenderMode(GL_RENDER); processHits(hits, nameBuffer); glutPostRedisplay(); }

The display callback function becomes trivial, because we have moved code to a helper function called to display or to check for hits:
glClear(GL_COLOR_BUFFER_BIT); draw_objects(GL_RENDER); glFlush();

The draw_objects function does the graphics functions, but "labels" each selectable object with an integer code:
void draw_objects(GLenum mode) { if (mode == GL_SELECT) glLoadName(1); glColor3f(1.0,0.0,0.0); glRectf(-0.5,-0.5,1.0,1.0); if (mode == GL_SELECT) glLoadName(2); glColor3f(0.0,0.0,1.0); glRectf(-1.0,-1.0,0.5,0.5); }

The processHits function considers all objects within N pixels (N was specified in gluPickMatrix) of the user click.
void processHits(GLint hits, GLuint buffer[]) { unsigned int i, j; GLuint names, *ptr; printf("hits = %d\n", hits); ptr = (GLuint *) buffer; for (i = 0; i < hits; i++) { names = *ptr; ptr += 3; /* skip over number of names and depths */ for (j = 0; j < names; j++) { if (*ptr == 1) printf("red rectangle\n"); else printf("blue rectangle\n"); ptr ++; } } }

Cameras

OpenGL's synthetic camera model emulates real world imaging. 3D images within a particular view volume are projected onto a 2D surface, as viewed from a particular center of projection. 6 degrees of freedom specify the camera position and where it is looking. Three angles specify what is up. Six additional values specify the viewing volume. orthographic projection camera is at infinity, projection plane is near the objects glOrtho(left,right,bottom,top,near,far) 3D orthographic projection defines a viewable box gluLookAt(ex,ey,ez,ax,ay,az,ux,uy,uz) Specify eye position and orientation. gluPerspective(yangle, aspectratio, near, far) 3D perspective projection, defines a viewable box Notice that glOrtho, and gluPerspective, use "camera coordinates" not world coordinates. "near" and "far" are distances from the camera. lecture #16 began here

Building objects
Graphics objects may be composed using either code or data.
void cube() { glColor3f(1.0,0.0,0.0); glBegin(GL_POLYGON); glVertex3f(-1.0,-1.0,-1.0); glVertex3f(-1.0,1.0,-1.0); glVertex3f(-1.0,1.0,1.0); glVertex3f(-1.0,-1.0,1.0); glEnd(); /* other 6 faces similar) */ }

Generally it will be preferable to store the graphics in a data structure (array, list, tree) and write code that walks the structure.

Hidden Surface Removal


There are several approaches to drawing only the things that should be visible in a 3D scene. The "painter's algorithm" says to sort the objects by distance from the camera, and draw the farther things first, and the nearer ones on top ("painting over") the farther ones. This approach may be too inefficient (all that "wasted paint" corresponds to wasted CPU cycles and/or awkward visual effects), and is a poor match for the graphics rendering pipeline. We need an approach that removes the hidden parts mathematically before submitting primitives for rendering. OpenGL uses z-buffers, or depth buffers, which are extra memory buffers, to track different objects' relative depths. This is built-in, but you have to turn it on to get its benefits. Also, if any of your objects are see-through, you will need to read more details on z-buffering in the OpenGL references.
glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE|GLUT_DEPTH); glEnable(GL_DEPTH_TEST);

... glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

GLU and GLUT objects


Among the available graphics primitives, OpenGL offered us lines, triangles, quads, and polygons. These are sufficient for any scene, but not very convenient for certain common shapes. GLU adds to these: spheres, cylinders, and disks. GLUT adds cones, toruses, tetrahedrons, octahedrons, dodecahedrons, icosahedrons, and a teapot. We may look at some of these more complex primitives later after introducing lighting and textures, since the structures used to manipulate these primitives in some cases use lighting and texture information.

OpenGL Transformations
The early sections of OpenGL Primer Chapter 5 review 3D transformations that we've seen earlier. Transformations are built-in to OpenGL, greatly simplifying the programmer's job. Here are some highlights.

Points and directions (vectors) may be represented similarly, but they aren't the same. translating to a positive z value may move an object behind the camera in its default location. typical translation example:
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -1.0); /* move object away/in front of camera*/

multiple objects, independent world coordinates


glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -1.0); /* move object 1 */ glutWireTetrahedron(); glLoadIdentity(); glTranslatef(0.0, 0.0, -3.0); /* move object 2, further away */ glutWireCube();

multiple objects, 2nd object relative to first. Note transformations are concatenated/composed together by default.
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -1.0); /* move object 1 */ glutWireTetrahedron(); glTranslatef(0.0, 0.0, -2.0); /* move object 2, -2 further than object 1 */ glutWireCube();

positive rotation == counterclockwise. Rotation's fixed point is the origin. the last transformation specified in the program is the first one applied. typical rotation:
glMatrixMode(GL_MODELVIEW); glLoadIdentity();

glTranslatef(x, y, z); /* move object back from origin */ glRotatef(angle, dx, dy, dz); /* rotate about axis specified by vector */ glTranslatef(-x, -y, -z); /* move object to origin */

glScalef(sx,sy,sz) works very much like you think it would

Hierarchical Model Example


Consider Figure 5.9, it is representative of many many complex 3D objects composed from simpler pieces. A robot arm is an aggregate of: a base, a lower arm piece, and an upper arm piece. We could render all three pieces independently, but we'd be losing something: they are attached and have some common points and relative positions. We can infact have a mixture of local per-object transformations and relative/shared/accumulative transformations.
void base() { glPushMatrix(); /* make our local copy */ glRotatef(-90.0, 1.0, 0.0, 0.0); gluCylinder(p, BASE_RADIUS, BASE_RADIUS, BASE_HEIGHT, 5, 5); glPopMatrix(); } void lower_arm() { glPushMatrix(); /* make our local copy */ glTranslate(0.0,0.5*LOWER_ARM_HEIGHT,0.0); /* translate to our center */ glScalef(LOWER_ARM_WIDTH, LOWER_ARM_HEIGHT, LOWER_ARM_WIDTH); glutWireCube(1.0); glPopMatrix(); } void upper_arm() { glPushMatrix(); /* make our local copy */ glTranslate(0.0,0.5*UPPER_ARM_HEIGHT,0.0); /* translate to our center */ glScalef(UPPER_ARM_WIDTH, UPPER_ARM_HEIGHT, UPPER_ARM_WIDTH); glutWireCube(1.0); glPopMatrix(); } void display() { glClear(GL_COLOR_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glColor3f(1.0,0.0,0.0); /* isn't this poor way to say "red" ? */ glRotatef(theta[0], 0.0, 1.0, 0.0); base(); glTranslatef(0.0,BASE_HEIGHT,0.0); glRotate(theta[1], 0.0, 0.0, 1.0); lower_arm(); glTranslatef(0.0,LOWER_ARM_HEIGHT,0.0); glRotatef(theta[2], 0.0, 0.0, 1.0); upper_arm(); glutSwapBuffers(); }

What about more complex multi-piece objects such as the running man in Figure 5.10? With the right combination of pushes and pops, code similar to the above example would work... but its much cooler to do it as a tree traversal:
typedef struct treenode { GLfloat m[16]; void (*f)(); struct treenode *sibling, *child; } treenode; void traverse(treenode *root) { if (root == NULL) return; glPushMatrix(); glMultMatrix(root->m); root->f(); traverse(root->child); glPopMatrix(); traverse(root->sibling); } void display() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); traverse(torso_root); glutSwapBuffers(); } where treenode *torso_root = malloc(sizeof(treenode)); torso_root->f = torso; glLoadIdentity(); glRotatef(theta[0], 0.0, 1.0, 0.0); glGetFloatv(GL_MODELVIEW_MATRIX, torso_root->m); torso_root->sibling = NULL; torso_root->child = head_node; ... etc.
lecture #17 began here

You might also like