A C++ Camera Class for Simple OpenGL FPS Controls

This is the third post of three, where we finally get to create a Camera class which encapsulates all the important properties of a camera suitable for FPS controls. I could, and indeed did, have this written to just use three floats for the camera position, three for the rotation, three for the movement speed etc – but it makes more sense to use a vector class to encapsulate those values into a single item and provide methods for easy manipulation, so that’s what I’ve done.

The end result of this is that although the Camera class now depends on the Vec3 class, the Camera class itself is now more concise and easier to use. If you don’t like the coupling you can easily break it and return to individual values, but I think I prefer it this way. Oh, and this class is designed to work with GLFW, although it could be very easily modified to remove that requirement and be used with SDL or something instead. In fact, we only ever use the glfwSetMousePos(x, y) method to reset the mouse position to the centre of the screen each frame!

Anyways, let’s look at the header first to see the properties and methods of the class:

Camera.h Header

#ifndef CAMERA_H
#define CAMERA_H
 
#include <iostream>
#include <math.h>         // Used only for sin() and cos() functions
 
#include <GL/glfw.h>      // Include OpenGL Framework library for the GLFW_PRESS constant only!
 
#include "Vec3.hpp"       // Include our custom Vec3 class
 
class Camera
{
    protected:
        // Camera position
        Vec3<double> position;
 
        // Camera rotation
        Vec3<double> rotation;
 
        // Camera movement speed. When we call the move() function on a camera, it moves using these speeds
        Vec3<double> speed;
 
        double movementSpeedFactor; // Controls how fast the camera moves
        double pitchSensitivity;    // Controls how sensitive mouse movements affect looking up and down
        double yawSensitivity;      // Controls how sensitive mouse movements affect looking left and right
 
        // Window size in pixels and where the midpoint of it falls
        int windowWidth;
        int windowHeight;
        int windowMidX;
        int windowMidY;
 
        // Method to set some reasonable default values. For internal use by the class only.
        void initCamera();
 
    public:
        static const double TO_RADS; // The value of 1 degree in radians
 
        // Holding any keys down?
        bool holdingForward;
        bool holdingBackward;
        bool holdingLeftStrafe;
        bool holdingRightStrafe;
 
        // Constructor
        Camera(float windowWidth, float windowHeight);
 
        // Destructor
        ~Camera();
 
        // Mouse movement handler to look around
        void handleMouseMove(int mouseX, int mouseY);
 
        // Method to convert an angle in degress to radians
        const double toRads(const double &angleInDegrees) const;
 
        // Method to move the camera based on the current direction
        void move(double deltaTime);
 
        // --------------------------------- Inline methods ----------------------------------------------
 
        // Setters to allow for change of vertical (pitch) and horizontal (yaw) mouse movement sensitivity
        float getPitchSensitivity()            { return pitchSensitivity;  }
        void  setPitchSensitivity(float value) { pitchSensitivity = value; }
        float getYawSensitivity()              { return yawSensitivity;    }
        void  setYawSensitivity(float value)   { yawSensitivity   = value; }
 
        // Position getters
        Vec3<double> getPosition() const { return position;        }
        double getXPos()           const { return position.getX(); }
        double getYPos()           const { return position.getY(); }
        double getZPos()           const { return position.getZ(); }
 
        // Rotation getters
        Vec3<double> getRotation() const { return rotation;        }
        double getXRot()           const { return rotation.getX(); }
        double getYRot()           const { return rotation.getY(); }
        double getZRot()           const { return rotation.getZ(); }
};
 
#endif // CAMERA_H

Now for the implementation:

Camera.cpp Class

#include "Camera.h"
 
const double Camera::TO_RADS = 3.141592654 / 180.0; // The value of 1 degree in radians
 
Camera::Camera(float theWindowWidth, float theWindowHeight)
{
	initCamera();
 
	windowWidth  = theWindowWidth;
	windowHeight = theWindowHeight;
 
	// Calculate the middle of the window
	windowMidX = windowWidth  / 2.0f;
	windowMidY = windowHeight / 2.0f;
 
	glfwSetMousePos(windowMidX, windowMidY);
}
 
