Using OpenGL in Visual C++: Part I Writing an OpenGL Program

by Alan Oursland

Series Overview

With the release of NT 3.5, OpenGL became a part of the Windows operating system. Now with support for OpenGL in Windows 95 and Windows 98 and low priced graphics accelerators becoming readily available even on low end machines, the prospects of using OpenGL on any Windows machine is becoming more attractive every day. If you are interested in creating quality 2-D or 3-D graphics in Windows, or if you already know another variant of GL, keep reading. This tutorial will show you how to use OpenGL and some of its basic commands.

GL is a programming interface designed by Silicon Graphics. OpenGL is a generic version of the interface made available to a wide variety of outside vendors in the interest of standardization of the language. OpenGL allows you to create high quality 3-D images without dealing with the heavy math usually associated with computer graphics. OpenGL handles graphics primitives, 2-D and 3-D transformations, lighting, shading, Z-buffering, hidden surface removal, and a host of other features. I'll use some of these topics in the sample programs following; others I'll leave to you to explore yourself. If you want to learn more about OpenGL you can search the MSDN website for the keyword "OpenGL".

Here is the list of topics covered in this series:

  • Writing an OpenGL Program
  • Simple 2-D Graphics
  • Transformations and the Matrix Stack
  • Simple 3-D Graphics
[Download the example source] Writing an OpenGL Program

The first program demonstrated here will show you the minimum requirements for setting up a Windows program to display OpenGL graphics. As GDI needs a Device Context (DC) to draw images, OpenGL requires a Rendering Context (RC). Unlike GDI, in which each GDI command requires that a DC is passed into it, OpenGL uses the concept of a current RC. Once a rendering context has been made current in a thread, all OpenGL calls in that thread will use the same current rendering context. While multiple rendering contexts may be used to draw in a single window, only one rendering context may be current at any time in a single thread.

The goal of this sample is to create and make current an OpenGL rendering context. There are three steps to creating and making current a rendering context:

  • Set the window's pixel format.
  • Create the rendering context.
  • Make the rendering context current.

Take the following steps to create the project:

  • Create a new Project Workspace of type "MFC AppWizard (exe)". Select the directory you where you want the project directory to be created, and type "GLSample1" as the project name. Click "Create" to enter the AppWizard. Following is a list of the steps in the AppWizard and the parameters you should enter in each of them. Any parameters not listed are optional.
  • Single Document Interface
  • Database support: None
  • Compond Document Support: None
  • Docking Toolbar: OFF (optional)
    Initial Status Bar: OFF (optional)
    Printing an Print Preview: OFF (Printing OpenGL images is accomplished by creating an RC using a printer DC. If you would like to experiment with this later, without rebuilding everything, go ahead and turn this option on).
    Context-Sensitive Help: OFF (optional)
    3D Controls: ON (optional)
  • Use the MFC Standard style of project
    Generate Source File Comments: Yes
    Use the MFC library as a shared DLL.
  • Keep everything at the default.
    Press Finish

Check the "New Project Information" dialog to make sure everything is as it should be and press OK. The new project will be created in the subdirectory "GLSample1".

First we will include all necessary OpenGL files and libraries in this project. Select "Project-Settings" from the menu. Click on the "Link" tab (or press Ctrl-Tab to move there). Select the "General" category (it should already be selected by default), and enter the following into the Object/Library Modules edit box: "opengl32.lib glu32.lib glaux.lib". Press OK. Now open the file "stdafx.h". Insert the following lines into the file:

#define VC_EXTRALEAN  // Exclude rarely-used stuff from Windows headers
#include <afxwin.h> // MFC core and standard components
#include <afxext.h> // MFC extensions
#include <gl\gl.h>
#include <gl\glu.h>
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h> // MFC support for Windows 95 Common Controls

#endif // _AFX_NO_AFXCMN_SUPPORT

OpenGL requires the window to have styles WS_CLIPCHILDREN and WS_CLIPSIBLINGS set. Edit OnPreCreate so that it looks like this:

