Chapter 6. Creating a Windows OpenGL View Class

 

To a programmer, it’s never concrete ’til they see code.

 
 --John Vlissides

Encapsulating OpenGL

So far we’ve covered the basic OpenGL operations and have learned how to create an OpenGL window under Windows. The simplest way to use OpenGL is to encapsulate and keep all this functionality in one place. After all, the process of creating an OpenGL window when using the Microsoft Foundation Classes (MFC) is going to be the same, no matter what the objective of your OpenGL program is going to be. Thus we can encapsulate all this initialization and setup, using C++ as a reusable class for viewing OpenGL scenes. If you’ve never used MFC before or if you use another framework, the next section will give you a quick overview of how MFC works. If you’re an old hand at MFC programming, you’ll recognize what’s going on. In any event, the class structure we create will be used throughout the rest of the book, in order to simplify the creation of OpenGL programs. If fact, you might want to use the class for your own OpenGL programs.

Architecture of the Model–View–Controller

In the mid-1970s, Smalltalk was the ultimate object-oriented programming language. Smalltalk came with its own operating environment. You didn’t run a program on your PC; you first ran the Smalltalk environment, which in turn ran your program. While this made it very handy for things like garbage collection, it also meant that the program was intimately tied to the input system, since a running program was, in essence, part of the operating system.

Program design entailed breaking your program into three parts. The first part was the Model, or database. All the data that was unique to your program resided there. The next part was the View of the data. If your model consisted (in some internal form) of a table of data, a possible view might have been a spreadsheet or a graph. In other words, a view was just a way of displaying some or all of the data to the user. The third part of the architecture was the Controller, or the methods for the user to manipulate the model or the view. All user input and gestures came from the controller. All display items belonged to a view. All data associated with the current program was stored in the model.

This architecture was very successful and spawned many imitations, such as the Microsoft Foundation Classes. MFC programs consist of a Document and a View, which correspond to the Smalltalk Model and View. The Controller aspect is shared between Windows and the message processing of your program’s windows and frames. Thus when your program starts, you usually inform the frame window (where all the normal commands, such as File or Edit, are processed) to open a document. When the document is read in, it usually opens up a default view. If your program has multiple views, you can switch between them or open up additional windows. All these windows contain views of the same document.

The base view class in MFC is called the CView class. When you use the Developer Studio to create a new MFC Windows–executable project (the Developer Studio’s term for the source code and make files to produce an executable or library), the Developer Studio will create an application framework, consisting of an application file (the main window stuff), a document file (the default single-document-interface file), and a view file (containing the default view file). You can then compile and execute this program. It won’t do anything, but it will run. Both the document and the view will be empty, but the messaging loops are in place, the window gets resized when it should, and you have an excellent starting place for the construction of a program.

One reason for my focusing on MFC is its popularity. Of course, the other major reason is that Microsoft is ultimately responsible for supporting OpenGL under Windows. If you’re serious about OpenGL programming, you may not use the Microsoft compiler, but its collection of examples, source code, and documentation for OpenGL is unequaled.

The CView class provides the basic functionality for programmer-defined view classes. A view is attached to a document and acts as an intermediary between the document and the user. The purpose of the view is to render an image of the document on the screen and to interpret user input as operations on the document or changes to the view. In the next section I’ll take you through the step-by-step creation of COpenGLView, an OpenGL-aware view class derived from the CView class. Unfortunately Microsoft deemed it unessential to add an OpenGL view class to MFC when updating to version 4.0, perhaps due in part to the problems of printing and GDI drawing associated with OpenGL 1.0 rendering contexts. Therefore we must not only live with these limitations but also create OpenGL views from scratch. Thus creating the COpenGLView class will enable us to create default OpenGL windows by simply using it. As we go along, we’ll add to the basic functionality of the class. You’ll see that we can encapsulate quite a few things once for later reuse.

The first step is to create a framework for the OpenGL viewing class. We’ll let the Developer Studio create a Windows executable using MFC. This will create a framework in which we can build and test the COpenGLView class. When we’re done, it’s a simple matter to move the source code and header to another subdirectory, where we can either use the class directly or create a library or DLL.

Building the OpenGL View Class Framework

The first step is to create a new project, using the Developer Studio. You’ll need to perform the following steps:

  1. Select the new file as Project Workspace.

  2. Select the target as an MFC AppWizard exe.

  3. Name the project “OpenGL” in the location of your choice.

  4. Select Single-Document-Interface (SDI).

  5. Turn off all other options (no OLE, ODBC, 3D controls, and so on).

  6. If necessary, edit the view class file names to make them “COpenGLView” for both the source and header files.

When you’re done you should see the screen indicated in Figure 6.1.

Creating an OpenGL Framework using MFC

Figure 6.1. Creating an OpenGL Framework using MFC

Continue through the last dialog box. By then Developer Studio will have created the COpenGLView class file skeleton and a framework where we can test it. At this point you might want to compile the application just to make sure that there are no errors in your build process. If the project builds successfully, you’re ready to proceed to the next step: incorporating all of the steps necessary to make the view class capable of displaying OpenGL images.

Customizing the View for OpenGL

If you open up the ClassWizard and select the COpenGLView class, you’ll see that the message handlers for OnDraw() and PreCreateWindow() are already passed onto the class. We’ll need to add functions for the following additional messages:

  • WM_CREATE (for OnCreate)

  • WM_DESTROY (for OnDestroy)

  • WM_ERASEBACKGROUND (for OnEraseBkground)

  • WM_SIZE (for OnSize)

You add functions by selecting the message and then clicking on the Add Function button. Listing 6.1 shows you what you should have in your COpenGLView when you’re done: the CPP file for the framework for the COpenGLView class.

