OpenGL on OS/2 - A Model Viewer - Part 1

Written by Perry Newhook

Introduction
''Here is a link to the source code for this column. Ed.''

Here we are again with another column on OpenGL. This is the start of the second year of this column (this is the thirteenth column), and over the past year we have covered a lot of topics. From the early basics of how to create a basic window, to advanced topics such as lighting, texture mapping and NURBS, we have covered everything we need to create a complete application. However, coming up with an interesting application that incorporates most of these functions and is also not too complicated is the hard part.

Thanks to Doug LaRue for e-mailing this idea to me. He got it from a recent Dr. Dobb's article ( "A Windows 3D Model Viewer for OpenGL" by Jawed Karim, Dr. Dobb's Journal, July 1998 ). The application that we are going to create over the next few months is an OpenGL based model viewer for Quake II characters (I can hear the distant screams of joy already!) If you don't have Quake II you can download a demo version from the id site. It has to be Quake II because that is the only character data that I know how to extract.

Design
The first thing we need to do is to decide on the features that we want our modeller to have. The Dr. Dobb's version was very simple; what I hope to do in the coming months is to start with a simple viewer, and gradually build it up, feature by feature until we have a complete application. If there is enough interest (and I have enough time), we could also put in simple editing capabilities.

The data for all of the characters is stored in a single file (extension .MD2), except for the texture data which is stored separately (.PCX). For now we will simply load in the first model as a wireframe, and as our application develops, add the ability to select multiple models and to place the texture skins on top.

We also need the ability to rotate our model on the screen, and to zoom in and out. The easiest way to do this via mouse movement, The usual way to do this is to rotate and tilt the object in the direction of mouse movement when the left mouse button is pressed, and to zoom when the Ctrl - mouse button combination or middle mouse button is pressed. We can use the right mouse button to bring up a menu for things like loading a different character, or switching between different views such as point, outline, or solid display. Since we are going to be using the mouse to rotate and zoom, we could also spin the object by letting go of the mouse button while rotating. This application will use the idle function to control our painting. This will cause a paint update whenever it has finished the last one, instead of at a fixed interval that you would get with a timer function. This gives the benefit that the faster computer you have, the better performance you get.

Code Skeleton
This month we will setup the base code that creates the windows and manipulates the object. For our object we will just draw a simple object that will serve as a placeholder for our model. Next month's column will deal with reading the model in and displaying it. To be as portable as possible, we will be writing the code for the GLUT toolkit.

First we create our main window which is identical to the GLUT code we presented two columns ago. Here it is again for convenience:

The main routine: /* setup and create the window */ glutInitWindowSize( 350, 400 ); glutInit( &argc, argv ); glutInitDisplayMode( GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH ); glutCreateWindow( "OpenGL Demon 13 - Quake II Viewer" ); /* setup stuff in here */ glutDisplayFunc( paintWindow ); glutReshapeFunc( setWindowSize ); glutIdleFunc( idleFunc ); glutMainLoop; return( 0 ); The first section of the above code sets the initial window size, and creates the window. The second section creates three callback functions: glutDisplayFunc tells GLUT that when it needs to paint the windowit should call the paintWindow function. glutReshapeFunc tells GLUT that reshape requests should be handled by setWindowSize. glutIdleFunc tells GLUT to call idleFunc whenever it is not doing anything else.

The Popup Menus
We can now also create a menu that we want displayed when we click the right mouse button. For now we can have two menu items: "Load Model...", which brings up as a submenu all of the available models; and "Display As..." which gives us four choices of how we want our model displayed: "Points", "Wireframe", "Solid", and "Texture". We also want an "Exit" menu selection.

GLUT has a variety of functions that enable you to create a rudimentary menu system. Note that these menus must be popup menus available from a mouse button click; no facilities exist to add a menubar to the top of the window. The menu functions we are interested in are: glutCreateMenu glutDestroyMenu glutGetMenu glutSetMenu glutAddMenuEntry glutAddSubMenu glutChangeToMenuEntry glutChangeToSubMenu glutAttachMenu glutDetachMenu To create a menu we call (can you guess?) glutCreateMenu. You pass this function the address of the function that will process your menu selections. It will then return to you an identifier that you can use to refer to that menu at a later time. Once you create the menu, it now becomes the current menu, and any GLUT functions that act on the current menu will act on it.

Adding menu entries to the menu is done with the glutAddMenuEntry and glutAddSubMenu functions. Each takes two parameters: the first is a character string that identifies what the menu entry will be, and the second is a menu identifier. For glutAddMenuEntry, the identifier is what will be passed to the menu function identified in glutCreateMenu when that menu item is selected. For glutAddSubMenu, the identifier is what is returned from glutCreateMenu, and is the menu that will be shown when that item is selected. In this manner, menus and submenus can be cascaded together.

Related to the above are two functions that allow you to change a menu that is already defined. glutChangeToMenuEntry takes as parameters 1) an offset into the current menu to alter, 2) a string to be the new menu entry, and 3) the new id for that entry. Similarly there is a glutChangeToSubMenu function.

Since glutAddSubMenu and glutAddMenuEntry only work on the current menu, and we need several menus defined when we create cascading popups; we sometimes need a way to change what is referred to as the current menu. This is accomplished with glutSetMenu passing it the id of the menu created earlier that you wish to be current. If you want to query what the current menu is, call glutGetMenu.

