The Practical Physicist's OpenGL tutorial

Edward S. Boyden, Ph. D.
[ Back to home page ]

Brief Introduction

OpenGL and OpenInventor are well-known API extensions for developing 3D graphics applications on a great many platforms, from IRIX and Solaris to Windows 95 and NT. As the technology for making fast PC graphics boards becomes cheaper, many developers and researchers are recognizing the great aesthetic and information-bearing potential of OpenGL, especially in relation to the (until recently) bland Win32 graphics libraries. It is the intention of this little article to explain the ideas behind OpenGL so that people who need its functionality will take advantage of it.

The basic model for an OpenGL application is a state machine: colors, positions, angles, and projections are all defined in global matrices, and any drawing-command will be composed with these matrices before being displayed on the screen. Therefore a command such as

      glColor3f(.7,.3,.2)
will set the current color to be at 70% of its maximum red intensity, 30% of its maximum green intensity, and 20% of its maximum blue intensity. And the color will stay that way until (1) another
      glColor3f
command replaces the current color, (2) the color is cleared (i.e., set to some user-predefined default) through
      glClear(GL_COLOR_BUFFER_BIT),
(3) the current color is pushed onto the stack using
      glPushAttrib(),
or (4) another color is popped off the stack (using
      glPopAttrib()
) to replace the current one. The stack is one of the most useful tools of GL programming: without a stack, we would forever be saving temporary variables and struggling to remember the various colors that we need in our scene. As befits a state model, we can always look up the current color using
      glGet(GL_CURRENT_COLOR).

Believe it or not, that's essentially the entire GL language. You can apply those facts about color-states to translation matrices, rotation matrices, projection matrices, depth buffers, and just about every other aspect of 3D graphics. A thorough understanding of these terms requires only a good sense of geometrical intuition. Instead of throwing cosines and coordinates at you, I will go into some examples after stating two rules:
1. Every operation is a matrix.
2. When you declare a translation, a rotation, a projection, a scaling, or any other geometric operation on a GL object, you are multiplying the current state transformation matrix on the right by the matrix which represents your new operation.

What does this mean? First, it means that every operation is a matrix: after any operation, the coordinates of a point are replaced by linear combinations of its components. (For the group theory fans in the audience, we are constructing a subgroup of GL3(R), including but not limited to SO3, permutation matrices, and 3-dimensional lattice bases, resulting in a crystallographic group in 3-space.) Also, the order in which the transformations occur is in reverse order: since we multiply new matrices on the right of the current state matrix, those new matrices are applied to the raw points before the rest of the matrices. For example, let's say your current transformation matrix is M, which represents a huge quantity of translations, rotations, etc. that you've artistically worked on, and you want to rotate a rectangle a few more degrees: if the rotation matrix is a 35 degree rotation about the vector {1,4,1}, then the command glRotate(35,1,4,1) will multiply M on the right by a matrix P, where P represents the operation "35 degree rotation about the vector {1,4,1}". Got all that?

Now tell me that in English...

The first thing to specify is the viewport, which is essentially the location (within your Window) where the GL-stuff will be put. There is a simple way to do that: just call glViewport(lowerleftX, lowerleftY, widthofView, heightofView).

A very simple way to handle all of the point-of-view transformations is to use the gluPerspective, gluLookAt functions. This will save you all the rotations, translations, etc. necessary to look properly to a scene. These extremely useful procedures have the following syntaxes:

      gluPerspective(fovy, aspect, near, far);
      gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);