Example 6.1. The COpenGLView Framework

// COpenGLView.cpp : implementation of the COpenGLView class
//

#include "stdafx.h"
#include "OpenGL.h"

#include "OpenGLDoc.h"
#include "COpenGLView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

////////////////////////////////////////////////////////////
// COpenGLView

IMPLEMENT_DYNCREATE(COpenGLView, CView)

BEGIN_MESSAGE_MAP(COpenGLView, CView)
    //{{AFX_MSG_MAP(COpenGLView)
        // NOTE—the ClassWizard will add and remove
        // mapping macros here.
        //    DO NOT EDIT what you see in these blocks
        // of generated code!
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

////////////////////////////////////////////////////////////
// COpenGLView construction/destruction

COpenGLView::COpenGLView()
{
    // TODO: add construction code here

}
COpenGLView::~COpenGLView()
{
}

BOOL COpenGLView::PreCreateWindow(CREATESTRUCT& cs)
{
    // TODO: Modify the Window class or styles here
    // by modifying the CREATESTRUCT cs

    return CView::PreCreateWindow(cs);
}

////////////////////////////////////////////////////////////
// COpenGLView drawing

void COpenGLView::OnDraw(CDC* pDC)
{
    COpenGLDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // TODO: add draw code for native data here
}

////////////////////////////////////////////////////////////
// COpenGLView diagnostics

#ifdef _DEBUG
void COpenGLView::AssertValid() const
{
    CView::AssertValid();
}

void COpenGLView::Dump(CDumpContext& dc) const
{
    CView::Dump(dc);
}

COpenGLDoc* COpenGLView::GetDocument()
  // non-debug version is inline
{
    ASSERT(m_pDocument->IsKindOf(
                 RUNTIME_CLASS(COpenGLDoc)));
    return (COpenGLDoc*)m_pDocument;
}
#endif //_DEBUG

////////////////////////////////////////////////////////////
// COpenGLView message handlers
int COpenGLView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO: Add your specialized creation code here

    return 0;
}

void COpenGLView::OnDestroy()
{
    CView::OnDestroy();

    // TODO: Add your message handler code here

}

BOOL COpenGLView::OnEraseBkgnd(CDC* pDC)
{
    // TODO: Add your message handler code here and/or
    // call default

    return CView::OnEraseBkgnd(pDC);
}

void COpenGLView::OnSize(UINT nType, int cx, int cy)
{
    CView::OnSize(nType, cx, cy);

    // TODO: Add your message handler code here
}

The first step is to add the include statements for the OpenGL header files. I’ve added mine to the COpenGLView.h file because whenever I’ll be using the class, I’ll also be needing these headers. Placing them in the header for the view class means that I don’t have to add them to my file. If you have religious convictions that prevent you from nesting header files, place them wherever you like. In any event you’ll need to add something that looks like the following:

// Include the OpenGL headers
#include "glgl.h"
#include "glglu.h"
#include "glglaux.h"

We’ll be adding some routines that will make use of the auxiliary library; hence the glaux.h include. If you’re using a compiler that doesn’t provide the auxiliary library, you can skip this include.

The next step is to add the OpenGL libraries to the link step. Select Build-Settings, and then select the Link tab. Select the Category of Input. Add the following line to the Object/Library Modules edit control:

opengl32.lib glu32.lib glaux.lib

Again, if you don’t have the auxiliary library, you won’t be able to add the glaux.lib. This is again a good point at which to try rebuilding the program. If you build successfully, you’re all set to start editing the functions.

Editing PreCreateWindow()

In chapter 2 we said that you can’t create an OpenGL window without setting the WS_CLIPSIBLINGS and WS_CLIPCHILDREN style. In addition, you can’t set the CS_PARENTDC bit. Make these changes to your PreCreateWindow() member function. When it’s done, it should look like this:

BOOL COpenGLView::PreCreateWindow(CREATESTRUCT& cs)
{
    // TODO: Add your specialized code here and/or call
    // the base class
    // An OpenGL window must be created with the following
    // flags and must not include CS_PARENTDC for the
    // class style.
    cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

    return CView::PreCreateWindow(cs);
}

After each change, it’s good practice to try a test compile to make sure that everything is compiling correctly. Don’t try to execute the program yet, because we’re in the intermediate stages of bringing OpenGL up. The next step is to modify the OnEraseBkgnd() member function.

Editing OnEraseBkgnd()

Although it’s not really necessary, we’ll edit the OnEraseBkgnd( ) member function simply to turn it off. We’ll do this by returning TRUE immediately from the function. Since OpenGL will be erasing its own window, there’s no reason for Windows to do it as long as your OpenGL viewport takes up the entire client area. The edited code should look like this:

BOOL COpenGLView::OnEraseBkgnd(CDC* pDC)
{
    // TODO: Add your message handler code here and/or
    // call default

    // comment out original call
    // return CView::OnEraseBkgnd(pDC);
    return TRUE; // tell Windows not to erase the background
}

Editing OnCreate() and Setting up a Pixel Format and a Rendering Context

We’re about to get into the meat of the Windows OpenGL interdependent code. At this point I’d like to introduce some error-checking code. There are many, many things that you may forget to do correctly when setting up a window for OpenGL rendering, and it’s good to have all the help possible.

The first step is to stick in some diagnostic error messages that we’ll make use of when something goes wrong when setting up a window for OpenGL. An alternative is to stick in Asserts, but with this method it’s possible to put in some code that checks for an error message. While we’re doing this, we’ll also add some code we’ll use for the OnCreate() function. In the COpenGLView.h file, add the following code before the last brace:

