OpenGL on OS/2 - A More Complex Scene

From EDM2
Revision as of 20:49, 19 March 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Perry Newhook

Introduction

Well now that we have created our first real 3D OpenGL application, our next program will use the matrix stack to allow us to create a more complex scene. Once we learn how to rotate and translate parts of our image with ease, we can create any scene we wish.

Fix Alert!

It has come to my attention that some of you are having problems compiling the applications given in the last few articles. The problem arises in an incompatibility between the Open Class library file igrafctx.hpp and the OpenGL file pgl.h. Instead of waiting for IBM to supply a fix, a quick solution suggested is to comment out the '#define None 0x0' line in pgl.h and replace it with something like '"#define Pgl_None 0x0'.

//#define None 0x0
#define Pgl_None 0x0

This should enable you to compile without any further problems.

Thanks to Alex Spanidis for bringing this to my attention and to David Janovy for the suggested solution.

The Matrix Stacks

In the process of positioning parts of our scene, we make extensive use of the matrix stack. This was covered a bit in column two, but for completeness, we will cover it more extensively here. There are actually three matrix stacks to use. They are the projection matrix stack, the modelview stack, and the third is the texture stack. You can select between the three stacks with the glMatrixMode() command and the parameters GL_PROJECTION, GL_MODELVIEW and GL_TEXTURE. We will only deal with the first two types of matrix stacks in this column, as that is all that you need to display an image. Texture matrices will not be dealt with here but will be covered in full on its own in a later column.

Matrices can be pushed on (saved) and popped off (restored) of the stack with the glPushMatrix() and glPopMatrix() commands. All other commands that operate on the matrix stacks operate on the topmost matrix. Everything that we have been doing with the matrix stacks in our programs to date have been simply accessing the topmost matrix on the stack. You can think of the stacks as a place where you can save previously compiled matrices with the push command, and restore them later when you need them with the pop command. It is an error to pop off of an empty matrix stack or to push onto a full stack.

Since the projection matrix is typically only used to set up the viewing volume of the scene, its depth (the number of matrices that can be stored on it) is quite small -usually two but can be more. Typically you do not want to compose projection matrices so most applications simply call glLoadIdentity() before performing the desired projection functions ( like gluOrtho*() or gluPerspective() ). One possible use of the projection stack would be to draw 2D text on a 3D window. Since text is most easily drawn using glOrtho2D() you could save the current 3D projection, switch to ortho, draw the text and restore the previously saved projection.

The main matrix stack for rotation and translation of objects is the modelview matrix stack. It has a minimum depth of 32 but can be greater depending upon implementation. If you want to know how deep the modelview stack is for OS/2, you can call the function glGetIntegerv() with the parameter GL_MAX_MODELVIEW_STACK_DEPTH to determine its exact depth.

We have used the modelview matrix before in last months' example. In that example we issued the following commands:

   // select modelview matrix
   glMatrixMode( GL_MODELVIEW );
   // no rotations
   glLoadIdentity();
   // move the squares back a bit
   glTranslatef( 0.0, 0.0, -50.0 );

What we are actually doing in the above is first specifying that we are to use the modelview matrix. Next the command glLoadIdentity() stores an identity matrix on the top of the stack. An identity matrix is one that has no rotation and no translation. It is characterized by a 4x4 matrix of all zeros except for those matrix elements that have x=y in which case the element value is equal to one. This matrix has the property that any matrix M multiplied by the identity matrix equals the original matrix M (M*I=M). Any matrix operation such as glRotate*() or glTranslate*() first composes a matrix to do what the command specifies, and then post multiplies it to the top matrix of the selected matrix stack. Therefore whenever we need to clear any previously done translations or rotations, such as the beginning of drawing scene, we need to do a glLoadIdentity() command. The next command that we did composes a translate matrix that translates by fifty units in the negative z direction. This matrix is then multiplied by the matrix on the top of the stack (identity), and is placed on the top of the stack replacing what was previously there. Calling other translate and rotate commands would likewise multiply matrices created by them to the top matrix on the stack and replace it.

