Welcome to Changhai Lu's Homepage

I know nothing except the fact of my ignorance.

- Socrates

 
INFO
 
 
 
Email Me
All English Contents
 
STATS
 
 
 
Article Views:
21,951
Site Views:
16,352,251

OpenGL Tutorial (3)

- by Changhai Lu -

Abstract: In this lesson, we will go over the major OpenGL libraries, and then continue our journey by writing and analyzing our first OpenGL program.

<< Prev Tutorial

1. OpenGL libraries

In the world of OpenGL programming, in addition to the basic OpenGL library, you will constantly see two other libraries - the OpenGL Utility library (GLU) and the OpenGL Utility Toolkit (GLUT) - come into the play. In this section, let's have a quick review on all these major OpenGL libraries first.

OpenGL is designed as an efficient hardware-independent and cross-platform graphics interface for 3D rendering and 3D hardware acceleration. The core OpenGL library has the following two characteristics:

  1. It only provides a small (but powerful) set of low-level drawing operations that handles geometric primitives.

    • Pros: Allows the programmer a great deal of control and flexibility.
    • Cons: All higher-lever drawing needs to be done in terms of the basic operations which means a great amount of work.
  2. It doesn't include functions for interfacing with any particular windowing system or input devices.

    • Pros: It makes the OpenGL specification truely cross-platform.
    • Cons: Application developers must provide platform-specific functions for the application to work on any given platform which again means a great amount of work.

It is to overcome the disadvantages carried by these two characteristics of the core OpenGL library, that the GLU and GLUT libraries come to the rescue.

The GLU library was developed to provide both useful functions encapsulate the basic OpenGL commands and complex components supporting advanced rendering techniques, similar to the MFC in Windows programming. Apprently GLU is targeted on the first disadvantage of the basic OpenGL library we mentioned above.

The GLUT library, on the other hand, was developed to provide a platform-independent interface to the windowing system and input devices, thus targeted on the second disadvantage of the basic OpenGL library. The implementation of the GLUT library is of course platform-dependent, but the interface it provides to the programmers is platform-independent. GLUT is quite good for small programs such as as demos, but is usually not powerful and flexible enough to support real applications.

This tutorial assumes you use Visual C++ 6.0 on Windows. With Visual C++ 6.0 installed, you should already have all the necessary files for the basic OpenGL and GLU libriries. GLUT, on the other hand, is probably not included. The OpenGL website will provide you with sufficient information for getting GLUT whenever needed.

You might see another library called the AUX library in the literature. The AUX library was developed early in the OpenGL history and is considered obsolete and replaced by GLUT now.

In addition to these major libraries, most operating systems provide additional supports to OpenGL (for instance the wiggle functions on Windows API), we will introduce them when needed.

The following is a list of the locations of all the files needed for using each of the three major OpenGL libraries:

  • The OpenGL library
    • Header File: [Compiler Directory]\Include\GL\GL.H
    • Lib File File: [Compiler Directory]\Lib\OPENGL32.LIB
    • DLL File: [System Directory]\OPENGL32.DLL
  • The GLU library
    • Header File: [Compiler Directory]\Include\GL\GLU.H
    • Lib File File: [Compiler Directory]\Lib\GLU32.LIB
    • DLL File: [System Directory]\GLU32.DLL
  • The GLUT library
    • Header File: [Compiler Directory]\Include\GL\GLUT.H
    • Lib File File: [Compiler Directory]\Lib\GLUT32.LIB
    • DLL File: [System Directory]\GLUT32.DLL

Where [Compiler Directory] is the Visual C++ compiler directory, usually at C:\Program Files\Microsoft Visual Studio\VC98. [System Directory] is the Windows system directory, depending on the type of your system, usually at C:\WINNT\System32 (for NT series) or C:\Windows\System (for 9x series).

Another thing that is helpful to remember is: functions in those libraries fall into a nice naming scheme so you can easily tell to which library a function belongs. The scheme is fairly simple: names for OpenGL/GLU/GLUT functions have prefix gl/glu/glut.

2. First OpenGL program

Fig-1: First OpenGL program
Fig-1: First OpenGL program

Now we are ready to write our first OpenGL program which will draw a triangle on the screen (Fig-1). You might wonder why don't we continue our "Hello world!" tradition and draw a text string as our first OpenGL program? The reason lies in the fact that OpenGL is designed primarily for rendering graphics. Viewed as graphical objects, texts are certain more "advanced" and thus more complicated than triangles. Triangle is, in some sense, the atom in the OpenGL world. Most video cards construct complicated objects using trangles.

As we learned in Lesson Two, the Windows GDI makes use of a device context to keep track of settings (colors, fonts, etc) of GDI drawing functions. OpenGL, being platform independent, has its own data structure called rendering context that does a similar job for the OpenGL drawing functions. Before any OpenGL drawing can be performed a rendering context must be specified, which - under Windows - requires a device context (and particularly its pixel format) be specified first so the rendering context can attach to.

