Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
66 views

TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 1 - CodeProject

This document summarizes an OpenGL game tutorial for creating a 2D game in C++ on Windows. It discusses: 1) Setting up the project in Visual Studio and configuring OpenGL. 2) Creating a message loop to receive window events and dispatch messages. 3) Creating the main window and setting up OpenGL for 2D drawing. 4) Drawing simple shapes to test the OpenGL configuration.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
66 views

TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 1 - CodeProject

This document summarizes an OpenGL game tutorial for creating a 2D game in C++ on Windows. It discusses: 1) Setting up the project in Visual Studio and configuring OpenGL. 2) Creating a message loop to receive window events and dispatch messages. 3) Creating the main window and setting up OpenGL for 2D drawing. 4) Drawing simple shapes to test the OpenGL configuration.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

TetroGL: An OpenGL Game Tutorial in

C++ for Win32 Platforms - Part 1


Cedric Moonen Rate me: 4.92/5 (43 votes)
26 Aug 2008 CPOL 27 min read 248.7K 3.5K 159 44

Learn to create a Win32 message loop and game window and how to set-up OpenGL properly
for 2D games

Download source - 7.42 KB

Foreword
This series of articles focuses on 2D game development with C++ and OpenGL for Windows
platform. The target is to provide a game like the classic block puzzle game by the end of the
series. We will not only focus on OpenGL but also talk about the designs that are commonly
used in game programming with a full object oriented approach. You should already be familiar
with the C++ language in order to get the maximum out of this series. There is a message
board at the bottom of the article that you can use if you have questions, remarks or
suggestions.

The series is divided into three articles:

Part 1: covers the win32 message loop, the window creation and the setting-up of
OpenGL. You will also learn how to draw some simple shapes.
Part 2 : Covers resources handling and displaying simple animations.
Part 3: groups everything together and talk about the game logic.
Contents
Introduction
Project Settings
The Message Loop
The Main Window
Creating the Window
The Window Procedure
Exception Handling
Setting up OpenGL
Drawing Simple Shapes
Modeling Transformations
Conclusion
References
Acknowledgements
History

Introduction
This part of the article focuses on setting up an OpenGL window in a Windows environment.
We will learn how to create a message loop to receive notifications and how to create the main
window that will be used for drawing. Then, we will see how to configure OpenGL properly for
a 2 dimensions game. Finally, when everything is ready to start, we will learn how to display
some basic shapes in the newly created OpenGL window.

Project Settings
We will start by creating a new project and configuring the different options. The tutorial
project has been created with Visual Studio 2005 but it can be easily applied for another
compiler. Start by creating a new project of type "Win32 Console Application" and giving it an
appropriate name, then click Ok. In the creation wizard, select the type "Windows application"
(not console) and check the "Empty project" option (we don't really need code that is
generated for us).

When this is done, add a new source file Main.cpp to the project (if there are no source files in
the project, some options are not accessible). Now open the project options and go to the
"Linker" category -> "Input". In the "Addition Dependencies" option, add opengl32.lib. This tells
the linker that it has to use the OpenGL library when linking the project.
Next, we will disable UNICODE because we don't need it and it makes things a bit more
complicated. Go into "C/C++" -> "Preprocessor" and click on "Preprocessor Definitions". A
button will appear on the right, click on it and in the dialog that pops up, uncheck the "Inherit
from parent or project defaults". This will disable UNICODE which is inherited from the project
default.

Now that the project settings are properly configured, we are ready to look at some code. Let's
first examine how a Win32 application receives and processes events (keyboard, mouse, ...).
The Message Loop
The system (Windows) creates a message queue for each application and pushes messages in
this queue whenever an event occurs on a window of that specific application. Your application
should then retrieve and process those messages in order to react upon them. This is what is
called the message loop and it is the heart of all Win32 applications.

A typical message loop looks like this:

