OpenGL on OS/2 - A Simple 3D Application

From EDM2
Jump to: navigation, search

Written by Perry Newhook

A Simple 3D Application

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

Introduction

Last month we created our first OpenGL program. It took a fair amount of work just to draw a simple 2D square on the screen! You may ask yourself what the point of it was. We could have easily drawn the same thing using OS/2 native commands with far less effort. Well, we will soon see that it does not take that much extra effort to change that 2D square into any 3D object we like. And adding dazzling lighting and animation to our object is practically just as easy, something that could never be done using OS/2 GPI commands alone. The first step is to change our current 2D program into a 3D one.

In our previous program, we used the command gluOrtho2D() to create our OpenGL window. There is also a 3D version of this command: glOrtho(). The general command is:

glOrtho( left, right, bottom, top, near, far );

With exception of the near and far parameters, this looks identical to the gluOrtho2D() command we used previously. Actually gluOrtho2D() simply calls glOrtho() with near=0 and far=1. We had already made made a 3D window but we didn't know it! To show this make the following changes to the program: change:

gluOrtho2D( -10.0, 10.0, -10.0, 10.0 )

to:

glOrtho( -10.0, 10.0, -10.0, 10.0, 0.0, 10.0 );

and change our glVertex2f() command to the following:

float coords[8][3] = { 5.0, 5.0, 1.0,
                      -5.0, 5.0, 1.0,
                      -5.0, -5.0, 1.0,
                       5.0, -5.0, 1.0,
                       7.0, 7.0, 10.0,
                      -7.0, 7.0, 10.0,
                      -7.0, -7.0, 10.0,
                       7.0, -7.0, 10.0 };
glBegin( GL_LINE_STRIP );
  // front face
  glVertex3fv( coords[0] );
  glVertex3fv( coords[1] );
  glVertex3fv( coords[2] );
  glVertex3fv( coords[3] );
  // back face
  glVertex3fv( coords[4] );
  glVertex3fv( coords[5] );
  glVertex3fv( coords[6] );
  glVertex3fv( coords[7] );
  glEnd();

Here I have used the vector form of the glVertex*() command to make it more readable and to save typing.

We will also change our polygons from filled to simple outline so we can see through and see the back of our square. The command to do this is glPolygonMode(). This command takes two parameters; the first specifies which side of the polygon the commands affects, either GL_FRONT, GL_BACK or GL_FRONT_AND_BACK. The second parameter specifies how to draw the indicated face; either GL_POINT, GL_LINE, or GL_FILL. The default is GL_FILL for both front and back facing polygons. The obvious question that stems from this is 'What is a front or back facing polygon?' Well as you might of guessed there is a function that specifies this. glFrontFace() lets you identify how front faces are identified. GL_CW indicates that the side of the polygon that has the vertices ordered in a clockwise direction around the centre of the polygon will be the front face. Similarly, GL_CCW specifies that the counter-clockwise face will be the front face. GL_CCW is the default face.

Place the command:

glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );

at the end of the initOpenGL() function. You can now see through the closer face of the cube to the back face. You can also clearly see that the back face is exactly the same size as the front face. This is the effect of using an orthographic transformation: depth has no effect on the apparent size of the image. If you want your scene to mimic real life, you will need to use gluPerspective() to create a perspective matrix. This gives the visual effect of objects in the distance looking smaller than similar sized objects in the foreground. An obvious example of this visual effect is the way a road that extends into the distance appears to diminish in width. We need to add one feature before we get into gluPerspective(): we need a resize handler to notify us that the screen size and aspect has changed. A resize handler can be created by deriving a new class from IResizeHandler:

#include <isizehdr.hpp>
class GLResizeHandler : public IResizeHandler
{
   public:
      GLResizeHandler( GLWindow *_window )
      : window( _window )
      { }
   protected:
      virtual Boolean windowResize( IResizeEvent &event );
   private:
      GLWindow *window;
};

And now we override the windowResize() function that gets called whenever the user resizes the window.

Boolean IVWindowResizeHandler::windowResize(IResizeEvent &event )
{
  // tell the paint handler the new size
  window->glPaintHandler().setWindowSize( event.newSize() );
  return false;
}

The idea here is to simply intercept the window resize message from OS/2 and call our OpenGL routines that resize the size of the OpenGL drawing canvas. We do not call OpenGL routines directly inside the resize handler because we want to keep all of our GL specific code isolated into one section, namely the GLPaintHandler class. This is not only good style from a readability and maintenance point of view, but will also become necessary when we decide to multithread our rendering later on.

