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

From EDM2
Jump to: navigation, search

Written by Perry Newhook

Part 1

Part 2

Part 3

Part 4

Introduction

This month we continue from last months column where we began creation of a Quake II model viewer. Of course, once this program is finished, you can replace the section of code that reads quake models, and make it able to read in any model type that you wish.

Credits

While the code here is mine, credit for information on the file structures and how to decode them goes to code written by John Carmack of id Software, and Jawed Karim for his article in the July issue of Dr.Dobb's Journal. This code as well as the Quake II models can be obtained form id's website

The Home Assignment

Last column we ran into a problem with using the middle mouse button. What I had intended for mouse control is to have the left mouse button control rotating of the model, the right button pops up the menu, and the middle (or both buttons together) causes the model to zoom in and out. The problem we ran into, which is a problem with GLUT, is that GLUT is intercepting holding down both mouse buttons as a right button click. As a home assignment, I had asked you to think up a solution to work around our problem. Since no one emailed me their solution, I'm just giving you mine. So there. Nyah.

I've taken the simplistic approach of adding another menu selection called 'Movement', that lets the user select between two movement types: rotation and zooming. Add the following code that creates another submenu:

  /* third sub menu */
  sub3 = glutCreateMenu( menuFuncMotionSelect );
  glutAddMenuEntry( "Rotate", MENU_MOTION_ROTATE );
  glutAddMenuEntry( "Zoom", MENU_MOTION_ZOOM );

  glutAddSubMenu( "Motion Type...", sub3 );

The function menuFuncMotionSelect() will be called when the 'Rotate' or 'Zoom' menu functions are selected, and the corresponding values MENU_MOTION_ROTATE and MENU_MOTION_ZOOM are passed in as the parameter value. Based on this value, we will set the correct motion type when the mouse function is called:

  void menuFuncMotionSelect( int id )
  {
     /* find out which motion type was selected */
     switch( id )
     {
        case MENU_MOTION_ROTATE:
           selectedMotion = MOVE_ROTATE;
           break;
        case MENU_MOTION_ZOOM:
           selectedMotion = MOVE_ZOOM;
           break;
     }
  }

Inside the mouseFunc(), we change the down state code to:

  case GLUT_DOWN:
     /* store the down spot for later */
     zoomPos = y;
     mouseState = selectedMotion;
     break;

Note: During testing I found that this method of menu processing does not work with the GLUT libraries bundled with V1.0 of the OpenGL library. However it does work with the GLUT library in OpenGL v1.1, so please use that one, or a later one instead. If, like me, you continue to use OpenGL v1.0 because of the speed increases it provides over v1.1, simply delete (or rename) the glut.dll from v1.0 and replace it with the one from v1.1.

Now the only thing missing for rotate and zoom functionality is the rotation code that goes inside mouseMotionFunc() (this was also a reader assignment). The way I have implemented it is a cute little trick that enables you to do rotations in three dimensions (X, Y and Z) simultaneously using only the two dimensional mouse. Here's how:

Think of the object model as a ball floating in front of you, able to spin but not move in position. Now think of the mouse as your hand and you want to rotate that ball around. To rotate that ball along its vertical axis (the Y), you would move your hand across its face either to the right to left. Similarly to rotate about the horizontal axis, you would move your hand vertically either up or down across its face. Now to rotate about the Z axis (along the sight of the viewer), you would move your hand across the side of the ball. It does not matter which side you move your hand along; left or right, top or bottom, just as long as is does not go across the front of the ball (the part facing you). Any arbitrary axis can be rotated about by combining portions of any or all of the X, Y and Z rotation actions.

Now, to do this with the mouse, we are going to calculate how far we have travelled in the mouse x and y directions since we started a rotate command (remember that we store the position that the motion started so this part is easy). We also have to determine whether we are pushing on the front of the object, for a X or Y rotation, or on the side of the object for a Z axis rotation. For this we calculate how far away from the centre of the screen the action is taking place. For example if we moved the mouse vertically in the centre of the object, this would translate into an entirely X rotation, while doing the same action on the side of the screen would be a Z rotation (again think of spinning a ball in space)

The following code section calculates how far we are away from the centre of he screen, with 0.0 being right at the centre, and 1.0 at the edge.

  /* calculate percentages from centre of screen */
  float horPct = (float)(x - windowWidth/2 ) / (float)(windowWidth/2);
  float verPct = (float)(y - windowHeight/2 ) / (float)(windowHeight/2);