C++
MSG Message;
Message.message = (~WM_QUIT);
// Loop until a WM_QUIT message is received
while (Message.message != WM_QUIT)
{
if (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
{
// If a message was waiting in the message queue, process it
TranslateMessage(&Message);
DispatchMessage(&Message);
}
else
{
// Do processing stuff here...
}
}

PeekMessage retrieves a message from the queue if any; the PM_REMOVE tells PeekMessage that
messages should be removed from the queue. The message will be stored in the first argument
and the function returns nonzero if a message was retrieved. The second argument of the
function lets you specify a window handle for which the messages have to be retrieved. If NULL
is supplied, messages for all windows of the application will be retrieved. The third and fourth
parameters let you specify a range for the messages that should be retrieved. If 0 is supplied
for both, all messages will be retrieved.
The purpose of the TranslateMessage function is to translate virtual-keys messages
(WM_KEYDOWN and WM_KEYUP) into character messages (WM_CHAR). A WM_CHAR message will be
generated by a combination of WM_KEYDOWN and WM_KEYUP messages.
Finally the DispatchMessage will redirect the message to the correct window procedure. As we
will see later, each window in your application has a specific function (called a window
procedure) that processes those messages.
So, this snippet of code tries to extract a message from the queue. If a message was available, it
will be dispatched to the correct window procedure. If no message was available, we do some
processing specific to the application. Once a WM_QUIT message is retrieved, the loop is exited,
which terminates the application.

If we look at the code of this first tutorial, we can see that the message loop is wrapped into a
class called CApplication. Let's take a closer look at this class. First the class declaration:

C++
// The application class, which simply wraps the message queue and process
// the command line.
class CApplication
{
public:
CApplication(HINSTANCE hInstance);
~CApplication();

// Parses the command line to see if the application


// should be in fullscreen mode.
void ParseCmdLine(LPSTR lpCmdLine);
// Creates the main window and starts the message loop.
void Run();

private:
HINSTANCE m_hInstance;
// Specifies if the application has to be started in fullscreen
// mode. This option is supplied through the command line
// ("-fullscreen" option).
bool m_bFullScreen;
};

The ParseCmdLine function is quite straightforward: it simply checks if an argument "-


fullscreen" is present in the command line. In that case, the flag m_bFullScreen is set to true.

Let's look at the Run function:

C++ Shrink ▲

void CApplication::Run()
{
// Create the main window first
CMainWindow mainWindow(800,600,m_bFullScreen);

MSG Message;
Message.message = ~WM_QUIT;
DWORD dwNextDeadLine = GetTickCount() + FRAME_TIME;
DWORD dwSleep = FRAME_TIME;
bool bUpdate = false;

// Loop until a WM_QUIT message is received


while (Message.message != WM_QUIT)
{
// Wait until a message comes in or until the timeout expires. The
// timeout is recalculated so that this function will return at
// least every FRAME_TIME msec.
DWORD dwResult = MsgWaitForMultipleObjectsEx(0,NULL,dwSleep,QS_ALLEVENTS,0);
if (dwResult != WAIT_TIMEOUT)
{
// If the function returned with no timeout, it means that at
// least one message has been received, so process all of them.
while (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
{
// If a message was waiting in the message queue, process it
TranslateMessage(&Message);
DispatchMessage(&Message);
}

// If the current time is close (or past) to the


// deadline, the application should be processed.
if (GetTickCount() >= dwNextDeadLine)
bUpdate = true;
else
bUpdate = false;
}
else
// On a timeout, the application should be processed.
bUpdate = true;

// Check if the application should be processed


if (bUpdate)
{
DWORD dwCurrentTime = GetTickCount();
// Update the main window
mainWindow.Update(dwCurrentTime);
// Draw the main window
mainWindow.Draw();

dwNextDeadLine = dwNextDeadLine + FRAME_TIME;


}

// Process the sleep time, which is the difference


// between the current time and the next deadline.
dwSleep = dwNextDeadLine - GetCurrentTime();
// If the sleep time is larger than the frame time,
// it probably means that the processing was stopped
// (e.g. the window was being moved,...), so recalculate
// the next deadline.
if (dwSleep>FRAME_TIME)
{
dwSleep = FRAME_TIME;
dwNextDeadLine = GetCurrentTime() + FRAME_TIME;
}
}
}

The first line of the function simply creates the main window. We will see in the next chapter
what it does exactly. For now, just imagine that this creates and displays the main window with
a specific width and height and in fullscreen or not. As you might see, the loop itself is a bit
different than what we saw before. The reason is simple: in general for a 2D game, you don't
need to refresh the screen as fast as you can. Refreshing it at a constant rate, is sufficient to
display animation and do the processing stuff. In our case, we defined a constant (FRAME_TIME)
that specifies the time in msec between two frames.
We could do something simpler: in the first message loop example we saw, we could replace
the "// Do processing stuff here..." by a check to see if 30 msec elapsed since the last update:

C++
else
{
// Do processing stuff here...
if(GetCurrentTime() >= dwLastUpdate+30)
{
dwLastUpdate = GetCurrentTime();
// Update the main window
mainWindow.Update(dwCurrentTime);
// Draw the main window
mainWindow.Draw();
}
}

That will work fine except for the fact that it is busy waiting: if no messages are received, we will
loop continuously and eat all available CPU time. This is not really nice because the CPU is used
for doing nothing.
A best approach would be to wait until a message arrives or until we reached the next refresh
deadline. That's what the MsgWaitForMultipleObjectsEx function does. In brief, we can specify
multiple objects on which we would like to wait, but we are only interested in messages (so,
that's why we specify 0 objects in the first argument and a NULL for the second argument). This
function will wait without consuming CPU cycles until either the timeout period expires
(specified in the 3rd argument) or when a message has been received. You can specify a filter
for messages to be received in the 4th parameter, but we are interested in all messages. When
the function times out, it returns WM_TIMEOUT, which is used in the code to detect when it is
time to refresh the screen and update the game logic. If the function didn't time out, it means
that one or more messages are waiting in the queue, so we extract all of them using
PeekMessage (the function returns FALSE when no messages are in the queue anymore). Whe
then determine if the application should be processed or not. At the end of the function, we
recalculate the sleep time depending on the next deadline. If this sleep time is bigger then the
frame time, it means that the current time was bigger than the next deadline (negative
overflow). This typically happens when the window is moved or resized: during this time, the
application is not processed anymore. In that case, we simply recalculate a new deadline and
sleep time based on the current time.

Great, so now we have a message loop to dispatch the messages to the correct window. But
there's something missing: the window itself. So let's look at how this window is created and
how the messages sent to it are processed.

The Main Window


Creating the Window

As we saw before, we only had to create an instance of the CMainWindow class in


the Run() method of our application class to create the main window. So let's take a look at the
constructor, that's where all the stuff is handled.

C++ Shrink ▲

CMainWindow::CMainWindow(int iWidth, int iHeight, bool bFullScreen)


: m_hWindow(NULL), m_hDeviceContext(NULL), m_hGLContext(NULL),
m_bFullScreen(bFullScreen)
{
RegisterWindowClass();

RECT WindowRect;
WindowRect.top = WindowRect.left = 0;
WindowRect.right = iWidth;
WindowRect.bottom = iHeight;

// Window Extended Style


DWORD dwExStyle = 0;
// Windows Style
DWORD dwStyle = 0;

if (m_bFullScreen)
{
DEVMODE dmScreenSettings;
memset(&dmScreenSettings,0,sizeof(dmScreenSettings));
dmScreenSettings.dmSize = sizeof(dmScreenSettings);
dmScreenSettings.dmPelsWidth = iWidth;
dmScreenSettings.dmPelsHeight = iHeight;
dmScreenSettings.dmBitsPerPel = 32;
dmScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

// Change the display settings to fullscreen. On error, throw


// an exception.
if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)
!= DISP_CHANGE_SUCCESSFUL)
{
throw CException("Unable to switch to fullscreen mode");
}

dwExStyle = WS_EX_APPWINDOW;
dwStyle = WS_POPUP;
// In fullscreen mode, we hide the cursor.
ShowCursor(FALSE);
}
else
{
dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
dwStyle = WS_OVERLAPPEDWINDOW;
}

// Adjust the window to the true requested size


AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
// Now create the main window
m_hWindow = CreateWindowEx(dwExStyle,TEXT(WINDOW_CLASSNAME),
TEXT("Tutorial1"),
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dwStyle,
0, 0, WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL, NULL,
GetModuleHandle(NULL),
this);
if (m_hWindow==NULL)
throw CException("Cannot create the main window");

CreateContext();
InitGL();
ShowWindow(m_hWindow,SW_SHOW);
// Call OnSize manually because in fullscreen mode it will be
// called only when the window is created (which is too early
// because OpenGL is not initialized yet).
OnSize(iWidth,iHeight);
}
It looks like a lot of code but it is not that complicated. The first thing we do is
call RegisterWindowClass which will, as its name states, registers the window class for our
application. So what is a window class? Basically, it is a template that is used to define a
window: you can specify an icon, a background brush, a cursor and other things. Every window
is an instance of such a class. Let's take a look at the implementation of this function:

C++
void CMainWindow::RegisterWindowClass()
{
WNDCLASS WindowClass;
WindowClass.style = 0;
WindowClass.lpfnWndProc = &CMainWindow::OnEvent;
WindowClass.cbClsExtra = 0;
WindowClass.cbWndExtra = 0;
WindowClass.hInstance = GetModuleHandle(NULL);
WindowClass.hIcon = NULL;
WindowClass.hCursor = 0;
WindowClass.hbrBackground = 0;
WindowClass.lpszMenuName = NULL;
WindowClass.lpszClassName = WINDOW_CLASSNAME;

RegisterClass(&WindowClass);
}

What it does is register a new class instance (which is called Tutorial1) and the only thing we
specify is the window procedure that will be called when messages are retrieved for that
window. This is the OnEvent function of the class. If you look closely at the function declaration,
you will notice that it is a static function. The reason for that is very simple: non-static
member functions don't have the same prototype as global functions even if they have the
same argument list. It is because an implicit parameter is passed to the function:
the this parameter which identifies the instance of the class on which the function is
called. Static member functions do not follow the same rule, because they don't belong to a
specific instance (they are shared among all instances of the class). The WNDCLASS structure
accepts only global or static member functions for the lpfnWndProc parameter. We will see
later the consequences of that.

Now, back to the CMainWindow constructor. The next thing we do there is check if the window
should be in fullscreen. If that is the case, we switch to fullscreen mode (by
calling ChangeDisplaySettings). If this function call fails, we throw an exception. We will talk
more in detail about exceptions and exception handling in a following chapter.

We will now create the main window but first, we need to adjust the rectangle size because the
window caption and borders are eating up a bit of the size. To correct that, we simply
call AdjustWindowRectEx. This function doesn't have any effect if we are in fullscreen mode. We
finally call CreateWindowEx which will create the window with the required style. The second
parameter of the function specifies the window class to use (which will of course be the
window class we registered earlier). In the last parameter of the function, we pass
the this pointer (the pointer to this CMainWindow instance). We will see later why we do so. If
the window creation fails, we also throw an exception.
The CreateContext and InitGL functions will initialize OpenGL properly, but we will see that in
a following chapter.

The Window Procedure

We just created a new window by calling CreateWindowEx and we specified that the window
should use the window class we registered earlier. This window class uses the OnEvent function
as a window procedure. Let's take a look at this function:

C++
LRESULT CMainWindow::OnEvent(HWND Handle, UINT Message, WPARAM wParam, LPARAM lParam)
{
if (Message == WM_NCCREATE)
{
// Get the creation parameters.
CREATESTRUCT* pCreateStruct = reinterpret_cast<CREATESTRUCT*>(lParam);

// Set as the "user data" parameter of the window


SetWindowLongPtr(Handle, GWLP_USERDATA,
reinterpret_cast<long>(pCreateStruct->lpCreateParams));
}

// Get the CMainWindow instance corresponding to the window handle


CMainWindow* pWindow = reinterpret_cast<CMainWindow*>
(GetWindowLongPtr(Handle, GWLP_USERDATA));
if (pWindow)
pWindow->ProcessEvent(Message,wParam,lParam);

return DefWindowProc(Handle, Message, wParam, lParam);


}

As you remember, this function is a static function. The function will be called when a
message is received and dispatched to our main window. It accepts four parameters:

Handle: The handle of the window to which the message is sent to


Message: The message Id
wParam: Optional message parameter
lParam: Optional message parameter

Depending on the type of message, some additional information will be stored in


the wParam, lParam or both (e.g. a mouse move message contains the mouse coordinates, a
key down event contains the key code...).

As this function is static, we don't have access to other non-static class member, which is of
course not very useful in our situation. But, don't panic, there's an easy solution for that, and
it's the reason why we passed the this pointer in the last argument of CreateWindowEx. One
of the first message that will be sent to your window procedure is the WM_NCCREATE message.
When this message is received, the lParam argument contains a pointer to
a CREATESTRUCT structure, which contains information about the window creation, which are in
fact the parameters that were passed in the CreateWindowEx call. The lpCreateParams field
contains the additional data, which is in our case the pointer to the CMainWindow instance.
Unfortunately, this additional data is not sent with every message, so we need a way to store
this pointer for later use. That's what we are doing by calling SetWindowLongPtr: this function
lets you save some user data (GWLP_USERDATA) for a specific window (identified by its handle). In
this case, we save the pointer to the class instance. When other messages are received, we will
simply retrieve this pointer by calling (GetWindowLongPtr), and then call a non-static function
on the pointer: ProcessEvent, which is in charge of processing the message.
The WM_NCCREATE message is not the first one that is sent, that's why we need to check if the
call to GetWindowLongPtr did return something else than NULL.

Let's look at the ProcessEvent function:

C++
void CMainWindow::ProcessEvent(UINT Message, WPARAM wParam, LPARAM lParam)
{
switch (Message)
{
// Quit when we close the main window
case WM_CLOSE :
PostQuitMessage(0);
break;
case WM_SIZE:
OnSize(LOWORD(lParam),HIWORD(lParam));
break;
case WM_KEYDOWN :
break;
case WM_KEYUP :
break;
}
}

Not too much code here, but this function will be filled in the next tutorials as we need to
handle some events. The WM_CLOSE message is sent when the user clicks on the red cross of
the window. At this time, we need to send a WM_QUIT message in order to exit the main loop
and quit the program. A WM_SIZE message is sent whenever the window is resized, with the
new size contained in the lParam (LOWORD and HIWORD are two macros that extract the first 2
bytes and the last 2 bytes from the parameter). When such message is received, we delegate
the resizing handling to our OnSize member function. Some other messages will be handled
later: WM_KEYDOWN when a key is pressed, WM_KEYUP when a key is released, ...

Up to now, the only thing our program does is create an empty window and display it on the
screen (in fullscreen mode or not).

Exception Handling
Error management is an important point for all programs, and this is also true for games: you
don't want your game to crash because a resource is missing. My preferred way to handle
errors for games is to use exceptions. It is much more convenient than returning error codes
from functions (and routing them where I want the error to be handled). The main reason is
that I can delegate the error handling in one single place: in my main function, where all my
exceptions will be caught. Let's first take a look at our exception class, which is quite basic:
C++
class CException : public std::exception
{
public:
const char* what() const { return m_strMessage.c_str(); }

CException(const std::string& strMessage="") : m_strMessage(strMessage) { }


virtual ~CException() { }

std::string m_strMessage;
};

So, nothing fancy here: our exception class inherits from std::exception (which is not
mandatory but is considered good practice). We simply override the what() function which
returns the error message. I kept the scenario quite simple here, but for a bigger game, you
might want to specialize this exception into specific ones: out of memory, resource missing, file
loading failed, ... This could prove handy because sometimes it is useful to filter the exceptions.
A typical example is when the user of your game wants to load a file (containing a previous
saved game) which is corrupted. In that case, the load file function will throw an exception but
you don't want to exit the program because of that. Displaying a message to the user telling
him that the file is corrupted is what you would like to do. You can then easily catch all 'file
corrupted' exceptions at an early stage and let all the others be routed to your main exception
handling function. After all, if some resources are missing when loading the file, this is probably
a critical error and you might want to exit the program.

So, how does my main function look like and how do I handle the exceptions ?

C++
int WINAPI WinMain(HINSTANCE Instance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT)
{
try
{
// Create the application class,
// parse the command line and
// start the app.
CApplication theApp(Instance);
theApp.ParseCmdLine(lpCmdLine);
theApp.Run();
}
catch(CException& e)
{
MessageBox(NULL,e.what(),"Error",MB_OK|MB_ICONEXCLAMATION);
}

return 0;
}

Pretty easy to understand, isn't it ? We already saw what the CApplication class is doing and
for the exception handling, we simply wrap everything inside a little try/catch block. When an
exception is thrown somewhere in the program, we simply display an error message with the
text of the exception and we nicely exit the program. Note that as theApp is local to the
function, it will be destroyed at the end of the function and its destructor will be called.
Setting up OpenGL
If you remember, in our CMainWindow constructor, we were calling two
functions: CreateContext and InitGL. I didn't explain yet what those functions do, so let's
correct that now. CreateContext will initialize the rendering context so that OpenGL primitives
can be drawn on the window:

C++ Shrink ▲

void CMainWindow::CreateContext()
{
// Describes the pixel format of the drawing surface
PIXELFORMATDESCRIPTOR pfd;
memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1; // Version Number
pfd.dwFlags = PFD_DRAW_TO_WINDOW | // Draws to a window
PFD_SUPPORT_OPENGL | // The format must support OpenGL
PFD_DOUBLEBUFFER; // Support for double buffering
pfd.iPixelType = PFD_TYPE_RGBA; // Uses an RGBA pixel format
pfd.cColorBits = 32; // 32 bits colors

if (!(m_hDeviceContext=GetDC(m_hWindow)))
throw CException("Unable to create rendering context");

int PixelFormat;
// Do Windows find a matching pixel format ?
if (!(PixelFormat=ChoosePixelFormat(m_hDeviceContext,&pfd)))
throw CException("Unable to create rendering context");
// Set the new pixel format
if(!SetPixelFormat(m_hDeviceContext,PixelFormat,&pfd))
throw CException("Unable to create rendering context");
// Create the OpenGL rendering context
if (!(m_hGLContext=wglCreateContext(m_hDeviceContext)))
throw CException("Unable to create rendering context");
// Activate the rendering context
if(!wglMakeCurrent(m_hDeviceContext,m_hGLContext))
throw CException("Unable to create rendering context");
}

The first part of the function fills a PIXELFORMATDESCRIPTOR with the correct information: the
buffer is used to draw to a window, must support OpenGL and uses double buffering (to avoid
flickering). We then call ChoosePixelFormat to see if this pixel format is supported. The
function returns a pixel format index (or 0 if no matching pixel format was found). Once we
have the index of the pixel format, we set the new format by calling SetPixelFormat. We then
create the OpenGL rendering context by calling wglCreateContext. Finally, by
calling wglMakeCurrent, we specify that all subsequent OpenGL calls made by the thread are
drawn on this device context. You can also see that if an error is encountered while creating the
context, an exception is thrown and will be handled in our main function.

The InitGL function is rather simple:

C++
void CMainWindow::InitGL()
{
// Enable 2D texturing
glEnable(GL_TEXTURE_2D);
// Choose a smooth shading model
glShadeModel(GL_SMOOTH);
// Set the clear color to black
glClearColor(0.0, 0.0, 0.0, 0.0);

// Enable the alpha test. This is needed


// to be able to have images with transparent
// parts.
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.0f);
}

