This article introduces a general purpose, interactive, 3-D stereo-enabled OpenGL(TM) movie viewer I recently developed. A single source file, dispui.c, contains the GL Utility Toolkit (GLUT)-based user interface code for scene rotation, translation, clipping, scaling, speed control, coloring, and stereo. To enjoy multi-platform, stereo visualization of your movie data, you need only supply code for movie frame rendering and object selection. The cujmovie.c demonstration source file that accompanies this article on the CUJ website should serve as a useful starting point.
I completed this work as part of a larger project to update our computational chemistry laboratory's molecular dynamics trajectory viewer, MD Display [1]. Molecular dynamics (MD) calculations allow chemists to study the dynamic interactions of molecular systems. Starting from a classical model of the various forces between bonded and non-bonded atoms in molecules, and injecting thermal motion, MD calculations numerically solve Newton's second law of motion to generate a trajectory of movie frames. Each frame contains the computed coordinates of all the atoms in the system at each femtosecond time step. The computations are lengthy, scaling by the square of the number of atoms in the simulation. Here in the Lybrand lab at Vanderbilt University, we typically perform these calculations using the AMBER package [2] on large CPU clusters.
Once this trajectory file has been generated on the cluster, MD Display allows us to visualize it on a desktop workstation (see Figure 1). Molecular systems are typically very complex. To help us separate the forest from the trees, MD Display supports stereo glasses and allows us to selectively translate, rotate, scale, clip, and color our molecular movies under mouse and keyboard control. Additionally, MD Display interactively measures inter-atom distances and bond angles, and reports other dynamic information of chemical interest. When we couple MD Display's visualization power with a quantitative decomposition of energies in the trajectory, we gain insight into the behavior of molecular systems - insight that can help us rationalize and predict experimental results or even aid in drug discovery.
MD Display was written to the original SGI(TM) GL API in the early 1990s. My project was to transform this legacy code to ANSI C, recode the graphics for better performance through OpenGL, and, in the process, make MD Display a multi-platform application.
As an example of how this work might enhance your own movie-rendering project, I have supplied a simple rendering demo on the CUJ website: cujmovie.c. This demo draws a few rotating objects and allows the user to selectively change their rendering style or color (see Figure 2). If your code renders frames from "The Matrix Reloaded," my code should give your users the power to place themselves anywhere in the action. (I am not responsible for any resulting injuries!)
Why GLUT? In my former life as a business owner and software developer, I had experienced both power and frustration with Rapid Application Development (RAD) tools such as Borland Builder and Microsoft Visual Studio. While RAD power comes through its jumpstart on application development, there are frustrations in the learning curve to environment mastery, platform restrictions, and the recurring nuisance of having to essentially rewrite applications as the RAD tools "evolve." For the MD Display project, application performance and longevity in an academic setting were more important than platform-specific bells and whistles. I wanted to create an application that could run on any platform and be maintained by future programmers possessing only minimal ANSI C and OpenGL experience. Mark Kilgard's cross-platform OpenGL Utility Toolkit (GLUT) was an excellent fit for my development goals.
GLUT is well known to graphics developers. GLUT lets you explore the OpenGL code Tutorial examples in many popular texts [3, 4, 5] without the clutter of platform-specific windowing APIs. Mark Kilgard designed the GLUT toolkit to expedite learning of OpenGL and he makes no restriction on its redistribution.
GLUT is well documented, elegant, and efficient. The entire library consists of fewer than 100 functions, all described in a 62-page reference manual [6]. An important group of these functions create, resize, and move windows and subwindows. However, the most important interactions between GLUT and client application code is through callbacks that GLUT makes to the functions you write to respond to user interface events. For example, after you create a window, you supply a pointer to a function GLUT will call when it is time to draw the window. In other words, you "register a callback." You can register additional callbacks for the handling of mouse and keyboard events. Thus, the resulting code for window creation often looks something like this:
int yourMainWindow =
glutCreateWindow("Window Title");
glutDisplayFunc(yourDisplayFunction);
glutMouseFunc(yourMouseFunction);
glutKeyboardFunc(yourKeyboardFunc);
Through this callback approach, GLUT hides platform-dependent GUI event loops
and message decoding. If you have registered a callback for an event, that code
is called directly by GLUT when GLUT sees it in its event loop. Otherwise, GLUT
ignores the event (or takes reasonable and conservative default actions). Additionally,
GLUT can create subwindows on existing windows (or subwindows). To support interactive
movie viewing, dispui.c will often redraw several subwindows in response
to a user interface event. Fortunately, GLUT assigns all windows and subwindows
a unique integer ID when they are created. And, GLUT maintains a separate set
of callback function pointers for each window and subwindow.
Of course, this article is not intended to be a training guide for GLUT. The textbook and online resources in the references do a superb job of that. Instead, I'm going to show how GLUT's library routines can be used to create more intricate user interface components than those that are typically seen in textbook examples.
GLUT supports a timer callback that is essential to displaying movies. After rendering a frame, MovieDisplayFunc() in dispui.c calls
glutTimerFunc(nMilliseconds, MovieTimeFunc, movieTimerKey)This instructs GLUT to call MovieTimerFunc() after nMilliseconds have elapsed. In turn, MovieTimerFunc() increments the current frame number and calls glutPostRedisplay() on the movie window, inducing its redisplay. GLUT calls MovieDisplayFunc() to draw the next frame, which will again call glutTimerFunc(), and the cycle will repeat. Violá, we have a movie.
The movieTimerKey parameter mentioned above is used to avoid race conditions. When the user makes a change to the current scene, such as a re-coloring, we want the user to immediately see the result. Were this not a movie, we would call:
glutSetCurrentWindow(movieWindow); glutPostRedisplay()As a result of the above code, GLUT would call MovieDisplayFunc(). However, since this is a movie, we want MovieDisplayFunc() to not only update the scene but also schedule the next frame. But outstanding timer events from earlier frame displays should be ignored.
The solution is for user-initiated events to increment the global movieTimerKey prior to calling glutPostRedisplay(). Now, when MovieTimerFunc() is called with a keyValue that does not match the current global movieTimerKey, MovieTimerFunc() will do nothing. In short, when your code wants to trigger a movie scene redraw, be sure to call MovieAdvanceTimerKeyAndPostRedisplay() instead of calling glutPostRedisplay() directly.
A more detailed example of GLUT's latent flexibility follows. While GLUT can display text strings and respond to keyboard events, it lacks a built-in text string input capability. To allow keyboard selection of objects (atoms in MD Display) without interrupting the movie run, I only needed a popup box to display a prompt string and accept user input at a flashing cursor. Knitting together a subwindow with a timer and keyboard event handler provided a solution for my project.
To input text from your code, call the dispui.c function:
KeyboardInputStart(yourPromptString, yourFunctionToBeCalledAtEnd);
KeyboardInputStart() creates a popup box for text input as a subwindow in the top left corner of the movieWindow.
keyboardWindow = glutCreateSubWindow( movieWindow, 20, 10, keyboard_window_width, 50);It then registers the address of my functions that will handle keyboard events.
glutKeyboardFunc(MasterKeyboardFunc); glutSpecialFunc(MasterSpecialFunc);Note: So that all keystrokes may be processed as input, MasterKeyboardFunc() and MasterSpecialFunc() are registered as the keyboard event handlers for all windows and subwindows in dispui.c.
KeyboardInputStart() next sets the current global cursor state variable to visible and registers the subwindow display function for the keyboardWindow:
keyboardCursorOn = 1; glutDisplayFunc(KeyboardDisplayFunc);When KeyboardDisplayFunc() is in turn called by GLUT, it clears the subwindow to blue, frames it in a white box, and draws the prompt string. Then, it draws the emerging user input string below the prompt using dispui.c's DrawString() function:
DrawString(nKeyboardWindowOffset, winHeight-37,keyboardTextFont, keyboardInputString);If keyboardCursorOn == 1, KeyboardDisplayFunc() draws the cursor as a small rectangle to the right of the end of the input text:
glRecti(nKeyboardWindowOffset + inputWidth, winHeight-39, inputWidth+15,winHeight-38);For text input to feel right aesthetically, I wanted the cursor to flash every 400 milliseconds. KeyboardDisplayFunc() concludes by scheduling GLUT to call KeyboardTimerFunc().
keyboardTimerKey++; glutTimerFunc(400, KeyboardTimerFunc,keyboardTimerKey);When KeyboardTimerFunc() is called 400 msecs later, it flips the cursor flag and requests GLUT to redraw the keyboard subWindow:
void KeyboardTimerFunc(int keyValue)
{
if (keyValue == keyboardTimerKey)
{
// FlipCursor
keyboardCursorOn = ! keyboardCursorOn;
glutSetWindow(keyboardWindow);
glutPostRedisplay();
}
}
The above glutPostRedisplay() triggers a call to KeyboardDisplayFunc()
and the user sees a flashing cursor as KeyboardDisplayFunc() redraws the
entire subwindow.
As will be discussed shortly, the keyboardTimerKey is used to avoid race conditions, in a manner analogous to the movieTimerKey discussed above.
A flashing cursor is uninteresting unless the system can also accept input from the keyboard. When a key is struck, MasterKeyboardFunc() is called by GLUT (recall that we registered this callback after creating the text input subwindow). MasterKeyboardFunc() terminates input if ENTER or ESCape are pressed. Otherwise, it adds the keystroke to the input string buffer:
keyboardInputString[input_strlen++] = key; keyboardInputString[input_strlen] = 0;Then, it will set keyBoardCursorOn=1 in preparation for the next character. By incrementing the keyboardTimerKey, other cursor Flip timers that might be pending are ignored by KeyboardTimerFunc. Finally, GLUT is instructed to redraw the keyboard subwindow:
keyboardCursorOn = 1; keyboardTimerKey++; glutSetWindow(keyboardWindow); glutPostRedisplay();Were it not for the keyboardTimerKey strategy, each new keystroke would initiate a new wave of redraw events. The cursor would appear to flash faster and faster as keys are typed. The above code works in concert to give a new cursor as each character is typed. Moreover, the cursor flashes at a steady rate.
When the user presses <ENTER> or <ESC>, dispui.c calls yourFunctionToBeCalledAtEnd() with the input string and terminating keystroke as parameters.
The cujmovie.c demo calls KeyboardInputStart() for string input during coloring of objects as well as when a new center of rotation is being chosen. The demo also shows how mouse selection of the objects is simultaneously available during text string input.
displayRenderFrameInPerspectiveBox(int frame);Before dispui.c calls this function, all user-requested OpenGL transformations will have been set up for you. You are free to immediately start drawing the scene with OpenGL primitives. If you are experienced with GLUT, resist the urge to call glutSwapBuffers() at the end of your function, since dispui.c may need to do more drawing to complete a stereo or tri-view image.
The real power of the user interface code comes not from its display of your movie, but rather from the user interactions that it facilitates. With MD Display, users interactively pick atoms, or sets of atoms, for labeling, distance measurement, and coloring. In the cujmovie.c example, you can pick objects to change their rendering style. Your "Matrix Reloaded" rendered application might allow user-termination of evil Matrix agents. In any case, when the user clicks on an object in your scene, you will want to know about it.
Selection of objects with the mouse is a bit tricky in a 3D application. The mouse coordinates are only 2D - and the 3D scene may have been translated, rotated, and scaled. How do you decide which object has been selected? Fortunately, OpenGL provides a lot of machinery to help us out here, which is nicely documented in the references. Some of this machinery, such as the Pick Matrix, and Selection Buffer, is managed by dispui.c. You must provide two functions that dispui.c will call after the user clicks on the movie window.
When the mouse is clicked in the movie window, dispui.c calls glRenderMode(GL_SELECT) and then calls the first of your functions:
displayRenderFrameInSelectMode(int frame);In this function, you will render the scene similarly to your code for displayRenderFrameInPerspectiveBox(). However, here you need only render those objects that you consider to be mouse selectable. As you render, you will enumerate your objects to OpenGL by calling glLoadName(objectNumber). Under the control of dispui.c, OpenGL will append information about objects near the mouse click to dispui.c's selection buffer. These records of object numbers and locations are called "hits."
Second, dispui.c will call your function
displayProcessHits(Glint hits, const Gluint* selectBuf)to give you the opportunity to act on the hit records in the selection buffer. For each hit, OpenGL will have stored four integers of information in the selectBuf (see references [3-5] for details). You will need to sift through the selectBuf and take action.
My cujmovie.c example code reviews the selectBuf to isolates the "hit" closest to the user (i.e., the smallest Z value). By default, cujmovie.c toggles that object's rendering between solid and wire-frame. If the text input box for coloring of objects is active when the hits are being processed, then clicking on objects colors them. Cujmovie.c accomplishes this through a static global function pointer, ObjectPickedFunc(), which is set during keyboard input, and called by displayProcessHits() when nonzero. Similar code allows a change to the center of rotation. I hope you find these examples reasonably straightforward to follow in the source code.
[2] D.A. Case, D.A. Pearlman, J.W. Caldwell, T.E. Cheatham III, J. Wang, W.S. Ross, C.L. Simmerling, T.A. Darden, K.M. Merz, R.V. Stanton, A.L. Cheng, J.J. Vincent, M. Crowley, V. Tsui, H. Gohlke, R.J. Radmer, Y. Duan, J. Pitera, I. Massova, G.L. Seibel, U.C. Singh, P.K. Weiner and P.A. Kollman (2002), AMBER 7, University of California, San Francisco.
[3] Woo, Mason, Jackie Neider, Tom Davis, and Dave Shreiner. OpenGL Programming Guide, 3rd edition. (Boston, MA: Addison-Wesley, 1999).
[4] Kilgard, Mark. OpenGL Programming for the X Window System. (Boston MA: Addison-Wesley, 1996).
[5] Wright, Richard S. and Michael Sweet. OpenGL SuperBible, Second Edition. (Indianapolis, IN: Waite Group Press, 2000).
[6] Kilgard, Mark, and Nate Robbins. "GLUT: Graphics Library Utility Toolkit", http://freeware.sgi.com/Installable/glut-3.7.html, <http://www.xmission.com/~nate/glut.html>.
Chris Moth cofounded Daisy Systems, Inc. in 1982, and served as president of the firm until he sold it to Teleflora, LLC in 1998. At Teleflora, he served as VP of Technology Strategy through 2002. He holds Masters Degrees in both Computer Science and Mathematics. Currently, he is conducting research at Vanderbilt University towards a Ph.D. in Chemistry. You can contact him via email at chris.moth@vanderbilt.edu or by visiting his website at: <http://www.structbio.vanderbilt.edu/~cmoth>.