GLFW3 Basecode with FPS Camera Controls

Basecode is funny thing – when you start a new project, do you really start from scratch? A complete blank slate? Or do you make a copy of the last project you worked on which is similar and modify it? Often, you’re going to want to start from some pre-existing functional base, but what’s stable and functional enough? Do you really want to go with a framework like Cinder or Processing to hold your code? Or go with a full-on engine like Unity or Unreal Engine 4 or some other engine?

I’m going to write a game at some point in the future, and I want to go it (almost) alone – I don’t want to be locked into someone elses constructs and patterns, or drag-and-drop functionality in which I have absolutely no idea how it works – I want to think for myself and create what’s basically my own engine, where I understand how it fits together and how each piece works. This doesn’t necessarily mean that everything needs to be worked out from first principles, but it should be possible to make all the important architectural decisions. This means that I want precise control over:

  • At least one OpenGL window, with controllable context details (preferably multiple windows with a shared projection)
  • Painless keyboard and mouse handlers
  • File handling of common types (load and use this 3D model/sound file/settings file)
  • Help with prototyping via simple drawing calls

Which brings us back to basecode being a funny thing – you get to make the architectural decisions, and live with the consequences. If you decide to go with an engine, then you’re going to learn the engine – not the fundamental technologies or aspects of the code that make the engine work. So if you grab some fantastic engine and you go:

  1. Load this spaceship model, which is made of these different materials,
  2. There’s a light which is at (1000, 200, 300) in world space (and perhaps a dozen other lights),
  3. Draw the spaceship from my (i.e the camera’s) location.

But what does that actually teach you, as a developer? How do you load the model from file? How is the lighting model applied to the vertices? Where the hell is the spaceship in relation to you, let alone the surface normals of the spaceship with regard to the light-source(s) with regard to the camera? In an engine, you don’t care – you let the engine work it out for you, and you learn nothing. Or maybe you learn the engine – which means you learn to trust someone else to think instead of you having to think for yourself.

Which finally brings us back to basecode being a funny thing… I’ve been thinking about this for weeks, and below is the OpenGL/GLFW3 basecode I’ve written to open a window, draw some grids for orientation, and allow for ‘FPS-esque’ mouse and keyboard controls. The main.cpp is listed below, which shows you how the program itself runs – everything else you’ll need to look at for yourself – but I promise you this:

  • Every single piece of this code is clear in its use and serves a purpose.
  • Every single piece of this code performs its job in the simplest, most straight forward manner possible. If the option is to be clever or readable, then I pick readable every time. Saying that, I think I used an inline if-statement once i.e. “if (raining) ? putUpUmbrella() : keepUmbrellaDown();”. Honestly, when you see it, you’ll be okay.
  • Every single piece of this code is documented to explain not only WHAT the code is doing, but (where appropriate) WHY it is doing it. When I used to work as as Subsystem Integration and Test engineer, we would write software build instructions with the goal that your Mum should be able to build the software image from the simple, accurate, non-ambiguous instructions. If you didn’t think your Mum could build it, then you re-worked the instructions until you thought that she could.

I’ll add some additional utility classes to this over time, but for now, this basecode will get a window with FPS controls up and running and display some grids via shaders for orientation – and everything should be simple, straight-forward and clear. Enjoy!

Code::Blocks projects for both Windows and Linux (libraries included for Windows) can be found here: GLFW3_Basecode_Nov_2014.7z.

Update – Feb 2015: There were issues using this code in Visual Studio 2010 as it doesn’t support strongly typed enums or the R” notation (although VS2012 onwards does), and the libraries packaged were the Code::Blocks versions (which was intended – the above version is specifically for Code::Blocks) – so here’s a modified & fully working Visual Studio 2010 version: GLFW3-Basecode-VS2010.7z.

/***
Project: GLFW3 Basecode
Version: 0.5
Author : r3dux
Date   : 21/1/2014
Purpose: Basecode to setup an OpenGL context with FPS camera controls and draw some grids.
***/
 
#include <iostream>
 
// Define that we're using the static version of GLEW (glew32s) so that it gets built
// into our final executable.
// NOTE: This MUST be defined before importing GLEW!
#define GLEW_STATIC
 
// Include the GL Extension Wrangler. Note: GLEW should always be the very first include
#include <GL/glew.h>
 
#include <GLFW/glfw3.h>                 // Include GL Framework. Note: This pulls in GL.h for us.
 