We first enable the 2D texturing. Without this call, we won't be able to apply textures to shapes
on the screen. Those textures will be loaded from file and used to display the different game
elements. We then choose a smooth shading model. This is not really important in our case,
but it simply tells OpenGL if the points of a primitive (a basic shape, like a triangle or a
rectangle) have different colors, they will be interpolated. We'll see later what it does on a
concrete example.We then specify a clear color. This color is used to clear the color buffer
before drawing anything to it. Finally, we enable the alpha testing. This is needed if we want to
render some parts of a texture transparent. Suppose for example that you want to draw a ship
on the screen and that this ship is loaded from a file. The ship doesn't have a rectangular shape
so, you would like to make the texture around the ship transparent so that you don't have a
white rectangle in which you have your ship. This is done by using an alpha channel that
specifies the opacity of a pixel (this will be covered more in details in the second article). Once
the alpha testing has been enabled, we need also to select which function will be used to
discard pixels depending on their alpha channel. This is done through the glAlphaFunc: we
specify that all pixels with an alpha channel greater (GL_GREATER) than the specified threshold
(0) will be discarded (not drawn). Other alpha functions also exist (GL_LESS, GL_EQUAL, ...).

Let's now take a look at the OnSize function. If you remember, this function is called whenever
the window is resized (and at least once, at the window creation):

