OpenGL is a device- and operating system-independent library for three-dimensional graphics and graphics rendering. OpenGL was originally developed by Silicon Graphics Inc. (SGI) for use on their high-end graphics workstations. Since then, OpenGL has become a widely accepted standard with implementations on many operating system and hardware platforms, including the Windows NT and Windows 95 operating systems.
In addition to the standard OpenGL Library implementation, Windows also provides a series of functions that integrate OpenGL with the operating system. In particular, functions are provided that associate OpenGL rendering contexts with GDI device contexts. These Windows extensions to the OpenGL Library are identified by names that begin with wgl. In addition to these OpenGL extensions, a series of new Win32 API functions has also been defined to facilitate certain aspects of OpenGL programming.
The OpenGL Library is large and complex. If you wish to have access to a comprehensive set of manuals, you should consider purchasing The OpenGL Reference Manual from the OpenGL Architecture Review Board, or The OpenGL Programming Guide by Jackie Neider, Tom Davis, and Mason Woo. Both guides are published by Addison-Wesley.
In this chapter, in addition to presenting a brief (and far from comprehensive!) overview of the OpenGL Library, I place the focus on using OpenGL from Windows and MFC applications.
The purpose of the OpenGL Library is to render two- and three-dimensional objects into a frame buffer, such as the pixel memory of your computer's graphics hardware.
The OpenGL Library is fundamentally procedural. What this means is that in your application, you don't describe what an object looks like; instead, you specify how an object is to be drawn. Complex geometric objects are described in terms of simple elements that your application defines.
The OpenGL Library implementation follows the client-server model. OpenGL clients and servers need not even reside on the same machine.
At the basic level, the OpenGL Library deals with vertices. A vertex is a point, for example the end point of a line, or a corner of a polygon. Vertices can be two- or three-dimensional.
At the next level are primitives. Primitives consist of a group of one or more vertices. For example, a rectangle described as a set of four vertices is a primitive.
How vertices are assembled into primitives and how primitives are drawn into a frame buffer are controlled by a variety of settings. For example, applications can specify a three-dimensional transformation matrix that defines how the coordinates of an object are translated into coordinates on the drawing surface.
In addition to its ability to draw points and lines, OpenGL can also draw surfaces, apply lighting specifications, and use texture bitmaps.
Another set of features enables applications to selectively use or discard pixels. For example, drawing a pixel can be made conditional upon properties such as the pixel's depth or its opacity.
A greatly simplified view of how OpenGL works is presented in Figure 41.1.
Figure 41.1. Simplified overview of OpenGL operations.
Before the OpenGL Library can be used, a number of initialization steps must be executed.
Every Windows OpenGL application must associate a rendering context with a device context. The device context must be a display device context or a memory device context that is compatible with the display device context. To set up a rendering context, applications must first use the SetPixelFormat Win32 function to set up a pixel format for the device; next, they must call wglCreateContext with the device context handle as its parameter. If successful, wglCreateContext returns a rendering context handle of type HGLRC.
OpenGL under Windows recognizes two types of pixel data modes: RGBA formats and color index-based modes. When the RGBA mode is selected, pixel colors are specified in the form of RGB color values. When color index mode is selected, pixel colors are selected from the system palette using an index value. These two modes become relevant on palette-based 256-color devices (many VGA-compatible display cards). When your application uses the RGBA mode on such a device, it must manage its own palette, and respond to Windows palette notification messages.
There are specific requirements that must be met by a window that is to be used for OpenGL operations. Specifically, such windows cannot be created using a window class that has the CS_PARENTDC style set. The window itself must have the WS_CLIPCHILDREN and the WS_CLIPSIBLINGS styles in order to be compatible with OpenGL.
Note that to increase your application's performance, you may wish to use a window class that has a null background brush; the window background will be erased through the OpenGL Library anyway.
Before a rendering context can be used, it must be set up as the current context using the wglMakeCurrent function. This function takes two parameters, one of which is a device-context handle. Interestingly, this handle does not need to be identical to the handle used in wglCreateContextbut it must refer to the same device). Thus it is possible, for example, to set up an OpenGL rendering context using a device-context handle returned by GetDC, but use wglMakeCurrent with a device-context handle returned by BeginPaint.
Once a rendering context is ready to accept commands, you may wish to send additional initialization commands; for example, you may wish to erase the frame buffer before drawing, set up coordinate transformations, configure light sources, or enable and disable other options.
One initialization step that cannot be omitted is the call to the glViewport function. Through this function, you can set up or modify the size of the rendering viewport. Typically, you should call this function once when the rendering context is initialized, and subsequently every time your application receives a WM_SIZE message indicating that its window size has changed.
Most OpenGL drawing consists of a series of vertex operations enclosed between a pair of glBegin and glEnd calls. The glBegin call identifies the type of primitive that subsequent vertex operations define; glEnd marks the end of constructing the primitive. For example, the following series of calls constructs a pentagon:
glBegin(GL_POLYGON); glVertex2d(0.0, 1.0); glVertex2d(-0.951057, 0.309017); glVertex2d(-0.587785, -0.809017); glVertex2d(0.587785, -0.809017); glVertex2d(0.951057, 0.309017); glEnd();
The glBegin function can be used to define a variety of primitives. Table 41.1 lists the allowable parameters for this function.
glBegin Parameter |
Description |
GL_POINTS |
A series of points |
GL_LINES |
A series of lines |
GL_LINE_STRIP |
A connected group of line segments |
GL_LINE_LOOP |
A connected, closed group of line segments |
GL_TRIANGLES |
A set of triangles |
GL_TRIANGLE_STRIP |
A set of connected triangles |
GL_TRIANGLE_FAN |
A set of connected triangles |
GL_QUADS |
A set of quadrilaterals |
GL_QUAD_STRIP |
A set of connected quadrilaterals |
GL_POLYGON |
A polygon |
In the case when glBegin defines a set of connected primitives, specific rules govern how vertices of a primitive are reused as vertices of the subsequent primitive. For example, if GL_LINE_STRIP is specified, the vertex representing the end point of a line segment also becomes the starting point of the next line segment.
In addition to basic OpenGL functions, Microsoft's OpenGL implementation provides two additional OpenGL libraries.
The OpenGL Utility Library (GLU) contains a series of functions that deal with texture support; coordinate transformation; rendering of spheres, disks, and cylinders; B-spline curves and surfaces; and error handling. Additionally, the GLU Library provides polygon tessellation functions; these functions can be used to break down complex or concave polygons into simple convex polygons (the only kind that OpenGL can handle).
The OpenGL Programming Guide Auxiliary Library (GLAUX), in addition to providing functions for handling several three-dimensional objects, also provides functions to manage and run an OpenGL application. These functions are most useful for quick porting OpenGL applications from other environments. In particular, these functions provide basic window management, implement a simple message loop, and provide a window procedure for basic message handling. However, these library functions are not intended for use in production applications.
Now for a look at a very simple OpenGL application. This application, shown in Listing 41.1, displays a cube. The cube is slightly rotated to show a three-dimensional appearance, and is lit from the side. In its simplicity, this application is the OpenGL version of a Windows Hello, World application.
#include <windows.h> #include <GL/gl.h> #include <GL/glu.h> HGLRC hglrc; void DrawHello(HWND hwnd) { HDC hDC; PAINTSTRUCT paintStruct; RECT clientRect; GLfloat lightPos[4] = {-1.0F, 2.0F, 0.2F, 0.0F}; hDC = BeginPaint(hwnd, &paintStruct); if (hDC != NULL) { GetClientRect(hwnd, &clientRect); wglMakeCurrent(hDC, hglrc); glViewport(0, 0, clientRect.right, clientRect.bottom); glLoadIdentity(); glClear(GL_COLOR_BUFFER_BIT); glColor4d(1.0, 1.0, 1.0, 1.0); glRotated(30.0, 0.0, 1.0, 0.0); glRotated(15.0, 1.0, 0.0, 0.0); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_POSITION, lightPos); glBegin(GL_QUADS); glNormal3d(0.0, -1.0, 0.0); glVertex3d(0.5, -0.5, 0.5); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, -0.5, -0.5); glVertex3d(0.5, -0.5, -0.5); glNormal3d(0.0, 0.0, -1.0); glVertex3d(-0.5, -0.5, -0.5); glVertex3d(-0.5, 0.5, -0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(0.5, -0.5, -0.5); glNormal3d(1.0, 0.0, 0.0); glVertex3d(0.5, -0.5, -0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, -0.5, 0.5); glNormal3d(0.0, 0.0, 1.0); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, -0.5, 0.5); glNormal3d(-1.0, 0.0, 0.0); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, -0.5); glVertex3d(-0.5, -0.5, -0.5); glNormal3d(0.0, 1.0, 0.0); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(-0.5, 0.5, -0.5); glEnd(); glFlush(); wglMakeCurrent(NULL, NULL); EndPaint(hwnd, &paintStruct); } } LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_PAINT: DrawHello(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR d3, int nCmdShow) { MSG msg; HWND hwnd; WNDCLASS wndClass; HDC hDC; PIXELFORMATDESCRIPTOR pfd; int iPixelFormat; if (hPrevInstance == NULL) { memset(&wndClass, 0, sizeof(wndClass)); wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = WndProc; wndClass.hInstance = hInstance; wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszClassName = "HELLO"; if (!RegisterClass(&wndClass)) return FALSE; } hwnd = CreateWindow("HELLO", "HELLO", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); hDC = GetDC(hwnd); memset(&pfd, 0, sizeof(pfd)); pfd.nSize = sizeof(pfd); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL; pfd.iPixelType = PFD_TYPE_RGBA; pfd.iLayerType = PFD_MAIN_PLANE; pfd.cDepthBits = 16; iPixelFormat = ChoosePixelFormat(hDC, &pfd); SetPixelFormat(hDC, iPixelFormat, &pfd); hglrc = wglCreateContext(hDC); ReleaseDC(hwnd, hDC); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) DispatchMessage(&msg); wglMakeCurrent(NULL, NULL); wglDeleteContext(hglrc); return msg.wParam; }
The following sections explain this application's method of operation.
Note that for the sake of simplicity, I did not include any palette initialization in this application. For this reason, the application may not behave properly on systems configured for 16 or 256 colors.
The first series of OpenGL calls in this application begins in WinMain, immediately after the application's window has been created. After obtaining a device-context handle for the client area of this window, the device context's pixel format is set to a pixel format obtained through ChoosePixelFormat. The ChoosePixelFormat function can be used to identify pixel formats for a specific device that best match a set of required characteristics.
Note that although we are using the RGBA data mode, this application does not handle palette notification messages. This is done in order to keep the application as simple as possible; in a production application, you would certainly not want to omit creating and managing a palette that is appropriate for your application.
After the pixel format has been specified, a rendering context is created by a call to wglCreateContext. The rendering context handle is saved in a global variable that will be accessed from within other functions.
When all initializations have been completed, the application enters its message loop. After the message loop terminates, cleanup is performed by calling wglMakeCurrent and wglDeleteContext before the application terminates.
The application's simple window procedure processes only two messages: WM_PAINT and WM_DESTROY. When a WM_PAINT message is received, the window procedure calls the DrawHello function; it is in this function where OpenGL drawing operations take place.
The first step in DrawHello is to select the rendering context as the current context and set the viewport size by calling glViewport. The viewport size was obtained by a call to the Win32 GetClientRect function. Next, the frame buffer is erased, and an identity transformation matrix is loaded.
The transformation matrix is changed by two subsequent rotations, specified by calls to glRotated. The first call rotates the view around the vertical axis. The second call tips the view forward by rotating it around the horizontal axis. As a result, we will see the cube from a viewpoint somewhat above and to the left of the cube.
The rotations are followed by calls that enable lighting mode and specify a light source. The code specifies a single light source that illuminates the cube from the left and above.
With all this initialization work complete, actual drawing can begin. A series of six quadrilaterals is drawn, representing the six sides of the cube. For each of the quadrilaterals, the normal vector is defined by a separate call to glNormal3d. When the construction of the six primitives is complete, a call to glFlush is used to ensure that all OpenGL operations are complete, and then the device context is released and the function returns.
This application can be compiled simply from the command line. I called the source file cube.c; to compile this file, type the following:
cl cube.c user32.lib gdi32.lib opengl32.lib
Note that applications that use the GLU Library or the GLAUX Library must also specify glaux.lib or glu32.lib on the command line. And because OpenGL is computation-intensive, it might be a useful idea to compile with the appropriate optimization flags set.
The application should display a window with a three-dimensional image of a cube rendered in it, similar to that shown in Figure 41.2.
Figure 41.2. Running the cube.exe Windows application.
The OpenGL Library can easily be utilized from MFC applications as well. To enable the OpenGL libraries, add the appropriate library names to your project settings (Figure 41.3).
Figure 41.3. Adding the OpenGL libraries to MFC project settings.
When initializing the OpenGL Library in an MFC application, it is important to remember which window you wish to use for a rendering context. For example, if it is a view window that will serve as the rendering context, it is this window that should be used when the OpenGL rendering context is created.
The MFC OpenGL application I created is based on an AppWizard-generated single document interface application skeleton.
In this application, we draw a cube identical to the cube drawn in the C application discussed earlier. The cube is drawn into the application's view window. Accordingly, the first task after creating the application's skeleton is to modify the view class's PreCreateWindow member function, to ensure that the view window is created with the appropriate flags.
The modified version of this function is shown in Listing 41.2.
BOOL CCUBEView::PreCreateWindow(CREATESTRUCT& cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN; return CView::PreCreateWindow(cs); }
As you can see, the change to this function is simple; it consists only of adding the WS_CLIPSIBLINGS and WS_CLIPCHILDREN flags to the window style to ensure proper operation of the OpenGL libraries.
Much more extensive initialization work is performed in the view class's OnCreate member function. This member function must be added using ClassWizard or the WizardBar, as a handler function for WM_CREATE messages. The implementation of this function, shown in Listing 41.3, creates a rendering context after setting a pixel format for the view window's device context.
int CCUBEView::OnCreate(LPCREATESTRUCT lpCreateStruct) { PIXELFORMATDESCRIPTOR pfd; int iPixelFormat; CDC *pDC; if (CView::OnCreate(lpCreateStruct) == -1) return -1; // TODO: Add your specialized creation code here pDC = GetDC(); memset(&pfd, 0, sizeof(pfd)); pfd.nSize = sizeof(pfd); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL; pfd.iPixelType = PFD_TYPE_RGBA; pfd.iLayerType = PFD_MAIN_PLANE; pfd.cDepthBits = 16; iPixelFormat = ChoosePixelFormat(pDC->m_hDC, &pfd); SetPixelFormat(pDC->m_hDC, iPixelFormat, &pfd); m_hglrc = wglCreateContext(pDC->m_hDC); ReleaseDC(pDC); return 0; }
The rendering context handle is stored in the member variable m_hglrc. This member variable should be added to the declaration of the view class in the Attributes section, as follows:
class CCUBEView : public CView { ... // Attributes public: CCUBEDoc* GetDocument(); HGLRC m_hglrc; ...
The actual drawing of the cube is performed in the OnDraw member function of the view class. This member function, shown in Listing 41.4, is very similar to the DrawHello function of the C application presented earlier in this chapter. After making the rendering context current, the function performs a series of initializations, including setting the size of the viewport, applying coordinate transformations, and setting up lighting. Afterwards, four quadrilaterals that together comprise the cube are drawn.
void CCUBEView::OnDraw(CDC* pDC) { CRect clientRect; GLfloat lightPos[4] = {-1.0F, 2.0F, 0.2F, 0.0F}; CCUBEDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here GetClientRect(&clientRect); wglMakeCurrent(pDC->m_hDC, m_hglrc); glViewport(0, 0, clientRect.right, clientRect.bottom); glLoadIdentity(); glClear(GL_COLOR_BUFFER_BIT); glColor4d(1.0, 1.0, 1.0, 1.0); glRotated(30.0, 0.0, 1.0, 0.0); glRotated(15.0, 1.0, 0.0, 0.0); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_POSITION, lightPos); glBegin(GL_QUADS); glNormal3d(0.0, -1.0, 0.0); glVertex3d(0.5, -0.5, 0.5); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, -0.5, -0.5); glVertex3d(0.5, -0.5, -0.5); glNormal3d(0.0, 0.0, -1.0); glVertex3d(-0.5, -0.5, -0.5); glVertex3d(-0.5, 0.5, -0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(0.5, -0.5, -0.5); glNormal3d(1.0, 0.0, 0.0); glVertex3d(0.5, -0.5, -0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, -0.5, 0.5); glNormal3d(0.0, 0.0, 1.0); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, -0.5, 0.5); glNormal3d(-1.0, 0.0, 0.0); glVertex3d(-0.5, -0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, -0.5); glVertex3d(-0.5, -0.5, -0.5); glNormal3d(0.0, 1.0, 0.0); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, 0.5); glVertex3d(0.5, 0.5, -0.5); glVertex3d(-0.5, 0.5, -0.5); glEnd(); glFlush(); wglMakeCurrent(NULL, NULL); }
Note that this implementation does not take into account the fact that the MFC framework also calls the view class's OnDraw function when drawing into a printer-device context. In its present state, attempts to use this application for printing will fail.
To run the application, compile and execute it from the Build menu. The application's window should appear similar to that shown in Figure 41.4.
Figure 41.4. Running the cube.exe MFC application.
Note that this application, as its non-MFC counterpart, includes no palette initialization and may not work properly on systems configured with 16 or 256 colors.
OpenGL is a library of high-quality three-dimensional graphics and rendering functions. The library's device- and platform-independence make it a library of choice for developing portable graphical applications.
OpenGL drawings are constructed from primitives; primitives are simple items such as lines or polygons, which in turn are composed of vertices.
The OpenGL Library assembles primitives from vertices while taking into account a variety of settings, such as color, lighting, and texture. Primitives are then processed in accordance with transformations, clipping settings, and other parameters; at the end of the rasterization process is pixel data deposited into a frame buffer.
The Windows implementation of the OpenGL Library consists of the core library, utility functions (GLU), and auxiliary functions (GLAUX). The auxiliary library can be used to easily create simple stand-alone OpenGL applications, as it implements a message loop and a window procedure internally. However, due to the simplicity of implementation, this library should not be used in production applications.
Windows also provides a set of extension functions (WGL) that facilitate the use of OpenGL functions in the context of the Windows GDI. Furthermore, a set of new functions has been added to the Win32 API to support pixel formats and OpenGL double buffering.
The main steps of creating a Windows OpenGL application are as follows:
If you plan to run your application on 256-color devices, add handling for custom palettes.