OpenGL on OS/2 - A Simple Application

From EDM2
Revision as of 04:46, 5 March 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Perry Newhook

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

Introduction

Last month I introduced OpenGL for OS/2 as a 3D graphics language. In this column, I will show how to create an OpenGL application to perform some simple drawing. This application will be the basis for most of our demonstrations to come in future columns.

The first thing we have to do is create a standard window with a client area for drawing an OpenGL object. Although I will be demonstrating the OpenGL code using VisualAge C++ and the Open Class user interface libraries, I will try to separate out the OpenGL code so that you can replace the window functions with your own code if you are not using VisualAge. The VisualAge specific code is actually very small so conversion to whatever API set you are using should not be a problem.

First we need a paint handler. This is a standard paint handler derived from the IBM User Interface Library. What happens is when the application receives a notification that it needs repainting, instead of receiving a WM_PAINT message in the message handler function, the UIL simply calls the function called paintWindow() which is a virtual function of the IPaintHandler class.

#include <ipainhdr.hpp>

class GLPaintHandler : public IPaintHandler
{
  public:
    // openGL initialization routine
    void initOpenGL( IWindow &_window );
    protected:
        virtual Boolean paintWindow( IPaintEvent &event );
  private:
    HGC hgc;
    IWindow &window;
};

I have also placed the OpenGL initialization routine inside the GLPaintHandler class for convenience. The implementation of this initialization will be discussed shortly. First we will create a simple frame window with a title bar. Inside the frame window is a child window called a canvas. It is to this canvas that we will attach the paint handler because that is where we want our OpenGL painting to occur.

#include <iframe.hpp>
#include <icanvas.hpp>

class GLWindow : public IFrameWindow
{
  public:
    // constructor
    GLWindow();
    ~GLWindow();

  private:
    // paint handler
    GLPaintHandler paintHandler;
};

// constructor
GLWindow::GLWindow()
: IFrameWindow( "OpenGL Demonstration", 0x1000, IFrameWindow::defaultStyle()
         | IFrameWindow::noMoveWithOwner | IWindow::synchPaint ),
   canvas( 0x8008, this, this )
{
  // setup
  setClient( &canvas );

  // attach the paint handler
  paintHandler.handleEventsFor( &canvas );

  // draw on screen
  sizeTo( ISize( 500, 400 ));
  setFocus();
  show();

  // initialize openGL
  paintHandler.initOpenGL( &canvas );
}

// destructor
GLWindow::~GLWindow()
{
  // detach the paint handler
  paintHandler.stopHandlingEventsFor( this );
}

That is all of the VisualAge specific code that we need to create our window. All further code will be simple OpenGL API calls although they will be in functions inside of our GLPaintHandler class.

One thing to notice is that we initialize OpenGL after we created, sized and shown our window. This is necessary as the creation of the OpenGL context will fail if the window we are binding the context to is not displayed first. This is a common oversight when first creating an OpenGL application. If you can't get anything to display, the most likely cause is the context creation failed because the window was not visible.

Now we get into the PGL commands that link the OpenGL commands to our display device. Remember that OpenGL by itself has no windowing commands; that functionality is handled by the PGL API command set. The first thing we need to do is to create a graphic context for OpenGL to draw into. This is done using the pglCreateContext() command. One item of information that command needs is how we want the display configured: whether we are writing directly to the screen or indirectly through the use of OS/2 BitBlit. This command will also configure the colour resolution and the number of screen buffers we need.

'Number of buffers?' you say? Yes, OpenGL has many more than just the one buffer that you use to write to the screen. There are actually eleven buffers that OpenGL defines, although you will generally only use a small subset for any one application. The buffer definitions are:

  • Colour Buffer: This is usually the buffer that image data gets written to. It contains either color-index data, RGB data, or RGBA data. (A stands for Alpha which is used in colour blending. This will be discussed in an upcoming issue.) There can be up to four colour buffers, front-left, front-right, back-left and back-right. The left and right buffers are used for stereo viewing. If stereo is not supported, OpenGL uses only the left buffers. Front and back buffers are used for double buffered images. Single buffered images use only the front buffer. Double buffering is a method where you draw the image on the back buffer, and when complete, swap it with the front buffer. In this manner the user sees the screen update instantaneously instead of watching the drawing occur. There are also up to four non-displayable auxiliary colour buffers that are left for the user to define their purpose.
  • Depth Buffer or Z-Buffer: This is a buffer used to calculate what object is in front of another object for hidden surface removal.
  • Stencil Buffer: Used to restrict drawing areas to certain portions of the screen. In this manner if you have a static part of the scene that only needs to be drawn once, you can use the stencil buffer to restrict drawing to only the part of the screen that requires updating. For example an instrument panel on a flight simulator may need to be drawn only once while the scene through the window needs to be updated every frame.
  • Accumulation Buffer: Used to accumulate multiple images into a final image before drawing to the screen. One use of this is in antialiasing images, where the scene is redrawn several times, each time in a slightly shifted position to remove aliasing problems and the dreaded line-jaggies.