C++
void CMainWindow::OnSize(GLsizei width, GLsizei height)
{
// Sets the size of the OpenGL viewport
glViewport(0,0,width,height);

// Select the projection stack and apply


// an orthographic projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0,width,height,0.0,-1.0,1.0);
glMatrixMode(GL_MODELVIEW);
}

It receives as parameter the new size of the window. The first thing we do here is
call glViewport. This function specifies which section of the window will be used by OpenGL
for drawing. You can for example limit the drawing to a portion of the full window. In our case,
we will use the full window as the viewport. By default, OpenGL will use the full window size so
this call is not necessary (only for educational purposes).

Now we'll glMatrixMode. In order to understand what it does, let me first explain that OpenGL
uses three matrix stacks at different stages of the process. These stacks are:

GL_MODELVIEW: This matrix stack affects the objects in your scene. In case of our tutorial,
these objects will simply be textured rectangles. By manipulating this matrix stack, you
will be able to translate, rotate and scale the objects in your scene.
GL_PROJECTION: This matrix stack affects how the objects in your scene will be projected
on the viewport. By manipulating this stack, you can specify which kind of projection
should be applied to your objects.
GL_TEXTURE: This matrix stack defines how the textures will be manipulated before being
applied to objects. We won't manipulate this stack in this tutorial.

