Let's talk separately about the three main pieces from above. The first piece is the drawable. The drawable is the actual
canvas where the OpenGL drawing is to occur. On the IRIX side the drawable can be any OpenGL capable window, an
OpenGL capable pixmap, or a Pbuffer (an offscreen rendering area that is hardware accelerated). On Windows NT you can
render to any OpenGL capable window or OpenGL capable bitmap. The main difference between IRIX and Windows NT
here is that on Windows NT all of the WGL functions refer to the HDC as the drawable as opposed to using the HWND
(handle to a window). On IRIX the GLX functions refer to the drawable directly. The HDC is a handle to a device context.
Each window in Windows NT can have a number of HDCs. Each thread or process that requests the device context for an
HWND gets its own unique HDC. In order to obtain the HDC for a given window there is a function called GetDC. The HDC
can be obtained only after the window has been created.
The second piece is the XVisualInfo structure or on
Windows NT the PixelFormat. This piece is needed to tell
OpenGL and the operating system how the drawable is to be
used. Since the drawable is just a canvas, we need this
second piece to explain how we are going to use the canvas
(for example, RGBA or color index rendering, pixel depth,
Z buffer depth, and so on). If we set up a canvas for color
index rendering, then making RGBA calls like glColor3f
won't make any sense, and likewise if we configure the
canvas for RGBA rendering, calls like glIndexi won't make
any sense. On IRIX boxes the decision of how to configure
the canvas can be made only once, and it needs to be made
before the drawable is created. As well on IRIX, any process
or thread that uses that drawable must do so with the original
configuration. On Windows NT the configuration has to be
set after the drawable has been created, and it can only be set
once per thread or process. Once the PixelFormat has been
set for a given HDC in a given thread, it can't be changed
for that thread. This means that other threads or processes
can reference the same drawable or window using a different
configuration because each thread or process will get its own
HDC.
The third piece is the OpenGL graphics context. The
graphics context maintains the OpenGL state. On both IRIX
and Windows NT the OpenGL graphics context is
compatible only with a single configuration, and this is why
the configuration must be set before the graphics context is
created. Once created, the graphics context can be used with
any drawable that has the same configuration as the one with
which the graphics context was created. An OpenGL
graphics context must be made current for any rendering
commands to go through to the graphics pipeline and
actually get rendered. Only a single graphics context may be
current to a single thread at a time. This means that each
thread can have only one current graphics context and a
graphics context may not be current to two different threads
at the same time.
OpenGL in Standard Windows
Now that we have a basic understanding of the pieces
needed to render OpenGL, it is time to look at how those
pieces are created and how this fits into the framework of a
simple standard windows application. The three main topics
that I am going to discuss here are how to create an OpenGL
compatible window, what to do once the window has been
created, and handling important messages.
For matters of this discussion I am going to consider a
simple windows application as one in which there is a single
main window that is registered and created in the WinMain
function.
How to Create an OpenGL Compatible Window
A simple windows program would have a WinMain function
that registers and creates a main window. It is this main
window that will act as the canvas. On Windows 95 and 98
you need to set up the window class to use its own DC so
that DCs aren't shared. On Windows NT this isn't
necessary, but is still a good idea. It is also a good idea to set
up the main window to clip its children and sibling
windows. This is the only change that is needed before
window creation in order to support OpenGL rendering. All
of the other changes occur after window creation. Below I
have included a simple WinMain function with the changed
sections added in bold:
int APIENTRY WinMain(HINSTANCE _instance, HINSTANCE _prevInst,
LPSTR _cmdLine, int _cmdShow)
{
MSG msg ;
WNDCLASSEX wndClass;
char *className = "OpenGL";
char *windowName = "Simple OpenGL Program";
winWidth = winHeight = 500;
wndClass.cbSize = sizeof (WNDCLASSEX) ;
wndClass.style = CS_HREDRAW | CS_VREDRAW |
CS_OWNDC;
wndClass.lpfnWndProc = WndProc;
wndClass.cbClsExtra = 0 ;
wndClass.cbWndExtra = 0 ;
wndClass.hInstance = _instance ;
wndClass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndClass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndClass.hbrBackground = (HBRUSH)
GetStockObject(WHITE_BRUSH);
wndClass.lpszMenuName = NULL ;
wndClass.lpszClassName = className ;
wndClass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
RegisterClassEx (&wndClass) ;
wnd = CreateWindow(className, windowName,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
WS_CLIPSIBLINGS,
100, // initial x position
100, // initial y position
winWidth, // winWidth
winHeight, // winHeight
NULL, // parent window handle
(HMENU) NULL, // window menu handle
_instance, // program instance handle
NULL) ;
ShowWindow(wnd, _cmdShow);
UpdateWindow(wnd);
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
Once the window is created there are five main messages that need to be handled in an OpenGL program (actually there are
quite a few others, but five is the minimum). The following messages need to be handled: WM_CREATE, WM_PAINT,
WM_SIZE, WM_ERASEBKGND, and WM_DESTROY. I will explain each of these messages and what needs to be done once
they arrive.
What to Do Once the Window Has Been Created
The first is the WM_CREATE message. This message tells us that the window has been created, so this is the first opportunity
to get the HDC and configure the drawable the way we want it. I usually set up an Init function to handle all of this. The Init
function has to complete the following four tasks in order: get the HDC, choose the PixelFormat, set the PixelFormat, and
create the OpenGL graphics context. The following sample Init function shows each of the four tasks in bold:
void Init()
{
PIXELFORMATDESCRIPTOR pfd;
int pixelFormat;
dc = GetDC(wnd);
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 24;
pfd.cRedBits = 0;
pfd.cRedShift = 0;
pfd.cGreenBits = 0;
pfd.cGreenShift = 0;
pfd.cBlueBits = 0;
pfd.cBlueShift = 0;
pfd.cAlphaBits = 0;
pfd.cAlphaShift = 0;
pfd.cAccumBits = 0;
pfd.cAccumRedBits = 0;
pfd.cAccumGreenBits = 0;
pfd.cAccumBlueBits = 0;
pfd.cAccumAlphaBits = 0;
pfd.cDepthBits = 0;
pfd.cStencilBits = 0;
pfd.cAuxBuffers = 0;
pfd.iLayerType = PFD_MAIN_PLANE;
pfd.bReserved = 0;
pfd.dwLayerMask = 0;
pfd.dwVisibleMask = 0;
pfd.dwDamageMask = 0;
pixelFormat = ChoosePixelFormat(dc, &pfd);
DescribePixelFormat(dc, pixelFormat,
sizeof(PIXELFORMATDESCRIPTOR),
&pfd);
if (pfd.dwFlags & PFD_NEED_PALETTE ||
pfd.iPixelType == PFD_TYPE_COLORINDEX )
BuildPalette( &pfd );
if ( pfd.dwFlags & PFD_DOUBLEBUFFER )
doubleBuffered = TRUE:
else
doubleBuffered = FALSE;
if(SetPixelFormat(dc, pixelFormat, &pfd) == FALSE)
exit(1);
rc = wglCreateContext(dc);
wglMakeCurrent( dc, rc );
SetupScene();
wglMakeCurrent( NULL, NULL );
}
The first task is to get the HDC. The HDC can be obtained
from the HWND or handle to the main window once that
window has been created by calling GetDC. It is worthwhile
to keep a copy of the HDC around since it is used for
creating a new context, making the context current, and
swapping buffers.
The second task is to choose the PixelFormat. Since the
PixelFormat is just an int that references a format from the
list of available formats, it is really more similar to a Visual
Id than it is to a VisualInfo structure. You can use the
ChoosePixelFormat function to get a PixelFormat that
fulfills your needs, or you can go through the list of
available formats and manually choose the one that suits
you. All you really need is the int in order to set the
PixelFormat on the HDC. Note that the format-choosing
algorithm of ChoosePixelFormat isn't quite the same as the
algorithm of glXChooseVisual. By this I mean that
glXChooseVisual assumes the values that you pass in are a
minimum requirement and therefore returns NULL if
nothing matches. ChoosePixelFormat, on the other hand,
assumes the values you pass in are more like a suggestion
and it will return the closest match that it can find even if it
doesn't meet your requirement (you might even get back a
ColorIndex PixelFormat when you requested RGBA).
The third task is to set the pixel format on the HDC. Setting
the PixelFormat on the HDC is what causes the HDC to take
on a certain configuration. This can be done only once per
thread and can't be changed once it is set. You use the
SetPixelFormat function to accomplish this, passing in the
int of the PixelFormat that you intend to use.
The last task that needs to be accomplished in the Init
function is to create an OpenGL graphics context. This is
needed to keep track of the OpenGL state and to allow
rendering calls to go to the graphics pipe. You obtain the
graphics context by calling wglCreateContext and passing in
the HDC. The graphics context returned can be used to
render to any HDC that was configured with the same
PixelFormat. It is important that the PixelFormat be set on
the HDC before the call to wglCreateContext, otherwise
OpenGL won't know how the context is to be configured
when it is created. In case you are wondering about the call
to
BuildPalette,
I will explain this in the follow-up article on Windows Palettes and OpenGL.
Handling Important Messages
At this point we have the three main pieces that I spoke
about in
"OpenGL Rendering Overview,"
so you are probably wondering why we need to handle any more
windows messages. Why can't we just start rendering now?
Well, in reality we can, but there are a few more things that
we should do so that things will continue to run smoothly
after our first rendering pass. This is where the other four
messages come in: WM_PAINT, WM_SIZE,
WM_ERASEBKGND, and WM_DESTROY. Now that we
have the graphics context, we will need to make it current
before calling any OpenGL functions.
When the WM_PAINT message comes in, it means that
Windows wants you to redraw the contents of your window.
I have found that in the Windows NT world it is best to do
what Windows wants when it wants you to, so when the
WM_PAINT message comes in I usually choose to call my
main draw function. You will notice a call to ValidateRect at
the end of my draw function. This is needed to tell Windows
that I have drawn to the window and any exposed areas have
been addressed. A simple draw function follows.
void Draw()
{
wglMakeCurrent(dc, rc);
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glMatrixMode( GL_MODELVIEW );
glPushMatrix();
glTranslatef( 0.0, 0.0, -250.0 );
glColor4f( 1.0, 0.0, 0.0, 1.0 );
glBegin( GL_QUADS );
glVertex2f( -20.0f, -20.0f );
glVertex2f( -20.0f, 20.0f );
glVertex2f( 20.0f, 20.0f );
glVertex2f( 20.0f, -20.0f );
glEnd();
glPopMatrix();
if ( doubleBuffered )
SwapBuffers(dc);
else
glFlush();
wglMakeCurrent( NULL, NULL );
ValidateRect(wnd, NULL);
}
The WM_SIZE message is just a message telling you that the
window size has changed. The WM_SIZE message is a good
time to reset your OpenGL viewport to the new window
size. The call to InvalidateRect in the function below tells
Windows that the window has changed and forces it to send
a WM_PAINT message to the window. A sample resize
function follows:
void Resize()
{
wglMakeCurrent(dc, rc);
glViewport(0,0,winWidth, winHeight);
InvalidateRect(wnd, NULL, FALSE);
wglMakeCurrent( NULL, NULL );
}
The WM_ERASEBKGND message is a special message that
Windows sends when it wants you to clear the background
of your window before it sends a WM_PAINT message. If
you don't handle the WM_ERASEBKGND message,
Windows will handle it for you, and this can cause quite a
bit of unnecessary flickering. You need to return a nonzero
value to this message; otherwise, Windows will assume that
the window hasn't been erased and will mark it as still
needing to be erased.
I added the WM_DESTROY message to the list since it is
sent when the window is being destroyed and is the best
place to do any cleanup that needs to be done. Generally I
use this opportunity to delete the OpenGL graphics context
and release the HDC back to the system. A simple cleanup
function follows:
void Quit()
{
wglMakeCurrent(NULL, NULL);
wglDeleteContext(rc);
ReleaseDC(wnd, dc);
PostQuitMessage(0);
}
OpenGL Integrated with MFC
Integrating OpenGL with MFC isn't all that different from
getting OpenGL to work in a standard Windows application.
All the same pieces are needed in the same order; it is just
accessing those pieces within the MFC framework that is a
bit different. I won't spend a lot of time explaining MFC,
but a base overview would probably be useful. In this
section I will cover setting up Visual Studio for OpenGL,
handling the CView creation, and dealing with messages.
Setting up Visual Studio for OpenGL
MFC supplies an application framework with four main
parts that allows application writers to skip most of the
skeleton code for an application and only add the code that
is specific to their program. The four parts are the CWinApp
(main application class), CMainFrame (outer window frame
including menus), CView (main work area), and CDocument
(data behind the application). Windows calls this the
Document/View architecture; there are quite a few articles
in the MSDN about this topic, so I won't add to the
confusion here. The basic concept is that the CMainFrame
acts as the container for any and all CViews within the
application. The CView acts like a window into the data (for
instance, you can view the data as a bar chart, a pie chart, or
a list of numbers). The CDocument is where all the actual
data is stored. There is nothing in MFC that requires you to
follow this framework, but Microsoft makes it somewhat
painful if you don't. You can actually have a CView that
contains all of the data and not use the CDocument at all.
For our purposes we will try to use the framework as it was
intended.
Since the CView is the window into the data and it contains
our canvas, it is the most logical place for us to set up and
store all of our necessary rendering pieces. We should set up
our drawable, configure it, and create our graphics context
all within the confines of the CView class. The CView class
is also going to be responsible for handling all of our
relevant messages. The CView will only rely upon the
CDocument to draw the data when it is ready to update the
drawable; otherwise, it should handle almost everything else.
For the sake of this article I am assuming that you know
enough about Visual C++ to at least create a Single
Document Interface (SDI) MFC application. It is just a
matter of clicking the correct buttons in the application
wizard. Once you have a base SDI MFC application then we
can start modifying it to support OpenGL.
The first step is to include the OpenGL header files so that
we have prototypes for the OpenGL functions. I have found
that the StdAfx.h file that the appwizard creates for you is
the best place to add these includes. We start by opening the
StdAfx.h file and including the following two headers near
the end of the file: <gl/gl.h> and <gl/glu.h>. Once this is
done we need to add the OpenGL and GLU libraries to the
link line so that we pick up the OpenGL functions when we
compile. On Windows NT the default OpenGL library is
called opengl32.lib and the GLU library is called glu32.lib.
We need to add these to the link line by doing the following:
Click on the Project pull-down and select the Settings
button. When the dialog appears go to the Link tab and add
opengl32.lib and glu32.lib to the Object/Library Modules
field and then click OK. That's it; we are now ready to start
adding code.
Handling the CView Creation
The changes we make will follow the same flow as for the
standard Windows application. We will start with the
window creation. Just like for the standard application, we
need to make sure the window is created with its own DC
and that it clips its siblings and children. Since this needs to
occur before the window is created, it needs to be handled in
a convenient function called PreCreateWindow. The
appwizard automatically adds this method to the CView
class for you, so you should be able to find it in your
*View.cpp file. We can make the needed changes by
modifying the CREATESTRUCT that is passed in before it
gets passed along to the base class PreCreateWindow
function. Your function will end up looking something like
the following:
BOOL CSimpleMFCView::PreCreateWindow
(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles
here by modifying
// the CREATESTRUCT cs
cs.style |= ( WS_CLIPCHILDREN |
WS_CLIPSIBLINGS | CS_OWNDC );
return CView::PreCreateWindow(cs);
}
At this point we have done the exact same task as modifying
the WinMain function in the standard Windows application,
and it is necessary to handle the first window message,
WM_CREATE. As a matter of fact, we can cut and paste the
code logic from our
Init function
into our
WM_CREATE message handler with only a minor change.
Since the CView class keeps a copy of the HWND for us in a
data member called m_hWnd, we don't need to store this
value in a wnd pointer; otherwise, the code is identical.
The first step is to create the message handler for our
WM_CREATE message. Use the Class Wizard to add an
OnCreate method to the CView class. This method will
handle all of the OpenGL initialization, including getting a
handle to the DC, creating the graphics context, and setting
up the OpenGL state.
Go to the OnCreate function of your view class and enter
the Init code. When you are finished the function should
look like this:
int CSimpleMFCView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: Add your specialized creation code here
PIXELFORMATDESCRIPTOR pfd;
int pixelFormat;
m_hDC = ::GetDC(m_hWnd);
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 24;
pfd.cRedBits = 0;
pfd.cRedShift = 0;
pfd.cGreenBits = 0;
pfd.cGreenShift = 0;
pfd.cBlueBits = 0;
pfd.cBlueShift = 0;
pfd.cAlphaBits = 0;
pfd.cAlphaShift = 0;
pfd.cAccumBits = 0;
pfd.cAccumRedBits = 0;
pfd.cAccumGreenBits = 0;
pfd.cAccumBlueBits = 0;
pfd.cAccumAlphaBits = 0;
pfd.cDepthBits = 0;
pfd.cStencilBits = 0;
pfd.cAuxBuffers = 0;
pfd.iLayerType = PFD_MAIN_PLANE;
pfd.bReserved = 0;
pfd.dwLayerMask = 0;
pfd.dwVisibleMask = 0;
pfd.dwDamageMask = 0;
pixelFormat = ChoosePixelFormat(m_hDC, &pfd);
DescribePixelFormat(m_hDC, pixelFormat,
sizeof(PIXELFORMATDESCRIPTOR),
&pfd);
if (pfd.dwFlags & PFD_NEED_PALETTE ||
pfd.iPixelType == PFD_TYPE_COLORINDEX )
BuildPalette( &pfd );
if ( pfd.dwFlags & PFD_DOUBLEBUFFER )
doubleBuffered = TRUE;
else
doubleBuffered = FALSE;
if(SetPixelFormat(m_hDC, pixelFormat, &pfd) == FALSE)
exit(1);
m_hRC = wglCreateContext(m_hDC);
wglMakeCurrent( m_hDC, m_hRC );
SetupScene();
wglMakeCurrent( NULL, NULL );
return 0;
}
We are going to store the graphics context (m_hRC) and the
device context (m_hDC) on the view so that we can access
them when we need to. Also note that we need to make sure
we call the global ::GetDC function because there is also a
GetDC method on the CView class that would return a CDC,
a DC class, instead of the HDC that we want.
Dealing with Messages
After modifying the OnCreate function we need to deal with
the actual drawing of the scene. In the standard Windows
application we handled the redraw whenever we saw the
WM_PAINT message. When dealing with MFC the
application wizard automatically creates an OnDraw method
that is called whenever a redraw is needed. It is in the
OnDraw method that we will add the code from our
draw function.
We can cut and paste the logic from the
standard Windows section, replacing the HDC and HGLRC
with the appropriate member variables. When we are done
the function should look like the following:
void CSimpleMFCView::OnDraw(CDC* pDC)
{
CSimpleMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
wglMakeCurrent(m_hDC, m_hRC);
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glMatrixMode( GL_MODELVIEW );
glPushMatrix();
glTranslatef( 0.0, 0.0, -250.0 );
glColor4f( 1.0, 0.0, 0.0, 1.0 );
glBegin( GL_QUADS );
glVertex2f( -20.0f, -20.0f );
glVertex2f( -20.0f, 20.0f );
glVertex2f( 20.0f, 20.0f );
glVertex2f( 20.0f, -20.0f );
glEnd();
glPopMatrix();
if (doubleBuffered )
SwapBuffers(m_hDC);
else
glFlush();
wglMakeCurrent( NULL, NULL );
ValidateRect(NULL);
}
For simplicity's sake I added the drawing logic to the CView class, but in most programs I add the logic to the CDocument
class. I would start by adding a public method called draw to my CSimpleMFCDoc class and then call this method from the
CView class. In this case the OnDraw method would look like this:
void CSimpleMFCView::OnDraw(CDC* pDC)
{
CSimpleMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
wglMakeCurrent(m_hDC, m_hRC);
pDoc->Draw();
if (doubleBuffered )
SwapBuffers(m_hDC);
else
glFlush();
wglMakeCurrent( NULL, NULL );
ValidateRect(NULL);
// Do not call CView::OnPaint() for painting messages
}
The draw logic from the CView class could then be moved into the CDocument class. This would allow the document, which
contains all of the data, to decide how it should be drawn.
We now need to add message handlers for three other messages just like we did for the WM_CREATE message. We need
handlers for the WM_SIZE, WM_ERASEBKGND, and WM_DESTROY messages. Use the Class Wizard to create methods
named Onsize, OnEraseBkgnd, and OnDestroy. We are going to add the code logic from the standard
Windows functions.
When we are done the functions should look like the following:
OnSize (refer also to void Resize
code sample)
void CSimpleMFCView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// TODO: Add your message handler code here
wglMakeCurrent( m_hDC, m_hRC );
glViewport( 0, 0, cx, cy);
wglMakeCurrent( NULL, NULL );
InvalidateRect(NULL);
}
OnEraseBkgnd (refer also to WM_ERASEBKGND
description)
void CSimpleMFCView::OnEraseBkgnd(CDC* pDC)
{
// TODO: Add your message handler code here and/or call default
return 1;
}
OnDestroy (refer also to void Quit
code sample)
void CSimpleMFCView::OnDestroy()
{
CView::OnDestroy();
// TODO: Add your message handler code here
wglMakeCurrent( NULL, NULL );
wglDeleteContext( m_hRC );
::ReleaseDC( m_hWnd, m_hDC );
}
With this you should now be able to get a simple MFC program with OpenGL working properly.
This article covered OpenGL rendering, OpenGL in standard Windows, and the integration of OpenGL with the Microsoft
Foundation Classes. Look for a discussion of Windows palettes and OpenGL in the next issue of Developer News.
feedback