Next we calculate how far to roll in each axis based on how far we have moved in each mouse axis from the initial position:

  /* get percentages for rotations */
  yRoll =  (1.0 - fabs(verPct))*(x - xDownPos);
  xRoll = (1.0 - fabs(horPct))*(y - yDownPos);
  zRoll = horPct/2*(yDownPos - y) - verPct/2*(xDownPos - x);

The amount to rotate is the total linear distance the mouse has travelled:

  /* amount is distance between current and initial pos */
  amount = sqrt( (xDownPos-x)*(xDownPos-x) + (yDownPos-y)*(yDownPos-y));

In our draw statement we now add the following code that uses the rotations that we calculated:

  /* no rotations */
  glLoadIdentity();

  /* move the image back from the camera */
  glTranslatef( 0.0, 0.0, zoom );

  /* rotate image about its origin */
  if( amount )
    glRotatef( amount, xRoll, yRoll, zRoll );

  /* draw the object */
  ...

If you try the code now, you will find that you can only do one rotation; performing a second rotation will snap the object back to its original starting point. We need a way to accumulate rotations so that past rotations are not lost. What we will do is to keep a storage rotation matrix around. When the mouse rotation selection is completed (by releasing the mouse button), this incremental rotation is added to the stored rotation.

Matrices in OpenGL are standard 4x4 graphics matrices. For those who know graphics, OpenGL uses the correct standard of having the translation on the right hand side, not that evil 'standard' of having the translation on the bottom. Instead of multiplying the matrices manually, we are going to get OpenGL to multiply them for us and read back out the result. The following function rotates by the amount given above, post multiplies by our stored matrix and reads the result back into the stored matrix.

  /* update our stored matrix with a new rotation */
  glLoadIdentity();
  if( amount )
    glRotatef( amount, xRoll, yRoll, zRoll );
  glMultMatrixf( rotationMatrix );
  glGetFloatv( GL_MODELVIEW_MATRIX, rotationMatrix );