OpenGL will always work with the matrix that is currently on top of each stack, but using a stack
might be useful because you can then push the current matrix down the stack to be used later.
We will see at the end of this tutorial a more concrete example of that.

After this little explanation, we are back to our code: what glMatrixMode does is that it simply
tells OpenGL which matrix stack will be affected by the next operations. In your code, we select
the projection stack. We then load the identity matrix in the stack (which simply resets the
current matrix to the identity matrix) and then we specify that we would like an orthographic
projection of the objects on the viewport. We finally switch back to the default matrix, which is
the model view matrix.

You might wonder what is this orthographic projection? Let's take a deeper look at what it
does. You can have two different projections in OpenGL: the perspective or the orthographic
projection. Instead of going into a detailed explanation, I'll put two pictures showing the two
projections.

Orthographic projection.
Perspective projection.

As you can see, a perspective projection is the way to go if you develop a 3D game: it will be
similar as what your eyes can see as objects that are far from the camera will look small. An
orthographic projection on the other hand won't distort objects: a cube at a distance will look
the same size as a cube just in front of the camera (given they are the same size). For a 2D
game, I prefer to use an orthographic projection because then I don't have to take the z
position into account: I can give whatever value and the object won't be smaller or bigger
depending of this value.

The arguments you pass to glOrtho are the coordinates of the viewing volume
(left, right, bottom, top, nearVal and farVal). The values you choose here will in fact define
the 'units' you will be working with: OpenGL doesn't define any units on its own. For example,
I've chosen the window width as the width of my viewing volume. It means that if I move an
object 1 unit to the left, it will move 1 pixel. You will also often see values from 0.0 to 1.0 for
left/bottom and right/top. In that case, one unit is the width of window in the horizontal
direction and is the height of the window in the vertical direction. In 2D games, I prefer to use
the first option because if I want to draw two textures next to each other, I know exactly how
much I have to move my second texture: it is the width of the first texture (e.g. if my textures
are 24 pixels width, my second texture will be moved 24 units to the right). On the other hand,
if I want to position something in the middle of my window, I have to take into consideration
the width of the window. For the other option, 0.5 units is the middle of the window. That's just
a matter of choice but as I am familiar with MFC and GDI, I tend to use the first option to have
the same feeling. You might also have noticed another point: I gave a value of height for the
bottom and of 0 for the top. It means that my top and bottom are inverted. Here also, it is just
a matter of choice: the Y axis in OpenGL goes from the bottom to the top, which is the
opposite as what I'm used to do (window coordinates start at the top of the window to the
bottom of the window).