public:
    virtual BOOL SetupPixelFormat( void );
    virtual BOOL SetupViewport( int cx, int cy );
    virtual BOOL SetupViewingFrustum( GLdouble );
    virtual BOOL SetupViewingTransform( void );
    virtual BOOL RenderScene( void );

private:
    void SetError( int e );
    BOOL InitializeOpenGL();

    HGLRC m_hRC;
    CDC* m_pDC;

    static const char* const _ErrorStrings[];
    const char* m_ErrorString;

This adds some member function prototypes that we’ll use later on, some RC and DC variables, and some error string pointers. To initialize these variables, we’ll modify the default constructor by adding initializers:

COpenGLView::COpenGLView() :
    m_hRC(0), m_pDC(0), m_ErrorString(_ErrorStrings[0])
{
    // TODO: add construction code here
}

The last step is to set the static error codes in the CPP file. I’ve made them static strings rather than resource them, since we’ll be copying the source files around a bit. Since these are static, they must be in the implementation and not in the header. This means that the following lines are added to the CPP file:

const char* const COpenGLView::_ErrorStrings[]= {
    {"No Error"}, // 0
    {"Unable to get a DC"}, // 1
    {"ChoosePixelFormat failed"}, // 2
    {"SelectPixelFormat failed"}, // 3
    {"wglCreateContext failed"}, // 4
    {"wglMakeCurrent failed"}, // 5
    {"wglDeleteContext failed"}, // 6
    {"SwapBuffers failed"}, // 7
    };

These errors cover the general problems that can occur when setting up an OpenGL window. To set the error, we’ve got a special function that stores only the first error specified:

void COpenGLView::SetError( int e )
{
    // if there was no previous error,
    // then save this one
    if ( _ErrorStrings[0] == m_ErrorString )
        {
        m_ErrorString = _ErrorStrings[e];
        }
}

That covers recording errors as they occur. The next step is to initialize OpenGL by selecting a pixel format, a DC, and an RC. We’ll simplify this task by breaking it into parts. We’ll add a function call to OnCreate() to the new private member function InitializeOpenGL(). It’s a small change to OnCreate() and looks like this:

int COpenGLView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO: Add your specialized creation code here

    InitializeOpenGL();

    return 0;
}

Of course, the real functionality is hidden inside the new member function, InitializeOpenGL(). This function needs to create a DC, select a pixel format for this DC, create an RC associated with this DC, and select the RC. (You might note that we’re using the faster method of holding onto both the RC and the DC for the life of the viewing window.)

Like most of the helper functions, InitializeOpenGL() returns a Boolean. A TRUE value signals that the function succeeded; a FALSE value signals failure. This is one of the points where the error messages come into play. Let’s examine the OpenGL setup step by step.

BOOL COpenGLView::InitializeOpenGL()
{
    m_pDC = new CClientDC(this);

    if ( NULL == m_pDC ) // failure to get DC
        {
        SetError(1);
        return FALSE;
        }

    if ( !SetupPixelFormat() )
        {
        return FALSE;
        }
    if ( 0 == (m_hRC =
        ::wglCreateContext( m_pDC->GetSafeHdc() ) ) )
        {
        SetError(4);
        return FALSE;
        }

    if ( FALSE ==
        ::wglMakeCurrent( m_pDC->GetSafeHdc(), m_hRC ) )
        {
        SetError(5);
        return FALSE;
        }

    // specify black as clear color
    ::glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    // specify the back of the buffer as clear depth
    ::glClearDepth( 1.0f );
    // enable depth testing
    ::glEnable( GL_DEPTH_TEST );

    return TRUE;
}

This function gets a DC for our client area, which comes from the client area of the frame window. If this fails, we set the error variable and return FALSE. This is how the error function is used throughout the COpenGLView class.

The next step is to select a pixel format, which is such a complicated process that we’ve delegated it to its own function. The next two steps are to create a rendering context and then to make that RC current. Problems in setting up OpenGL windows usually occur during one of these three steps, so if you’re having problems, such as an OpenGL window that never gets painted, this should be the first place you look.

Editing OnCreate() and Setting up a Pixel Format and a Rendering Context

The next step is to select the pixel format. We’re eventually going to allow customization of this function later on, since selecting the optimum pixel format depends entirely on what the program is intended for. In this case we’re going to use the OpenGL view class to examine various models. Thus this default pixel format will be for a double-buffered RGB window for animated 3D rendering. Note that we need only a temporary PIXELFORMATDESCRIPTOR, since we’ll use it only once. (If you need to get the current pixel format, you can always query it.) Thus our initial pixel function looks like this:

BOOL COpenGLView::SetupPixelFormat()
{
    PIXELFORMATDESCRIPTOR pfd =
        {
        sizeof(PIXELFORMATDESCRIPTOR),// size of this pfd
        1,                      // version number
        PFD_DRAW_TO_WINDOW |    // support window
          PFD_SUPPORT_OPENGL |  // support OpenGL
          PFD_DOUBLEBUFFER,     // double buffered
        PFD_TYPE_RGBA,          // RGBA type
        24,                     // 24-bit color depth
        0, 0, 0, 0, 0, 0,       // color bits ignored
        0,                      // no alpha buffer
        0,                      // shift bit ignored
        0,                      // no accumulation buffer
        0, 0, 0, 0,             // accum bits ignored
        16,                     // 16-bit z-buffer
        0,                      // no stencil buffer
        0,                      // no auxiliary buffer
        PFD_MAIN_PLANE,         // main layer
        0,                      // reserved
        0, 0, 0                 // layer masks ignored
        };
    int pixelformat;
    if ( 0 == (pixelformat =
        ::ChoosePixelFormat(m_pDC->GetSafeHdc(), &pfd)) )
        {
        SetError(2);
        return FALSE;
        }

    if ( FALSE == ::SetPixelFormat(m_pDC->GetSafeHdc(),
        pixelformat, &pfd) )
        {
        SetError(3);
        return FALSE;
        }

    return TRUE;
}