We now just have to call this function when the mouse is released:

  switch( button )
  {
    case GLUT_LEFT_BUTTON:
       switch( state )
       {
          case GLUT_DOWN:
             . . .
             break;
          case GLUT_UP:
             mouseState = MOVE_NONE;
             updateRotation();
             amount = 0;
             break;

Try this now (you can get the code up to this point here to ensure that you have rotation and zooming working corectly from the menus. The object should move naturally with the motion of the mouse.

Next we will try reading in the model structure.

Loading Models

The models that we are going to be loading in are Quake II '.MD2' files. You can find them in the Quake directory under 'players', split into male and female subdirectories. So that you can follow along with my descriptions of what is happening, I am using as the example a file called 'tris.md2', although you can use any model that you happen to have.

Quake II models are generally split up into two files: the .md2 file contains all of the geometric information for that model, including the vertexes, the solid mesh information and the animation sequences. The second file (.pcx) contains the texture image that is applied to the surface. For now we will ignore the textre and concentrate on only the geometry.

The most important parts of the .md2 file format (at least for our purposes) are:

  • an array of 3D vertex coordinates
  • a triangle mesh list, indicating how to connect the points to form a solid
  • texture coordinates, indicating how to connect the textures to each triangle

The triangle mesh itself is not made up of vertexes, but of indexes to those vertexes. When a mesh is placed over a number of points, vertices are referenced several times as each triangle has to share edjes with those around it in order to create a solid. Storing only the vertices means that there is no waste of space in duplication of those vertices.

Sincle models in Quake II are animated (legs and arms move, etc), each position is also stored in the .md2 file as an animation frame. While the mesh indicies remain the same, the point list is repeated with each vertex in a slightly different spot. Each new block of points is called a frame, and there are enough frames in the file to cover the full range of character motion.

At the beginning of the file is a file header. It contains all of the information necessary to find out how many of each item there are (i.e. how many frames, vertices, trangles, etc.) and the offsets from the start of the file to each item. The file header is structured as follows:

  typedef struct _modelHeader
  {
    int ident;        // identifies as quake II header  (IDP2)
    int version;      // mine is 8
    int skinwidth;    // width of texture
    int skinheight;   // height of texture
    int framesize;    // number of bytes per frame
    int numSkins;     // number of textures
    int numXYZ;       // number of points
    int numST;        // number of texture
    int numTris;      // number of triangles
    int numGLcmds;
    int numFrames;    // total number of frames
    int offsetSkins;  // offset to skin names (64 bytes each)
    int offsetST;     // offset of texture s-t values
    int offsetTris;   // offset of triangle mesh
    int offsetFrames; // offset of frame data (points)
    int offsetGLcmds;
    int offsetEnd;    // end of file
  } modelHeader;

The first thing that we want to do is to load in the file and associate the above header to the start of it. Using the offsets, we then have easy access to the parts of the file we are interested in. It would be a good idea at this point to verify that we have a Quake II file via the 'ident' parameter of the header, but we will skip this step in this demonstration.

Let's start off by extracting the point data. At this point we will only be dealing with the first frame of data. The offset from the start of the file to the first frame of data is at modelHeader->offsetFrames. At the beinning of each frame there is a structure that defines the name of the model, and the scaling and translation used for that frame. Immediately following the frame header is the point data itself, packed into an integer as one byte each for the x, y, and z values, followed by a byte which indicates an index into a lighting array. There are exactly modelHeader->numXYZ integer packed vertices.

The two structures used to encode the pointlist is given by the following:

  typedef struct _framePoint
  {
     unsigned char v[3];             // the vertex
     unsigned char lightNormalIndex;
  } framePoint;

  typedef struct _frame
  {
     float scale[3];                 // vetex scaling
     float translate[3];             // vertex translation
     char name[16];                  // name of this model
     framePoint fp[1];               // start of a list of framePoints
  } frame;

Before we can use these points, we need to extract them back out into floating point values. We do this by taking each vertex, multiply it by the scaling factor, and then add in the translation. The code that performes this is the following:

  x = frm->scale[0] * frm->fp[i].v[0] + frm->translate[0];
  y = frm->scale[1] * frm->fp[i].v[1] + frm->translate[1];
  z = frm->scale[2] * frm->fp[i].v[2] + frm->translate[2];

Where i varies from 0 to the the total number of vertices given by modelHeader->numXYZ.

At this point we have all of the information we need to diplay the model as a point cloud. When I create the above x, y, and z points, I am storing them in a structure that I pass back to the calling application for display. You can download the code in it's entirety up to this point here

The first frame of 'tris.md2' loaded in and viewed from two different directions looks like the following:

OpenGL-model1.jpg OpenGL-model2.jpg

Isn't that cool! But hold on, it's not over yet! Now that we have our modeller working and can exctract data, the next step is to retrieve the triangle mesh so we can see how all of these points are meshed together into a solid.

There are modelHeader->numTris tringles in the mesh data, and their information is at offset modelHeader->offsetTris from the start of the model file. As we already have the point data, we don't need to repeat any point data here. Instead, each vertex in the triangle (i.e. three) is represented as an index into the point array we created earlier. Also stored with each triangle index trio, are the indexes into the s-t texture map coordinates, but we will be leaving texture mapping until next month (got to have something to look forward to, right?)

The data for each triangle is stored as the following structure:

  typedef struct _mesh
  {
     unsigned short meshIndex[3];     // indices to triangle vertices
     unsigned short stIndex[3];       // indices to texture coordinates
  } mesh;

Each structure completely defines one triangle. The structures are defined in the buffer one after another, up to the total number of triangles in the file. You can use the following code that uses these indicies to access the points of the triangle. The glVertex*() command in this instance takes a pointer to an array of X, Y, Z floating point values (3fv = vector to three floats).

  glVertex3fv( model->pointList[ model->triIndex[i].index[0] ].pt );
  glVertex3fv( model->pointList[ model->triIndex[i].index[1] ].pt );
  glVertex3fv( model->pointList[ model->triIndex[i].index[2] ].pt );

Where i ranges from 0 to the total number of triangles in the mesh.

That's it to making a mesh! Download the code and try it out. If you view the same 'tris.md2' now you should get something looking like the following:

OpenGL-model3.jpg OpenGL-model4.jpg

Wow, that's even cooler than the point clouds! Now we have a fairly functional model viewer that you can use to view Quake II models.

Things to try:

Modify the file routines to read in another type of file, such as VRML or some other game model.

  1. Change the mouse routines so that when you let go of the button while the mouse is moving, the model continues to rotate in the same direction. Letting go of the button while the mouse is not moving results in a stationary model.
  2. Any items that we are going to do next month.

Coming Up

You may have noticed that I commented out the point cloud display code to add the triangle mesh code. Next month we will be adding code that flips between the different display modes via the popup menu selection. We will also be adding a solid display mode with lighting, and adding in the texturing skin display. Once we get all of the display modes coded in, we will then tackle multiple frames and animate our model.

I hope you like this series of model building articles, as I certainly am having fun writing them. Send an e-mail to me if you have any comments or suggestions, or if you just want to let me know what you have been writing in OpenGL. I'm always interested in hearing of applications people are writing or have written.

From Italy (on business), ciao!