We also need to define the colour resolution. OpenGL allows both colour index and RGB values for colour. Since colour index is not recommended and many of the OpenGL functions are not defined in colour index mode, we will be exclusively using RGB values for colours. Now that we have decided to use RGB, we have to decide how many bits to allocate to each colour. Since we want to allow our application to run on 8, 16 and 24 bit colour depth screens, we will choose a bit number per colour that will fit into an 8 bit colour depth. OpenGL will then give us the largest size available but no smaller than what we specified. For example if we wanted our colour depth to be 5 bits per colour, the total bit depth is 15 bits. Therefore our request will succeed if our application was running on a computer with 16 or 24 bit colour, but would fail on an 8 bit colour display. For our demos we will pick 2-2-2 for our RGB colour depth. This allows us to run on all display types. When we specify a colour in OpenGL, we do not care what the colour depth is. OpenGL will give us the best approximation to the requested colour given the colour depth it actually received from the display.

We now have to call pglChooseConfig() and tell it our colour depth, how many buffers we want and of what type. We do this by passing it an attribute list. This is a simple list of attributes and values that terminates with the value 'None'. In our example, we want double buffering and an RGB depth of 2-2-2. Therefore our attribute list becomes:

int attribList[] = { PGL_RGBA, PGL_RED_SIZE, 2, PGL_GREEN_SIZE, 2,
                     PGL_BLUE_SIZE, 2, PGL_DOUBLEBUFFER, None };

The only other parameter for pglChooseConfig() is an anchor block handle, which we get with the command:

HAB hab =  IThread::current().anchorBlock();

pglChooseConfig() returns a pointer to a visual configuration (PVISUALCONFIG) that matches or exceeds what we requested. This structure is required by pglCreateContext() which was mentioned earlier. This structure should not be modified in any way and can be freed when we are done with it, which is very soon. The other parameters of pglCreateContext() allows us to share our display list among other contexts, and specify whether or not we have a direct context. A direct context permits OpenGL to write directly to the video buffer, bypassing the OS/2 drawing routines entirely. An indirect context forces OpenGL to use GpiBitBlit to draw on the screen. Although an indirect context causes the screen to act as a bitmap allowing the user to mix in Gpi drawing commands, an indirect context is usually far more efficient in screen drawing speed.

Now that we have created our context, we have to make it current for the thread and attach it to a certain window handle. This is accomplished with the command pglMakeCurent(). Using this command, we can have many different contexts created and flip between them, however only one context can be active per thread at any one time.

Now we have everything we need to create our context and attach it to our window. The code in its entirety to do this is:

void GLPaintHandler::initOpenGL( IWindow &_window )
{
  int attribList[] = { PGL_RGBA, PGL_RED_SIZE, 2, PGL_GREEN_SIZE, 2,
    PGL_BLUE_SIZE, 2, PGL_DOUBLEBUFFER, None };

  // save a reference to the window
  window = _window;

  // get a handle to anchor block
  HAB hab =  IThread::current().anchorBlock();

  // create a visual config
  PVISUALCONFIG pVisualConfig = pglChooseConfig( hab, attribList );

  // create an OpenGL context
  hgc = pglCreateContext( hab, pVisualConfig, NULL, true );

  // and make the context we got the current one
  Boolean val = pglMakeCurrent( hab, hgc, window->handle() );

  // free the visual config created above
  delete [] pVisualConfig;
}

Now assuming that pglMakeCurrent() returned true, everything went ok and we are now able to draw into our window! The first thing we need to do before we draw is to clear both front and back buffers.

First we specify the clear colour. We want to clear the screen to black so:

glClearColor( 0.0, 0.0, 0.0, 1.0 );

There are related commands that specify clear values for the other definable buffers; glClearAccum() sets the clear value for the accumulation buffer, glClearDepth() for the depth buffer, and glClearStencil() for the stencil buffer.

Most OpenGL colours are specified as a floating point value that ranges from 0.0 for off, to 1.0 for full intensity. Some functions allow different formats for colour, but all colours eventually get converted and clamped to the 0.0 to 1.0 range. The fourth component is the alpha value which was briefly discussed in the previous issue. Since we are not using the alpha value, where it is required we set it to 1.0.

Now that we specified the colour we are using, we want to tell OpenGL that we are going to draw to the front and back buffers:

