Introduction to C++ OpenGL Programming:
Welcome to another fine lesson in C++! Today you'll be introduced to the wonderful world of OpenGL. OpenGL is a fairly straight forward -- although at many times confusing -- concept. OpenGL gives the programmer an interface with the graphics hardware. OpenGL is a low-level, widely supported modeling and rendering software package, available on all platforms. It can be used in a range of graphics applications, such as games, CAD design, or modeling (to name a few).
OpenGL is the core graphics rendering option for many 3D games, such as Quake 3. The providing of only low-level rendering routines is fully intentional because this gives the programmer a great control and flexibility in his applications. These routines can easily be used to build high-level rendering and modeling libraries. The OpenGL Utility Library (GLU) does exactly this, and is included in most OpenGL distributions!
Unlike DirectX, OpenGL is only a graphics API; it doesn't include support for functionality such as sound, input, or networking (or anything not related to graphics). That's ok, however, because in future tutorials I will teach you how you how to use DirectX for these things.
OpenGL was originally developed in 1992 by Silicon Graphics, Inc, (SGI) as a multi-purpose, platform independent graphics API. Since 1992 all of the development of OpenGL has been headed by the OpenGL Architecture Review Board (ARB). This exclusive board is composed of the major graphics vendors and industry leaders. Some of these are Intel, IBM, NVIDIA, Microsoft, and Silicon Graphics.
OpenGL is a collection of several hundred functions that provide access to all of the features that your graphics hardware has to offer. Internally it acts like a state machine-a collection of states that tell OpenGL what to do. Using the API you can set various aspects of this state machine, including current color, blending, lighting effect, etc.
This is a very general introduction to OpenGL, and you may find other in-depth introductions elsewhere. There is a reason, however, to my generalized introduction. I don't want to slam you with specifics, but give you an idea as to what OpenGL is so that you may decide for yourself if this set of lessons is for you.
OpenGL vs. DirectX: A Comparison:
The competition between OpenGL and DirectX is possibly as well known as the wars waged between AMD and Intel enthusiasts. This topic has sparked the fires of many flame wars throughout the years, and I don't anticipate that changing anytime soon. I won't preach why I prefer OpenGL over DirectX, but rather lay out the facts and let you make that decision. So let's dive in!
Perhaps the most obvious difference is that DirectX, as opposed to OpenGL, is more than just a graphics API. DirectX contains tools to deal with such components of a game as sound, music, input, networking, and multimedia. On the other hand, OpenGL is strictly a graphics API. So what aspect of OpenGL sets it apart from the DirectX graphics component?
Well, first things first: both APIs rely on the use of the traditional graphics pipeline. This is the same pipeline that has been used in computer games since the early days of computer graphics. Although it has been modified in slight ways to adapt with advancements in hardware, the basic idea remains intact.
Both OpenGL and DirectX describe vertices as a set of data consisting of coordinates in space that define the vertex location and any other vertex related data. Graphics primitives, such as points, lines, and triangles, are defined as an ordered set of vertices. There is a difference in how each API handles how vertices are combined to form primitives.
There are a bunch of differences in the DirectX and OpenGL APIs, so I will list a few of those for you. This chart is based off the book, OpenGL Game Programming and a few of these may now be incorrect as new DirectX versions are released. If you wish to correct me, please do via one of the ways listed at the end of this tutorial.
Table 1.1:
Feature: OpenGL DirectX
Vertex Blending N/A Yes
Multiple Operating Systems Yes No
Extension Mechanism Yes Yes
Development Multiple member Board Microsoft
Thorough Specification Yes No
Two-sided lighting Yes No
Volume Textures Yes No
Hardware independent Z-buffers Yes No
Accumulation buffers Yes No
Full-screen Antialiasing Yes Yes
Motion Blur Yes Yes
Depth of field Yes Yes
Stereo Rendering Yes No
Point-size/line-width attributes Yes No
Picking Yes No
Parametric curves and surfaces Yes No
Cache geometry Display Lists Vertex Buffers
System emulation Hardware not present Let app determine
Interface Procedure calls COM
Updates Yearly Yearly
Source Code Sample SDK Implementation
So now you know what separates DirectX and OpenGL, and hopefully you have chosen based on facts which you would prefer, not on myths or opinions.
Introduction to Windows Programming and OpenGL:
For the programs we will be creating you will need a base understanding of the mechanics and structuring of the Windows operating system. Not to worry, however, because I am going to teach this to you!
Microsoft Windows is a multi-tasking operating system that allows multiple applications, referred to here on out as processes. Every process in Windows is given a certain amount of time, called a time slice, where the application is given the right to control the system without being interrupted by the other processes. The runtime priority and the amount of time allocated to a process are determined by the scheduler.
The scheduler is, simply put, the manager of this multi-tasking operating system, ensuring that each process is given the time and the priority it needs depending on the current state of the system.
When it comes to game developers, you will find a large common interest in multithreading. Processes can be broken down into threads, where each thread can execute its own code to allow for multitasking within a single application. The scheduling for threads is the same as that for processes, except that threads are what make up the process.
This means that within your games you can have multiple threads running that can perform multiple calculations at once and thus provide your game with multitasking within itself. To go a step farther we can do a quick explanation of fibers. In newer versions of Windows (Windows 98+) there is an even lower level execution object exists, called a fiber. Each thread in your process has the ability to house multiple fibers that can perform multiple operations at once, all in a single thread.
Windows is what is known as an event-driven operating system. What this means is that each process is executed based on the events they receive from the operating system. For instance, an application may sit at idle and wait for the user to press a key. When that key is pressed Windows will register an event to the application that the key is down.
Windows programming is difficult at all in most cases, and to start we'll use the most basic program ever created. Hello World is used in almost every class when you begin programming in any language, and that won't change here. So, shall we have a look at Hello World in Windows style? Sure!
Microsoft Windows is a multi-tasking operating system that allows multiple applications, referred to here on out as processes. Every process in Windows is given a certain amount of time, called a time slice, where the application is given the right to control the system without being interrupted by the other processes. The runtime priority and the amount of time allocated to a process are determined by the scheduler.
The scheduler is, simply put, the manager of this multi-tasking operating system, ensuring that each process is given the time and the priority it needs depending on the current state of the system.
When it comes to game developers, you will find a large common interest in multithreading. Processes can be broken down into threads, where each thread can execute its own code to allow for multitasking within a single application. The scheduling for threads is the same as that for processes, except that threads are what make up the process.
This means that within your games you can have multiple threads running that can perform multiple calculations at once and thus provide your game with multitasking within itself. To go a step farther we can do a quick explanation of fibers. In newer versions of Windows (Windows 98+) there is an even lower level execution object exists, called a fiber. Each thread in your process has the ability to house multiple fibers that can perform multiple operations at once, all in a single thread.
Windows is what is known as an event-driven operating system. What this means is that each process is executed based on the events they receive from the operating system. For instance, an application may sit at idle and wait for the user to press a key. When that key is pressed Windows will register an event to the application that the key is down.
Windows programming is difficult at all in most cases, and to start we'll use the most basic program ever created. Hello World is used in almost every class when you begin programming in any language, and that won't change here. So, shall we have a look at Hello World in Windows style? Sure!
#includeint APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MessageBox(NULL, "\tHello World!", "My first windows app", NULL); return 0; }
As you can see, it's pretty straight forward. Don't pay attention to the things that don't make sense, as in the next lesson we will go in-depth to explain WinMain () and other functions, as well as show how this and OpenGL work together!
WinMain() and the Windows Procedure:
Every Windows programming needs a main entry point. That being said, you may be wondering what this entry point is. The main entry point for any Windows program is called WinMain. The function prototype for WinMain is a little confusing at first, but as we continue to work with it you'll notice it becomes much easier to understand. Well. we've told you what it is; now were going to show you! Here's the prototype for WinMain:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd);
As you may have already noticed, the return type for WinMain is, and always will be, int. All 32-bit Windows operating system uses the calling convention WINAPI. This calling convention MUST be used to distinguish the function as the entry point. Now for the parameters. As you can see, WinMain uses four parameters when the program starts. Let's have a look at them, shall we?
hInstance - is a handle to your applications instance, where an instance can be considered to be a single run of your application. The instance is used by windows as a reference to your application for event handling, message processing, and various other duties. hPrevInstance - is always NULL. lpCmdLine - is a pointer string that is used to hold any command-line arguments that may have been specified when the application began. For example, if the user opened the Run application and typed myapp.exe myparameter 1, then lpCmdLine would be myparameter 1. nShowCMD - is the parameter that determines how your application's window will be displayed once it begins executing.Pretty simple, right? Don't worry if it's confusing, it will make sense soon enough! The Windows program you create is going to need some way to handle the messages that Windows will send to it. A few examples of these messages are: WM_CREATE, WM_SIZE, and WM_MOVE. There are TONS of these messages, and we'll show you how to handle a large number of them. To allow Windows to communicate with your application, we'll create a dandy little function called a Windows procedure. This most common name for this function is WndProc. This function MUST be created and used to determine how your application will respond to various events. The Windows procedure may also be called the event handler because, well, it responds to Windows events! So let's have a quick look at the prototype:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);This function is declared with a return type of LRESULT CALLBACK. The LRESULT type is used by Windows to declare a long integer, and CALLBACK is a calling convention used with functions that are called by Windows. The Windows Procedure is a function pointer, which allows you to call it whatever you want because the function's address will be assigned as a function pointer upon creation of the window class.
hwnd - Only important if you have several windows of the same class open at one time. This is used to determine which window hwnd pointed to before deciding on an action. message - The actual message identifier that WndProc will be handling. wParam and lParam - Extensions of the message parameter. Used to give more information and point to specifics that message cannot on its own.Well now you should have a better understanding of these two topics. You may be wondering when you get to see some openGL usage, and the answer is soon. We first need to cover the basics, let's remember, we're here to learn!
First Windows Application:
/* Trim fat from windows*/ #define WIN32_LEAN_AND_MEAN #pragma comment(linker, "/subsystem:windows") /* Pre-processor directives*/ #include "stdafx.h" #includeWell isn't this a whole mess of code! The first thing that you may have noticed is #define WIN32_LEAN_AND_MEAN. This syntax prevents Visual C++ from linking modules that you aren't going to need in your application./* Windows Procedure Event Handler*/ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT paintStruct; /* Device Context*/ HDC hDC; /* Text for display*/ char string[] = "Hello, World!"; /* Switch message, condition that is met will execute*/ switch(message) { /* Window is being created*/ case WM_CREATE: return 0; break; /* Window is closing*/ case WM_CLOSE: PostQuitMessage(0); return 0; break; /* Window needs update*/ case WM_PAINT: hDC = BeginPaint(hwnd,&paintStruct); /* Set txt color to blue*/ SetTextColor(hDC, COLORREF(0x00FF0000)); /* Display text in middle of window*/ TextOut(hDC,150,150,string,sizeof(string)-1); EndPaint(hwnd, &paintStruct); return 0; break; default: break; } return (DefWindowProc(hwnd,message,wParam,lParam)); } /* Main function*/ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX windowClass; //window class HWND hwnd; //window handle MSG msg; //message bool done; //flag saying when app is complete /* Fill out the window class structure*/ windowClass.cbSize = sizeof(WNDCLASSEX); windowClass.style = CS_HREDRAW | CS_VREDRAW; windowClass.lpfnWndProc = WndProc; windowClass.cbClsExtra = 0; windowClass.cbWndExtra = 0; windowClass.hInstance = hInstance; windowClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); windowClass.hCursor = LoadCursor(NULL, IDC_ARROW); windowClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); windowClass.lpszMenuName = NULL; windowClass.lpszClassName = "MyClass"; windowClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO); /* Register window class*/ if (!RegisterClassEx(&windowClass)) { return 0; } /* Class registerd, so now create window*/ hwnd = CreateWindowEx(NULL, //extended style "MyClass", //class name "A Real Win App", //app name WS_OVERLAPPEDWINDOW | //window style WS_VISIBLE | WS_SYSMENU, 100,100, //x/y coords 400,400, //width,height NULL, //handle to parent NULL, //handle to menu hInstance, //application instance NULL); //no extra parameter's /* Check if window creation failed*/ if (!hwnd) return 0; done = false; //initialize loop condition variable /* main message loop*/ while(!done) { PeekMessage(&msg,NULL,NULL,NULL,PM_REMOVE); if (msg.message == WM_QUIT) //check for a quit message { done = true; //if found, quit app } else { /* Translate and dispatch to event queue*/ TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; }
Moving on we come to the include line #include
The first function we arrive at is the WndProc function. We've discussed this before, so I am just going to highlight two lines I have added here.
HDC hDC; //device context Char string[] = "Hello World!"; //display textThese two lines are essential as they declare the device context that we are going to use to output to the window we create, and the string that we will display. As we continue on in the code we arrive at the switch statement. This switch is used to determine the message being passed to the windows procedure. In this particular instance we want to take a closer look at the WM_PAINT block.
case WM_PAINT: hDC = BeginPaint(hwnd,&paintStruct); /* Set txt color to blue*/ SetTextColor(hDC, COLORREF(0x00FF0000)); /* Display text in middle of window*/ TextOut(hDC,150,150,string,sizeof(string)-1); EndPaint(hwnd, &paintStruct); return 0; break;When the window is moved, resized, or is otherwise changed the window needs to be updated. The first noticeable change here is the use of the hDC device context. The win32 function BeginPaint() returns the graphics device context for the hwnd passed to it. You can then use this hDC to set the text color with SetTextColor() and follow up with the TextOut() function to display the text. If you want to know more about these functions, check out MSDN.
Next function up is the WinMain() function. Most of the WinMain() content is pretty straight forward, but were going to review a few parts of it for good measure. The msg variable holds the message received by PeekMessage() from the queue, and will be sent to TranslateMessage() and DispatchMessage(). The variable done is a Boolean value used by your message loop and will not equal true until a WM_QUIT message has been received from the queue to indicate that the application is about to be closed.
For easier understanding we will break the order of each setup task into a list.
1. Window-class setup 2. Window-class registration 3. Window Creation 4. Message loop with event handlerWe now have a fully working Windows application! I encourage you to toy around with the code and alter it to your liking. Don't be discouraged by errors or problems, for it is these things that make us better.
WGL and the Wiggle Functions in C++:
To maintain the portability of OpenGL, each operating system must supply functionality for specifying the rendering window before OpenGL can use it. In Windows the Graphics Device Interface uses a device context to remember settings about drawing modes and commands, but in OpenGL the rendering context is used to remember these things. You need to remember, however, that the device context is not a rendering context. The device context MUST be specified in a GDI call, unlike a rendering context, which is specified in an OpenGL call.
If you wish to render more than one window at once, multiple rendering contexts are allowed. You must ensure, however, that the proper rendering context is being used on the proper window. Another thing to remember is that OpenGL is thread-safe. You can have multiple threads rendering to the same device context at one time.
As I mentioned to you earlier, wiggle functions bring Windows API support into OpenGL. Well let's take this time to have a look at a few common wiggle functions:
Keep in mind that as with a device context, a rendering context must be deleted after you are finished with it. This is where the wglDeleteContext() function comes into play. Let's have a look at the prototype for good measure:
Both the wglCreateContext() and the wglMakeCurrent() functions should be called upon window creation. Let's look at a code snippet for an example of these in use:
If you wish to render more than one window at once, multiple rendering contexts are allowed. You must ensure, however, that the proper rendering context is being used on the proper window. Another thing to remember is that OpenGL is thread-safe. You can have multiple threads rendering to the same device context at one time.
As I mentioned to you earlier, wiggle functions bring Windows API support into OpenGL. Well let's take this time to have a look at a few common wiggle functions:
- wglCreateContext();
- wglDeleteContext();
- wglMakeCurrent();
HGLRC wglCreateContext(HDC hDC);This function should only be called after the pixel format for the device context has been set. Don't worry, pixel format is coming into teaching shortly.
Keep in mind that as with a device context, a rendering context must be deleted after you are finished with it. This is where the wglDeleteContext() function comes into play. Let's have a look at the prototype for good measure:
BOOL wglDeleteContext(HGLRC hRC);The name wglMakeCurrent() is highly accurate to what the function does. The function makes the rendering context passed to it the current rendering context. Makes a lot of sense, doesn't it? The device context used must have the same pixel format characteristics as the device context that was used to create the rendering context. This means that the device context used to create the rendering context does not need to be the same as the device context assigned to the wglMakeCurrent() function. Without further delay, let's see the prototype!
BOOL wglMakeCurrent(HDC hDC, HGLRC hRC);You need to ensure that the device context and the rendering context passed to the function have the same pixel format, or the function will not work. To deselect the rendering context you can simply pass NULL for the hRC parameter, or simply pass another rendering context.
Both the wglCreateContext() and the wglMakeCurrent() functions should be called upon window creation. Let's look at a code snippet for an example of these in use:
LRESULT CALLBACK WndProc (HWND hwnd, UNIT message, WPARAM wParam, LPARAM lParam) { static HGLRC hRC; //rendering context static HDC hDC; //device context switch (message) { case WM_CREATE: hDC = GetDC(hwnd); //get the device context for window hRC = wglCreateContext(hDC); //create rendering context wglMakeCurrent(hDC,hRC); //make rendering context current break; case WM_DESTROY: wglMakeCurrent(hDC,NULL); //deselect rendering context wglDeleteContext(hRC); //delete rendering context PostQuitMessage(0); //send wm_quit break; } }This code creates and destroys your OpenGL window.