Camera::~Camera()
{
	// Nothing to do here - we don't need to free memory as all member variables
	// were declared on the stack.
}
 
void Camera::initCamera()
{
	// Set position, rotation and speed values to zero
	position.zero();
	rotation.zero();
	speed.zero();
 
	// How fast we move (higher values mean we move and strafe faster)
	movementSpeedFactor = 100.0;
 
	pitchSensitivity = 0.2; // How sensitive mouse movements affect looking up and down
	yawSensitivity   = 0.2; // How sensitive mouse movements affect looking left and right
 
	// To begin with, we aren't holding down any keys
	holdingForward     = false;
	holdingBackward    = false;
	holdingLeftStrafe  = false;
	holdingRightStrafe = false;
}
 
// Function to convert degrees to radians
const double Camera::toRads(const double &theAngleInDegrees) const
{
	return theAngleInDegrees * TO_RADS;
}
 
// Function to deal with mouse position changes
void Camera::handleMouseMove(int mouseX, int mouseY)
{
	// Calculate our horizontal and vertical mouse movement from middle of the window
	double horizMovement = (mouseX - windowMidX+1) * yawSensitivity;
	double vertMovement  = (mouseY - windowMidY) * pitchSensitivity;
 
	std::cout << "Mid window values: " << windowMidX << "\t" << windowMidY << std::endl;
	std::cout << "Mouse values     : " << mouseX << "\t" << mouseY << std::endl;
	std::cout << horizMovement << "\t" << vertMovement << std::endl << std::endl;
 
	// Apply the mouse movement to our rotation vector. The vertical (look up and down)
	// movement is applied on the X axis, and the horizontal (look left and right)
	// movement is applied on the Y Axis
	rotation.addX(vertMovement);
	rotation.addY(horizMovement);
 
	// Limit loking up to vertically up
	if (rotation.getX() < -90)
	{
		rotation.setX(-90);
	}
 
	// Limit looking down to vertically down
	if (rotation.getX() > 90)
	{
		rotation.setX(90);
	}
 
	// If you prefer to keep the angles in the range -180 to +180 use this code
	// and comment out the 0 to 360 code below.
	//
	// Looking left and right. Keep the angles in the range -180.0f (anticlockwise turn looking behind) to 180.0f (clockwise turn looking behind)
	/*if (yRot < -180.0f)
	{
	    yRot += 360.0f;
	}
 
	if (yRot > 180.0f)
	{
	    yRot -= 360.0f;
	}*/
 
	// Looking left and right - keep angles in the range 0.0 to 360.0
	// 0 degrees is looking directly down the negative Z axis "North", 90 degrees is "East", 180 degrees is "South", 270 degrees is "West"
	// We can also do this so that our 360 degrees goes -180 through +180 and it works the same, but it's probably best to keep our
	// range to 0 through 360 instead of -180 through +180.
	if (rotation.getY() < 0)
	{
		rotation.addY(360);
	}
	if (rotation.getY() > 360)
	{
		rotation.addY(-360);
	}
 
	// Reset the mouse position to the centre of the window each frame
	glfwSetMousePos(windowMidX, windowMidY);
}
 
// Function to calculate which direction we need to move the camera and by what amount
void Camera::move(double deltaTime)
{
	// Vector to break up our movement into components along the X, Y and Z axis
	Vec3<double> movement;
 
	// Get the sine and cosine of our X and Y axis rotation
	double sinXRot = sin( toRads( rotation.getX() ) );
	double cosXRot = cos( toRads( rotation.getX() ) );
 
	double sinYRot = sin( toRads( rotation.getY() ) );
	double cosYRot = cos( toRads( rotation.getY() ) );
 
	double pitchLimitFactor = cosXRot; // This cancels out moving on the Z axis when we're looking up or down
 
	if (holdingForward)
	{
		movement.addX(sinYRot * pitchLimitFactor);
		movement.addY(-sinXRot);
		movement.addZ(-cosYRot * pitchLimitFactor);
	}
 
	if (holdingBackward)
	{
		movement.addX(-sinYRot * pitchLimitFactor);
		movement.addY(sinXRot);
		movement.addZ(cosYRot * pitchLimitFactor);
	}
 
	if (holdingLeftStrafe)
	{
		movement.addX(-cosYRot);
		movement.addZ(-sinYRot);
	}
 
	if (holdingRightStrafe)
	{
		movement.addX(cosYRot);
		movement.addZ(sinYRot);
	}
 
	// Normalise our movement vector
	movement.normalise();
 
	// Calculate our value to keep the movement the same speed regardless of the framerate...
	double framerateIndependentFactor = movementSpeedFactor * deltaTime;
 
	// .. and then apply it to our movement vector.
	movement *= framerateIndependentFactor;
 
	// Finally, apply the movement to our position
	position += movement;
}