The gluPerspective procedure essentially says "My GL program will appear to have a field-of-view angle of fovy, an aspect (width/height) ratio of aspect, and all the objects will fall between near and far (inches, meters, furlongs, units, etc.) away." The gluLookAt function essentially says "I am seeing the scene from (eyex, eyey, eyez), looking at (centerx, centery, centerz), and up is (upx, upy, upz)." (At first the up-vector requirement seems extraneous, but when you think about it, the line-of-sight axis provides no information as to whether you're standing on your head or not.) Now start drawing!

Time for...

Some Samples

We'll first go over the necessary requirements to do Windows programming with GL. That way you can do some experiments on your own, which is a good way to get a feel for GL. The online help is absolutely awful, but I've already told you enough that you should be able to get through the examples with little trouble.

To initialize a GL window, you'll want to run something akin to

HWND CreateGLWindow(int glxpos, int glypos, int glxsize, int glysize,
		  void*GLWndProc, HINSTANCE hInstance)
{      char  GLName[]="GLName";
       HWND hGLwnd;
       HDC hdc;
       HGLRC hglrc;
       WNDCLASS GLwndclass;
       PIXELFORMATDESCRIPTOR pfd = {
	sizeof(PIXELFORMATDESCRIPTOR),   // size of this pfd
	1,                     // version number
	PFD_DRAW_TO_WINDOW |   // support window
	PFD_SUPPORT_OPENGL |   // support OpenGL
	PFD_DOUBLEBUFFER,      // double buffered
	PFD_TYPE_RGBA,         // RGBA type
	24,                    // 24-bit color depth
	0, 0, 0, 0, 0, 0,      // color bits ignored
	0,                     // no alpha buffer
	0,                     // shift bit ignored
	0,                     // no accumulation buffer
	0, 0, 0, 0,            // accum bits ignored
	32,                    // 32-bit z-buffer
	0,                     // no stencil buffer
	0,                     // no auxiliary buffer
	PFD_MAIN_PLANE,        // main layer
	0,                     // reserved
	0, 0, 0 };	       // layer masks ignored
    int iPixelFormat;

          GLwndclass.style	   = CS_HREDRAW | CS_VREDRAW;
	  GLwndclass.lpfnWndProc   = GLWndProc;
	  GLwndclass.cbClsExtra    = 0 ;
	  GLwndclass.cbWndExtra    = 0 ;
	  GLwndclass.hInstance     = hInstance;
	  GLwndclass.hIcon         = LoadIcon (NULL, IDI_WINLOGO);
	  GLwndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
	  GLwndclass.hbrBackground = GetStockObject (BLACK_BRUSH) ;
	  GLwndclass.lpszMenuName  = "";
	  GLwndclass.lpszClassName = GLName ;
		  RegisterClass(&GLwndclass);
	 hGLwnd = CreateWindow (GLName,
			 "",
			 WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS,	
			 glxpos,
			 glypos,
			 glxsize,
			 glysize,
			 HWND_DESKTOP,
			 NULL,
			 hInstance,
			 NULL);
	hdc=GetDC(hGLwnd);
	iPixelFormat=ChoosePixelFormat(hdc, &pfd);
	SetPixelFormat(hdc, iPixelFormat, &pfd);
	hglrc = wglCreateContext (hdc); 
	wglMakeCurrent (hdc, hglrc);
	return hGLwnd;
}	

If you're not familiar with Windows programming, you probably didn't understand a word of that last block of code (except for maybe 'version number'). You might try using the auxiliary libraries, which are addictive and not really useful for much more than drawing little shapes on the screen with little interaction and an ugly console screen. In any case this is now a good excuse to learn Win32. (I hope to compile this into a DLL so all you Visual Basic addicts will have something to use, but I don't know how successful I'll be.)

So now you've got a window. What can you do with it? As a physicist you've probably always wanted to plot, in real time, an implicit function in beautiful 3D graphics. The following procedure sketches out this process (you'll need to add viewmode transformations and a projection transformation in order to rotate your 'eye' in order to see the results):

typedef float Vector[3];

float *fakevector(int length)
{	float *v;
	v=(float*)malloc(length*sizeof(float));
	if (!v) MessageBox(NULL, "There was a horrible
			memory leak, sorry.", "RED ALERT!", MB_OK);
	return v;}

void PlotFunctionGL(float (*TheFunction)(Vector), float xlower, float xhigher,
 		float ylower, float yhigher, float zlower, float zhigher,
		int xresolution, int yresolution)
{	float xSpacing, ySpacing;
	float* ourFakevector;
	int xi, yi;
	float xvalue, yvalue, zvalue,yMinus1zValue;

	xSpacing=(xhigher-xlower)/xresolution;
	ySpacing=(yhigher-ylower)/yresolution;
	ourFakevector = fakevector(yresolution);

//Left border of enclosure
	xi=0;
		for(yi=0;yi<yresolution;yi++) {
		  xvalue=xlower; yvalue=ylower+yi*ySpacing;			
		  yvalue=BinaryChop(TheFunction, xvalue, yvalue, zlower, zhigher);
		  if (zvalue%lt;zlower) zvalue=zlower; 
		  ourFakevector[yi]=zvalue; }
//Interior
   for(xi=1;xi<xresolution;xi++)
      for(yi=0;yi<yresolution;yi++)  {
		if (0==yi) {
		  xvalue=xlower+xi*xSpacing; yvalue=ylower;			
		  yvalue=BinaryChop(TheFunction, xvalue, yvalue, zlower, zhigher);
		  yMinus1zValue = zvalue;}
		else	//xi,yi>0
		{
		  xvalue=xlower+xi*xSpacing; yvalue=ylower+yi*ySpacing;
		  zvalue=BinaryChop(TheFunction, xvalue, yvalue, zlower, zhigher);
	if (zvalue<zlower) zvalue=zlower;
	if ((zvalue>zlower)||((yMinus1zValue>zlower)||
		((ourFakevector[yi-1]>zlower)||(ourFakevector[yi]>zlower))))
		    {
		        glBegin(GL_QUADS);
			  glVertex3f(xvalue,yvalue,zvalue);
			  glVertex3f(xvalue, yvalue-ySpacing, yMinus1zValue);
			  glVertex3f(xvalue-xSpacing, yvalue-ySpacing, ourFakevector[yi-1]);
			  glVertex3f(xvalue-ySpacing, yvalue, ourFakevector[yi]);
			glEnd(); }

		if ((xresolution-1)!=xi) ourFakevector[yi-1]=yMinus1zValue;
		if ((yresolution-1)!=yi) yMinus1zValue=zvalue; else ourFakevector[yresolution-1]=zvalue;
		}
   }
free(ourFakevector);
}

float BinaryChop(float (*TheFunction)(Vector), float xval, float yval, float zlower, float zhigher)
{
	float result;
	int numiters=20;
	float accuracy;
	float constructedValue[3];
	int iter=0;

accuracy=(zlower+zhigher)/((float)pow((float)2,(float)numiters));
constructedValue[0]=xval;constructedValue[1]=yval;constructedValue[2]=(zlower+zhigher)/2;
result=TheFunction(constructedValue);

do {
	if (result<0.0) {
		zlower=constructedValue[2];
		constructedValue[2]=(zlower+zhigher)/2; }
	else {
		zhigher=constructedValue[2];
		constructedValue[2]=(zlower+zhigher)/2; }
			result=TheFunction(constructedValue);
			iter++;
} while ((iter<numiters)&&(fabs(result)>accuracy));
if (iter>numiters-3) return -2*zlower; else return constructedValue[2];
}



Copyright © 1995-present, Ed Boyden