To use the glTranslate*() command we specify three parameters x, y, and z, which indicate how far to move in x, y and z coordinates from the current location. Specifying glTranslatef( 0.0, 0.0, 0.0 ) doesn't do anything while glTranslatef( 10.0, 0.0, -5.5 ) moves the drawing location 10 units in the x direction and -5-5 units in the z direction. The glRotate*() command requires four parameters; an angle to rotate, and x, y and z value that compose a vector to rotate about. glRotatef( 30.0, 1.0, 0.0, 0.0 ) rotates 30 degrees about the x axis, and glRotatef( 10.0, 0.0, 0.707, 0.707 ) rotates by 10 degrees about a vector that forms a 45 degree angle between the y and z axis. The vector specified should be a unit vector (i.e. x^2+y^2+z^2 = 1) but if it isn't, gl will convert it for you into a unit vector which causes a slight performance hit.

Calling successive rotate and translate commands is how we build up our scene. In this column we are going to build a solar system as our example program. For now lets think of how we would draw the Sun, Venus, the Earth and its moon and Mars piece by piece:

  1. Translate and rotate to position what we are drawing
  2. Draw the sun
  3. Remember where we are
  4. Rotate about the sun and translate out to position Mars
  5. Draw Mars
  6. Restore old position (to sun)
  7. Rotate about the sun and translate out to position the Earth
  8. Draw the earth
  9. Rotate about the earth and translate out to position Earth's moon
  10. Draw the moon
  11. Restore old position (to sun)
  12. Rotate about the sun and translate out to position Mars
  13. Draw Mars

Since each planet has a different period of rotation about the sun, we must continually return our position to the sun, rotate the required amount and then translate back out and draw. The Earths' moon rotates about the Earth, not the sun so that is why there is no restore before rotating the moon. If we had multiple moons, we would do a similar thing we are doing with the sun and store and restore its position, before we draw each of the moons.

Using Timers

We have our structure, so we are almost ready to draw our scene. There are however a few things we need to set up before we start. The first is the use of timers. We are going to synchronize our planet rotation with a timer so the rotations are accurate with respect to one another and occur at a fixed rate, kind of like a sped up version of reality. Since our timer function calculates new rotation angles and initiates a new draw, I have put our timer in the GLPaintHandler function. All we need is a function that takes no parameters and returns void. We will tell OS/2 to call this function at a rate we specify. We now need to create a class that represents our timer:

  #include <itimer.hpp>
  void timerFn();   // our timer function
  ITimer timer;     // itimer class

We specify the timer interval with the setInterval() member function of ITimer and tell it to start with the start() member function (fairly logical huh?). The code to do this is as follows:

  // set timer
  timer
    .setInterval( 100 )
    .start( new ITimerMemberFn0<GLPaintHandler>( *this,
       GLPaintHandler::timerFn ));

Without getting too much into it, this function specifies that I want an interval of 100 milliseconds, and I will start it in the class GLPaintHandler, the reference to the class being *this (the class we are in), and the function we are calling is GLPaintHandler::timerFn(). For those of you who do not read C++ that well, just trust me that it works.

One problem with the timer is that we must handle shutting it off properly. Trying to stop it in the destructor is too late because it can go off while we are deallocating objects, resulting in an exception being thrown. The solution is to declare a handler that gets called before the destructors are called, in which we can properly shut down the timer. To do this we create a class derived from <ihandler>, and we override the destroy() function. The class definition is as follows:

  #include <ihandler.hpp>
  class GLPaintDestroy : public IHandler
  {
    public:
      GLPaintDestroy( GLPaintHandler *_paintHandler )
        : paintHandler( _paintHandler )
        { }
      protected:
        virtual Boolean dispatchHandlerEvent( IEvent &event );
        virtual Boolean destroy( IEvent &event );
      private:
        GLPaintHandler *paintHandler;
  };