Rather than me explaining each individual piece of how to fit it together, here’s a worked example – it’s really quite easy to use:

#include <iostream>
#include <string>
 
#include <GL/glfw.h>      // Include OpenGL Framework library
 
#include "Camera.h"       // Include our Camera header so we can work with Camera objects
#include "FpsManager.hpp" // Include our FpsManager class
#include "Vec3.hpp"       // Include our Vec3 class
 
// Specify default namespace for commonly used elements
using std::string;
using std::cout;
using std::endl;
 
// Define a few constants for error conditions
const int GLFW_INIT_ERROR   = -1;
const int GLFW_WINDOW_ERROR = -2;
 
// Define a pointer to our camera object
Camera *cam;
 
// Define our window title to append the FPS stats to
string windowTitle = "FPS Controls Refactored | r3dux | Dec 2012";
 
// Create a FPS manager that locks to 60fps and updates the window title with stats every 3 seconds
FpsManager fpsManager(60.0, 3.0, windowTitle);
 
GLint windowWidth   = 800;              // Width of our window
GLint windowHeight  = 600;              // Heightof our window
 
GLint midWindowX    = windowWidth  / 2; // Middle of the window horizontally
GLint midWindowY    = windowHeight / 2; // Middle of the window vertically
 
GLfloat fieldOfView = 45.0f;            // Define our field of view (i.e. how quickly foreshortening occurs)
GLfloat near        = 2.0f;             // The near (Z Axis) point of our viewing frustum (default 2.0f)
GLfloat far         = 1500.0f;          // The far  (Z Axis) point of our viewing frustum (default 1500.0f)
 
// Callback function to handle keypresses
void handleKeypress(int theKey, int theAction)
{
	// If a key is pressed, toggle the relevant key-press flag
	if (theAction == GLFW_PRESS)
	{
		switch (theKey)
		{
		case 'W':
			cam->holdingForward = true;
			break;
		case 'S':
			cam->holdingBackward = true;
			break;
		case 'A':
			cam->holdingLeftStrafe = true;
			break;
		case 'D':
			cam->holdingRightStrafe = true;
			break;
		case '[':
			fpsManager.setTargetFps(fpsManager.getTargetFps() - 10);
			break;
		case ']':
			fpsManager.setTargetFps(fpsManager.getTargetFps() + 10);
			break;
		default:
			// Do nothing...
			break;
		}
	}
	else // If a key is released, toggle the relevant key-release flag
	{
		switch (theKey)
		{
		case 'W':
			cam->holdingForward = false;
			break;
		case 'S':
			cam->holdingBackward = false;
			break;
		case 'A':
			cam->holdingLeftStrafe = false;
			break;
		case 'D':
			cam->holdingRightStrafe = false;
			break;
		default:
			// Do nothing...
			break;
		}
	}
}
 
// Callback function to handle mouse movements
void handleMouseMove(int mouseX, int mouseY)
{
	cam->handleMouseMove(mouseX, mouseY);
}
 
