Setting up the OpenGL window

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:

  1. Include header files.
  2. Define global variables.
  3. Create the OpenGL window.
  4. Initialize the OpenGL window.
  5. Size the OpenGL window.
  6. Remove the OpenGL window.
  7. Create the Windows event handler.
  8. Create the 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.

Tip

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.

Including header files

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:

  • We are still running in Windows, so we must include windows.h
  • The core header for OpenGL is GL.h
  • There are some great utilities to make our lives easier in GLU.h
  • There are also useful utilities in glut.h

Defining global variables

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 application
  • hDC: This holds a reference to the GDI device context which is used for drawing in native Windows
  • hRC: This holds a reference to the OpenGL rendering context, used for rendering 3D
  • hWnd: This holds a reference to the actual window the application is running in

We 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.

Creating a function to create the OpenGL window

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.

Sizing the OpenGL window

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:

  • First, we take the width and height (ensuring that the height is never equal to 0), and use them to define the size viewport using the 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.
  • Next, we have to define the frustum. After telling OpenGL to use the projection matrix, we use the 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.

    Tip

    After you complete this chapter, you may try playing with the values of this function to see how it changes the rendering.

  • Finally, we tell OpenGL to use the model view from this point forward.

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.

Note

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.

Initializing the OpenGL window

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.

Creating a function to remove the OpenGL window

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 context
  • hDC: This is the Windows device context
  • hWnd: This is the handle to the Window
  • hInstance: This is the handle to the application

Tip

You may notice the two functions that start with wgl (wglMakeCurrent and wglDeleteContext). This stands for Windows GL and these are special OpenGL functions that only work in Windows.

Creating the OpenGL window

Now 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:

  • Set the window properties
  • Register the application with Windows—RegisterClass
  • Set up full screen mode if required—ChangeDisplaySettings
  • Create the Window—CreateWindowEx
  • Get a Windows device context—GetDC
  • Set the OpenGL pixel format—SetPixelFormat
  • Create an OpenGL rendering context—wglCreateContext
  • Bind the Windows device context and OpenGL rendering context together—wglMakeCurrent
  • Show the window—ShowWindow, SetForegroundWindow(hWnd), and SetFocus(hWnd)
  • Initialize the OpenGL Window—ReSizeGLScene, InitGL; create the WinMain function

The 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.

Creating the Windows event handler

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.

The Game loop

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.

The finale

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.105.190