BOOL CGLSample1View::PreCreateWindow(CREATESTRUCT& cs)
{
cs.style |= (WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
return CView::PreCreateWindow(cs);
}

The first set to creating a rendering context is to define the window's pixel format. The pixel format describes how the graphics that the window displays are represented in memory. Parameters controlled by the pixel format include color depth, buffering method, and supported drawing interfaces. We will look at some of these below. First create a new protected member function in the CGLSample1View class called "BOOL SetWindowPixelFormat(HDC hDC)" (my preferred method of doing this is right clicking on the class name in the Project Workspace and selecting "Add Function..." from the resulting pop-up menu. You may also do it manually if you wish) and edit the function so that it looks like this:

BOOL CGLSample1View::SetWindowPixelFormat(HDC hDC)
{
PIXELFORMATDESCRIPTOR pixelDesc;
pixelDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pixelDesc.nVersion = 1;
pixelDesc.dwFlags = PFD_DRAW_TO_WINDOW |
PFD_DRAW_TO_BITMAP |
PFD_SUPPORT_OPENGL |
PFD_SUPPORT_GDI |
PFD_STEREO_DONTCARE;
pixelDesc.iPixelType = PFD_TYPE_RGBA;
pixelDesc.cColorBits = 32;
pixelDesc.cRedBits = 8;
pixelDesc.cRedShift = 16;
pixelDesc.cGreenBits = 8;
pixelDesc.cGreenShift = 8;
pixelDesc.cBlueBits = 8;
pixelDesc.cBlueShift = 0;
pixelDesc.cAlphaBits = 0;
pixelDesc.cAlphaShift = 0;
pixelDesc.cAccumBits = 64;
pixelDesc.cAccumRedBits = 16;
pixelDesc.cAccumGreenBits = 16;
pixelDesc.cAccumBlueBits = 16;
pixelDesc.cAccumAlphaBits = 0;
pixelDesc.cDepthBits = 32;
pixelDesc.cStencilBits = 8;
pixelDesc.cAuxBuffers = 0;
pixelDesc.iLayerType = PFD_MAIN_PLANE;
pixelDesc.bReserved = 0;
pixelDesc.dwLayerMask = 0;
pixelDesc.dwVisibleMask = 0;
pixelDesc.dwDamageMask = 0;

m_GLPixelIndex = ChoosePixelFormat( hDC, &pixelDesc);
if (m_GLPixelIndex==0) // Let's choose a default index.
{
m_GLPixelIndex = 1;
if (DescribePixelFormat(hDC, m_GLPixelIndex,
sizeof(PIXELFORMATDESCRIPTOR), &pixelDesc)==0)
{
return FALSE;
}
}

if (SetPixelFormat( hDC, m_GLPixelIndex, &pixelDesc)==FALSE)
{
return FALSE;
}

return TRUE;

}

Now add the following member variable to the CGLSample1View class (again, I like to use the right mouse button on the class name and select "Add Variable..."):

int m_GLPixelIndex; // protected

Finally, in the ClassWizard, add the function OnCreate in response to a WM_CREATE message and edit it to look like this:

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

HWND hWnd = GetSafeHwnd();
HDC hDC = ::GetDC(hWnd);

if (SetWindowPixelFormat(hDC)==FALSE)
return 0;

return 0;
}

Compile the program and fix any syntax errors. You may run the program if you wish but at the moment, it will look like a generic MFC shell. Try playing with the pixel format descriptor. You may want to try passing other indices into DescribePixelFormat to see what pixel formats are available. I'll spend some time now explaining what the code does and precautions you should take in the future.

PIXELFORMATDESCRIPTOR contains all of the information defining a pixel format. I'll explain some of the important points here, but for a complete description look in the VC++ online help.

  • dwFlags   Defines the devices and interfaces with which the pixel format is compatible. Not all of these flags are implemented in the generic release of OpenGL. Refer to the documentation for more information. dwFlags can accept the following flags:

    PFD_DRAW_TO_WINDOW -- Enables drawing to a window or device surface.
    PFD_DRAW_TO_BITMAP -- Enables drawing to a bitmap in memory.
    PFD_SUPPORT_GDI -- Enables GDI calls. Note: This option is not valid if PFD_DOUBLEBUFFER is specified.
    PFD_SUPPORT_OPENGL -- Enables OpenGL calls.
    PFD_GENERIC_FORMAT -- Specifies if this pixel format is supported by the Windows GDI library or by a vendor hardware device driver.
    PFD_NEED_PALETTE -- Tells if the buffer requires a palette. This tutorial assumes color will be done with 24 or 32 bits and will not cover palettes.
    PFD_NEED_SYSTEM_PALETTE -- This flag indicates if the buffer requires the reserved system palette as part of its palette. As stated above, this tutorial will not cover palettes.
    PFD_DOUBLEBUFFER -- Indicates that double-buffering is used. Note that GDI cannot be used with windows that are double buffered.
    PFD_STEREO -- Indicates that left and right buffers are maintained for stereo images.
  • iPixelType   Defines the method used to display colors. PFD_TYPE_RGBA means each set of bits represents a Red, Green, and Blue value, while PFD_TYPE_COLORINDEX means that each set of bits is an index into a color lookup table. All of the examples in this program will use PFD_TYPE_RGBA.
  • cColorBits   Defines the number of bits used to define a color. For RGBA it is the number of bits used to represent the red, green, and blue components of the color ( but not the alpha). For indexed colors, it is the number of colors in the table.
  • cRedBits, cGreenBits, cBlueBits, cAlphaBits   The number of bits used to represent the respective components.
  • cRedShift, cGreenShift, cBlueShift, cAlphaShift   The number of bits each componet is offset from the beginning of the color.

