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

From EDM2
Revision as of 22:03, 6 October 2016 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Perry Newhook

Part 1

Part 2

Part 3

Part 4

Welcome to another segment of OpenGL on OS/2! This month we continue with part 4 of our Quake II model viewer. Last month we made our model viewer a lot more interesting by adding a solid view mode with both artificial light and texturing. This month we will be adding animation to our modeller.

Quick Note on the Bookstore

A few months ago I recommended two books from our Amazon bookstore: "OpenGL Programming Guide: The Official Guide to Learning OpenGL v1.1", and "OpenGL Reference Manual: The Official Reference Manual for OpenGL v1.1". Recently I received an email from a reader saying that he bought one of the books and that it was one of the best OpenGL purchases he's ever made. In my opinion these books are probably the last books you will ever need to buy in order to program in OpenGL effectively (well at least until OpenGL 1.2 comes out). If you haven't bought these books yet but are thinking about OpenGL programming, run to the bookstore link off the EDM/2 main page and pick them up. They'll make a great gift for Christmas!

One note about the Amazon site: Proceeds from the purchases made from this site go to keeping this site running; without your support this site could not have brought you the great programming articles it has over the past many years. If you do buy something from Amazon, be sure you do it directly from the EDM/2 link. If you surf around too much beforehand, Amazon may not register that it came from our site.

Animation Control

Before we enable animation, let's first create a menu that we can control the animation with. We can add a few useful commands such as rewind (go to first frame), play backwards, play forwards, single step back, single step forward and stop animation. Since we never used the "Load Model" menu (give me a break, I make this up as I go), let's change it to our animate menu.

We can change the menu to "Animate..." and the sub2 menu to the functions we want to add:

   /* main menu */
   ...
   glutAddSubMenu( "Animation...", sub2 );

   /* second sub menu */
   sub2 = glutCreateMenu( menuFuncPlay );
   glutAddMenuEntry( "Rewind", MENU_PLAY_REWIND );
   glutAddMenuEntry( "Play Reverse", MENU_PLAY_REV );
   glutAddMenuEntry( "Frame Reverse", MENU_PLAY_NEGSTEP );
   glutAddMenuEntry( "Stop", MENU_PLAY_STOP );
   glutAddMenuEntry( "Frame Advance", MENU_PLAY_POSSTEP );
   glutAddMenuEntry( "Play Forwards", MENU_PLAY_FWD );

I have also changed the name of the function from the original menuFuncLoad() to menuFuncPlay() just to be consistent. Now we have to add processing to that function.

Just like the other functions, munuFuncPlay() is called with the id of the menu that was selected. Based on this selection we will change two variables: one that states which frame we are going to show, and another that specifies which direction to increment the frame number for the next time we show it. In this section we will not do any bounds checking; we will leave the checking to see if it is a valid frame to just before we display it in the paintWindow() function. The menuFuncPlay() is as follows:

  void menuFuncPlay( int id )
  {
     /* animation control menu */
     switch( id )
     {
        case MENU_PLAY_REWIND:
           /* set frame back to first one and stay there */
           frameToShow = 0;
           frameDirection = 0;
           break;
        case MENU_PLAY_REV:
           /* play backwards */
           frameDirection = -1;
           break;
        case MENU_PLAY_NEGSTEP:
           /* go back one frame and stay there */
           frameToShow--;
           frameDirection = 0;
           break;
        case MENU_PLAY_STOP:
           /* stop animating */
           frameDirection = 0;
           break;
        case MENU_PLAY_POSSTEP:
           /* go forwards one frame and stay there */
           frameToShow++;
           frameDirection = 0;
           break;
        case MENU_PLAY_FWD:
           /* play forwards */
           frameDirection = 1;
           break;
     }
  }

Menu control for animation can sometimes be difficult, especially when you want to stop at a specific frame or step backwards and forwards quickly between frames. For this reason, lets also add keyboard control commands.

Adding a keyboard callback function is done with the GLUT function glutKeyboardFunction(). The callback function specified is passed three parameters. The first is the keyboard character that was pressed, and the next two holds the position of the mouse at the time the key was pressed. In our function, we do not need to use this position information.