Drawing Simple Shapes


Now that everything is set-up correctly, we will finally be able to draw some basic shapes on
our window. We are using double buffering to avoid flickering, this means that everything will
be written to an off-screen buffer and once the image is composed, the buffers will be
swapped, bringing the off-screen buffer to the screen and vice-versa. This avoids having to
draw directly on the buffer that is displayed on the screen. Let's look at
our CMainWindow::Draw() function where the drawing code should be:
C++
void CMainWindow::Draw()
{
// Clear the buffer
glClear(GL_COLOR_BUFFER_BIT);

SwapBuffers(m_hDeviceContext);
}

The first line of code simply clears the buffer using the clear color that was specified earlier in
our InitGL function (black). At the end of the function, we swap the buffers by
calling SwapBuffers. Our drawing code will be placed between these calls.

OpenGL allows you to draw some simple shapes, called primitives which can be points, lines
and polygons (most of the times, triangles and rectangles). These primitives are described by
their vertices, the coordinates of the points themselves, the endpoints of the line segments or
the corners of the polygons. For 2D games, we will probably limit ourselves to rectangles: when
textured, they allow you to display bitmaps which is almost all we need for a 2D game. For
more complex games (like 3D games), complex shapes can be created by assembling triangles
together to form a mesh. Let's draw a rectangle and a triangle on the screen: we will put this
code between the two function calls in our drawing function.