glDrawBuffer( GL_FRONT_AND_BACK );

Now we say that we want to clear, and tell it exactly which buffers we are clearing:

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

If we wanted to clear other buffers such as the accumulation buffer we could have specified those as well.

For double buffered animation, we draw our scene to the back buffer and once completed, swap the back with the front. What we have drawn now becomes visible and we draw our next scene on the new back buffer. Our previous commands specified that we wanted to draw to both the front and the back buffers simultaneously so we have to change that to the back buffer only:

glDrawBuffer( GL_BACK );

Remember that OpenGL acts like a state machine. If we did not do the above command. our drawing would go to both the front and back buffers because that was the last command that specified where we wanted our image drawn.

Everything is almost set for us to draw our image but we first must set up how we are looking at the screen, or in other words our camera setting and position. For 3D objects this can get quite complicated, so we will leave this until a later column to explain. For now just to get something on the screen, we are going to draw a 2D object. To do this we tell OpenGL that we want an orthographic projection of our object onto the screen. This ignores (for now) the effect of distance and viewing angle on our object. The command to do this is part of the GL utility commands. These commands are either a simplification of regular OpenGL commands or more commonly, a compilation of several lower level GL commands that are normally called together. These commands are identified by the prefix glu. The command we need is gluOrtho2D(). All you specify are the coordinates of the limits of the screen. Think of it as positioning a piece of graph paper behind your window. The borders of the window define the limits of the viewable space, and drawing a point at a certain (x,y) coordinate will place that point on the graph paper. We will choose (10,10) and (-10,-10) to mark the upper right and lower left boundaries of our viewable area. The general command is: gluOrtho2D( left, right, bottom top ); and in our case it is gluOrtho2D( -10.0, 10.0, -10.0, 10.0 );

This command is executed on the projection matrix stack because it affects the viewing volume. Therefore we must first select the projection matrix with the command glMatrixMode() with the parameter GL_PROJECTION. Once we are finished with changing the viewing volume we must set our transformation back to the model matrix stack by calling the same function with a GL_MODELVIEW parameter.

Once we have set up definition of our viewscreen, everything is set for us to draw our image. For now we will draw a simple red square bounded by (5,5), (-5,-5). A more thorough description of how to draw different objects will be presented next month.

glColor() is the command needed to specify our colour that we wish to use. Again because OpenGL is a state machine, our colour must be specified first. That same colour will be used for every subsequent drawing until we call glColor() again. glColor(), along with many others, is not a callable function but instead refers to a family of functions. This type of function uses a postfix qualifier at the end of the function name to specify the number of parameters and their type. I generally like to use floating point numbers for colours because it essentially gives me an infinite array of colours to work with. The hardware interface will translate what I request into the closest colour match that it can. This allows me to write programs that will run on 8-bit colour machines, but will also use a 24 (or 32 or 64) bit machine to its full capability. Colours are declared by specifying the RGB components of the colour you wish to use. We want red, and we are specifying three floating point parameters so the command becomes:

glColor3f( 1.0, 0.0, 0.0 );

If we wanted to specify the colour as unsigned bytes, the command would have been:

glColor3ub( 255, 0, 0 );

Now we issue the command to draw the square. First we tell OpenGL that we are going to draw a square, or more generally, a quadrilateral. This is done by specifying the vertices of what we want to draw between a glBegin() and glEnd() pair.

glBegin( GL_QUADS );
  glVertex2f( 5.0, 5.0 );
  glVertex2f( -5.0, 5.0 );
  glVertex2f( -5.0, -5.0 );
  glVertex2f( 5.0, -5.0 );
glEnd();

Because we said that we were drawing GL_QUADS, OpenGL expects a multiple of 4 vertex commands in between the glBegin() and glEnd() commands. If there are more than 4 vertices specified, multiple polygons are drawn. If there is not a multiple of 4 vertices specified, the last incomplete polygon is not drawn.

We have now completed everything we need to draw our square! Just remember to link with opengl.lib, and libaux.lib, (and compile with /Gm+ or multithreading on for the Open Class libraries) and it should work. Also remember to define 'OS2' on the compiler command line for the OpenGL libraries. A complete listing is included at the end of the article showing the exact include files that need to be specified. I have also added some cleanup commands in the destructors just to be tidy.

Next month we will expand on our program and describe how to set up a 3D viewing transformation, and later how to draw more complicated objects, how to do rotation, translations. and scaling. Until then try replacing GL_QUADS above with the other possibilities: GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_POLYGON, GL_QUAD_STRIP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN. Don't forget to change the number of vertices to match what you are drawing. Also try to change colour part way through rendering by inserting glColor() commands between the glBegin() and glEnd() command pair.