// Include the GL Mathematics library
#define GLM_FORCE_RADIANS               // We must work in radians in newer versions of GLM...
#include <glm/glm.hpp>                  // ...so now that's defined we can import GLM itself.
 
// Include our custom classes
#include "Camera.h"
#include "Grid.h"
#include "Utils.h"
 
// Save ourselves some typing...
using std::cout;
using std::endl;
using glm::vec3;
using glm::vec4;
using glm::mat4;
using glm::mat3;
 
// ---------- Global variables ----------
 
// Window and projection settings
GLsizei windowWidth       = 800;
GLsizei windowHeight      = 600;
float vertFieldOfViewDegs = 45.0f;
float nearClipDistance    = 1.0f;
float farClipDistance     = 2000.0f;
 
// Misc
int  frameCount = 0;              // How many frames we've drawn
int  frameRate  = 60;             // Target frame rate -we'll assume a 60Hz refresh for now
bool leftMouseButtonDown = false; // We'll only look around when the left mouse button is down
 
// Matricies
mat4 projectionMatrix; // The projection matrix is used to perform the 3D to 2D conversion i.e. it maps from eye space to clip space.
mat4 viewMatrix;       // The view matrix maps the world coordinate system into eye cordinates (i.e. world space to eye space)
mat4 modelMatrix;      // The model matrix maps an object's local coordinate system into world coordinates (i.e. model space to world space)
 
// Pointers to two grids
Grid *upperGrid, *lowerGrid;
 
// Camera. Params: location, rotation (degrees), window width & height
Camera camera(vec3(0.0f), vec3(0.0f), windowWidth, windowHeight);
 
// Callback function to resize the window and set the viewport to the correct size
void resizeWindow(GLFWwindow *window, GLsizei newWidth, GLsizei newHeight)
{
    // Keep track of the new width and height of the window
    windowWidth  = float(newWidth);
    windowHeight = float(newHeight);
 
    // Recalculate the projection matrix
    projectionMatrix = glm::perspective(vertFieldOfViewDegs, GLfloat(windowWidth) / GLfloat(windowHeight), nearClipDistance, farClipDistance);
 
    // Viewport is the entire window
    glViewport(0, 0, windowWidth, windowHeight);
 
    // Update the midpoint location in the camera class because it uses these values, too
    camera.updateWindowMidpoint(windowWidth, windowHeight);
}
 
// Callback function to handle keypresses
void handleKeypress(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    // User hit ESC? Set the window to close
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
    {
        glfwSetWindowShouldClose(window, GL_TRUE);
    }
    else
    {
        camera.handleKeypress(key, action);
    }
}
 
// Callback function to handle mouse movement
void handleMouseMove(GLFWwindow *window, double mouseX, double mouseY)
{
    // We'll only look around when the left mouse button is down
    if (leftMouseButtonDown)
    {
        camera.handleMouseMove(window, mouseX, mouseY);
    }
}
 
// Callback function to handle mouse button presses
void handleMouseButton(GLFWwindow *window, int button, int action, int mods)
{
    // Button press involves left mouse button?
    if (button == GLFW_MOUSE_BUTTON_1)
    {
        if (action == GLFW_PRESS)
        {
            glfwSetCursorPos(window, windowWidth / 2, windowHeight / 2);
            leftMouseButtonDown = true;
        }
        else // Action must be GLFW_RELEASE
        {
            leftMouseButtonDown = false;
        }
    }
}
 