When we process the keypress, instead of reproducing the logic in menuFuncPlay(), we'll simply call menuFuncPlay() with the parameter that corresponds to the key we have pressed. I've picked keys that make some vague sort of sense (maybe not though, I'm watching the Simpsons and drinking beer while I'm writing this). Here is the keyboard callback function:

  void keyboardFunc( unsigned char key , int x, int y )
  {
     /* keyboard accelerator commands */
     switch( key )
     {
        case 'w':
           menuFuncPlay( MENU_PLAY_REWIND );
           break;
        case 'b':
           menuFuncPlay( MENU_PLAY_REV );
           break;
        case 'r':
           menuFuncPlay( MENU_PLAY_NEGSTEP );
           break;
        case 's':
           menuFuncPlay( MENU_PLAY_STOP );
           break;
        case 'a':
           menuFuncPlay( MENU_PLAY_POSSTEP );
           break;
        case 'p':
           menuFuncPlay( MENU_PLAY_FWD );
           break;
     }
  }

Now that we have keyboard accelerators mapped, we need a way for the user of the application to know what keys have been mapped. Just like regular OS/2 menus can indicate the accelerator key by underscoring the associated letter, GLUT menus can do the same thing by adding a tilde (~) just before the letter to be underscored. Change the sub2 menu creation code to indicate which letter is the acceleration key:

   /* second sub menu */
   sub2 = glutCreateMenu( menuFuncPlay );
   glutAddMenuEntry( "Re~wind", MENU_PLAY_REWIND );
   glutAddMenuEntry( "Play ~Backwards", MENU_PLAY_REV );
   glutAddMenuEntry( "Frame ~Reverse", MENU_PLAY_NEGSTEP );
   glutAddMenuEntry( "~Stop", MENU_PLAY_STOP );
   glutAddMenuEntry( "Frame ~Advance", MENU_PLAY_POSSTEP );
   glutAddMenuEntry( "~Play Forwards", MENU_PLAY_FWD );

Now that we have all of the animation control we need, we can figure out how to decode the different animation frames from the Quake II file.

Animation Frame Extraction

You may not realize it, but we've already partially completed this section. When we extracted the model data for display in part 2 of this series, what we actually extracted was the first frame of the animation sequence. In this section we will simply generalize the code we've already created to be able to read in any number of frames.

When we extracted the first frame of data, we extracted two pieces: the point data, and the mesh data used to connect the point data into a solid. For each successive frame the Quake II model file only stores the new positions of the points; the mesh data stays exactly the same for each frame.

The first frame of data is stored in the .MD2 file at offset mdh->offsetFrames from the beginning of the file, and there are mdh->numXYZ points in this frame. If you remember, mdh is our pointer to the Quake II file header). The code we use now is:

    /* offset to points in first frame */
    frm = (frame *)&buffer[mdh->offsetFrames];

    for( i=0; i<mdh->numXYZ; i++ )
    {
       pointList[i].pt[0] = frm->scale[0] * frm->fp[i].v[0] + frm->translate[0];
       pointList[i].pt[1] = frm->scale[1] * frm->fp[i].v[1] + frm->translate[1];
       pointList[i].pt[2] = frm->scale[2] * frm->fp[i].v[2] + frm->translate[2];
    }

Subsequent frames follow the first frame in the order that they are to be shown. Each frame is mdh->framesize big so you don't have to calculate the frame size yourself. When we allocate our own buffer that stores the scaled and translated points, we have to allocate enough memory for all of the frames. We can convert the above code to the following to read multiple frames:

    /* loop for number of frames in file */
    for( j=0; j<mdh->numFrames; j++ )
    {
       /* offset to points in frame */
       frm = (frame *)&buffer[ mdh->offsetFrames + mdh->framesize * j ];
       pointListPtr = (coord *)&pointList[ mdh->numXYZ * j ];
       for( i=0; i<mdh->numXYZ; i++ )
       {
          pointListPtr[i].pt[0] = frm->scale[0] * frm->fp[i].v[0] +
                                  frm->translate[0];
          pointListPtr[i].pt[1] = frm->scale[1] * frm->fp[i].v[1] +
                                  frm->translate[1];
          pointListPtr[i].pt[2] = frm->scale[2] * frm->fp[i].v[2] +
                                  frm->translate[2];
       }
    }