As we said before, OpenGL doesn't include any function to interface with its host operating system. In our example, even though everything is simple, a little bridge connecting the OpenGL rendering context and the Windows device context is still needed. Fortunately, such a bridge is already available on all the major operating systems. On Windows, it is provided by a subset of the so-called wiggle functions (whose names are all prefixed with wgl). OpenGL allows for multiple rendering contexts to be created for a single device context, therefore it is necessary that you specifically make a rendering context current after its creation (and before its using).

In synthesize, the steps you need to go before doing an OpenGL rendering are the following:

  • Get a handle to the device context
  • Setup pixel format for the device context
  • Create a rendering context associated to the device context
  • Make the rendering context current

These steps should be done in the WM_CREATE handler when a program starts.

Now let's take a look at the code:

#include <windows.h>
#include <gl/gl.h>

HDC global_hdc;  // A global handle to the device context

// Setup pixel format for the device context
void SetupPixelFormat(HDC hdc)
{
    static PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR), 1,
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
        PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        16, 0, 0, PFD_MAIN_PLANE, 0, 0, 0, 0
    };
    int index = ChoosePixelFormat(hdc, &pfd);
    SetPixelFormat(hdc, index, &pfd);
}

// Do OpenGL rendering
void MyRendering() {
    // Reset the back buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();

    // Drawing - on the back buffer

    glBegin(GL_TRIANGLES);
    glVertex3f(0.0f, 0.0f, 0.0f);
    glVertex3f(1.0f, 0.0f, 0.0f);
    glVertex3f(1.0f, 1.0f, 0.0f);
    glEnd();

    // Swap the back buffer with the front buffer
    SwapBuffers(global_hdc);
}

// Window procedure - the message handler
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HDC         hdc = NULL;
    HGLRC       hrc = NULL;
    PAINTSTRUCT ps;
     
    switch (msg) {

        case WM_CREATE:
            // Get a handle to the device context
            hdc = BeginPaint(hwnd, &ps);
            global_hdc = hdc;
            // Setup pixel format for the device context
            SetupPixelFormat(hdc);
            // Create a rendering context associated to the device context
            hrc = wglCreateContext(hdc);
            // Make the rendering context current

            wglMakeCurrent(hdc, hrc);
            break;
        case WM_CLOSE:
            // De-select the rendering context
            wglMakeCurrent(hdc, NULL);
            // Release the rendering context
            wglDeleteContext(hrc);
            // Release the device context
            EndPaint(hwnd, &ps);
            PostQuitMessage(0);

            break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }

    return 0;
}