C++
glBegin(GL_QUADS);
glVertex3i(50,200,0);
glVertex3i(250,200,0);
glVertex3i(250,350,0);
glVertex3i(50,350,0);
glEnd();

glBegin(GL_TRIANGLES);
glVertex3i(400,350,0);
glVertex3i(500,200,0);
glVertex3i(600,350,0);
glEnd();

Specifying vertices (calls to glVertex3i) should always be wrapped inside


a glBegin/glEnd pair. The argument supplied to glBegin defines the type of shape we are
drawing. You can draw multiple shapes within the same glBegin/glEnd pair, you simply have to
provide enough vertices: e.g. if you want to draw two rectangles, you have to provide 8
vertices. The arguments you provide to glVertex3i are the coordinates of the vertex, which
depend on how the projection was defined (remember what we did in
the CMainWindow::OnSize() method). I've chosen to stick to window coordinates for this
example. The '3i' at the end of the function specifies the number and type of arguments to the
function. Several versions of this function exist: from two to four arguments which can be
integers, floats, doubles, signed, unsigned, arrays, ... Simply select the one that is the most
suited to your needs.

You can also specify a color for each of the vertices of your shape, so let's try some nice things
here:
C++
glBegin(GL_QUADS);
glColor3f(1.0,0.0,0.0); glVertex3i(50,200,0);
glColor3f(0.0,1.0,0.0); glVertex3i(250,200,0);
glColor3f(0.0,0.0,1.0); glVertex3i(250,350,0);
glColor3f(1.0,1.0,1.0); glVertex3i(50,350,0);
glEnd();

glBegin(GL_TRIANGLES);
glColor3f(1.0,0.0,0.0); glVertex3i(400,350,0);
glColor3f(0.0,1.0,0.0); glVertex3i(500,200,0);
glColor3f(0.0,0.0,1.0); glVertex3i(600,350,0);
glEnd();

