OpenGL Tutorials:
This tutorial will cover how to create a simple Empty OpenGL Window from scratch. We will be creating a Desktop OpenGL Application with ability to operate in both “windowed” and “full-screen” modes.
First and foremost, this will require knowledge of Windows API. OpenGL is implemented on top of it. So, it is recommended that you know how to create blank windows before diving into setting up OpenGL.
{$ADSENSE1} lol?
I will assume the use the window-creation base code from my windows tutorials on this site. It will be used as the base for the OpenGL app we will be adding on top of it. This tutorial will focus specifically on OpenGL initialization process.
Creating a Blank Window in Windows API. If you have no idea how to program and/or show a window on the screen as a desktop application, may I suggest reading windows tutorial? And when you’re done come back here. If you’ve created Windows WIN32API based applications before, we’re ready to start.
Below, I will just go over the minimum requirements of creating an OpenGL window. By the end of this tutorial I will put everything together and demonstrate each step for creating an OpenGL application.
In the end we will have window/full-screen initialization base code that will be used throughout tutorials on this site. More information would be available from a wide variety of OpenGL books you can find online. OpenGL has been around for quite some time. Be careful not to purchase an outdated book!
To make it easier to tackle this comprehensive tutorial I decided to divide it into 4 distinct parts so you can move forward one part at a time. Let’s review.
- i. Window Device and OpenGL Rendering Contexts
- ii. OpenGL Initialization and Other Init Functions
- iii. Windowed and Full-Screen Modes
- iv. Putting It All Together
The first part will describe the Window Device Context and the OpenGL Rendering Context. These are probably the most hard parts of this tutorial.
I think I should start with the general idea of how by using the Windows OS you would draw graphics inside a window (GDI, not OpenGL) and that leads us to the Device Context.
i. Window Device and OpenGL Rendering Contexts
I don’t think I described the window device context in detail previously other than I said that the DC is used for drawing in a window, and we’ve successfully used it in the windows tutorials to draw pixels in the window. According to the Microsoft Visual C++ help files the device context is…
a Windows data structure containing information about the drawing attributes of a device such as a display or a printer. All drawing calls are made through a device-context object, which encapsulates the Windows APIs for drawing lines, shapes, and text. Device contexts allow device-independent drawing in Windows. Device contexts can be used to draw to the screen, to the printer, or to a metafile.
As you can see DC covers a wide variety of functionality from drawing to memory operations. Many times in normal (non-OpenGL) windows applications the DC is used with GDI to draw graphics such as lines and points.
If you don’t want to understand how OpenGL works with windows and rather start learning to draw polygons you can look at the base source code provided in this tutorial and skip to the next tutorial. But for the information-geeks I’ll describe it further. In the following paragraphs I explain how OpenGL works in Windows.
A Brief Introduction to How OpenGL works on Windows
OpenGL was designed as a platform-independent API. In other words when you call a function to display a polygon for example, it does not know you’re drawing that polygon in a Windows environment and therefore you need to inform OpenGL about it before calling that function. As you saw in the introduction to 3D tutorial OpenGL uses a special naming convention for functions. It also has a number of functions written to work specifically with Windows, which, on top of the normal “gl” prefix are prefixed with a “w”, for “windows”. Note, that by using these functions or even creating a window to say the least you basically declare that your program will not be cross-platform. I think that should be obvious. I will cover platform-independent OpenGL if there is enough demand for it. For now I just want to concentrate on Windows, since that’s the only OS I have. Anyway, using these wgl (wiggle) functions in association with a few more windows and OpenGL tricks you create a window. But lets get back to informing OpenGL of the environment we will be using.
The information you will provide OpenGL with varies between What color depth should be used? Should OpenGL use a Double Buffer for animation? And the like. But how do you provide OpenGL with this information? Windows has a structure called PIXELFORMATDESCRIPTOR which contains all of this information. Why not use the Device Context with OpenGL as with other normal GDI calls? The answer is that the Windows DC is limited to 2D graphics only and doesn’t have (or cares to) enough room to describe additional 3D information since Windows is a 2D-graphics based platform; that additional information is described in the pixel format descriptor and the OpenGL Rendering Context (more on it later). However you actually DO need to use the window DC with OpenGL, and we’ll see how, in a few minutes. But first, here’s how the pixel format descriptor structure is defined:
typedef struct tagPIXELFORMATDESCRIPTOR { // pfd
WORD nSize; // size of this structure
WORD nVersion; // version
DWORD dwFlags; // properties of the pixel buffer
BYTE iPixelType; // specifies the type of pixel data
BYTE cColorBits; // color depth of pixels
BYTE cRedBits; // ...the following bytes we're not
BYTE cRedShift; // interested in during initialization...
BYTE cGreenBits;
BYTE cGreenShift;
BYTE cBlueBits;
BYTE cBlueShift;
BYTE cAlphaBits;
BYTE cAlphaShift;
BYTE cAccumBits;
BYTE cAccumRedBits;
BYTE cAccumGreenBits;
BYTE cAccumBlueBits;
BYTE cAccumAlphaBits;
BYTE cDepthBits; // size of depth buffer
BYTE cStencilBits; // number of bits used for stencil buffer
BYTE cAuxBuffers;
BYTE iLayerType;
BYTE bReserved;
DWORD dwLayerMask;
DWORD dwVisibleMask;
DWORD dwDamageMask;
} PIXELFORMATDESCRIPTOR;
I skipped comments for many fields because this structure is described in the MVC++ documentation and is accessible by going to the Help menu. Other than that, the uncommented fields are not used during initialization. All of these fields are set during the creation process of the window, and are only set once. It is possible to change it to a new description, but you will need to destroy and re-create the window again. In reality there are two PFD implementations – native Windows (Software) implementation and the Hardware implementation. Each implementation can have a number of different PFDs, which depends on what your hardware and your drivers support. The software implementation always has 24 possible descriptions. The hardware implementation has a various number of possibilities, depending on the drivers and the hardware itself. I am telling you this because this will give you an idea of how many possible variations (mostly different resolutions and color depths) the Windows OS can possibly work in with OpenGL.
There is a handy windows function which enumerates all possible configurations. That function is DescribePixelFormat. You can use this function to determine possible features of your system.
int DescribePixelFormat
(
HDC hdc, // device context of interest
int iPixelFormat, // pixel format selector
UINT nBytes, // size of buffer pointed to by ppfd
LPPIXELFORMATDESCRIPTOR ppfd
// pointer to structure to receive pixel
// format data
This function also returns the maximum pixel format index (how many different PFD variations are available on your system). You need to pass it the device context of the window being created along with a few other (commented) parameters. One of the steps of creating an OpenGL window is selecting the Pixel Format Description the window will be using. When you select a PFD from many possible combinations that your system supports, you should assume that you are not 100% granted to use it. What I mean by it is that lets say your desktop resolution is set to 1024x768x16. You cannot tell OpenGL to use 24 or 32 bit for your application because it’s impossible to have two different color modes at once. 16-bit depth in this case, however, will work. But how do you know which settings the system is booted with? Windows provides you with yet another utility function ChoosePixelFormat which takes care of the issue by attempting to match the current screen settings with the ones specified in the provided device context and the PFD structure. The parameters of this function are the device context and a pointer to the PFD structure which contains the programmer-defined configuration. That configuration has to be selected manually to whatever you would like your application to support (say 640x480x32). So, to finish this all off, without any more confusion, here are the steps of setting up a desired configuration. (if possible, by the hardware implementation, otherwise it will find the closest match in native software mode).
// number of available formats
int indexPixelFormat = 0;
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, // useles parameters
16,
0,0,0,0,0,0,0
};
// Choose the closest pixel format available
indexPixelFormat = ChoosePixelFormat(hdc, &pfd);
// Set the pixel format for the provided window DC
SetPixelFormat(hdc, indexPixelFormat, &pfd);
Later on, when you examine the source code, you will see that I enclosed these lines in a function SetGLFormat which is called during the window creation process. Lets go over the parameters briefly.
First you need to specify the size of the structure, this is a must. The next parameter is 1, it specifies the version of the structure. Next, we see a series of flags.
PFD_DRAW_TO_WINDOW specifies that the buffer can draw to a window.
PFD_SUPPORT_OPENGL is what it is; OpenGL support
PFD_DOUBLEBUFFER specifies that a double-buffer system will be used.
PFD_TYPE_RGBA each pixel has 4 components: red, green, blue, alpha (rgba)
An alternative to PFD_TYPE_RGBA is PFD_TYPE_COLORINDEX supporting color-indexed modes (each pixel is looked up in the color table). We’re not going to use that here.
I need to go back to window device context for a minute here. Recall that before drawing anything in a window you’d like to have a device context for that window, associated with whatever device you’re rendering to. As you also recall from my earlier windows tutorials you create such context with a call to GetDC; a function that takes the handle of a created window and in return, returns the DC associated with that window. To let your window know you’re not going to draw anything anymore (usually during the application shutdown) you release the device context with a call to ReleaseDC. All this is demonstrated in my windows tutorials. Now that I refreshed your memory a bit, lets continue discovering new things.
In the early days of windows (and I mean Windows 3.1 here), you needed to allocate and release the DC every time you wanted to draw something because your application could only have so much memory allocated for the DC at a time (remember there were other applications running at the same time in Win 3.1 too even though it wasn’t a fully multi-task OS). GetDC and ReleaseDC are quite expensive calls even on relatively recent Win OS’es (95/98/2000) and you wouldn’t want to call these functions each frame of animation. There is a “new” way of doing this. When you specify the CS_OWNDC window style flag in the WNDCLASSEX structure’s wc.style member, only ONE device context is created and used throughout the program thus avoiding the memory alloc./de-alloc. bottleneck. Why couldn’t it be done back in Windows 3.1? Keep in mind that back then the memory resources were very limited.
Well this brings us all the way to the next topic that I mentioned once earlier. And it is the OpenGL Rendering Context. (As if the window device context wasn’t enough!). This is very identical to the window DC, and it also holds all kinds of information about the window you’re drawing in. The difference is that that information is useful to OpenGL, and not GDI and window drawing functions, as in the window DC case. As you might imagine there can be a number of windows displayed at the same time. Initially OpenGL doesn’t know what window to render to. The truth is, you will need to associate the window DC with OpenGL to tell it where to draw its graphics to. All of the new information is then stored in the OpenGL rendering context. The cross-Windows/OpenGL based function wglCreateContext does just that for us. This function returns a handle to this so-called rendering context which is defined as HGLRC (Handle to an OpenGL Rendering Context), as opposed to a Device Context Handle: HDC.
HGLRC hglrc = wglCreateContext(hdc);
To prevent any other currently operating windows draw graphics in the OpenGL window, you need to notify the window you’re creating that it will be in fact the only window the OpenGL will be drawing to and no other data should be rendered into the window view. To do this you specify two more window style flags when creating the window. These flags are WS_CLIPCHILDREN and WS_CLIPSIBLINGS.
It is possible to have more than one OpenGL rendering contexts in one application for example, say, for two different views in a frame based MFC application. You can only use one rendering context at a time. To make the desirable context active, you call wglMakeCurrent and pass the hdc and hglrc handles to it. In our case we will only have one OpenGL device context since we will only have one window. When you’re done with your application you have to release both contexts. It is done in the shutdown part of the application in the source code as you will see when you browse through it. This is exactly the function that does that for us, straight from the source code:
void SysShutdown (void)
{
wglMakeCurrent(hdc, NULL); // release current device context
wglDeleteContext(hglrc); // delete rendering context
PostQuitMessage(0); // make sure the window will be destroyed
}
This rounds up our discussion of the Windows device context and the OpenGL rendering context. You don’t need to understand these 100% but they needed a more than average explanation indeed. When I was just beginning I knew that feeling of coding down something from a book or a tutorial without knowing how it actually works. And this is probably why I’m trying to explain everything in detail here. Of course I might skip some even more deeper explanation but we don’t need to go there. And with this thought lets get to the next topic of this tutorial which is going to be a lot more exciting (but only compared to this one!).
ii. OpeGL Initialization Functions
OpenGL has a number (over 300 to my knowledge) functions, such as for drawing polygons on the screen, defining light sources in 3d space, defining texture coordinates on your polygons and the list goes on. There are a few functions OpenGL provides us with for window and 3D environment initialization purposes. In this section I want to go over these functions and the rest of functions from the base code, so when you read the source you won’t have to think about what those functions do. This will also prevent heavy commenting in the source code which makes things ugly.
All of the functions listed below are the meat of the base code. So, study them before you look at the source code. I think it’s better to explain different parts of the code first, rather than just going through the code function by function explaining what those functions do; this way you will learn all required parts first and then you will put everything together in the last part of this tutorial. Anyway, here is the list of all OpenGL (and non-OpenGL) functions REQUIRED for my base code, followed by a brief explanation.
Basically, all you need to draw in a window with OpenGL is to make a call to the two following functions after the window has been created.
HGLRC wglCreateContext(HDC hdc); creates an OpenGL rendering context
bool wglMakeCurrent(HDC hdc, HGLRC hglrc); make HGLRC current rendering context
It’s true, but just what – and more importantly – HOW are you going to draw anything in the window? Remember I talked about perspective and projection in the Introduction to 3D tutorial? OpenGL has to know about these things as well. For example, what projection you will be using? (orthographic or perspective-correct), what is the angle of the viewing volume sides, what polygon shading model you will be using? Should we use hardware back-face removal? and so on. For all of these purposes OpenGL has a function. After the window has been created I will call the InitOpenGL function which will set everything up, such as things mentioned above. Lets take a look at other functions OpenGL provides us with, which will be called from within InitOpenGL.
glClearColor(float red, float green, float blue, float alpha); is used to indicate the red, green, blue and alpha components of the color the video buffer will be cleared with when you make a call to glClear(GL_COLOR_BUFFER_BIT); (this function is described a little later). I want to add that when I specify a float for an OpenGL function parameter, it’s not necessary defined as a float in OpenGL, but with an OpenGL native type, for example GLclampf instead of a float because they’re basically identical. The reason for this is that it is easier to comprehend for a C/C++ programmer
glClearDepth(float depth); same as above but this time it specifies the value to clear the depth buffer with. The actual clearning of the depth buffer occurs during a call to glClear(GL_DEPTH_BUFFER_BIT). If you wanted to clear both the video buffer and the depth buffer at once, you would OR the two flags: glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glEnable(int cap); This function has many uses. It can “enable” many features of OpenGL; the feature you want to enable is specified in the flag cap. In the base code, within InitOpenGL we will use this function to enable Depth Sorting. This lets OpenGL sort polygons so that the polygons that are drawn behind or crossing other polygons are always displayed right in the 3D space, and not in the order you are drawing them in. For this to work we enable the depth buffer by calling glEnable(GL_DEPTH_TEST); Later on we will use glEnable to enable a bunch of other interesting things and they will be done for us by OpenGL on command!
glDepthFunc(int func); is used to determine the condition used during the Depth Sorting algorithm. This comparison only takes place if depth test (or depth sorting) is enabled by glEnable. The Depth Buffer is also known as the Z-buffer and is used to store only the Z values in a screen WIDTH by HEIGHT array. When you draw a triangle on the screen, only its Z values are stored in that buffer. When you draw a new rectangle at that same position but maybe rotated a little, the Z value of that triangle is compared to whatever was in the Z-buffer previously and if a condition is met during each scanline rendering, the pixel is rendered at the corelated X, Y and Z. In our case, we will call glDepthFunc(GL_LEQUAL); GL_LEQUAL being “less or equal”. That means that the Z value will pass (or the pixel will get rendered at that location) if it is less than or equal to the currently stored Z value in the depth buffer at that position.
glShadeModel(int mode); selects the polygon shading model. mode is the flag representing the shading mode. This flag can be set to either GL_FLAT or GL_SMOOTH. GL_SMOOTH shading is the default shading model, causes the computed colors of vertices to be interpolated as the primitive is rasterized, typically assigning different colors to each resulting pixel fragment. GL_FLAT shading selects the computed color of just one vertex and assigns it to all the pixel fragments generated by rasterizing a single primitive. In either case, the coputed color of a vertex is the result of lighting, if lighting is enabled, or it is the current color at the time the vertex was specified, if lighting is disabled.
glHint(int target, int mode); specifies implementation-specific hints. target specifies the behavior to be controlled. In other words, this is what you want to change. mode is HOW you want to change it. mode specifies the properties of target to be selected. What we will do is specify the quality of polygon-rendering by targeting GL_PERSPECTIVE_CORRECTION_HINT and setting GL_FASTEST mode for it. What it will do, is give our polygons are normally-interploated look which is the fastest rendering mode. It is actually up to you, and you can specify the mode to be GL_NICEST in which case you will get the best quality look in exchange for some performance loss. Just call glHint(GL_PERSPECTIVE_CORRECTION_HINT, mode); with either GL_FASTEST or GL_NICEST as the mode parameter, whatever works best for you.
Again, all of the above will be called from InitOpenGL, which is called when a WM_CREATE message occurs. I think it is the most reasonable place to initialize everything. InitOpenGL is not the only function called during a WM_CREATE message. Also note, the WM_CREATE message is sent only AFTER the window is created with CreateWindowEx. In the source code I commented where the WM_CREATE code execution takes place, and it takes place in the CreateWnd function, and in fact, it is generated by calling CreateWindowEx.
CreateWnd on the other hand is the function I call from within WinMain before the main loop takes place to set the chain of events that will eventually initialize a desired OpenGL window. CreateWnd is really all you need to change the appearance of your application; you can create a windowed or a full-screen application by only changing one flag. This function is explained in more detail below.
The following are the additional (non-OpenGL; my own) functions from the base code that take care of initialization and uninitialization of the application itself, not necessary OpenGL. Lets take a look.
CreateWnd(HINSTANCE hinst, int width, int height, int depth, int type); This function creates the window. It is called from WinMain before entering the main program loop. You need to pass it the handle to the window (defined as HINSTANCE hinstance in WinMain), the resolution of the screen and the color depth you want for your application (defined in winmain.h; simply change these parameters in winmain.h to whatever you want and you’re good to go, no need for new code), and finally you specify the type of application through the type parameter. This parameter must be either WINDOWED or FULLSCREEN. These two are also defined in winmain.h and as you can tell they identify what mode your application will be running in, full-screen or windowed. Here is an example of using it. Lets say we want to create a 640x480x16 full-screen application. Inside WinMain, before the main program loop starts, you would type the following:
CreateWnd(hinstance, 640, 480, 16, FULLSCREEN); And the rest of the code will build this type of OpenGL app. To make it even easier, the application screen parameters are defined in winmain.h so if you want your program to have a different resolution, other than originally specified in the base code, just open up winmain.h and change those parameters.
void SetGLFormat (void); is normally called after the window is created. You can always look up the insides of this function in the source code not to mention I covered that already, but to refresh your memory it picks the apropriate pixel format description from the many enumerated ones for your application. The pixel format descriptions were explained earlier in this tutorial as well.
void SysShutdown (void); is what I use to deallocate all application memory and just uninitialize whatever was initialized, all in one function. This function is normally called from WndProc, when a WM_DESTROY message occurs. And it occurs after the window had been destroyed. What’s inside of this function is explained just slightly above if you scroll up a little.
void SysSetDisplayMode (int width, int height, int depth); Set screen to whatever resolution you want. This function is called after the window had been created, from function CreateWnd, IF type flag is set to FULLSCREEN.
void SysRecoverDisplayMode (void); is called when WM_DESTROY message is received to set the screen resolution back to what it originally was before calling SysSetDisplayMode.
I should mention that the last 3 functions are defined in a separate header file – system.h (and system.cpp) If I come up with more “system” related calls in the future tutorials I will add more functions to my system function library. This is done so winmain.h and winmain.cpp don’t get cluttered up with code that doesn’t belong to window creation and the main message loop.
The last 2 functions are explained in more detail in the section III. But lets get back to OpenGL related functions. There are a few more functions we need to know! These are mostly used for specifying how your objects will appear on the screen on the algorithmic level because they deal with matrices and projections.
void glViewport(int x, int y, int width, int height); is a function that links the relationship between the 3D-to-2D projected image with the window. This function wants to know the dimensions of your window, but you can specify a little smaller viewport rectangle. This is for cases when you don’t want the viewing rectangle to take whole window. You’d like to call it every time the window is resized in windowed mode to adjust to the new size.
Actually this is a little different as the window coordinates. The x and y actually specify the LOWER-LEFT corner and the width and height specify the dimensions of the window in pixels, but can be smaller than the window. By specifying x and y both to be, say 10, and width and height the width and height of the window minus 20, the image will be rendered into the window with a border of 10 pixels wide around the viewport.
void glMatrixMode(int mode); this function selects the current matrix stack. This is done so that any consequent matrix-related function will be applied to the matrix stack specified by mode. That way you can apply changes to a variety of matrix stacks using the same functions. The possible mode values are GL_MODELVIEW; selects model view matrix stack, GL_PROJECTION; selects projection matrix stack and GL_TEXTURE; selects texture matrix stack.
To really understand what this means you need a somewhat solid understanding of matrixes. You see, with OpenGL you don’t need to actually worry about matrices and what they are, but it’s handy to know and understand them, indeed. I recommend searching the net for the 3D matrix FAQ, or getting a book that explains matrices.
void glLoadIdentity(void); loads the currently selected (by glMatrixMode) matrix with the identity matrix. If you know your matrices, that means that the contents of the matrix are cleared. This is to say, to reset the matrix and start modifying it from scratch (by consequent calls). Lets take a look at the two following calls.
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
These two functions will load the identity matrix into the projection matrix.
Is this all? No. But we’re getting closer (to the end of this tutorial, that is). I held this one last OpenGL init.-related topic to this point for a number of reasons. First, because it is related to both the windowed mode and the full-screen mode and it relates so in different ways for each mode. The windowed mode application calls the following function (Resize) as many times as the window is re-sized in addition to the mandatory first time.
In full-screen mode, you cannot re-size the window, and therefore it is only called once, during initialization. The first time is an absolute must, because it initializes the perspective and how your objects will appear on the screen! Imagine this logical step for a second: when you resize your window, you need to adjust the perspective to it somehow as well so the view and all objects are adjusted to the new window rectangle.
If this step is skipped, you will see some unwanted things, like the perspective view being reset or clearly not showing the objects with the right ratio between the object sizes and the window size. Here is the magic function:
void Resize (int width, int height)
{
if (height <= 0)
height = 1;
int aspectratio = width / height;
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, aspectratio, 0.2f, 255.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
The initial if statement makes sure height is never 0, this is to prevent a division by 0 and then this is all mostly OpenGL function calls that I explained above, except one. gluPerspective. Which sets up the perspective projection matrix. Lets explore the 4 parameters it takes, visually.
gluPerspective(double fovy, double aspect, double zNear, double zFar);
The first parameter fovy is the field-of-view angle in vertical direction. aspect is the aspect ratio between the width and height of the viewing plane. zNear and zFar are the distances from the viewing point to the near and far planes respectively. The former two values should never be negative.
Again, gluPerspective is called each time the window is resized, making sure the view will get resized appropriately to suit the current width and height of the window. The function Resize is at least called once to initialize the perspective and portview. This is done during the execution of the WM_CREATE message thread. WM_CREATE is only received once after the window has been created but not yet shown on the screen.
iii. Windowed and Full-Screen modes
Now it’s time to actually implement everything that’s been said and put it all to action. But before I do so, I want to cover one last topic. As you may already know you can create both window based and full-screen applications. There are two different ways to initialize each. The main difference between the two is that during initialization of a window you don’t switch the screen resolution to something specific, say 640×480 as opposed to a full-screen application, and the window can be any size at all, not depending on the screen resolutions supported by your hardware. So when we will create a full-screen application, the extra step will be setting the screen resolution to something. And when we’re done with our program, we need to switch back to the default resolution. The function provided for us by Windows for that purpose is ChangeDisplaySettings. You can switch to a desired resolution and siwtch back to the default resolution with this function. To switch resolution, you need to fill a DEVMODE struct with some information about the mode you’re switching to and pass this info to the ChangeDisplaySettings function. Here are examples of how this can be done:
DEVMODE dmode;
memset(&dmode, 0, sizeof(DEVMODE));// clear all memory
dmode.dmSize=sizeof(DEVMODE); // it's required to specify the size of this struct
dmode.dmPelsWidth = width; // width and height of desired resolution
dmode.dmPelsHeight = height;
dmode.dmBitsPerPel = bits; // color depth; 8/16/32 bit
dmode.dmFields = DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
// change resolution, if possible
if (ChangeDisplaySettings(&dmode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
{
// if not... failed to change resolution
}
// enter main loop somewhere here
// restore resolution at the end of your program
ChangeDisplaySettings(NULL, 0)
When setting the screen resolution, ChangeDisplaySettings takes two parameters: a pointer to the DEVMODE struct and the full screen mode flag. On the other hand, when restoring resolution back to what it previously used to be you don’t have to specify these. Simply pass NULL and 0 as parameters and if everything goes well (as it should) the screen resolution will change back. Don’t use ChangeDisplaySettings(NULL, 0) before actually setting the resolution because it is meaningless. In any case, for more information about this function look it up in the MVC++ Documentation by selecting it and hitting F1. I wrapped the functionality of these two calls into previously mentioned functions SysSetDisplayMode and SysRecoverDisplayMode as you will see in the base code. Again, these two functions are stored in system.h
iv. Putting It All Together
Finally, we need to put everything together. This is the most relaxing chapter of the whole tutorial. Now we know (almost) everything and it’s time to put it into action. In this part I will describe everything needed to initialize an OpenGL window step by step (no, not again, just as far as the functions go!). I will describe which function should be called and where and why. After that you will be ready to download and modify the base code.
Lets think for a while, where does a Windows-based program start? The answer is the WinMain function. The window is being created and all components are being initialized before we enter the main message loop. Then the window starts to stream messages and let WinProc process those messages. Putting the knowledge you gained in this tutorial and those facts, our hierarchy of steps to create an OpenGL application is as follows:
0. Declare variables
1. Create and register the window class
2. Switch resolution if FULLSCREEN flag is used
3. Create the window (WM_CREATE message occurs at this point)
4. Get window device context
5. Select pixel format description
6. Create the OpenGL rendering context
7. Make the OpenGL rendering context current rc
8. Call Resize to initialize the view and perspective
9. Call InitOpenGL to initialize rendering style and misc. stuff
10. Show the window
11. Enter the main message loop
12. Render a frame
13. Swap video buffers
14. See if a WM_SIZE message is received (the window has been resized), if so call Resize.
… repeat 12 to 14 until exit (the loop is broken)
15. Shutdown the program and release hdc and hglrc
Lets go through it in a step by step fasion.
0. Declare variables (this is actually winmain.h! comments cover explanations nicely)
// OpenGL base code
// written-updated: september 2002-2009
// winmain.h
// from:
// www.falloutsoftware.com/programming.php4
#define WINDOWED 0 // predefined flags for initialization
#define FULLSCREEN 1
#define SCRWIDTH 640 // width of the window
#define SCRHEIGHT 480 // height of the window
#define SCRDEPTH 16 // bit depth
#define WNDCLASSNAME "GLClass" // window class name
#define WNDNAME "OpenGL base code" // string that will appear in the title bar
extern HDC hdc; // device context handle
extern HGLRC hglrc; // OpenGL rendering context
extern HWND hwnd; // window handle
extern int screenw; // when window is resized, the new dimensions...
extern int screenh; // ...are stored in these variables
extern int screenmode; // FULLSCREEN or WINDOWED?
extern bool quit; // indicates the state of application
extern indexPixelFormat; // number of available pixel formats
1. Create and register the window class (this takes place as the first step in WinMain)
MSG msg;
WNDCLASSEX ex;
ex.cbSize = sizeof(WNDCLASSEX);
ex.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC;
ex.lpfnWndProc = WinProc;
ex.cbClsExtra = 0;
ex.cbWndExtra = 0;
ex.hInstance = hinstance;
ex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
ex.hCursor = LoadCursor(NULL, IDC_ARROW);
ex.hbrBackground = NULL;
ex.lpszMenuName = NULL;
ex.lpszClassName = WNDCLASSNAME;
ex.hIconSm = NULL;
RegisterClassEx(&ex);
2. Switch resolution if FULLSCREEN flag is used & 3. Create the window
void CreateWnd (HINSTANCE &hinst, int width, int height, int depth, int type)
{
// center position of the window
int posx = (GetSystemMetrics(SM_CXSCREEN) / 2) - (width / 2);
int posy = (GetSystemMetrics(SM_CYSCREEN) / 2) - (height / 2);
// set up the window for a windowed application by default
long wndStyle = WS_OVERLAPPEDWINDOW;
screenmode = WINDOWED;
if (type == FULLSCREEN) // create a full-screen application if requested
{
wndStyle = WS_POPUP;
screenmode = FULLSCREEN;
posx = 0;
posy = 0;
// change resolution before the window is created
SysSetDisplayMode(screenw, screenh, SCRDEPTH);
}
// create the window
hwnd = CreateWindowEx(NULL,
WNDCLASSNAME,
WNDNAME,
wndStyle|WS_CLIPCHILDREN|WS_CLIPSIBLINGS,
posx, posy,
width, height,
NULL,
NULL,
hinst,
NULL);
// at this point WM_CREATE message is sent/received
// the WM_CREATE branch inside WinProc function will execute here
Right after the window is created with CreateWindowEx, a WM_CREATE message is sent. The execution continues at WM_CREATE branch in the WinProc function. Lets take a look at what’s going on in WM_CREATE thread, step by step. Steps 4 through 10 list what happens inside the scope of case WM_CREATE { ..here.. } which takes place inside the WinProc function.
4. Get window device context
if ((hdc = GetDC(hwnd)) == NULL) // get device context
{
MessageBox(hwnd, "Failed to Get the Window Device Context",
"Device Context Error", MB_OK);
SysShutdown();
break;
5. Select pixel format description
SetGLFormat(); // select pixel format
6. Create the OpenGL rendering context
if ((hglrc = wglCreateContext(hdc)) == NULL) // create the rendering context
{
MessageBox(hwnd, "Failed to Create the OpenGL Rendering Context",
"OpenGL Rendering Context Error", MB_OK);
SysShutdown();
break;
7. Make the OpenGL rendering context current rc
if ((wglMakeCurrent(hdc, hglrc)) == false) // make hglrc current rc
{
MessageBox(hwnd, "Failed to make OpenGL Rendering Context current",
"OpenGL Rendering Context Error", MB_OK);
SysShutdown();
8. Call Resize to initialize the view and perspective
Resize(SCRWIDTH, SCRHEIGHT);
9. Call InitOpenGL to initialize rendering style and misc. stuff
OpenGLInit();
10. Show the window
ShowWindow(hwnd, SW_SHOW); // everything went OK, show the window
UpdateWindow(hwnd);
At this point, the window is displayed and we’re ready to enter the main message loop. The execution is returned back to CreateWnd and then instantly to WinMain, from the WM_CREATE “thread”. So, we go back into WinMain at the line right after the call to CreateWnd function which continues the program:
Steps 11, 12 and 13 are summarized below
11. Enter the main message loop
12. Render a frame
13. Swap video buffers
All of the above 3 steps are done in the following code clip
// The message loop
while (!quit) {
if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
if (msg.message == WM_QUIT)
quit = true;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
RenderFrame();
SwapBuffers(hdc);
if (GetAsyncKeyState(VK_ESCAPE))
SysShutdown();
As you can see, what’s added to the normal window loop (described in the windows tutorials) is RenderFrame(); which draws the scene, SwapBuffers(hdc); which “flips” the back buffer to the primary buffer (just like in the 6th DirectDraw tutorial, if you read it) and a test whether the Escape key is hit; if it is the program is terminated by calling SysShutdown. SysShutdown is also described in the last step (step 15).
14. See if a WM_SIZE message is received (the window has been resized), if so call Resize.
// somewhere in WinProc...
// Tutorial note: like WM_CREATE, this is processed inside WinProc.
// The switch/case statement tests for the message to be WM_SIZE in this case,
// and if it is, I call the Resize function, and store the new window
// width and height in variables screenw and screenh. The new resized width
// of the window is passed to WinProc as the low word of lparam. the new height
// of the window is passed to WinProc as the high word of lparam.
case WM_SIZE:
{
// resize the viewport after the window had been resized
Resize(LOWORD(lparam), HIWORD(lparam));
screenw = LOWORD(lparam);
screenh = HIWORD(lparam);
break;
}
15. Shutdown the program and release hdc and hglrc
void SysShutdown (void)
{
wglMakeCurrent(hdc, NULL); // release device context in use by rc
wglDeleteContext(hglrc); // delete rendering context
PostQuitMessage(0); // make sure the window will be destroyed
if (screenmode == FULLSCREEN) // if FULLSCREEN, change back to original res.
SysRecoverDisplayMode();
Final words
And this is how OpenGL and Windows fit together. What I’ve just explained is how my base code works. You can modify it to anything you want and I hope this won’t be too hard. As a tutorial goodie, not only did I include the source code in the zipped file for this tutorial but also a printable version of the functions that I use in my base code, as I explained them above. The good thing is that now you can print it out and when you don’t know what function does what you can quickly look it up in the list. That additional file is named funclist.txt.
This has really covered the basics of the initialization processes in detail and you are now ready to go deeper into GL world. Compile and run the source code. You should see a blank window. I don’t think it’s fair for so much text covered, do you? But at least you now know what’s going on behind the scenes of initialization – most of the time a skipped topic of many tutorials. By the way the source code provided in this tutorial is the base code I will be using throughout the series of GL tutorials, unless I come up with something that will make it worth modifying it. It might not be 100% perfect but if you find any mistakes please let me know. However, as I can see it works well.
One thing to note, it’s not that I am a comment-hating person, but most of my tutorial source code will not have a lot of comments. Most of the things are covered in tutorials themselves, so why repeat it again in the source code? It only destroys the structure of code.
I will comment here and there on most important parts of the code but I will not comment each line. I will comment the extern variable declarations in header files. Many people don’t like heavy commenting in tutorials because when they come to using that code, they might prefer their own style of commenting and I’m sure there can be other reasons as well!
{$ADSENSE1}
Let’s see what happens next. I should cover the basic primitive drawing tutorials relatively quick. Then I will go on to more advanced topics such as terrain generation and maybe even BSP trees and PVS and maybe a Quake MD2 loader. So anyway, see you next time.