Thursday, March 30, 2017

Orbital Aero Model: Controls Input and Camera

Keyboard / Mouse Input


A major change from the OpenGL tutorials is how I'm manipulating the camera. I've generalized the control inputs to some extent so they'll be easier to expand later for tasks beyond the camera.

GLFW captures keyboard and mouse inputs and passes them to callbacks in my application. Since those callbacks have to be to static functions, I created a singleton ControlsInput object that contains the callback functions. The callback is to static functions, but those static functions get the singleton instance of the class so all the relevant variables can be kept within the scope of the class. Confusing? Here's some code...

I initialize the ControlsInput in main:


int _tmain(int argc, _TCHAR* argv[])
{
   .
   ControlsInput *controlsInput = ControlsInput::instance();
   controlsInput->setCallbacks(window, &camera);
   .


The setCallbacks function registers the callbackKeyPress static function:


void ControlsInput::setCallbacks(GLFWwindow* window, Camera *newCamera)
{
.
glfwSetKeyCallback(window, ControlsInput::callbackKeyPress);
.
}


When the static callback function gets called by an event, it gets the singleton instance of the ControlsInput and processes the input using that object:


void ControlsInput::callbackKeyPress(GLFWwindow* window, int keyCode, int scanCode, int action, int mode)
{
ControlsInput *controlsInput = ControlsInput::instance();
.
controlsInput->keysPressed[keyCode] = true;
.
}


Right now, the keyboard and mouse controls are only sent to adjust the camera, but I plan on using ControlsInput to funnel them to a variety of functions. So below you'll see everything call camera.process<Whatever>, but that will be greatly expanded at some point.

And now full source code for reference...

ControlsInput.h

#pragma once

class Camera;
struct GLFWwindow;

class ControlsInput
{
private:
static ControlsInput *singletonInstance;

public:
ControlsInput(void);
~ControlsInput(void);

//ControlsInput(GLFWwindow* window);

Camera *camera;

void setCallbacks(GLFWwindow* window, Camera *camera);

bool mouseInitialized;
float mouseLastX;
float mouseLastY;

bool keysPressed[1024];

static void callbackKeyPress(GLFWwindow* window, int keyCode, int scanCode, int action, int mode);
static void callbackMouseMovement(GLFWwindow* window, double xPosition, double yPosition);
static void callbackMouseScroll(GLFWwindow* window, double xOffset, double yOffset);

bool isKeyPressed(int keyCode);

static ControlsInput *instance();
};


ControlsInput.cpp

#define GLEW_STATIC
#include "..\GLEW\include\glew.h"
#include "..\GLFW\include\glfw3.h"

#include "Camera.h"
#include "ControlsInput.h"

ControlsInput *ControlsInput::singletonInstance = 0;

ControlsInput::ControlsInput(void)
{
return;
}

// Logger Destructor
ControlsInput::~ControlsInput(void)
{
return;
}

void ControlsInput::setCallbacks(GLFWwindow* window, Camera *newCamera)
{
this->mouseInitialized = false;

this->camera = newCamera;

// glfw can't accept callbacks from object functions.
// Pass in a static class function that gets the singleton instance for the object variables.
glfwSetKeyCallback(window, ControlsInput::callbackKeyPress);
glfwSetCursorPosCallback(window, ControlsInput::callbackMouseMovement);
glfwSetScrollCallback(window, ControlsInput::callbackMouseScroll);

return;
}

// Is called whenever a key is pressed/released via GLFW
void ControlsInput::callbackKeyPress(GLFWwindow* window, int keyCode, int scanCode, int action, int mode)
{
ControlsInput *controlsInput = ControlsInput::instance();
if (keyCode == GLFW_KEY_ESCAPE && action == GLFW_PRESS)

glfwSetWindowShouldClose(window, GL_TRUE);
}

if (keyCode >= 0 && keyCode < 1024)
{
if (action == GLFW_PRESS)
{
controlsInput->keysPressed[keyCode] = true;
// if inputs going to camera...
controlsInput->camera->processKeyPress(keyCode, true); // (action == GLFW_PRESS));
}
else if (action == GLFW_RELEASE)
{
controlsInput->keysPressed[keyCode] = false;
// if inputs going to camera...
controlsInput->camera->processKeyPress(keyCode, false); // (action == GLFW_PRESS));
}
}

return;
}