void initGL()
{
	// ----- GLFW Settings -----
 
	glfwDisable(GLFW_MOUSE_CURSOR); // Hide the mouse cursor
 
	glfwSwapInterval(0);            // Disable vsync
 
	// ----- Window and Projection Settings -----
 
	// Set the window title
	glfwSetWindowTitle("Solar System FPS Controls Mk2| r3dux.org | Dec 2012");
 
	// Setup our viewport to be the entire size of the window
	glViewport(0, 0, (GLsizei)windowWidth, (GLsizei)windowHeight);
 
	// Change to the projection matrix, reset the matrix and set up our projection
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
 
	// The following code is a fancy bit of math that is eqivilant to calling:
	// gluPerspective(fieldOfView / 2.0f, width / height, near, far);
	// We do it this way simply to avoid requiring glu.h
	GLfloat aspectRatio = (windowWidth > windowHeight)? float(windowWidth)/float(windowHeight) : float(windowHeight)/float(windowWidth);
	GLfloat fH = tan( float(fieldOfView / 360.0f * 3.14159f) ) * near;
	GLfloat fW = fH * aspectRatio;
	glFrustum(-fW, fW, -fH, fH, near, far);
 
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
 
	// ----- OpenGL settings -----
 
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);              // Set out clear colour to black, full alpha
	glEnable(GL_DEPTH_TEST);                           // Enable the depth buffer
	glClearDepth(1.0f);                                // Clear the entire depth of the depth buffer
	glDepthFunc(GL_LEQUAL);		                       // Set our depth function to overwrite if new value less than or equal to current value
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Ask for nicest perspective correction
	glLineWidth(2.0f);			                       // Set a 'chunky' line width
}
 
// Function to draw a grid of lines
void drawGround(float groundLevel)
{
	GLfloat extent      = 600.0f; // How far on the Z-Axis and X-Axis the ground extends
	GLfloat stepSize    = 20.0f;  // The size of the separation between points
 
	// Set colour to white
	glColor3ub(255, 255, 255);
 
	// Draw our ground grid
	glBegin(GL_LINES);
	for (GLint loop = -extent; loop < extent; loop += stepSize)
	{
		// Draw lines along Z-Axis
		glVertex3f(loop, groundLevel,  extent);
		glVertex3f(loop, groundLevel, -extent);
 
		// Draw lines across X-Axis
		glVertex3f(-extent, groundLevel, loop);
		glVertex3f( extent, groundLevel, loop);
	}
	glEnd();
}
 
// Function to draw our scene
void drawScene()
{
	// Clear the screen and depth buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
	// Reset the matrix
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
 
	// Move the camera to our location in space
	glRotatef(cam->getXRot(), 1.0f, 0.0f, 0.0f); // Rotate our camera on the x-axis (looking up and down)
	glRotatef(cam->getYRot(), 0.0f, 1.0f, 0.0f); // Rotate our camera on the  y-axis (looking left and right)
 
	// Translate the ModelView matrix to the position of our camera - everything should now be drawn relative
	// to this position!
	glTranslatef( -cam->getXPos(), -cam->getYPos(), -cam->getZPos() );
 
	drawGround(-100.0f); // Draw lower ground grid
	drawGround(100.0f);  // Draw upper ground grid
 
	// ----- Stop Drawing Stuff! ------
 
	glfwSwapBuffers(); // Swap the buffers to display the scene (so we don't have to watch it being drawn!)
}
 