This is the only change we need to make to store multiple frames. Instead of frm pointing to only the first frame, for every frame we adjust frm to point to a new frames worth of data. Also, instead of pointList, the area where we store our converted points, we have a pointListPtr which changes for each frame we store. Now the only thing left to do is to read our frame data back out when we display it.

Animation Frame Display

From our animation control code above, we have two variables that control the animation sequencing: frameToShow and frameDirection. Since we didn't do any bounds checking when we set these variables, we have to do it now before we show the frame. The first thing we do is to increase the frameToShow by the direction we specified. If the direction is zero as in some of the commands, we end up with a still animation as the frames do not change. Second, we have to check that the frame we are about to show falls within an allowable range.

  /* change frame to show and bounds check */
  frameToShow += frameDirection;
  if( frameToShow >= model->numFrames )
     frameToShow = 0;
  if( frameToShow < 0 )
     frameToShow = model->numFrames-1;

Now that we have the correct frame to show, we can set a pointer to point to the correct frame of data. Where we currently use the stored pointList pointer to get the frame of data, the pointList pointer now is a pointer to a series of frames. Before we try and display our frame, we have to get this pointer to point to the frame we want.

  /* create a pointer to the frame we want to show */
  pointListPtr = &model->pointList[ model->numPoints * frameToShow ];

And that is all we need to do! For the display code we just use this new pointer we created instead of the model->pointList pointer we used previously. You can make the changes above and try it out. What you should have now is a completely controllable, animation capable, Quake II modeller! Cool! The gif animation below is created from six frames of the running sequence. The model shown is the female tris.md2 model with the voodoo.pcx texture applied.

OpenGL-aniquake.gif

Cleanup

Just to tidy up some loose ends, we have yet to implement a function in the main menu called 'Exit'. This is a very simple menu function whose id gets passed to the menuFuncMain() function. For completeness I'll include the function below.

  void menuFuncMain( int id )
  {
     /* main menu commands */
     switch( id )
     {
        case MENU_EXIT:
           exit( 0 );
           break;
     }
  }

That's all of the changes we need to our code. You can download the complete viewer here.

Things to Try

There are some feature that you can add yourself to make this program even better:

  1. Instead of displaying the frames in the idle time (on fast computers the display may go too quickly) create a timer so that you have precise control over the number of frames to show per second. Create a menu that allows you to adjust the number of frames per second.
  2. Show onscreen the current frame and total number of frames (ie 'Frame 3 of 205'). Hint: To display the text correctly in one position regardless of the size of the screen when in gluPerspective() mode is difficult. A better solution is to draw the model in gluPerspective and then switch to glOrtho2D() when you draw the text overlay.
  3. Add a background image (this is very advanced).
  4. Grab each frame and write the frames out as an AVI movie (advanced but OpenGL part is easy)

And so we come to the end of our modeller development. I hope everyone liked this series. Over the past four months, we've created a modeller with a lot of features:

  • show as points
  • show wireframe
  • show solid with artificial lighting
  • show solid with colour texture
  • streaming animation or frame by frame control
  • rotation and zoom model positioning capability

This series was as a result of a reader feedback, so if you have any ideas for topics, send them in and I'll see what I can do. Over the last sixteen months (wow!) of OpenGL columns, we've covered a lot of topics, so many in fact that I've run out of new ideas to cover.

This issue therefore marks the end of my regular monthly OpenGL columns. Future OpenGL columns will instead be feature articles as topics come up. I've really enjoyed writing these articles and I've also enjoyed reading all of the viewer feedback I've received. I hope that these articles have introduced you to the exciting world of OpenGL programming, and that some of you have tried programming OpenGL on your own. OpenGL is a wonderful world, that will get only more exciting as more OpenGL capable accelerated video boards make their way down to the user market.

Happy Holidays, hope you have a Merry Christmas and see you soon in OS/2-OpenGL land!