Putting this all together, we can create our menu in the following way: /* create the menu */ /* first sub menu */ sub1 = glutCreateMenu( menuFuncDisplay ); glutAddMenuEntry( "Points", MENU_SHOW_POINTS ); glutAddMenuEntry( "Wireframe", MENU_SHOW_WIREFRAME ); glutAddMenuEntry( "Solid", MENU_SHOW_SOLID ); glutAddMenuEntry( "Texture", MENU_SHOW_TEXTURE ); /* second sub menu */ sub2 = glutCreateMenu( menuFuncLoad ); glutAddMenuEntry( "Default", MENU_LOAD_DEFAULT ); /* main menu */ menuId = glutCreateMenu( menuFuncMain ); glutAddSubMenu( "Display As...", sub1 ); glutAddSubMenu( "Load Model...", sub2 ); glutAddMenuEntry( "Exit", MENU_EXIT ); Once we have our menus created we need to attach it to our application. This is done by (TADA!) glutAttachMenu. This attaches the current menu to the mouse button identified in the parameter. Valid mouse buttons to attach to are: GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON, and GLUT_RIGHT_BUTTON. We will attach it to the right button.

Here is how we attach: /* attach as popup to button */ glutSetMenu( menuId ); glutAttachMenu( GLUT_RIGHT_BUTTON ); Note that the glutSetMenu function call is redundant. All it does is set the current menu for attachment to our main menu id. This is not necessary as the current menu is our main menu, since every time we call glutCreateMenu the menu returned becomes our current menu. I've left this in however as a safety precaution as this may not be true if we change around the order of menu creation.

Currently our menu functions do nothing, but we'll add functionality as our application develops.

Object Manipulation
Now we want to add the capability to rotate our object around on the screen and to zoom it in and out. First let's do the zoom capability.

Since this is functionality selectable by the mouse buttons, we need to add a mouse handler. This is done by glutMouseFunc (doesn't this guy have the most logical naming conventions you've ever seen?) The function called will take four parameters: 1) the button that was pressed (same as the identifiers for glutAttachMenu, 2) the state of the button, either GLUT_UP or GLUT_DOWN, and 3), 4) the x and y position of the mouse when the button was pressed.

Our setup for the mouse function can be grouped with the other functions: glutMouseFunc( mouseFunc ); And our mouse function itself becomes: void mouseFunc( int button, int state, int x, int y ) {     static int pos; switch( button ) {        case GLUT_MIDDLE_BUTTON: /* zoom */ switch( state ) {              case GLUT_DOWN: /* store the down spot for later */ pos = y;                 break; case GLUT_UP: /* take the difference between the down and up spots as a zoom factor */ zoom += ( y - pos ); break; }           break; }  } Here we simply store the position of the cursor when the middle button is pressed, and use the difference in positions when the button is released to calculate how much our object is zoomed by.

Note: For those of you who cannot get your middle mouse button working, a fix has been made to the glut library after OpenGL for OS/2 was released. You can get it from ftp://ps.boulder.ibm.com/rs6000/developer/os2/OpenGL/glut.zip. This contains a dll which should replace your current glut.dll.

The only problem with this method is you cannot see the zoom happening until the mouse button is released (since the mouse function is only called when the mouse button changes state). To see the change happen in real time, we need another function: glutMotionFunc. This callback function is called whenever a mouse button is pressed and the mouse moves across the window. Unfortunately, the only values passed to the callback function are the mouse position; no button state information is transferred. So we need to combine the two functions: the mouse handler sets the state of the movement action (to either nothing, zooming or rotating, depending on what button is pressed), and the motion function then acts on that state. Our code is now modified to be: /* setup */ glutMouseFunc( mouseFunc ); glutMotionFunc( mouseMotionFunc ); Our mouse function sets the state and the initial conditions: void mouseFunc( int button, int state, int x, int y ) {     switch( button ) {        case GLUT_MIDDLE_BUTTON: /* zoom */ switch( state ) {              case GLUT_DOWN: /* store the down spot for later */ zoomPos = y;                 mouseState = MOVE_ZOOM; break; case GLUT_UP: mouseState = MOVE_NONE; break; }           break; }  } While the motion function acts on the state set: void mouseMotionFunc( int x, int y ) {     /* find out what action to perform */ switch( mouseState ) {        case MOVE_ZOOM: /* change zoom and reset reference */ zoom += ( zoomPos - y ); zoomPos = y;           break; }  } That's it for this month. Implementing the rotation is an activity I'll leave up to the reader (don't worry, there are several ways to do it and if you can't get it, I'll be presenting mine next month). Also next month, we will start on our Quake II model decoding.

The code and compiled executable for this month can be downloaded [openglsrc.zip here]. One note about the source is that I've changed the zoom function from the middle button to the left button. The menu function we added was intercepting the middle click as a right click. Normally I would have checked the keyboard state to differentiate between rotating and zooming using glutGetModifiers, but this function is version 3.x and OS/2 is still using 2.x. If anyone wants to compile the freely available GLUT source from http://reality.sgi.com/opengl/ or has already compiled a version, please let me know so we can make this publicly available. If not, I'll have to think of something creative.

Until next month! Keep GLing!