Inside the dispatchHandlerEvent we check for the WM_DESTROY message, when we get it we call destroy() which stops the timer.

Auxiliary Library Functions

To draw our sun, planets and moons we need to draw a sphere. To create this sphere from point, line and polygon primitives would be prohibitive and difficult. Luckily one of the add-on libraries, the auxiliary libraries, contains several pre-made models that we can use. The auxiliary library, identified by routines prefixed by aux*, actually contain all of the routines necessary for window initialization and display, keyboard and mouse input, and background processing. All we intend to use here though are the routines that define the models. To use the routines we simply have to include the auxiliary library include file aux.h, and to link with libaux.lib.

Each three dimensional model that is defined comes in two types: a wireframe version and a solid version. The solid version contains shading and surface normal information that is used when we apply lighting. The model functions are as follows:

  void auxWireSphere( GLdouble radius );
  void auxSolidSphere( GLdouble radius );
  void auxWireCube( GLdouble size );
  void auxSolidCube( GLdouble size );
  void auxWireCube( GLdouble width, GLdouble height, GLdouble depth );
  void auxSolidCube( GLdouble width, GLdouble height, GLdouble depth );
  void auxWireTorus( GLdouble innerRadius, GLdouble outerRadius );
  void auxSolidTorus( GLdouble innerRadius, GLdouble outerRadius );
  void auxWireCylinder( GLdouble radius, GLdouble height );
  void auxSolidCylinder( GLdouble radius, GLdouble height );
  void auxWireIcosahedron( GLdouble radius );
  void auxSolidIcosahedron( GLdouble radius );
  void auxWireOctahedron( GLdouble radius );
  void auxSolidOctahedron( GLdouble radius );
  void auxWireTetrahedron( GLdouble radius );
  void auxSolidTetrahedron( GLdouble radius );
  void auxWireDodecahedron( GLdouble radius );
  void auxSolidDodecahedron( GLdouble radius );
  void auxWireCone( GLdouble radius, GLdouble height );
  void auxSolidCone( GLdouble radius, GLdouble height );
  void auxWireTeapot( GLdouble size );
  void auxSolidTeapot( GLdouble size );

As you can see there are a lot of useful models predefined for you. We will be using auxWireSphere() and auxSolidSphere().

The Sample Application

Since we added a bit of code that is necessary before we render our scene, the complete source code developed so far can be downloaded glcol4a.zip here.

We have outlined what we need to draw our scene, and all there is to do now is to translate our outline into gl calls. Our commands will be inserted into the GLPaintHander::paintWindow() function right after the glClear() command.

First as discussed above, we need to cancel out any translations and rotations on top of the matrix stack by loading it with the identity matrix:

  // no rotations
  glLoadIdentity();

Now we need to position the scene away from the camera. For a first guess we will choose 500 units. This can and will change depending upon how much we want to put in our scene.

  // move the image back from the camera
  glTranslatef( 0.0, 0.0, -500.0 );

Next we start to draw our scene, starting with the sun. To get an idea of what the model of the sphere looks like, we will first draw all of the bodies as wire spheres, changing them to solid spheres later. The radius is arbitrary and not to scale but you are free to choose any size you wish.

  // draw a yellow sun
  glColor3f( 1.0, 1.0, 0.0 );
  auxWireSphere( 20.0 );

As a first test, lets just draw the earth and its moon. Because we are not drawing any other planets, we have no need for glPushMatrix() and glPopMatrix() at this time. We will use it a little later when we make the scene more complicated by adding other planets and rotations.

The current drawing position is in the centre of the sphere we just drew. Initially the drawing position is directly in front of the camera. To picture where items are going to be drawn in the scene, it is necessary to understand the concept of a reference frame. Create a reference frame with the first three fingers of your right hand. Make your thumb point straight up, your index finger point straight ahead, and your 'driving' finger point to the left. Each finger should be at a 90 degree with respect to the other finger. Now give your fingers names: your thumb is the Y-axis, your index finger is the Z-axis and the driving finger is the X-axis. While keeping keeping your fingers at the same position relative to each other, turn your hand so you are pointing at yourself. Your thumb should still be pointing up. This is the initial orientation of the reference frame in OpenGL without and rotations: looking at it from a screen perspective, the x-axis increases towards the right, the y-axis increases towards the top and the z-axis increases towards the viewer (-z into the screen).