// WinMain function - the entry point
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

                   LPSTR lpCmdLine, int iCmdShow)
{
    HWND     hwnd;
    MSG      msg;
    WNDCLASS wndclass;

    // Specify a window class

    wndclass.style         = 0;
    wndclass.lpfnWndProc   = WndProc;
    wndclass.cbClsExtra    = 0;
    wndclass.cbWndExtra    = 0;
    wndclass.hInstance     = hInstance;
    wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);

    wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName  = NULL;
    wndclass.lpszClassName = "ExampleClass";

    // Register the window class
    RegisterClass(&wndclass);

    // Create a window based on the window class
    hwnd = CreateWindow("ExampleClass", "Example", WS_OVERLAPPEDWINDOW,
                        0, 0, 200, 200, NULL, NULL, hInstance, NULL); 

    // Display the window on the screen
    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);

     
    // An alternative message loop
    bool quit = false;
    while(!quit) {
        PeekMessage(&msg, hwnd, NULL, NULL, PM_REMOVE);
        if (msg.message == WM_QUIT) {
            quit = true;

        } else {
            MyRendering();
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return msg.wParam;
}

The code is developed in the framework set in the previous lessons, and - as a basic philosophy of all my tutorials - is made as simple as possible. Everything that stays the same as before is gray out for us to focus on the new part.

To test the program, simply create an empty Win32 Application, paste the code over and link the OpenGL library to the program (to do so, go to the Project menu, open the Settings window, click on the Link tab and add opengl32.lib into the beginning of the Object/library modules list). This example is so simple that no GLU or GLUT library is ever needed. The Windows wiggle functions are declared in a header file called wingdi.h that is already included in the master header file windows.h (so you don't see it explicitly in the program). You should see Fig-1 as the output of the program.

The first thing we look at is the code in the WM_CREATE handler that implements the four steps of setting a rendering context (notice the comments in the code are in one-to-one correspondence with the steps): A handle to the device context is obtained using the BeginPaint function similar to what we did in the Win32 "Hello world!" example. We then assign this handle to a global variable global_hdc so other functions (to be more specific: the MyRendering function) can access it. To make the code a bit more modular, we introduced a function SetupPixelFormat to setup the pixel format for the device context and invoke this function in the message handler to accomplish the 2nd step. The SetupPixelFormat function takes the device context as input. The first thing it does is to specify a data structure called PIXELFORMATDESCRIPTOR which Windows uses to describe the pixel format of a device context. Among the values assigned to PIXELFORMATDESCRIPTOR, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER means to support window, OpenGL and something called the double buffering. (we don't need double buffering for the current static drawing example, but we leave it here so as we move on there will be no need to touch this part of code again). 32 is color mode (32 bit color). A complete description of the fields in PIXELFORMATDESCRIPTOR can be found in Reference 1 and 4 listed below. What you specify in the PIXELFORMATDESCRIPTOR may not be supported by the device context (for instance you may have specified a 32 bit color mode while the device context only supports 24 bit color), so we use the function ChoosePixelFormat that examines the device context and returns an index of the best match to the specified PIXELFORMATDESCRIPTOR. This index is then used by the SetPixelFormat function to finally setup the pixel format for the device context. The 3rd and 4th steps are done straight-forwardly using two wiggle functions: wglCreateContext and wglMakeCurrent.

As all the four steps are finished, we are ready to do the rendering. As explained in the Appendix in Lesson One, we place the rendering code in the message loop. We wrapped the rendering code into a function MyRendering and invokes that function in the loop, that's the only line of new code in the WinMain function.

In the function MyRendering, the glClear function at the beginning clears out both the color buffer and depth buffer, which makes the background of buffer black. The glLoadIdentity function brings the coordinate system back to the default configuration which is located at the center of the window (or screen if the program is running in full-screen mode) and has its x-axis pointing to the right, y-axis pointing to the up and z-axis pointing towards the user. In other words, the glClear function resets the graphic properties while the glLoadIdentity function resets the geometric properties of the buffer. Together they reset the buffer. After reset, we begin to draw the triangle. Most primitive objects in OpenGL are drawn by enclosing a series of coordinate sets that specify vertices and (optionally) other informations between Begin/End pairs. The glBegin function takes a primitive type as input which basically tells the program what primitive objects to expect. In our case the mode value is GL_TRIANGLE which means what's in between the Begin/End pair are vertices for triangle(s). Each vertex in OpenGL is specified by calling the Vertex3f function which takes the x, y, z coordinates of the vertex as input. The values of these coordinates are all in relative scale. By default, a value 1.0 for a coordinate represents the distance from the center of the screen to the edge of the screen alone that coordinate (so 1.0 along different axis doesn't have to have the same absolute length unless the screen happens to be square), thus the three vertices in our example are: the center of the screen (0.0f, 0.0f, 0.0f), the right edge of the screen (1.0f, 0.0f, 0.0f) and the up-right corner of the screen (1.0f, 1.0f, 0.0f), as we can see in Fig-1. All these three vertices sit on the plane of the screen (because the z-coordinate is zero). Remember that we are using the double buffering technique, all the drawing are done on the (invisible) back buffer. So once the drawing is done, we swap the back buffer with the front buffer for the device context using the SwapBuffers Win32 API function (which takes the handle to the device context as input). Now the triangle is displayed on the screen.

The last thing I would like to mention here is that if you looked carefully into the code, you might have noticed a little difference between the rendering code in Win32 and OpenGL. In Win32, every GDI function requires a handle to a device context to work, while OpenGL rendering functions are - as shown in our example - used without acquiring a handle to a rendering context. This is because the function wglMakeCurrent makes it unambiguous for the program to know which rendering context should be used.

That's pretty much all we need to do for our first OpenGL program. At the end, we release our rendering context and device context in the WM_CLOSE handler right before the program exits. The release of the resources should be done in the following order (in one-to-one correspondence with the comments in the code):

  1. De-select the rendering context - by making NULL as the current rendering context
  2. Release the rendering context - by calling the wglDeleteContext function
  3. Release the device context - by calling the EndPaint function

What's next?

In the next lesson, we will enhance our program by doing transformations, adding colors and handling window resizing messages. We will also learn how to setup a full-screen mode for OpenGL programs.

>> Next Tutorial

Glossary

Double buffering - Double buffering is a technique used by graphic systems to support smooth animation through two color buffers: the front color buffer and back color buffer (both are associated to the device context). With double buffering, you render all your scenes in the (off-screen) back buffer. After the scene is finished rendering, you swap the back buffer with the front buffer. This way, the rendering process is hidden from the user. Double buffering technique eliminates the flickering effects that usually arise when scenes are rendered only on the front buffer.

References

  1. Kevin Hawkins, Dave Astle, et al, OpenGL Game Programming, Prima Publishing, 2001.
  2. NeHe Productions, NeHe OpenGL Tutorials, Available on the web.
  3. Mark Segal, Kurt Akeley, The OpenGL Graphics System: A Specification (Version 1.4), 2002.
  4. Microsoft Corporation, MSDN Library, comes with Visual Studio 6.0, also available on the Microsoft website.

The regular comment period of this article is over
Please come back in the first 7 days of any month to leave comment