Once we initialize our structure, we try to find the system pixel format that is closest to the one we want. We do this by calling:

m_hGLPixelIndex = ChoosePixelFormat(hDC, &pixelDesc);

ChoosePixelFormat takes an hDC and a PIXELFORMATDESCRIPTOR*, and returns an index used to reference that pixel format, or 0 if the function fails. If the function fails, we just set the index to 1 and get the pixel format description using DescribePixelFormat. There are a limited number of pixel formats, and the system defines what their properties are. If you ask for pixel format properties that are not supported, ChoosePixelFormat will return an integer to the format that is closest to the one you requested. Once we have a valid pixel format index and the corresponding description we can call SetPixelFormat. A window's pixel format may be set only once.

Now that the pixel format is set, all we have to do is create the rendering context and make it current. Start by adding a new protected member function to the CGLSample1View class called "BOOL CreateViewGLContext(HDC hDC)" and edit it so that it looks like this:

BOOL CGLSample1View::CreateViewGLContext(HDC hDC)
{
m_hGLContext = wglCreateContext(hDC);
if (m_hGLContext == NULL)
{
return FALSE;
}

if (wglMakeCurrent(hDC, m_hGLContext)==FALSE)
{
return FALSE;
}

return TRUE;
}

Add the following member variable to the CGLSample1View class:

HGLRC m_hGLContext; // protected

Edit OnCreate to call the new function:

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

HWND hWnd = GetSafeHwnd();
HDC hDC = ::GetDC(hWnd);

if (SetWindowPixelFormat(hDC)==FALSE)
return 0;

if (CreateViewGLContext(hDC)==FALSE)
return 0;

return 0;
}

Add the function OnDestroy in response to a WM_DESTROY message and edit it to look like this:

void CGLSample1View::OnDestroy() 
{
if(wglGetCurrentContext()!=NULL)
{
// make the rendering context not current
wglMakeCurrent(NULL, NULL) ;
}

if (m_hGLContext!=NULL)
{
wglDeleteContext(m_hGLContext);
m_hGLContext = NULL;
}

// Now the associated DC can be released.
CView::OnDestroy();
}

And lastly, edit the CGLSample1View class constructor to look like this:

CGLSample1View::CGLSample1View()
{
m_hGLContext = NULL;
m_GLPixelIndex = 0;
}

Once again compile the program and fix any syntax errors. When you run the program it will still look like a generic MFC program, but it is now enabled for OpenGL drawing. You may have noticed that we created one rendering context at the beginning of the program and used it the entire time. This goes against most GDI programs where DCs are created only when drawing is required and freed immediately afterwards. This is a valid option with RCs as well, however creating an RC can be quite processor intensive. Because we are trying to achieve high performance graphics, the code only creates the RC once and uses it the entire time.

CreateViewGLContext creates and makes current a rendering context. wglCreateContext returns a handle to an RC. The pixel format for the device associated with the DC you pass into this function must be set before you call CreateViewGLContext. wglMakeCurrent sets the RC as the current context. The DC passed into this function does not need to be the same DC you used to create the context, but it must have the same device and pixel format. If another rendering context is current when you call wglMakeCurrent, the function simply flushes the old RC and replaces it with the new one. You may call wglMakeCurrent(NULL, NULL) to make no rendering context current.

Because OnDestroy releases the window's RC, we need to delete the rendering context there. But before we delete the RC, we need to make sure it is not current. We use wglGetCurrentContext to see if there is a current rendering context. If there is, we remove it by calling wglMakeCurrent(NULL, NULL). Next we call wglDeleteContext to delete out RC. It is now safe to allow the view class to release the DC. Note that since the RC was current to our thread we could have just called wglDeleteContext without first making it not current. Don't get into the habit of doing this. If you ever start using multi-threaded applications that laziness is going to bite you.

Congratulations on your first OpenGL program, even if it doesn't do much! If you already know OpenGL on another platform read the tips below and go write the next killer graphics applications. If you don't know OpenGL keep reading. I'll give you a tour of some of its functions.

OpenGL Tips:

  • Set the viewport and matrix modes in response to a WM_SIZE message.
  • Do all of your drawing in response to a WM_PAINT message.
  • Creating a rendering context can take up a lot of CPU time. Only create it once and use it for the life of your program.
  • Try encapsulating your drawing commands in the document class. That way you can use the same document in different views.
Developed Under:

Visual C++ 5+