Now we can more fully understand our first operation: we translated 500 units along the negative z axis, effectively pushing the reference frame (the drawing point) away from us so we are looking at it. Because we are using a perspective transformation the further we push our reference frame away, the smaller it gets, causing objects drawn there to appear smaller.

We are going to view our solar system from directly above the plane of rotation. Therefore, using our hand as a reference, we realize that to rotate the planets, we need to rotate about the z-axis. We will then translate out along one of the other axis (I'll pick the x-axis) to position the planets.

Add the following to the paint routine:

  // position the earth
  glRotatef( earth, 0.0, 0.0, 1.0 );
  glTranslatef( -60.0, 0.0, 0.0 );
  // and draw it blue
  glColor3f( 0.0, 0.0, 1.0 );
  auxWireSphere( 5.0 );

  //position the earth's moon
  glRotatef( earthMoon, 0.0, 0.0, 1.0 );
  glTranslatef( -10.0, 0.0, 0.0 );
  // and draw it grey
  glColor3f( 0.5, 0.5, 0.5 );
  auxWireSphere( 1.0 );

What we have done is to rotate the position for the earth about the z-axis, translate out and draw it. The drawing point is at the centre of the earth which is exactly where we need it to be to position the moon. We do the same thing that we did before to position the earth: rotate by the amount we need, translate out to position the moon, and draw it. Calculate the earth and earthMoon positions with the following code. Place it in the routine GLPaintHandler::timerFn().

  // recalculate positions
  earth += 1.0;
  if( earth > 360.0 )
    earth -= 360.0;

  earthMoon += 3.0;
  if( earthMoon > 360.0 )
    earthMoon -= 360.0;

  window.refresh();

The code simply increments the moon position three times as fast as the earth and signals a redraw once it is done. Since the timer function is getting called every 100 milliseconds, we can figure out the rotation period if we so chose.

Run the program so far to see what happens. You should see a small grey ball, rotating about a larger blue ball, which in turn is rotating about a stationary yellow ball. At this point you an change all of the auxWireSphere() calls to auxSolidSphere() ones so that the sun and planets are solids instead of wireframes.

The code up to this point can be obtained glcol4b.zip here.

Lets try now to add some more complexity by adding a few more planets. We will also change our rotation times to reflect a scaled reality. Add the loop scaling for Venus and Jupiter to timerFn():

  venus += 1.613;
  if( venus > 360.0 )
    venus -= 360.0;
  jupiter += 0.0843;
  if( jupiter > 360.0 )
    jupiter -= 360.0;

Also change earthMoon increment to 13.36. The planets and moon should now rotate at correct rates in relation to each other.

Now we need to add the drawing commands for Venus and Jupiter to the paintWindow() function. We must go back to our original plan where we decided we had to remember where we were before we drew. Looking at the code that we already have and matching it to the plan, we see that we already have items 1,2 and 7 through 10 completed. We will now add items 3 through 6 and 11 through 13.

3 through 6 are as follows:

  3) Remember where we are
  4) Rotate about the sun and translate out to position Mars
  5) Draw Mars
  6) Restore old position (to sun)

We can translate this line by line to OpenGL commands:

  // remember where we are
  glPushMatrix();
  // position venus
  glRotatef( earth, 0.0, 0.0, 1.0 );
  glTranslatef( -40.0, 0.0, 0.0 );
  // and draw it green
  glColor3f( 0.0, 1.0, 0.0 );
  auxSolidSphere( 5.0 );
  // restore old position
  glPopMatrix();
  // remember where we are
  glPushMatrix();

Items 11 through 13 are:

  11) Restore old position (to sun)
  12) Rotate about the sun and translate out to position Mars
  13) Draw Mars

