We are now going to add the code required to create an OpenGL window. We did this once for RoboRacer2D, but now, we are creating a 3D game and there will be some differences. Here's a look at what we need to do:
WinMain
function.Notice that we still have to create some code to satisfy Windows. We need an event handler to process Windows events, and we still need a main function to serve as the program entry point and run the main program loop. Everything else in this list is used to set up the OpenGL environment.
I listed the functions tasks that we need in an order that makes logical sense. When we actually implement the code, we will create things in a slightly different order. This is because some functions require another function to already be defined. For example, the function to create the OpenGL window calls the function to initialize the OpenGL window, so the initialize function is coded first.
The first step is to in include the appropriate headers. Add the following headers at the top of SpaceRacer3D.cpp
:
#include <windows.h> #include <glGL.h> #include <glGLU.h> #include "glut.h"
These are the same files that we used in the 2D project, but here is a quick description of each one so that you don't have to flip back:
windows.h
GL.h
GLU.h
glut.h
We need some global variables to hold onto references to Windows and OpenGL objects. Add the following lines of code just under the header lines:
HINSTANCE hInstance = NULL; HDC hDC = NULL; HGLRC hRC = NULL; HWND hWnd = NULL; bool fullscreen = false;
Here is a quick list of what these variables are for:
hInstance
: This holds a reference to this instance of the applicationhDC
: This holds a reference to the GDI device context which is used for drawing in native WindowshRC
: This holds a reference to the OpenGL rendering context, used for rendering 3DhWnd
: This holds a reference to the actual window the application is running inWe have also included a global fullscreen
variable. If you set this to true
, the game will run in fullscreen
mode. If you set this to false
, the game will run in windowed mode.
We will also include a forward reference to the Windows event handler. Add the following line of code:
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
A forward reference allows us to define a function whose actual implementation will appear later in the code. The code for WndProc
will be added later.
Next, we will create the function to size the OpenGL window. This function is called when the program starts as well as any time the window that the application is running in is resized. Add the following code:
void ReSizeGLScene(const GLsizei p_width, const GLsizei p_height) { GLsizei h = p_height; GLsizei w = p_width; if (h == 0) { h = 1; } glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f, (GLfloat)w / (GLfloat)h, 0.1f, 100.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); }
This code sets the size of the OpenGL window and prepares the window for rendering in 3D:
glViewport
function. The first two parameters are the x and y value of the lower left-hand corner of the viewport, followed by the width and the height. These four parameters define the size and location of the viewport.gluPerspective
function, which takes four parameters: the field of view (in degrees, not radians), the aspect ratio, the distance of the front clipping plane, and the distance of the rear clipping plane. The field of view is the angle from the center of the camera. The aspect ratio is the width divided by the height. These four parameters define the size of the frustum.If you compare this function to the GLSize
function that we used in RoboRacer2D, you will note one significant difference: we do not make a call to glOrtho
. Remember, RoboRacer2D was a 2D game. 2D games use an orthographic projection that removes perspective when the scene is rendered. You don't need perspective in a 2D game. Most 3D games use a perspective projection, which is defined by the gluPerspective
call.
OpenGL Matrices
Just before the gluPerspective
call, you will notice two functions: glMatrixMode
, and glLoadIdentity
. Remember from our discussion of matrices that a matrix is used to hold a set of values. OpenGL has many standard matrices, and one of them is the projection matrix, which is used to define the view frustum.
If we want to set the values of a matrix, we must first tell OpenGL that we want to work with this matrix. Next, we typically initialize the matrix, and finally, we make a call that sets the values of the matrix.
Looking at the code to set the view frustum, this is exactly what we do:
glMatrixMode(GL_PROJECTION)
: This tells OpenGL that we want to work with the projection matrix. Any matrix operations after this call will be applied to the projection matrix.glLoadIdentity()
: This sets the projection matrix to an identity matrix, thus, clearing any previous values.gluPerspective(45.0f, (GLfloat)w / (GLfloat)h, 0.1f, 100.0f)
: This sets the values of the projection matrix.You should get used to this pattern because it is used often in OpenGL: set a matrix to work with, initialize the matrix, then set the values of the matrix. For example, at the end of this function we tell OpenGL to use the model view matrix and initialize it. Any operations after this will affect the model view.
Add the following code to initialize OpenGL:
const bool InitGL() { glShadeModel(GL_SMOOTH); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClearDepth(1.0f); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); return true; }
This function initializes OpenGL by defining important settings that determine how a scene will be rendered:
glShadeModel
: This tells OpenGL that we want it to smooth the edges of the vertices. This greatly improves the look of our images.glClearColor
: This sets the color that is used each time glClear
is called to clear out the rendering buffer. It is also the default color that will show in the scene.glClearDepth(1.0f)
: This tells OpenGL that we want the entire depth buffer cleared each time glClear
is called. Remember, we are working in 3D now, and the depth buffer is roughly synonymous with the Z-axis.glEnable(GL_DEPTH_TEST)
: This turns on depth checking. Depth checking is used to determine if a particular piece of data will be rendered.glDepthFunc(GL_LEQUAL)
: This tells OpenGL how you want to perform the depth test. LEQUAL
tells OpenGL to write the data only if the z value of the incoming data is less than or equal to the z value of the existing data.glHint((GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST))
: This is an interesting function. glHint
means that this function is going to suggest that OpenGL use the settings passed as parameters. However, as there are many different types of devices, there is no guarantee that these settings will actually be enforced. The GL_PERSPECTIVE
hint tells OpenGL to use the highest quality when rendering perspective, while GL_NICEST
means focus on rendering quality rather than speed.Eventually, we will want to shut things down. Good programming dictates that we release the resources that were being used by the OpenGL window. Add the following function to our code:
GLvoid KillGLWindow(GLvoid) { if (fullscreen) { ChangeDisplaySettings(NULL, 0); ShowCursor(TRUE); } if (hRC) { wglMakeCurrent(NULL, NULL); wglDeleteContext(hRC); hRC = NULL; } if (hDC) { ReleaseDC(hWnd, hDC) hDC = NULL; } if (hWnd) { DestroyWindow(hWnd); hWnd = NULL; } UnregisterClass("OpenGL", hInstance) hInstance = NULL; }
First, we tell Windows to exit fullscreen mode (if we were running fullscreen) and turn the cursor back on. Then, we check each object that had a resource attached, release that object, then set it to null. The objects that need to be released are:
hRC
: This is the OpenGL rendering contexthDC
: This is the Windows device contexthWnd
: This is the handle to the WindowhInstance
: This is the handle to the applicationNow that we have the other OpenGL support functions defined, we can add the function to actually create the OpenGL window. Add the following code:
const bool CreateGLWindow(const char* p_title, const int p_width, const int p_height, const int p_bits, const bool p_fullscreenflag) { GLuint PixelFormat; WNDCLASS wc; DWORD dwExStyle; DWORD dwStyle; RECT WindowRect; WindowRect.left = (long)0; WindowRect.right = (long)p_width; WindowRect.top = (long)0; WindowRect.bottom = (long)p_height; fullscreen = p_fullscreenflag; GLfloat screen_height = (GLfloat)p_height; GLfloat screen_width = (GLfloat)p_width; hInstance = GetModuleHandle(NULL); wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = "OpenGL"; RegisterClass(&wc); if (fullscreen) { DEVMODE dmScreenSettings; memset(&dmScreenSettings, 0, sizeof(dmScreenSettings)); dmScreenSettings.dmSize = sizeof(dmScreenSettings); dmScreenSettings.dmPelsWidth = p_width; dmScreenSettings.dmPelsHeight = p_height; dmScreenSettings.dmBitsPerPel = p_bits; dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT; ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN); } if (fullscreen) { dwExStyle = WS_EX_APPWINDOW; dwStyle = WS_POPUP; ShowCursor(false); } else { dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; dwStyle = WS_OVERLAPPEDWINDOW; } AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); hWnd = CreateWindowEx(dwExStyle,"OpenGL", p_title, dwStyle | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, 0, 0, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top, NULL, NULL, hInstance, NULL); static PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, PFD_TYPE_RGBA, p_bits, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, PFD_MAIN_PLANE, 0, 0, 0, 0 }; hDC = GetDC(hWnd); PixelFormat = ChoosePixelFormat(hDC, &pfd); SetPixelFormat(hDC, PixelFormat, &pfd); hRC = wglCreateContext(hDC); wglMakeCurrent(hDC, hRC); ShowWindow(hWnd, SW_SHOW); SetForegroundWindow(hWnd); SetFocus(hWnd); ReSizeGLScene(p_width, p_height); InitGL(); return true; }
The purpose of CreateGLWindow
is to create a window with settings that allow it to work with OpenGL. The main tasks accomplished by this function are as follows:
RegisterClass
ChangeDisplaySettings
CreateWindowEx
GetDC
SetPixelFormat
wglCreateContext
wglMakeCurrent
ShowWindow
, SetForegroundWindow(hWnd)
, and SetFocus(hWnd)
ReSizeGLScene
, InitGL
; create the WinMain
functionThe WinMain
function is the entry point for the application. Add the following code:
int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { MSG msg; bool done = false; if (!CreateGLWindow("SpaceRacer3D", 800, 600, 16, false)) { return false; } StartGame(); int previousTime = glutGet(GLUT_ELAPSED_TIME); while (!done) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { done = true; } else { TranslateMessage(&msg); DispatchMessage(&msg); } } else { int currentTime = glutGet(GLUT_ELAPSED_TIME); float deltaTime = (float)(currentTime - previousTime) / 1000; previousTime = currentTime; GameLoop(deltaTime); } } EndGame(); return (int)msg.wParam; }
It calls all of the other functions to initialize Windows, and OpenGL then starts the main message loop, which we hijack and adapt to be our game loop. As we explained all of this code in Chapter 1, Building the Foundation we won't do it again here.
Finally, we have to have an event handler to receive events from Windows and process them. We created the forward declaration at the top of the code, and now we will actually implement the handler. Add the following code:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_SIZE: ReSizeGLScene(LOWORD(lParam), HIWORD(lParam)); return 0; default: return DefWindowProc(hWnd, message, wParam, lParam); } return false; }
This function will be called any time Windows sends an event to our program. We handle two events: WM_DESTROY
and WM_SIZE
:
WM_DESTROY
is triggered when the window is closed. When this happens we use PostQuitMessage
to tell our main game loop that it is time to stop.WM_SIZE
is triggered when the window is resized. When this happens, we call ReSizeGLScene
.We still need to add some stub functions for our game functions: StartGame
, Update
, Render
, EndGame
, and GameLoop
. Add the following code before the WinMain
function:
void StartGame() { } void Update(const float p_deltaTime) { } void Render() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); DrawCube(); SwapBuffers(hDC); } void EndGame() { } void GameLoop(const float p_deltatTime) { Update(p_deltatTime); Render(); }
These functions serve the same purpose that they did in RoboRacer2D. GameLoop
is called from the Windows main loop, and in turn calls Update
and Render
. StartGame
is called before the Windows main loop, and EndGame
is called when the game ends.
If you run the game right now, you will see a nice black window. This is because we haven't told the program to draw anything yet! It seemed unfair to do all this work and get a black screen, so if you want to do a little extra work, add the following code just before the StartGame
function:
void DrawCube() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glTranslatef(0.0f, 0.0f, -7.0f); glRotatef(fRotate, 1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); glColor3f(0.0f, 1.0f, 0.0f); glVertex3f(1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); glVertex3f(1.0f, 1.0f, 1.0f); glColor3f(1.0f, 0.5f, 0.0f); glVertex3f(1.0f, -1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); glVertex3f(1.0f, -1.0f, -1.0f); glColor3f(1.0f, 0.0f, 0.0f); glVertex3f(1.0f, 1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, 1.0f); glVertex3f(1.0f, -1.0f, 1.0f); glColor3f(1.0f, 1.0f, 0.0f); glVertex3f(1.0f, -1.0f, -1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f(1.0f, 1.0f, -1.0f); glColor3f(0.0f, 0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); glVertex3f(-1.0f, -1.0f, 1.0f); glColor3f(1.0f, 0.0f, 1.0f); glVertex3f(1.0f, 1.0f, -1.0f); glVertex3f(1.0f, 1.0f, 1.0f); glVertex3f(1.0f, -1.0f, 1.0f); glVertex3f(1.0f, -1.0f, -1.0f); glEnd(); fRotate -= 0.05f; }
Also, you need to make sure to declare the following global variable:
float frotate = 1.0f;
Now run the program, and you should see a colorful rotating cube. Don't worry about how this works yet—we will learn that in the next chapter.
18.117.105.190