After the PIXELFORMATDESCRIPTOR is set, ChoosePixelFormat() is called, followed by SetPixelFormat(). As always, if there is an error, we call the error function and return FALSE. And that completes the initialization and setup of the Windows portion of the OpenGL processing. Since we just created our window, let’s examine what we need to do when the window is destroyed.

Editing OnDestroy()

Unlike OnCreate(), destroying an OpenGL view is relatively easy. The code looks like this:

void COpenGLView::OnDestroy()
{
    CView::OnDestroy();

    // TODO: Add your message handler code here

    if ( FALSE == ::wglMakeCurrent( 0, 0 ) )
        {
        SetError(2);
        }

    if ( FALSE == ::wglDeleteContext( m_hRC ) )
        {
        SetError(6);
        }

    if ( m_pDC )
        {
        delete m_pDC;
        }
}

The steps are to first make the current RC noncurrent, delete it, and then delete the DC. Note that you don’t need to make the RC noncurrent if you are going to delete it, since wglDeleteContext() will do that for you. Also note that we call the error function but don’t return FALSE, since at this point it’s more important to clean up than to immediately return. Besides, if the rest of the program ran, we’d rather clean up all the RCs and DCs if we can figure out any other problems separately.

Editing OnSize()

OnSize(), one of the more interesting functions, is where you usually set up the viewport and the viewing frustum. We also set the viewing transformations here simply because it’s convenient. You don’t want to set up the viewing transformations in the painting routine unless you’re going to be modifying the viewing transformation as your program runs—when translating or rotating the viewpoint as the program animates, for example. This is the reason for the numerous member functions that get called: to enable you to easily override the default behavior.

The basic operations that occur in the OnSize() member function are setting up the viewport (by means of a member function), then selecting the projection matrix, initializing it, and setting up the viewing frustum (also by means of a member function). The last operations are selecting the Modelview matrix, initializing it, and then setting up the viewing transformations by calling another member function. This use of member functions gives us great flexibility in tailoring the COpenGLView class to our needs without rewriting a great deal of code. The edited OnSize() member function follows:

void COpenGLView::OnSize(UINT nType, int cx, int cy)
{
    CView::OnSize(nType, cx, cy);

    // TODO: Add your message handler code here
    GLdouble aspect_ratio; // width/height ratio

    if ( 0 >= cx || 0 >= cy )
        {
        return;
        }

    SetupViewport( cx, cy );

    // compute the aspect ratio
    // this will keep all dimension scales equal
    aspect_ratio = (GLdouble)cx/(GLdouble)cy;

    // select the projection matrix and clear it
    ::glMatrixMode(GL_PROJECTION);
    ::glLoadIdentity();

    // select the viewing volume
    SetupViewingFrustum( aspect_ratio );

    // switch back to the Modelview matrix and clear it
    ::glMatrixMode(GL_MODELVIEW);
    ::glLoadIdentity();

    // now perform any viewing transformations
    SetupViewingTransform();
}

In order to make the COpenGLView class as easy to use as possible, I’ve taken the approach that it should provide a default everything, including a default view. This allows the COpenGLView class easy incorporation into other programs; if it’s hooked up correctly, you’ll get a default working view with no more effort than just connecting the COpenGLView class into your program. This allows easy customization, since you can be assured that if your program suddenly fails to display correctly after editing a member function, it’s due to your editing, not to some other source. With that explanation in place, let’s examine the default view that the COpenGLView class sets up.

The first thing that OnSize() sets up is the default viewport. In this case it’s probably going to be exactly what you want—the OpenGL window to take up the entire client area.

BOOL COpenGLView::SetupViewport( int cx, int cy )
{
    // select the full client area
    glViewport(0, 0, cx, cy);
    return TRUE;
}

The next step is to set up the default viewing frustum. Again this is probably exactly what you want—a perspective view centered on the middle of the screen. If you’ll be using an orthographic or a custom perspective view, you’ll have to override this member function, but if you’re interested mostly in simple 3D perspective views, the default viewing frustum is probably just what you need.

BOOL COpenGLView::SetupViewingFrustum(GLdouble aspect_ratio)
{
    // select a default viewing volume
    gluPerspective(40.0f, aspect_ratio, .1f, 20.0f);
    return TRUE;
}

Finally, we set up the default viewing transform, which places the viewpoint down the positive z-axis and elevates it slightly. The viewpoint is directed at the origin. The viewpoint is set up here for simplicity; you could also set it up anywhere between the OpenGL RC selection and the model rendering. For static viewpoints you’d want it set up outside the OnDraw( ) command, since the viewpoint needs to be entered into the Modelview matrix only once.

If you wanted a dynamic viewpoint, you’d have to set it up every time the model was about to be rendered. The code for the default viewing transform looks like this:

BOOL COpenGLView::SetupViewingTransform()
{
    // select a default viewing transformation
    // of a 20 degree rotation about the X axis
    // then a -5 unit transformation along Z
    ::glTranslatef( 0.0f, 0.0f, -5.0f );
    ::glRotatef( 20.0f, 1.0f, 0.0f, 0.0f );
    return TRUE;
}

Note how the OnSize() member function selects the appropriate matrix before each operation and that when it ends, the Modelview matrix is selected by default. This is by design, since usually the OnDraw() member function will be called next, which means that we’ll be setting up further modeling transformations.