// Fire it up...
int main(int argc, char **argv)
{
	cout << "Controls: Use WSAD and the mouse to move around!" << endl;
 
	// Frame counter and window settings variables
	int redBits    = 8, greenBits = 8,    blueBits    = 8;
	int alphaBits  = 8, depthBits = 24,   stencilBits = 0;
 
	// Flag to keep our main loop running
	bool running = true;
 
	// ----- Intialiase GLFW -----
 
	// Initialise GLFW
	if (!glfwInit() )
	{
		std::cout << "Failed to initialise GLFW!" << endl;
		glfwTerminate();
		return GLFW_INIT_ERROR;
	}
 
	// Create a window
	if( !glfwOpenWindow(windowWidth, windowHeight, redBits, greenBits, blueBits, alphaBits, depthBits, stencilBits, GLFW_WINDOW))
	{
		std::cout << "Failed to open window!" << std::endl;
		glfwTerminate();
		return GLFW_WINDOW_ERROR;
	}
 
	// Call our initGL function to set up our OpenGL options
	initGL();
 
	// Instantiate our pointer to a Camera object providing it the size of the window
	cam = new Camera(windowWidth, windowHeight);
 
	// Set the mouse cursor to the centre of our window
	glfwSetMousePos(midWindowX, midWindowY);
 
	// Specify the function which should execute when a key is pressed or released
	glfwSetKeyCallback(handleKeypress);
 
	// Specify the function which should execute when the mouse is moved
	glfwSetMousePosCallback(handleMouseMove);
 
	// The deltaTime variable keeps track of how much time has elapsed between one frame and the next.
	// This allows us to perform framerate independent movement i.e. the camera will move at the same
	// overall speed regardless of whether the app's running at (for example) 6fps, 60fps or 600fps!
	double deltaTime = 0.0;
 
	while (running)
	{
		// Calculate our camera movement
		cam->move(deltaTime);
 
		// Draw our scene
		drawScene();
 
		// exit if ESC was pressed or window was closed
		running = !glfwGetKey(GLFW_KEY_ESC) && glfwGetWindowParam(GLFW_OPENED);
 
		// Call our fpsManager to limit the FPS and get the frame duration to pass to the cam->move method
		deltaTime = fpsManager.enforceFPS();
	}
 
	// Clean up GLFW and exit
	glfwTerminate();
 
	delete cam; // Delete our pointer to the camera object
 
	return 0;
}

Finally! Done! You can see a video of the first version of the FPS controls here – this code works identically, it’s just that the Camera is now in its own class, we’re using our own little Vec3 class to keep group and manipulate some values, and the whole thing works in a framerate independent manner thanks to the FpsManager class. Phew!

Cheers!

10 thoughts on “A C++ Camera Class for Simple OpenGL FPS Controls”

  1. Hello,
    I’ve tried converting your code to GLFW3, and I was able to make the mouse move, however, the WASD movement doesn’t work. I have the changes if you want I can e-mail them to you.

    1. I converted a project using the Camera class to GLFW3 the other day and ended up using this code:

      Somewhere in your setup code:

      glfwSetKeyCallback(window, handleKeypress); // Register keyboard handler function

      Somewhere in the main.cpp file:

      // Function to pass-through any keypresses to our Camera class
      static 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);
          }
      }

      Finally, somewhere in the Camera class:

      void Camera::handleKeypress(GLint key, GLint action)
      {
      	// If a key is pressed, toggle the relevant key-press flag
      	if (action == GLFW_PRESS || action == GLFW_REPEAT)
      	{
      		//cout << "Got keypress: " << key << "\t" << action << endl;
       
      		switch (key)
      		{
      			case 87: // 'w'
      				//std::cout << "forward key down!" << std::endl;
      				holdingForward = true;
      				break;
       
      			case 83: // 's'
      				//std::cout << "backward key down!" << std::endl;
      				holdingBackward = true;
      				break;
       
      			case 65: // 'a'
      				//std::cout << "left strafe key down!" << std::endl;
      				holdingLeftStrafe = true;
      				break;
       
      			case 68: // 'd'
      				//std::cout << "right strafe key down!" << std::endl;
      				holdingRightStrafe = true;
      				break;
       
      			default:
      				// Do nothing...
      				break;
      		}
      	}
      	else // If a key is released, toggle the relevant key-release flag
      	{
      		switch (key)
      		{
      			case 87: // 'w'
      				//std::cout << "forward key up!" << std::endl;
      				holdingForward = false;
      				break;
       
      			case 83: // 's'
      				holdingBackward = false;
      				break;
       
      			case 65: // 'a'
      				holdingLeftStrafe = false;
      				break;
       
      			case 68: // 'd'
      				holdingRightStrafe = false;
      				break;
       
      			default:
      				// Do nothing...
      				break;
       
      		} // End of switch block
       
      	} // End of else GLFW_RELEASE block
      }
  2. I realize this is an old post but I figured maybe someone will see this and get help from reading, but why are you handling the input in the Camera class. That totally gets in the way of almost anything you will need to do in the program later on. Make a seperate Input class and feed those values into the Camera class but handling it via the Camera class is absurd.

Leave a Reply

Your email address will not be published.

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