// Function to set up our OpenGL rendering context
void initGL(GLFWwindow *window)
{
    // ---------- Initialise GLEW ----------
 
    // Enable glewExperimental which ensures that all extensions with valid entry points will be exposed.
    glewExperimental = true;
 
    // Note: We MUST have an OpenGL rendering context open to initialise GLEW successfully!
    GLenum err = glewInit();
    if (GLEW_OK != err)
    {
        cout << "GLEW error: " << glewGetErrorString(err) << endl;
        glfwTerminate();
    }
    cout << "GLEW intialised successfully. Using GLEW version: " << glewGetString(GLEW_VERSION) << endl << endl;
 
    // Depending on the OpenGL context settings, calling glewInit() can sometimes cause a GL_INVALID_ENUM error.
    // As this issue isn't really our code's fault, we'll check the error here to clear it.
    //
    // Cause: In a core profile context, GL_EXTENSIONS is an invalid constant to pass to glGetString (...). You
    // must use the new glGetStringi (...) function. GLEW does not do this by default, given a core context
    // without being informed to use glGetStringi (...), GLEW will use glGetString (...) and will cause GL to
    // generate a GL_INVALID_ENUM error. In order to get GLEW to use glGetStringi (...) (which you should ONLY
    // do in an OpenGL 3.0+ context), set glewExperimental = true; before calling glewInit (...).
    //
    // Source: http://stackoverflow.com/questions/19453439/solved-opengl-error-gl-invalid-enum-0x0500-while-glewinit
    checkGLError("glewInit - harmless / ignore");
 
    // ---------- Setup OpenGL Options ----------
 
    glViewport( 0, 0, GLsizei(windowWidth), GLsizei(windowHeight) ); // Viewport is entire window
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);                            // Clear to black with full alpha
    glEnable(GL_DEPTH_TEST);                                         // Enable depth testing
    glDepthFunc(GL_LEQUAL);                                          // Specify depth testing function
    glClearDepth(1.0);                                               // Clear the full extent of the depth buffer (default)
    glEnable(GL_CULL_FACE);                                          // Enable face culling
    glCullFace(GL_BACK);                                             // Cull back faces of polygons
    glFrontFace(GL_CCW);                                             // Counter-clockwise winding indicates a forward facing polygon (default)
 
    // ---------- Setup GLFW Callback Functions ----------
 
    glfwSetWindowSizeCallback(window, resizeWindow);                 // Register window resize functiom
    glfwSetKeyCallback(window, handleKeypress);                      // Register keyboard handler function
    glfwSetCursorPosCallback(window, handleMouseMove);               // Register mouse movement handler function
    glfwSetMouseButtonCallback(window, handleMouseButton);           // Register mouse button handler function
 
    // ---------- Setup GLFW Options ----------
 
    glfwSwapInterval(1);                                             // Swap buffers every frame (i.e. lock to VSync)
    glfwSetInputMode(window, GLFW_CURSOR_DISABLED, GL_FALSE);        // Do not hide the mouse cursor
    glfwSetWindowPos(window, 200, 200);                              // Push the top-left of the window out from the top-left corner of the screen
    glfwSetCursorPos(window, windowWidth / 2, windowHeight / 2);     // Move the mouse cursor to the centre of the window
 
    checkGLError("InitGL");
}
 
// Function to perform our drawing
void drawFrame()
{
    // Move the camera
    camera.move(1.0f/ frameRate);
 
    // ---------- Matrix operations ----------
 
    // Reset our View matrix
    viewMatrix = mat4(1.0f);
 
    // Perform camera rotation
    viewMatrix = glm::rotate(viewMatrix, camera.getXRotationRads(), X_AXIS);
    viewMatrix = glm::rotate(viewMatrix, camera.getYRotationRads(), Y_AXIS);
 
    // Translate to our camera position
    viewMatrix = glm::translate(viewMatrix, -camera.getLocation() );
 
    // Create an identity matrix for the model matrix
    modelMatrix = mat4(1.0f);
 
    // ---------- Drawing operations ----------
 
    mat4 mvpMatrix = projectionMatrix * viewMatrix * modelMatrix;
    upperGrid->draw(mvpMatrix);
    lowerGrid->draw(mvpMatrix);
}
 