Editing OnDraw()

For this function we depart from the normal methodology of overriding base-class member functions to change their behavior, because there’s usually a sequence of events that occurs in OpenGL programs when it’s time to rerender a scene. These steps are as follows:

  1. Clear the buffer(s).

  2. (Optional) Set up the dynamic viewpoint or other custom viewports or viewing frustums.

  3. (Optional) Render the static portion of your model. This usually means that you render the background of your scene. Static models can be optimized for display, as we’ll see in the next chapter.

  4. Render the dynamic parts of your model.

The COpenGLView class provides additional member functions, each designed to handle one of these particular subtasks. The first subtask is the PreRenderScene() member function. The default implementation is empty, since we’ve already set up our viewing volume and have selected a viewpoint. The next subtask is the RenderStockScene() member function, which takes care of rendering the static, or “stock,” part of the view. We’ll provide a simple stock scene of a checkerboard surface on the x-z plane as a default stock scene.

Finally, we’ll call the last subtask, the RenderScene() member function, where you’ll be spending most of your time. This function is intended to provide you with a location to construct your model. In the default RenderScene() we’ll provide a simple default scene consisting of a blue wire-frame cube with a red wire-frame sphere inside it. Since the sphere and the cube are centered at the origin, they’ll show up as being embedded into the checkerboard’s surface. Now let’s take a look at the code.

void COpenGLView::OnDraw(CDC* pDC)
{
    COpenGLViewClassDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // TODO: add draw code for native data here

    ::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    PreRenderScene();

    ::glPushMatrix();
    RenderStockScene();
    ::glPopMatrix();

    ::glPushMatrix();
    RenderScene();
    ::glPopMatrix();

    ::glFinish();

    if ( FALSE == ::SwapBuffers( m_pDC->GetSafeHdc() ) )
        {
        SetError(7);
        }
}

Note that we clear the color and depth buffers first. If you’re interested in hidden-surface removal, you’ll need to enable depth testing (which we did at the end of the InitializeOpenGL() member function) and also clear the depth buffer, which we do here. We also wrapper the scene-rendering member functions with calls to push and pop the current matrix state, to save you from having to do it yourself. Finally, we call glFinish(), which makes sure that all OpenGL rendering calls are completed, and then we swap the buffers. We created a double-buffered pixel format when we set up our RC, and this is the spot where we make use of them.

As you can see, a lot of coordination is going on here, which is why there’s a multitude of member functions to take care of the various subtasks that you might want to modify. With this basic structure you should never have to override the OnDraw() member function; all you’ll need to do is write your own RenderScene() function to draw your scene and optionally override the other member functions to modify the program as you require. The subtask functions (except for PreRenderScene(), which is an empty member function at this point) are coded like this:

BOOL COpenGLView::RenderScene()
{
    // draw a red wire sphere inside a
    // light blue cube
    // rotate the wire sphere so it's vertically
    // oriented
    ::glRotatef( 90.0f, 1.0f, 0.0f, 0.0f );
    ::glColor3f( 1.0f, 0.0f, 0.0f );
    ::auxWireSphere( .5 );
    ::glColor3f( 0.5f, 0.5f, 1.0f );
    ::auxWireCube( 1.0 );
    return TRUE;
}
// Draw a square surface that looks like a
// black and white checkerboard
void COpenGLView::StockScene( )
{
    // define all vertices X Y Z
    GLfloat v0[3], v1[3], v2[3], v3[3], delta;
    int color = 0;

    delta = 0.5f;

    // define the two colors
    GLfloat color1[3] = { 0.9f, 0.9f, 0.9f };
    GLfloat color2[3] = { 0.05f, 0.05f, 0.05f };

    v0[1] = v1[1] = v2[1] = v3[1] = 0.0f;

    ::glBegin( GL_QUADS );

    for ( int x = -5 ; x <= 5 ; x++ )
        {
        for ( int z = -5 ; z <= 5 ; z++ )
            {
            ::glColor3fv( (color++)%2 ? color1 : color2 );

            v0[0] = 0.0f+delta*z;
            v0[2] = 0.0f+delta*x;

            v1[0] = v0[0]+delta;
            v1[2] = v0[2];

            v2[0] = v0[0]+delta;
            v2[2] = v0[2]+delta;

            v3[0] = v0[0];
            v3[2] = v0[2]+delta;
            ::glVertex3fv( v0 );
            ::glVertex3fv( v1 );
            ::glVertex3fv( v2 );
            ::glVertex3fv( v3 );
            }
        }
    ::glEnd();

}

The RenderScene() member function makes use of the auxiliary library functions to render a wire cube and a wire sphere. If your implementation doesn’t have the auxiliary library, you can use the utility library’s more complex routines to draw a cube and a sphere.

The RenderStockScene() member function is probably more interesting to you. This is the first real example we’ve seen of creating a model out of polygons. There are two for loops. The first draws a strip of touching squares (quads), and the second simply moves the origin over. You might be wondering why I’m not using a quad strip as opposed to constructing individual quads. The reason is that vertices that are shared between two polygons can’t have different colors when using smooth shading. Quad strips (and all the other strips and fans) are made for constructing a homogeneous surface, and thus we’re going to create our checkerboard out of individually colored squares. Plate 6.1 shows what you get when you run the program with the default member functions. The source code in this initial form of the COpenGLView class can be found in the “Chapter 6/Creating the OpenGL View Class” subdirectory. The Developer Studio files are all in this subdirectory, so you can edit and rebuild the files if you like or run the executable.

Using the COpenGLView Class

