Introduction
Welcome to our new series of tutorials about OpenGL, the greatest API for real-time rendering. While reading this series of tutorials you also learn more about Programming There are a lot of other tutorials about OpenGL around the net but you find this series a lot different. I tried to make these tutorials easy to follow and very practical. At first we build a Framework for our tutorials and we will extend it through the next tutorials and finally you can use it to make your own demos and games.
What you need to get started?
First you need to know C++ programming language; there are a lot of good books and tutorials around the net, just check the FAQ page and you can find some useful links. Why we choose C++ is simple, because C++ is the language of the choice of game developers. Almost all the commercial 3D games available in the market are written in C++ and this is not an accident! C++ is very powerful, very effective and rather easy to use language. A background of Win32 SDK programming can help a lot, I suggest you to check Win32 tutorials section here (You just need to know how to create a window using Win32 API). You also need a compiler and an editor, which could be gcc and vim or Visual Studio.NET express edition, I'm not a Microsoft fan but the second choice seems a lot better.
What is OpenGL?
OpenGL is a software interface to graphics hardware. You can access all the functionality of your graphics card using OpenGL. By using OpenGL you don't need to worry about which graphics hardware your application is running on. You only test your program on specified graphics hardware and OpenGL will guarantee that every "OpenGL compatible" hardware will run your program.
Creating the framework
If you like to do everything by your own now is the time! Start your IDE, create a Win32 empty project, and add opengl32.lib, glu32.lib, glaux.lib and winmm.lib in the dependency section. Our framework currently is very simple; we have two classes here: Application and GLWindow class.
GLWindow
GLWindow class encapsulates creation and setup of our main window. Here is the code for GLWindow class:
GLWindow.h
#pragma once
#pragma once
class GLWindow
{
private:
HWND hWnd;
HDC hDC;
HGLRC hRC;
bool fullscreen;
public:
GLWindow();
~GLWindow();
bool Initialize(int width, int height, bool fullscreen);
void Terminate();
void Swap(){ SwapBuffers(hDC); }
friend LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
}; |
GLWindow.cpp
#include "Global.h"
#include "GLWindow.h"
GLWindow::GLWindow(void)
{
hWnd = NULL;
hDC = NULL;
hRC = NULL;
}
GLWindow::~GLWindow(void)
{
}
bool GLWindow::Initialize(int width, int height, bool fullscreen)
{
WNDCLASS wc;
PIXELFORMATDESCRIPTOR pfd;
RECT WindowRect;
unsigned int PixelFormat;
DWORD dwStyle = WS_OVERLAPPEDWINDOW;
this->fullscreen = fullscreen;
ZeroMemory(&wc, sizeof(wc));
wc.style = CS_VREDRAW|CS_HREDRAW|CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszClassName = "GLCLASS";
RegisterClass(&wc);
WindowRect.left=0;
WindowRect.right=width;
WindowRect.top=0;
WindowRect.bottom=height;
AdjustWindowRect(&WindowRect, WS_OVERLAPPEDWINDOW, false);
if(fullscreen){
DEVMODE dm;
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm);
dm.dmSize = sizeof(DEVMODE);
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
dm.dmBitsPerPel = 32;
ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
dwStyle = WS_POPUP;
}
hWnd = CreateWindow("GLCLASS", "GameProgrammer.org",
dwStyle|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,
CW_USEDEFAULT, CW_USEDEFAULT,
WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL, NULL, wc.hInstance, NULL);
ZeroMemory(&pfd, sizeof(pfd));
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 24;
hDC = GetDC(hWnd);
if(!(PixelFormat = ChoosePixelFormat(hDC, &pfd)))
{
MessageBox(hWnd, "Failed to choose pixel format.", "ERROR", MB_OK);
return false;
}
if(!SetPixelFormat(hDC, PixelFormat, &pfd))
{
MessageBox(hWnd, "Failed to set pixel format.", "ERROR", MB_OK);
return false;
}
if(!(hRC = wglCreateContext(hDC)))
{
MessageBox(hWnd, "Failed to create the OpenGL rendering context.", "ERROR", MB_OK);
return false;
}
if(!wglMakeCurrent(hDC, hRC))
{
MessageBox(hWnd, "Failed to make current the OpenGL rendering context.", "ERROR", MB_OK);
return false;
}
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
glViewport(0, 0, width, height);
return true;
}
void GLWindow::Terminate()
{
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hRC);
ReleaseDC(hWnd, hDC);
if(fullscreen){
ChangeDisplaySettings(NULL, 0);
}
DestroyWindow(hWnd);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_KEYDOWN:
switch(wParam)
{
case VK_ESCAPE:
PostQuitMessage(0);
break;
}
break;
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
} |
Let's start with Initialize() function:
| bool Initialize(int width, int height, bool fullscreen); |
This function creates a window according to given arguments. First we must create and register a win32 "window class".
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = CS_VREDRAW|CS_HREDRAW|CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszClassName = "GLCLASS";
RegisterClass(&wc); |
Before creating the window using the registered class, we need to change desktop settings, this step is not necessary in windowed mode:
if(fullscreen)
{
DEVMODE dm;
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm);
dm.dmSize = sizeof(DEVMODE);
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
dm.dmBitsPerPel = 32;
ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
dwStyle = WS_POPUP;
} |
I've just got the default display settings from Windows by EnumDisplaySettings(), and changed the required fields then applied the settings using ChangeDisplaySettings().
Here we create a window using the registered class:
hWnd = CreateWindow("GLCLASS", "GameProgrammer.org",
dwStyle|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,
CW_USEDEFAULT, CW_USEDEFAULT,
WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL, NULL, wc.hInstance, NULL); |
At the next step before setup the OpenGL we need to define a pixel format for windows.
PIXELFORMATDESCRIPTOR pfd;
ZeroMemory(&pfd, sizeof(pfd));
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 24; |
PFD_SUPPORT_OPENGL indicates that we want to use OpenGL. cColorBits and cDepthBits specify number of the color and depth buffer bits per pixel. We talk about OpenGL buffers later so don't worry about these parameters for now.
Then we need to apply the pixel format to the current window:
hDC = GetDC(hWnd);
PixelFormat = ChoosePixelFormat(hDC, &pfd);
SetPixelFormat(hDC, PixelFormat, &pfd); |
GetDC returns the handle to window's device context; ChoosePixelFormat checks the requested pixel format to see whether it is supported by the device context or not, if it is supported, ChoosePixelFormat will return the corresponding ID. Finally, we can set the format by SetPixelFormat function.
Now we are ready to create an OpenGL rendering context. OpenGL rendering context is a layer between the OpenGL and the graphics device. We just need to create an OpenGL rendering context and activate it, that's all.
hRC = wglCreateContext(hDC);
wglMakeCurrent(hDC, hRC); |
Now OpenGL is prepared and we can draw everything we want, but OpenGL yet doesn't know about the viewport coordination.
| glViewport(0, 0, width, height); |
This pretty simple OpenGL command specifies that the entire window region can be drawn by OpenGL.
Now let's take a look at Terminate() function:
void GLWindow::Terminate()
{
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hRC);
ReleaseDC(hWnd, hDC);
if(fullscreen){
ChangeDisplaySettings(NULL, 0);
}
DestroyWindow(hWnd);
} |
Here we simply deactivate the rendering context by wglMakeCurrent, then delete the OpenGL rendering context by wglDeleteContext, then we just release the device context and finally delete the destroy the main window. And it's done ?
| void Swap(){ SwapBuffers(hDC); } |
Due to the fact that we chose the double buffered pixel format, OpenGL will render everything on the back buffer. Calling SwapBuffers function will copy the entire back buffer on the front buffer so you can see the scene.
The last function in the GLWindow class is the WndProc function which I'm not going to explain it, just refer to Win32 tutorials if you want to know more.
Application Class
Application class is where we put the application specific code. In this lesson we don't have much work:
Application.h
#pragma once
#include "GLWindow.h"
class Application
{
public:
Application();
~Application();
void Initialize();
void Update(float deltaTime);
void Render();
private:
GLWindow window;
}; |
Application.cpp
#include "Global.h"
#include "Application.h"
Application::Application()
{
}
Application::~Application()
{
}
void Application::Initialize()
{
window.Initialize(800, 600, false);
}
void Application::Update(float deltaTime)
{
}
void Application::Render()
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
window.Swap();
} |
In the Initialize() function we created a window calling window.Initialize(), the Update function is where we do our calculations leave it empty, finally the Render() function is where we are most interested.
| glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); |
You can use glClear() function to clear color, depth, stencil and accumulation buffers. Here we just cleared color and depth buffer. You may wonder what an OpenGL buffer is. OpenGL uses four types of buffers to store special information about every pixel. OpenGL commands will fill these buffers and OpenGL will use these buffers to create a 2D image from the 3D scene. We will talk about every buffer separately later.
Globals
I've created a header file to include external headers, so we don't need to include them in every source file.
Global.h
#pragma once
#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
#include <stdio.h>
#include <math.h> |
WinMain
This is the last part and in fact our entry point.
#include "Global.h"
#include "Application.h"
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
Application game;
MSG msg;
long prevTick, curTick = GetTickCount();
game.Initialize();
while(true)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message==WM_QUIT)break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
prevTick = curTick;
curTick = GetTickCount();
game.Update((curTick - prevTick)/1000.f);
game.Render();
}
return 0;
} |
First I've created an instance of the Application class and called the Initialize() function.
Application game;
game.Initialize(); |
Then we have our message loop which peeks the message from Windows and send to our message handler function (WndProc).
while(true)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message==WM_QUIT)break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
prevTick = curTick;
curTick = GetTickCount();
game.Update((curTick - prevTick)/1000.f);
game.Render();
} |
GetTickCount() function returns the time since system is initialized in milliseconds. We passed the delta time to the update function which later we use to animate the models.
OK, we are done. It wasn't such easy right? But don't worry! The initialization part is one of the most boring and hard parts. Because you don't now about the underlying parts of the system yet. As you continue to next tutorials you will find the answer of a lot of your questions.
Good luck!
Download Source
|