void ControlsInput::callbackMouseMovement(GLFWwindow* window, double xPosition, double yPosition)
{
ControlsInput *controlsInput = ControlsInput::instance();
if (!controlsInput->mouseInitialized)
{
controlsInput->mouseLastX = xPosition;
controlsInput->mouseLastY = yPosition;
controlsInput->mouseInitialized = true;
}

double xOffset = xPosition - controlsInput->mouseLastX;
double yOffset = controlsInput->mouseLastY - yPosition;  // Reversed since y-coordinates go from bottom to left

controlsInput->mouseLastX = xPosition;
controlsInput->mouseLastY = yPosition;

// if inputs going to camera...
controlsInput->camera->processMouseMovement(xOffset, yOffset);

return;
}

void ControlsInput::callbackMouseScroll(GLFWwindow* window, double xOffset, double yOffset)
{
ControlsInput *controlsInput = ControlsInput::instance();

// if inputs going to camera...
controlsInput->camera->processMouseScroll(yOffset);

return;
}

bool ControlsInput::isKeyPressed(int keyCode)
{
if (keyCode >= 0 && keyCode < 1024)
{
return keysPressed[keyCode];
}
else
{
return false;
}
}

ControlsInput*::ControlsInput::instance()
{
if (!singletonInstance)
{
singletonInstance = new ControlsInput;
}
return singletonInstance;
}


Camera


The Camera class includes 3 input functions: processKeyPress, processMouseMovement, and processMouseScroll. Those functions are called by the ControlsInput above, and they set variables that instruct how the camera to change.

The camera is defined at the start of main...


int _tmain(int argc, _TCHAR* argv[])
{
   .
Camera camera(glm::vec3(0.0, 0.0, 6367913 * 5 * Graphics::drawScale));


And then a camera.update is called in the main program loop...


while (isRunning)
{
glfwPollEvents();
camera.update(deltaTime);


The update functions takes the inputs that the callbacks recorded earlier and adjusts the camera.


void Camera::update(double timeStep_seconds)
{
GLfloat velocity = this->movementSpeed * timeStep_seconds;
if (this->isTranslatingForward)
{
this->position += this->front * velocity;
}
.
}


The rest of the camera code is basically straight from the OpenGL tutorial. As with everything else, it will be expanded upon greatly in the future, but it's working for now.

And full source code for reference...

Camera.h

#pragma once

#include <vector>

#include "..\GLEW\include\glew.h"
#include "..\GLM\include\glm.hpp"
#include "..\GLM\include\gtc\matrix_transform.hpp"

// An abstract camera class that processes input and calculates the corresponding Euler Angles, Vectors and Matrices for use in OpenGL.
class Camera
{
public:

// Default camera values.
static const GLfloat defaultYaw;
static const GLfloat defaultPitch;
static const GLfloat defaultSpeed;
static const GLfloat defaultSensitivity;
static const GLfloat defaultZoom;

// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods.
enum class MovementDirection
{
Forward,
Backward,
Left,
Right
};

// Constructor with vectors.
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), GLfloat yaw = defaultYaw, GLfloat pitch = defaultPitch);

// Constructor with scalar values.
Camera(GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch);

// Returns the view matrix calculated using Eular Angles and the LookAt Matrix.
glm::mat4 getViewMatrix();

// Processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems).
void processKeyPress(int keyCode, bool isKeyPressed);

// Processes input received from a mouse input system. Expects the offset value in both the x and y direction.
void processMouseMovement(double xoffset, double yoffset, bool constrainPitch = true);

// Processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis.
void processMouseScroll(double yoffset);

void update(double timeStep_seconds);

private:

// Camera Attributes.
glm::vec3 position;
glm::vec3 front;
glm::vec3 up;
glm::vec3 right;
glm::vec3 worldUp;

// Euler Angles.
GLfloat yaw;
GLfloat pitch;

// Camera options.
GLfloat movementSpeed;
GLfloat mouseSensitivity;
GLfloat zoom;

bool isTranslatingForward;
bool isTranslatingBackward;
bool isTranslatingLeft;
bool isTranslatingRight;

// Calculates the front vector from the Camera's (updated) Euler Angles.
void updateCameraVectors();
};


Camera.cpp

include "Camera.h"

#include "..\GLFW\include\glfw3.h"