Having the view class directly hooked into our application is not the final intent of the COpenGLView class. We now need to remove the direct linkages of the COpenGLView class in the application and to create our own subclass, based on the COpenGLView class. As it turns out, this is relatively simple. The first step is to create a subclass from our COpenGLView class and to modify it. Let’s create a subclass called MyView and override the stock-scene member function. The code to do this looks like this:

class CMyView : public COpenGLView
{
private:
    DECLARE_DYNCREATE(CMyView)
protected:
    virtual void RenderStockScene( void );
};

In addition to the RenderStockScene() member function that we’re going to override, we’ll need to add the DECLARE_DYNCREATE(CMyView) macro, which is required to enable objects derived from the CObject base class to be created dynamically at run time. MFC uses this ability to create new objects dynamically—for example, when it reads an object from disk during serialization. All view classes should support dynamic creation, because the framework needs to create them dynamically. The DECLARE_DYNCREATE macro is placed in the .h module for the class, and this header file is then required for all .cpp files that need access to objects of this class. If DECLARE_DYNCREATE is part of the class declaration, IMPLEMENT_DYNCREATE macro must be included in the class implementation. The source code for MyView class looks like this:

IMPLEMENT_DYNCREATE(CMyView, COpenGLView)

void CMyView::RenderStockScene ( )
{
    . . . . details in next section
}

As you can see, there’s not much to it—which is exactly how we want it.

The final step is to connect up our MyView class instead of the COpenGLView class. This occurs in the “app” source code, which is in the COpenGLViewClassApp.cpp file. Your particular “app” implementation file will depend on what you tell the Developer Studio to call it. A section in the InitInstance() member function of the app source dynamically creates the document from a template that includes the view class to use. We can simply change the name of the view class to use in the template to MyView and recompile. The section of the InitInstance() member function that needs to change looks like this:

...
    // Register the application's document templates.
    // Document templates serve as the connection between
    // documents, frame windows, and views.
    CSingleDocTemplate* pDocTemplate;
    pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(COpenGLViewClassDoc),
        RUNTIME_CLASS(CMainFrame),
        RUNTIME_CLASS(CMyView)); // <- Where it changes
        AddDocTemplate(pDocTemplate);
...

The last thing to do is implement the RenderStockScene() member function in the MyView class. Instead of a checkerboard pattern, let’s create a new pattern that consists of several triangles in red and blue that share a common vertex at the origin. The code for this function is as follows:

// Draw a square surface of red and blue triangles
// all touching the origin.
void CMyView::RenderStockScene( )
{
    // define all vertices   X     Y     Z
    GLfloat surface0[3] = { 0.0f, 0.0f, 0.0f };
    GLfloat surface1[3] = {+5.0f, 0.0f, 0.0f };
    GLfloat surface2[3] = {+5.0f, 0.0f,-5.0f };
    GLfloat surface3[3] = { 0.0f, 0.0f,-5.0f };
    GLfloat surface4[3] = {-5.0f, 0.0f,-5.0f };
    GLfloat surface5[3] = {-5.0f, 0.0f, 0.0f };
    GLfloat surface6[3] = {-5.0f, 0.0f,+5.0f };
    GLfloat surface7[3] = { 0.0f, 0.0f,+5.0f };
    GLfloat surface8[3] = {+5.0f, 0.0f,+5.0f };
    GLfloat surface9[3] = {+5.0f, 0.0f, 0.0f };

    // define the two colors
    GLfloat color1[3] = { 0.5f, 0.0f, 0.0f };
    GLfloat color2[3] = { 0.0f, 0.0f, 0.5f };
    ::glBegin( GL_TRIANGLES );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface1 );
        ::glVertex3fv( surface2 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface2 );
        ::glVertex3fv( surface3 );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface3 );
        ::glVertex3fv( surface4 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface4 );
        ::glVertex3fv( surface5 );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface5 );
        ::glVertex3fv( surface6 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface6 );
        ::glVertex3fv( surface7 );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface7 );
        ::glVertex3fv( surface8 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface8 );
        ::glVertex3fv( surface9 );
    ::glEnd();

}

We’ve defined all the vertices in an array and then created each triangle, using these shared vertices. When we run this customized implementation of our OpenGL view class, we get the scene shown in Plate 6.2, which shows the default scene from COpenGLView with our new stock scene from MyView.

Summary

After all these changes to our original COpenGLView class, the modified version and the implementation of the MyView class can be found in the “Chapter 6/Using the OpenGL View Class” subdirectory. Listing 6.2 is the declaration of the original COpenGLView class, in the header file. Listing 6.3 is the implementation of the COpenGLView class, as found in the COpenGLView.cpp file.

Example 6.2. The COpenGLView Header File

// COpenGLView.h : interface of the COpenGLView class
//////////////////////////////////////////////////////////
// Include the OpenGL headers
#include "glgl.h"
#include "glglu.h"
#include "glglaux.h"

class COpenGLView : public CView
{
protected: // create from serialization only
    COpenGLView();
    DECLARE_DYNCREATE(COpenGLView)
// Attributes
public:
    COpenGLViewClassDoc* GetDocument();
// Operations
public:

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(COpenGLView)
    public:
    virtual void OnDraw(CDC* pDC); // overridden to draw
                                   // this view
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
    //}}AFX_VIRTUAL

// Implementation
public:
    virtual ~COpenGLView();
#ifdef _DEBUG
    virtual void AssertValid() const;
    virtual void Dump(CDumpContext& dc) const;
#endif

// Generated message map functions
protected:
    //{{AFX_MSG(COpenGLView)
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    afx_msg void OnDestroy();
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg void OnSize(UINT nType, int cx, int cy);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()