int main()
{
    // ----- Initialiise GLFW, specify window hints & open a context -----
 
    // IMPORTANT: glfwInit resets all window hints, so we must call glfwInit FIRST and THEN we supply window hints!
    if (!glfwInit())
    {
        cout << "glfwInit failed!" << endl;
        exit(-1);
    }
 
    // Further reading on GLFW window hints: http://www.glfw.org/docs/latest/window.html#window_hints
 
    // If we want to use a a core profile (i.e. no legacy fixed-pipeline functionality) or if we want to
    // use forward compatible mode (i.e. only non-deprecated features of a given OpenGL version available)
    // then we MUST specify the MAJOR.MINOR context version we want to use FIRST!
    //glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    //glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4);
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 
    // Ask for 4x Anti-Aliasing
    glfwWindowHint(GLFW_SAMPLES, 4);
 
    // Create a window. Params: width, height, title, *monitor, *share
    GLFWwindow* window = glfwCreateWindow(GLsizei(windowWidth), GLsizei(windowHeight), "GLFW3 Basecode | Use WSAD to move & LMB to look around - Nov 2014 | r3dux.org ", NULL, NULL);
    if (!window)
    {
        cout << "Failed to create window - bad context MAJOR.MINOR version?" << endl;
        getKeypressThenExit();
    }
 
    // Make the current OpenGL context active
    glfwMakeContextCurrent(window);
 
    // Display the details of our OpenGL window
    displayWindowProperties(window);
 
    // -------------- Set up our OpenGL settings ---------------
 
    initGL(window);
 
    // ---------- Set up our grids ----------
 
    // Instantiate our grids. Params: Width, Depth, level (i.e. location of y-axis), number of grid lines
    upperGrid = new Grid(1000.0f, 1000.0f,  200.0f, 20);
    lowerGrid = new Grid(1000.0f, 1000.0f, -200.0f, 20);
 
    // ---------- Set up our matricies ----------
 
    // Specify the projection matrix
    projectionMatrix = glm::perspective(vertFieldOfViewDegs, GLfloat(windowWidth) / GLfloat(windowHeight), nearClipDistance, farClipDistance);
 
    // Reset the view and model and view matrices to identity
    viewMatrix  = mat4(1.0f);
    modelMatrix = mat4(1.0f);
 
    // ---------- Main loop ----------
 
    while ( !glfwWindowShouldClose(window) )
    {
        frameCount++;
 
        // Clear the screen and depth buffer
        glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 
        // Draw our frame
        drawFrame();
 
        // Swap the back and front buffers to display the frame we just rendered
        glfwSwapBuffers(window);
 
        // Poll for input
        glfwPollEvents();
    }
 
    // Check the final error state
    // NOTE: This MUST be called while we still have a valid rendering context (i.e. before we call glfwTerminate() )
    checkGLError("End");
 
    // Destroy the window and exit
    glfwDestroyWindow(window);
    glfwTerminate();
 
    return 0;
}

11 thoughts on “GLFW3 Basecode with FPS Camera Controls”

    1. I afraid I don’t own or have access to any Apple hardware so I can’t port it to XCode, however if you give me the exact error messages I might be able to advise the nature of the problem.

      Alternatively, perhaps ask if any of the nice people on StackOverflow would be willing to provide a configured XCode project version (as the code works fine on Windows and Linux it’s likely just a library linkage issue).

  1. When I try to compile the code in VS2010, I get the following errors:

    2>Grid.obj : error LNK2001: unresolved external symbol ___glewEnableVertexAttribArray
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewVertexAttribPointer
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewBufferData
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewBindBuffer
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGenBuffers
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewBindVertexArray
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGenVertexArrays
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewUseProgram
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewCreateProgram
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGetProgramiv
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewDetachShader
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewLinkProgram
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewAttachShader
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGetShaderiv
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewCompileShader
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewShaderSource
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewCreateShader
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGetAttribLocation
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewGetUniformLocation
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewDeleteProgram
    2>Grid.obj : error LNK2001: unresolved external symbol ___glewUniformMatrix4fv
    2>main.obj : error LNK2001: unresolved external symbol _glewExperimental
    2>C:\Users\User\Desktop\framework\SP2_Framework\SP2_Framework\Debug\Application.exe : fatal error LNK1120: 22 unresolved externals
    ========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

    Help would be much appreciated.

    1. Never mind, I got it to run. However, all I get is a white screen with the error message: “Could not add attribute: vertexLocation – location returned -1!”.

      1. If you used the provided Code::Blocks archive, did you replace the GLFW3 dll with the Visual Studio one? Compilers get fussy about using libraries compiled with other compilers, so trying to use a C::B compiled lib in VS is going to be problematic at best, it’s the same going VS to C::B.

        Trying grabbing a copy of GLFW3 from: http://www.glfw.org/download.html and replacing the file .\libs\Win32\glfw\lib\glfw3.dll with the one from the glfw3 zip .\lib-vc2010 folder.

        1. Looking into it more, if you’re using Visual Studio 2010 you need to make some modifications to the source – specifically, VS2010 doesn’t support strongly-typed enums or the R” notation of C++11/0x – I’ve just created a mostly working VC++ version, but the camera-look is being a dick… I’ll have a play and post the VS2010 version within a day or so.

    1. glm.hpp is included and everything works just fine. Unless you mean glm has changed and you would now need to pull in something else from when I originally wrote this?

  2. Hi mate. This is awesome and exactly what I was looking for. I take it the licence is beerware? Not that I have any commercial plans.

Leave a Reply to Tenkuru Cancel reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.