Specifying the current color is done by calling glColor3f, here also, several versions of the
function exist. For the floating point version, the full intensity corresponds to 1.0, and no
intensity corresponds to 0.0. If you run the code, you will see that the colors of each vertex
blend nicely together (it is the image that is on top of this article). That is because we've chosen
the GL_SMOOTH shading model when calling glShadeModel in
our CMainWindow::InitGL() function. If you change it into GL_FLAT, you'll see that the shapes
have only one color, which is the last supplied one.

Modeling Transformations
I will finish this tutorial by showing you what can be done by manipulating the model view
matrix stack. This won't be used in next tutorials (or even in the final game) but it is nice to
understand these concepts. That's the reason why I'll be quite brief on this subject.

I already talked a bit about the model view matrix stack and said that you can apply
transformations to this matrix which will affect the objects in your scene. I also explained that
using a stack instead of a single matrix can be useful when you want to save the current matrix
for later use. By calling glPushMatrix, you push the top matrix down the current selected stack
(which is the model view stack by default) and create a duplicate of this matrix on the top of
the stack. Once you have manipulated the model view matrix to affect certain objects in your
scene, you can pop back to the previous pushed matrix by calling glPopMatrix. This is
particularly useful when you have to draw elements that have children elements: the position
and rotation of the children depends on the position and rotation of the parent (e.g. a finger
on a robot hand depends on the position of the hand, which in turn depends on the position of
the robot's arm). In that case you apply the transformation for the parent element, push the
matrix down the stack, apply the transformations for the first child and draw it, then pop the
first matrix to reset to the position and rotation of the parent element. You can then draw the
second child by using the same method. Of course, those child elements can themselves have
child elements in which case you apply the same technique.

Applying transformations to the objects in your scene is done by loading a specific matrix in
the model view matrix stack. You can compound this matrix by hand but I guess that's
something you would like to avoid. That's why OpenGL provides three routines that can be
used for modeling transformations: glTranslate,glRotate and glScale. One thing you have to
take into consideration is that each call to such functions is equivalent to creating the
corresponding translation, rotation or scaling matrix and then multiply the current model view
matrix with this matrix (and storing the result in the model view matrix). It means that you can
'chain' these calls to produce the transformation you like. You might also know (or remember
from your math lessons) that matrix multiplication is not commutative. It means that the order
in which you call your functions is important. In fact, the last transformation command called in
your program is the first which is applied. You can look at it by imagining that you have to call
the transformations in the reverse order in which you would like them to be applied. Suppose
that you want to position an object at location (100,100) (we don't take z into account here)
and have it rotated 180� around the z axis (but still centered at the same location), then you
would need to apply the translation first and then rotate the object. If you do the opposite, the
translation would be applied first and then the rotation would be applied, which means your
object will be moved at location (100,100) and then rotated 180� around (0,0). Which means
it will end up in position (-100,-100).

I don't want to go into too much detail here because matrix manipulation and modeling
transformations are worth a full article on their own. I simply wanted to show you that
manipulating the model view matrix could prove quite powerful, for example if you want to
add some simple special effects (like rotation and scaling).

Conclusion
In this article, I've provided a basic framework that can be reused for writing 2D games. It
creates the main window and set-up OpenGL accordingly. We will see in the next article how to
texture the shapes with images that are loaded from files and how to efficiently manage those
resources.

References
The OpenGL programming guide (The Redbook): Great reference if you want to go more
into detail about OpenGL
Nehe tutorials: OpenGL tutorials, a bit out-dated but still useful
MSDN: MSDN doc about the message loop for Win32 applications
Window procedure as class members: article discussing ways of using a class member
function to process Windows messages.

Acknowledgements
I would like to thanks Andrew Vos for the nice projection images.
Thanks to the reviewers: Vunic, Andrew.
Thanks also to Jeremy Falcon, El Corazon and Nemanja Trifunovic for their advice and help.

History
23rd June, 2008:

Initial version

23rd August, 2008:

Added link to the second article in the Foreword section.


The Run method of the CApplication class has been adapted.
The OnEvent method of the CMainWindow class has been adapted.
Added support for blending in the InitGL method of the CMainWindow class.

License
This article, along with any associated source code and files, is licensed under The Code Project
Open License (CPOL)

Written By

Cedric Moonen
Engineer
Belgium

I am a 29 years old guy and I live with my girlfriend in Hoegaarden, little city from Belgium well
known for its white beer .
I studied as an industrial engineer in electronics but I oriented myself more towards software
development when I started to work.
Currently I am working in a research centre in mechatronica. I mainly develop in C++ but I also
do a bit of Java.
When I have so spare time, I like to read (mainly fantasy) and play electric guitar.

You might also like