    // The following was added to the COpenGLView class
    virtual BOOL SetupPixelFormat( void );
    virtual BOOL SetupViewport( int cx, int cy );
    virtual BOOL SetupViewingFrustum(GLdouble aspect_ratio);
    virtual BOOL SetupViewingTransform( void );
    virtual BOOL PreRenderScene( void ) { return TRUE; }
    virtual void RenderStockScene( void );
    virtual BOOL RenderScene( void );

private:
    BOOL InitializeOpenGL();
    void SetError( int e );

    HGLRC m_hRC;
    CDC* m_pDC;

    static const char* const _ErrorStrings[];
    const char* m_ErrorString;
};

#ifndef _DEBUG // debug version in COpenGLView.cpp
inline COpenGLViewClassDoc* COpenGLView::GetDocument()
    { return (COpenGLViewClassDoc*)m_pDocument; }
#endif
//////////////////////////////////////////////////////////

Example 6.3. The COpenGLView Source File

// COpenGLView.cpp : implementation of the COpenGLView class
//

#include "stdafx.h"
#include "OpenGL View Class.h"
#include "OpenGL View ClassDoc.h"
#include "COpenGLView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

const char* const COpenGLView::_ErrorStrings[]= {
            ("No Error"},                  // 0
            ("Unable to get a DC"},        // 1
            ("ChoosePixelFormat failed"},  // 2
            ("SelectPixelFormat failed"},  // 3
            ("wglCreateContext failed"},   // 4
            ("wglMakeCurrent failed"},     // 5
            ("wglDeleteContext failed"},   // 6
            ("SwapBuffers failed"},        // 7
            };

//////////////////////////////////////////////////////////
// COpenGLView

IMPLEMENT_DYNCREATE(COpenGLView, CView)

BEGIN_MESSAGE_MAP(COpenGLView, CView)
    //{{AFX_MSG_MAP(COpenGLView)
    ON_WM_CREATE()
    ON_WM_DESTROY()
    ON_WM_ERASEBKGND()
    ON_WM_SIZE()
    //}}AFX_MSG_MAP
    END_MESSAGE_MAP()

//////////////////////////////////////////////////////////
// COpenGLView construction/destruction

COpenGLView::COpenGLView() :
    m_hRC(0), m_pDC(0), m_ErrorString(_ErrorStrings[0])
{
    // TODO: add construction code here
}

COpenGLView::~COpenGLView()
{
}

BOOL COpenGLView::PreCreateWindow(CREATESTRUCT& cs)
{
    // TODO: Add your specialized code here and/or
    // call the base class

    // An OpenGL window must be created with the
    // following flags and must not include
    // CS_PARENTDC for the class style.
    cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

    return CView::PreCreateWindow(cs);
}

//////////////////////////////////////////////////////////
// COpenGLView drawing

void COpenGLView::OnDraw(CDC* pDC)
{
    COpenGLViewClassDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // TODO: add draw code for native data here

    ::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    PreRenderScene();

    ::glPushMatrix();
    RenderStockScene();
    ::glPopMatrix();
    ::glPushMatrix();
    RenderScene();
    ::glPopMatrix();

    ::glFinish();

    if ( FALSE == ::SwapBuffers( m_pDC->GetSafeHdc() ) )
        {
        SetError(7);
        }
}

/////////////////////////////////////////////////////////
// COpenGLView diagnostics

#ifdef _DEBUG
void COpenGLView::AssertValid() const
{
    CView::AssertValid();
}

void COpenGLView::Dump(CDumpContext& dc) const
{
    CView::Dump(dc);
}

COpenGLViewClassDoc* COpenGLView::GetDocument()
// non-debug version is inline
{
    ASSERT(m_pDocument->IsKindOf
        (RUNTIME_CLASS(COpenGLViewClassDoc)));
    return (COpenGLViewClassDoc*)m_pDocument;
}
#endif //_DEBUG

//////////////////////////////////////////////////////////
// COpenGLView message handlers

int COpenGLView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO: Add your specialized creation code here
    InitializeOpenGL();
    return 0;
}

/////////////////////////////////////////////////////////
// GL helper functions

void COpenGLView::SetError( int e )
{
    // if there was no previous error,
    // then save this one
    if ( _ErrorStrings[0] == m_ErrorString )
        {
        m_ErrorString = _ErrorStrings[e];
        }
}

BOOL COpenGLView::InitializeOpenGL()
{
    // Can we put this in the constructor?
    m_pDC = new CClientDC(this);

    if ( NULL == m_pDC ) // failure to get DC
        {
        SetError(1);
        return FALSE;
        }

    if (!SetupPixelFormat())
        {
        return FALSE;
        }

    if ( 0 == (m_hRC =
        ::wglCreateContext( m_pDC->GetSafeHdc() ) ) )
        {
        SetError(4);
        return FALSE;
        }

    if ( FALSE ==
       ::wglMakeCurrent( m_pDC->GetSafeHdc(), m_hRC ) )
       {
       SetError(5);
       return FALSE;
       }

    // specify black as clear color
    ::glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    // specify the back of the buffer as clear depth
    ::glClearDepth(1.0f);
    // enable depth testing
    ::glEnable(GL_DEPTH_TEST);

    return TRUE;
}