const GLfloat Camera::defaultYaw = -90.0f;
const GLfloat Camera::defaultPitch = 0.0f;
const GLfloat Camera::defaultSpeed = 100.0f;
const GLfloat Camera::defaultSensitivity = 0.25f;
const GLfloat Camera::defaultZoom = 45.0f;

Camera::Camera(glm::vec3 position, glm::vec3 up, GLfloat yaw, GLfloat pitch) : 
front(glm::vec3(0.0f, 0.0f, -1.0f)), 
movementSpeed(defaultSpeed), 
mouseSensitivity(defaultSensitivity), 
zoom(defaultZoom)
{
this->isTranslatingForward = false;
this->isTranslatingBackward = false;
this->isTranslatingLeft = false;
this->isTranslatingRight = false;

this->position = position;
this->worldUp = up;
this->yaw = yaw;
this->pitch = pitch;
this->updateCameraVectors();

return;
}

Camera::Camera(GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch) : 
front(glm::vec3(0.0f, 0.0f, -1.0f)), 
movementSpeed(defaultSpeed), 
mouseSensitivity(defaultSensitivity), 
zoom(defaultZoom)
{
this->isTranslatingForward = false;
this->isTranslatingBackward = false;
this->isTranslatingLeft = false;
this->isTranslatingRight = false;

this->position = glm::vec3(posX, posY, posZ);
this->worldUp = glm::vec3(upX, upY, upZ);
this->yaw = yaw;
this->pitch = pitch;
this->updateCameraVectors();

return;
}

glm::mat4 Camera::getViewMatrix()
{
return glm::lookAt(this->position, this->position + this->front, this->up);
}

void Camera::processKeyPress(int keyCode, bool isKeyPressed)
{
switch (keyCode)
{
case GLFW_KEY_W:
this->isTranslatingForward = isKeyPressed;
break;
case GLFW_KEY_S:
this->isTranslatingBackward = isKeyPressed;
break;
case GLFW_KEY_A:
this->isTranslatingLeft = isKeyPressed;
break;
case GLFW_KEY_D:
this->isTranslatingRight = isKeyPressed;
break;
}

return;
}

void Camera::processMouseMovement(double xoffset, double yoffset, bool constrainPitch)
{
xoffset *= this->mouseSensitivity;
yoffset *= this->mouseSensitivity;

this->yaw += (GLfloat)xoffset;
this->pitch += (GLfloat)yoffset;

// Make sure that when pitch is out of bounds, screen doesn't get flipped.
if (constrainPitch)
{
if (this->pitch > 89.0f)
{
this->pitch = 89.0f;
}
if (this->pitch < -89.0f)
{
this->pitch = -89.0f;
}
}

// Update Front, Right and Up Vectors using the updated Euler angles.
this->updateCameraVectors();

return;
}

void Camera::processMouseScroll(double yoffset)
{
if (this->zoom >= 1.0f && this->zoom <= 45.0f)
{
this->zoom -= (GLfloat)yoffset;
}
else if (this->zoom <= 1.0f)
{
this->zoom = 1.0f;
}
else if (this->zoom >= 45.0f)
{
this->zoom = 45.0f;
}

return;
}

void Camera::updateCameraVectors()
{
// Calculate the new Front vector.
glm::vec3 front;
front.x = cos(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
front.y = sin(glm::radians(this->pitch));
front.z = sin(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
this->front = glm::normalize(front);

// Also re-calculate the Right and Up vector.
this->right = glm::normalize(glm::cross(this->front, this->worldUp));  // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
this->up = glm::normalize(glm::cross(this->right, this->front));

return;
}

void Camera::update(double timeStep_seconds)
{
GLfloat velocity = this->movementSpeed * timeStep_seconds;
if (this->isTranslatingForward)
{
this->position += this->front * velocity;
}
if (this->isTranslatingBackward)
{
this->position -= this->front * velocity;
}
if (this->isTranslatingLeft)
{
this->position -= this->right * velocity;
}
if (this->isTranslatingRight)
{
this->position += this->right * velocity;
}

return;
}



I'm publishing the above code under the same license as the original learnopengl.com: "licensed under the public domain depicted by the terms of the CC0 1.0 Universal license as published by Creative Commons, either version 1.0 of the License, or (at your option) any later version." Link to license.




No comments:

Post a Comment