Converted to GL this becomes:

  // restore old position
  glPopMatrix();
  // position mars
  glRotatef( mars, 0.0, 0.0, 1.0 );
  glTranslatef( -80.0, 0.0, 0.0 );
  // and draw it green
  glColor3f( 0.0, 1.0, 0.0 );
  auxSolidSphere( 4.0 );

Note that we had to push the matrix back onto the stack again after we popped it off in step 6. This is because we pop off the remembered position again before we position and draw Mars. Since glPushMatrix() and glPopMatrix() must appear in pairs, we put in the extra push so that our pop is valid.

Compile this and run it. You should see three planets rotating around the sun each with a different period, and a small moon rotating about the earth. Congratulations! You have created your first real 3D application. Every scene created with OpenGL follows the exact same methods that we performed here: position a piece of the scene, draw a piece, go back to previous location or go to a new location and draw again. The timer routine can be used again, any time that an animation in the scene is required.

Z-Buffering

There is one change to our initialization routine that we did not have in the examples from previous columns; in this column we are enabling the z-buffer (or depth buffer). It gets enabled by the inclusion of the commands:

  // enable depth buffer
  glEnable( GL_DEPTH_TEST );

and modifying glClear() to:

  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT  );

What this does is during rendering, instead of simply placing every polygon rendered in the scene into the draw buffer, it writes the depth of the polygon (or the z-value, hence the name) into this buffer. Every time it draws something to a position, it checks to see if there was something drawn there previously. If there was something drawn there previously, then the new object is only drawn if it has a lower z-coordinate value, i.e. in front of the other object.

That's basically all there is to z-buffering. Just remember to clear the z-buffer at the beginning of the drawing cycle just like you clear the colour buffer. To see the effect of the z-buffering, rotate the solar system we just drew on its side a bit. Right after we position the sun away from the camera, lets rotate our scene so that we are viewing it almost edge on. Place the following glRotate() command after the first glTranslate() command in the paintWindow() function so that it looks like the following:

  // move the image back from the camera
  glTranslatef( 0.0, 0.0, -500.0 );
  // and rotate for the viewing position
  glRotatef( -70.0, 1.0, 0.0, 0.0 );

Running the program now we can clearly see that when the moon passes in front of the earth, that portion of the earth is obscured. A similar effect occurs when the earth passes in front of the sun and vice-versa.

Now remove the depth buffering (just comment out the glEnable( GL_DEPTH_TEST ) command in the initOpenGL() function. Running this version, you can instantly see that whatever was drawn first, always gets drawn in the foreground; the moon always obscures the earth and the sun, and the earth always obscures the sun. If you are drawing a scene where this depth compensation is not needed, then turn it off as the calculations while quick, do take a little time to calculate.

Things to Try

Here are a few things to try on your own if you want. They are listed in increasing order of difficulty:

  • Make the earth itself rotate (Hint: to see this you need to show the spheres as wireframe)
  • Put a satellite in orbit around the moon
  • Create a second moon with a satellite around it (Hint: nest the push pop pairs)
  • Create the entire solar system with correct periods of all the planets and major moons. (don't forget a ring around Saturn!) The times you need are:
Mercury 0.24 years
Venus 0.62 years
Earth 1.00 year
Moon 27.32 days
Mars 1.88 years
Jupiter 11.86 years
Callisto 16.69 days
Ganymede 7.16 days
Europa 3.55 days
Io 1.77 days
Saturn 29.46 years
Titan 15.95 days
Uranus 84.07 years
Triton 5.88 days
Pluto 248.6 years

Next month we will continue this example by showing how to view the solar system from different viewpoints in the scene (we will watch the sun rise and the moon pass overhead from the planets surface). We will also introducing lighting, and use it to give the sun some light to cast onto the planets. We will also show how different lighting parameters can cause different effects on our scene. Until then, happy GL-ing!

[NOTE: Here is a link to a zip of the glcol4final.zip final source code for this article. Ed.]