BOOL COpenGLView::SetupPixelFormat()
{
    static PIXELFORMATDESCRIPTOR pfd =
        {
        sizeof(PIXELFORMATDESCRIPTOR), // size of this pfd
        1,                         // version number
        PFD_DRAW_TO_WINDOW |       // support window
          PFD_SUPPORT_OPENGL |     // support OpenGL
          PFD_DOUBLEBUFFER,        // double buffered
        PFD_TYPE_RGBA,             // RGBA type
        24,                        // 24-bit color
        0, 0, 0, 0, 0, 0,          // color bits ignored
        0,                         // no alpha buffer
        0,                         // shift bit ignored
        0,                         // no accum buffer
        0, 0, 0, 0,                // accum bits ignored
        16,                        // 16-bit z-buffer
        0,                         // no stencil buffer
        0,                         // no auxiliary buffer
        PFD_MAIN_PLANE,            // main layer
        0,                         // reserved
        0, 0, 0                    // layer masks ignored
    };
    int pixelformat;

    if ( 0 == (pixelformat =
        ::ChoosePixelFormat(m_pDC->GetSafeHdc(), &pfd)) )
        {
        SetError(2);
        return FALSE;
        }

    if ( FALSE ==
        ::SetPixelFormat(m_pDC->GetSafeHdc(),pixelformat,&pfd))
        {
        SetError(3);
        return FALSE;
        }
    return TRUE;
}

void COpenGLView::OnDestroy()
{
    CView::OnDestroy();

    // TODO: Add your message handler code here

    if ( FALSE == ::wglDeleteContext( m_hRC ) )
        {
        SetError(6);
        }

    if ( m_pDC )
        {
        delete m_pDC;
        }
}

BOOL COpenGLView::OnEraseBkgnd(CDC* pDC)
{
    // TODO: Add your message handler code here and/or
    // call default

    // return CView::OnEraseBkgnd(pDC);
    return TRUE; // tell Windows not to erase the background
}

void COpenGLView::OnSize(UINT nType, int cx, int cy)
{
    CView::OnSize(nType, cx, cy);

    // TODO: Add your message handler code here
    GLdouble aspect_ratio; // width/height ratio

    if ( 0 >= cx || 0 >= cy )
        {
        return;
        }

    SetupViewport( cx, cy );

    // select the projection matrix and clear it
    ::glMatrixMode(GL_PROJECTION);
    ::glLoadIdentity();

    // compute the aspect ratio
    // this will keep all dimension scales equal
    aspect_ratio = (GLdouble)cx/(GLdouble)cy;

    // select the viewing volume
    SetupViewingFrustum( aspect_ratio );

    // switch back to the Modelview matrix
    ::glMatrixMode(GL_MODELVIEW);
    ::glLoadIdentity();

    // now perform any viewing transformations
    SetupViewingTransform();
}

/////////////////////////////////////////////////////////
// COpenGLView helper functions
BOOL COpenGLView::SetupViewport( int cx, int cy )
{
    // select the full client area
    ::glViewport(0, 0, cx, cy);
    return TRUE;
}

BOOL COpenGLView::SetupViewingFrustum(GLdouble aspect_ratio)
{
    // select a default viewing volume
    ::gluPerspective(40.0f, aspect_ratio, 0.1f, 20.0f);
    return TRUE;
}

BOOL COpenGLView::SetupViewingTransform()
{
    // select a default viewing transformation
    // of a 20 degree rotation about the X axis
    // then a -5 unit transformation along Z
    ::glTranslatef( 0.0f, 0.0f, -5.0f );
    ::glRotatef( 20.0f, 1.0f, 0.0f, 0.0f );
    return TRUE;
}

BOOL COpenGLView::RenderScene()
{
    // draw a red wire sphere inside a
    // light blue cube

    // rotate the wire sphere so it's vertically
    // oriented
    ::glRotatef( 90.0f, 1.0f, 0.0f, 0.0f );
    ::glColor3f( 1.0f, 0.0f, 0.0f );
    ::auxWireSphere( 0.5 );
    ::glColor3f( 0.5f, 0.5f, 1.0f );
    ::auxWireCube( 1.0 );
    return TRUE;
}
// Draw a square surface that looks like a
// black and white checkerboard
void COpenGLView::RenderStockScene()
{
    // define all vertices   X     Y     Z
    GLfloat v0[3], v1[3], v2[3], v3[3], delta;
    int color = 0;

    delta = 0.5f;

    // define the two colors
    GLfloat color1[3] = { 0.9f, 0.9f, 0.9f };
    GLfloat color2[3] = { 0.05f, 0.05f, 0.05f };

    v0[1] = v1[1] = v2[1] = v3[1] = 0.0f;

    ::glBegin( GL_QUADS );

    for ( int x = -5 ; x <= 5 ; x++ )
        {
        for ( int z = -5 ; z <= 5 ; z++ )
            {
            ::glColor3fv( (color++)%2 ? color1 : color2 );

            v0[0] = 0.0f+delta*z;
            v0[2] = 0.0f+delta*x;

            v1[0] = v0[0]+delta;
            v1[2] = v0[2];

            v2[0] = v0[0]+delta;
            v2[2] = v0[2]+delta;

            v3[0] = v0[0];
            v3[2] = v0[2]+delta;

            ::glVertex3fv( v0 );
            ::glVertex3fv( v1 );
            ::glVertex3fv( v2 );
            ::glVertex3fv( v3 );
            }
        }
    ::glEnd();

}

Try This

Once you get used to using the COpenGLView class (or your own implementation of it), you’ll find that creating an OpenGL view is as effortless as creating any other MFC view class. The next chapters will further modify the COpenGLView class to give it even greater capabilities. But before you read on, you might want to take the time to try some experiments of your own.

  • Try creating your own COpenGLView class, using the text.

  • Instead of having a wire cube and a sphere as the default scene, add in some lines that indicate the major axes, using green for the positive axes and red for the negative.

  • Replace the default scene with the solid-teapot routine from the auxiliary library. Then you’ll see why we had wire-frame objects in the default scene.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.180.111