Now we have to write the code that changes the window size. We add a function to our GLPaintHandler called setWindowSize(). This takes one parameter which is the new size of our window. In our create3DWindow() function we already have one of the commands we need. glViewport() sets the size of the gl display area to the size of the window. We can remove the call from this location and place it into our newly created setWindowSize(). We need to modify it slightly to deal with the parameter we pass instead of querying it directly.

Attach the resize handler to the canvas in the same way that the paint handler is attached using the command:

resizeHandler.handleEventsFor( &canvas )

You will also need to add the complementary command in the destructor and put:

resizeHandler( &canvas )

in the constructors initialization list. Run the program to see the changes.

You will notice that because of the way that we did our resizing, by making our rendering buffer match the size of the window, when the aspect ratio of the window is no longer 1:1 our squares are no longer square. That's because we did not change the dimensions of the viewing area with glOrtho(): it is still 20 units wide and 20 units high regardless of how big the window is. If this effect is not desired we can either change the parameters in glOrtho() when we do a window resize, or we can simply ensure that when we resize that our viewport remains square. Because the 3D perspective view has the aspect of the window built in to it, we do not run into this problem.

To add the perspective viewpoint, we have to change a few items of existing code. For this reason, there will be two sets of code available for download; the code up to this point will be one set and the modified code for perspective view will be will be the second set.

There are a few things that have to be decided upon before calling gluPerspective(). The first is the near and far clipping planes. These define part of the bounding box outside of which no rendering occurs. Choosing a smaller area to view speeds up the rendering process but you sacrifice area to display or to manipulate your scene. You also run the risk of putting your object completely outside of the viewing area resulting in nothing being displayed at all. The safest thing to do when first creating a scene is to create a ridiculously large viewing area to ensure that the scene gets drawn properly. After all of the other parameters are set and the scene is as you like it, you can then reduce the limits of the bounding box to increase performance.

To calculate the other sides of the bounding box, you also need to decide on the field of view in the x-z plane. This, together with the aspect ratio of the window determines the remaining four sides to fully encompass the viewing volume. The field of view is a fairly arbitrary setting to choose and choosing it wrong will simply result in your object looking too big or too small. The best way to think of this is for your eye to be a camera and the window your rendering gets displayed on a hole through which you can see a scene in the distance. The closer your eye gets to the hole, the larger volume that can be seen through it. By figuring out how far away you normally are from the screen for viewing, and how large the window is (which may change as the user resizes the window), you can figure out the field of view required by your application. If you are not too picky, you can figure out the expected size of the window and choose a fixed field of view. For a 20cm high window and the user is 60cm away, the vertical field of view works out to be about 20 degrees. Once we have decided upon the field of view, we need the aspect ratio which we get from our window resize handler.

Place the following commands inside our setWindowSize() function.

glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( FOVY, (float)window.size().width() /
     (float)window.size().height(), MINVIEWDISTANCE, MAXVIEWDISTANCE );
glMatrixMode( GL_MODELVIEW );

Since gluPerspective() modifies how an object is projected onto the screen, we must activate the projection matrix before calling it just like we did with glOrtho(). FOVY, MINVIEWDISTANCE and MAXVIEWDISTANCE are simply defines that are set to the boundary limits that we calculated earlier.

There are some minor procedural changes that we have to do to get this to display properly. First we have to move the object away from the viewing position so that we can see it properly. We do this by translating the object -50 units down the z-axis (remember by default we are looking down the -z axis). glTranslatef( x, y, z ) moves the object x, y and z units from the current position. Another change that we did was get rid of the create3DWindow() function. Since gluPerspective() inside the create3DWindow() function has to be called every time the screen aspect changes (i.e. when the screen is resized) that functionality can be combined into the setWindowSize() function. In our constructor, instead of calling the create3DWindow() function, we have to call the setWindowSize() function with the canvas size as a parameter.

Now when we run this we can see that the square in the background is smaller than the square in the foreground. We can now see the true 3D capabilities of OpenGL (well a tiny bit anyway). Playing around with the distances between the squares or the translation distance before drawing will enhance or lessen the effect. You will also notice that changing the aspect ratio of the window by resizing will not distort the image. This is due to the fact that the gluPerspective() command in the resize function uses the aspect ratio as one of the parameters. This alleviates you as a programmer from having to know (or care) what the screen you are writing to looks like.

Conclusion

Next month we will get more into translations and rotations and create a functioning solar system complete with planets and moons that rotate about their axis, and multiple viewpoints to view them from. This will enable you to get used to multiple camera positions, pushing and popping off matrix stacks, and object manipulation using glTranslate() and glRotate().