Support me!
If you enjoy these webpages and you want to show your gratitude, feel free to support me in anyway!
Like Me On Facebook! Megabyte Softworks Facebook
Like Me On Facebook! Megabyte Softworks Patreon
Donate $1
Donate $2
Donate $5
Donate $10
Donate Custom Amount
005.) Camera Pt.1 - Simple Walking Camera

... and welcome to the 5th tutorial of my OpenGL4 series! In this one, we will finally start interacting with the scene and we will be able to move around with a very simple camera! We will also discuss feature called vertical synchronization and how can you turn it on/off .

Simple Walking Camera

The camera that we will implement in this tutorial is going to be very simple. Basically, you can walk forward and backward and you can turn to left and right. That's it! It's not super cool and does not give you the greatest degree of freedom, but hey, it's just a start! What we have to do, is define the appropriate class, that holds the properties of our camera. This class is called SimpleWalkingCamera and it looks like this:

#pragma once

#include <functional>

#include <glm/glm.hpp>

class SimpleWalkingCamera
{
public:
   SimpleWalkingCamera(const glm::vec3& position, const glm::vec3& viewPoint, const glm::vec3& upVector, float moveSpeed = 10.0f, float rotationSpeed = 135.0f);

   void setMoveSpeed(float moveSpeed);
   void setRotationSpeed(float degreesPerSecond);
   void setControls(int forwardKeyCode, int backwardKeyCode, int rotateLeftKeyCode, int rotateRightKeyCode);

   glm::mat4 getViewMatrix() const;
   void update(std::function<bool(int)> keyInputFunc, std::function<float(float)> speedCorrectionFunc);

private:
   void moveBy(float distance);
   void rotateBy(float angleInDegrees);
   glm::vec3 getNormalizedViewVector() const;

   glm::vec3 _position;
   glm::vec3 _viewPoint;
   glm::vec3 _upVector;

   float _moveSpeed;
   float _rotationSpeed;
   int _forwardKeyCode;
   int _backwardKeyCode;
   int _rotateLeftKeyCode;
   int _rotateRightKeyCode;
};

As you can see, this class provides a convenient way of controlling and using camera. Let's examine the functions here:

  • SimpleWalkingCamera(const glm::vec3& position, const glm::vec3& viewPoint, const glm::vec3& upVector, float moveSpeed = 10.0f, float rotationSpeed = 135.0f)
    With this method, we can construct our camera - where it is located, where does it look to, what's the up vector and how fast should it move / rotate.
  • void setMoveSpeed(float moveSpeed)
    You can change the movement speed of the camera with this one.
  • void setRotationSpeed(float degreesPerSecond)
    Same goes for the speed of rotation (how many degrees per second do you want to rotate).
  • void setControls(int forwardKeyCode, int backwardKeyCode, int rotateLeftKeyCode, int rotateRightKeyCode)
    Sets the keys to control camera with (by default, it's classic WSAD keys).
  • glm::mat4 getViewMatrix()
    Gets the view matrix of the camera depending on current position, rotation etc.
  • void update(std::function keyInputFunc, std::function speedCorrectionFunc)
    Handles updating of camera. User of this function has to provide a function, that handles key input and another one, that corrects values depending on time (explained below).
  • void moveBy(float distance)
    Private function, that moves the camera by the given value in the looking direction.
  • void rotateBy(float angleInDegrees)
    Private function, that rotates the camera by the given angle.
  • glm::vec3 getNormalizedViewVector()
    Private function, that gets view vector of unit size (used for moving).

So as you can see from the code above, the camera has several configurable parameters - you can set the moving speed, rotation speed and controls. I will now try to explain most important parts of code.

glm::vec3 getNormalizedViewVector()
glm::vec3 SimpleWalkingCamera::getNormalizedViewVector() const
{
   return glm::normalize(_viewPoint - _position);
}

This one is really simple. We subtract the position from the point we are looking upon. This way, we will get the direction vector, that camera should move to. This is all encapsulated in the function glm::normalize, because in the end, we want to have the correct direction, but the length of the vector should equal 1.0.

void moveBy(float distance)
void SimpleWalkingCamera::moveBy(float distance)
{
   glm::vec3 vOffset = getNormalizedViewVector();
   vOffset *= distance;
   _position += vOffset;
   _viewPoint += vOffset;
}

Now that we can calculate normalized direction vector, moving becomes super simple! All we have to do, is to get this vector and multiply it by the distance we want to walk by. Because this direction vector has been normalized, we will calculate vOffset vector with the exact distance length! All that remains now is to add this calculated offset to both eye and viewpoint position . To go backwards, you just have to set the negative distance and that's it!

void rotateBy(float angleInDegrees)
void SimpleWalkingCamera::rotateBy(float angleInDegrees)
{
   glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), glm::radians(angleInDegrees), glm::vec3(0.0f, 1.0f, 0.0f));
   glm::vec4 rotatedViewVector = rotationMatrix * glm::vec4(getNormalizedViewVector(), 0.0f);
   _viewPoint = _position + glm::vec3(rotatedViewVector.x, rotatedViewVector.y, rotatedViewVector.z);
}

We know how to move forward and backward, but it would be really boring, if we could not rotate to the left and right. That's what this function does. Parameter angleInDegrees tells us, by how much do we want to rotate. We will use glm library again to achieve our goal. First of all, we calculate the rotation matrix. We start with identity matrix (glm::mat4(1.0)) and we want to rotate by angleInDegrees around glm::vec3(0.0f, 1.0f, 0.0f) axis. Because glm works with radians, we have to use function glm::radians to convert from degrees to radians.

Once we have the rotation matrix, all that remains is to multiply it with our normalized view vector. Because it is glm::mat4, we need to add the homogeneous coordinate w. This time, it is set to 0.0, not 1.0, because this is a vector, not a point in space! Now we can perform multiplication and we shall obtain rotate view vector! The last step is to set the view vector relative to the camera position by adding camera position and rotated vector. Now, our viewpoint and eye are both in correct positions and distance between them is 1.0, because rotation does not change the length of vector.

void update(std::function<bool(int)> keyInputFunc, std::function<float(float)> speedCorrectionFunc)
void SimpleWalkingCamera::update(std::function<bool(int)> keyInputFunc, std::function<float(float)> speedCorrectionFunc)
{
   if (keyInputFunc(_forwardKeyCode)) {
      moveBy(speedCorrectionFunc(_moveSpeed));
   }

   if (keyInputFunc(_backwardKeyCode)) {
      moveBy(-speedCorrectionFunc(_moveSpeed));
   }

   if (keyInputFunc(_rotateLeftKeyCode)) {
      rotateBy(speedCorrectionFunc(_rotationSpeed));
   }

   if (keyInputFunc(_rotateRightKeyCode)) {
      rotateBy(-speedCorrectionFunc(_rotationSpeed));
   }
}

The signature of this function looks really strange to be honest . But the point is, that this function requires two functions as input - one, that handles keyboard input (we have one in OpenGLWindow class - keyPressed) and one that corrects the floating point values depending on time passed (we also have one in OpenGLWindow class - sof). The reason for this complication here is, that I did not want the camera class to be dependent on any other class, so that you can possibly use it in your own projects, that's why .

The rest of the function is pretty self-explanatory - we are checking, if the user has pressed the keys, that are set to control camera with and we react accordingly by either moving forward / backward or rotating left / right.

glm::mat4 SimpleWalkingCamera::getViewMatrix()
glm::mat4 SimpleWalkingCamera::getViewMatrix() const
{
   return glm::lookAt(_position, _viewPoint, _upVector);
}

This is the last important function here - using the position, viewpoint and up vector of camera, we construct the view matrix, that can be set to shader programs. I guess this has been everythiing important here, let's move on to the usage of this class!

Using the camera

Now that the camera has been fully implemented, let's just use it! We create the camera in the main file like this.

SimpleWalkingCamera camera(glm::vec3(0.0f, 8.0f, 20.0f), // position
glm::vec3(0.0f, 8.0f, 19.0f), // viewpoint
glm::vec3(0.0f, 1.0f, 0.0f)); // up vector

Camera's Y position is 8.0f - a bit above ground level, which has Y coordinate 0.0. You might see, that viewpoint's Z coordinate is 1 unit deeper into the screen, so that the camera is looking "into the screen". The second important place of this tutorial is setting the view matrix. It can be found in the renderScene function, just before we render anything (we set the view matrix only once):

mainProgram["matrices.viewMatrix"] = camera.getViewMatrix();

The last important part of code is located in handleInput function, it looks a bit strange on the first sight:

camera.update([this](int keyCode) {return this->keyPressed(keyCode); },
[this](float f) {return this->sof(f); });

This strange looking construct is actually just passing the functions to call as parameters using C++ feature lambda functions! Long story short - we are constructing a new, anonymous function in place (it has no name as you can see). The this keyword between square brackets means, that we need to capture the current class (this) - capture means, that inside the function, we want to access the OpenGLWindow class, that's why we capture this (we are in OpenGLWindow class) and we can call the function keyPressed(keyCode), which handles input, and sof, which adjusts floating value according to time passed between frames .

And that's it! That's all the important stuff regarding usage of camera .

Rendering "houses"

In this tutorial, we're rendering two rows of strange looking houses . This is also a good exercise of matrices usage. Let's analyze the left row of houses shortly:

for (int i = 0; i < 10; i++)
{
    // Lets' predefine some sizes
    auto houseBottomSize = 10.0f;
    auto roofTopSize = 12.0f;

    // First, calculate the basic position of house
    auto modelMatrixHouse = glm::mat4(1.0);
    modelMatrixHouse = glm::translate(modelMatrixHouse, glm::vec3(-40.0f, 0.0f, -125.0f + i * 25.0f));

    // Render bottom cube of the house
    glm::mat4 modelMatrixBottom = glm::translate(modelMatrixHouse, glm::vec3(0.0f, houseBottomSize / 2.0f, 0.0f));
    modelMatrixBottom = glm::rotate(modelMatrixBottom, rotationAngleRad, glm::vec3(0.0f, 1.0f, 0.0f));
    modelMatrixBottom = glm::scale(modelMatrixBottom, glm::vec3(houseBottomSize, houseBottomSize, houseBottomSize));
    mainProgram["matrices.modelMatrix"] = modelMatrixBottom;
    glDrawArrays(GL_TRIANGLES, 4, 36);

    // Render top (roof) of the house
    auto translateTopY = houseBottomSize + roofTopSize / 2.0f - 1.0f;
    glm::mat4 modelMatrixTop = glm::translate(modelMatrixHouse, glm::vec3(0.0f, translateTopY, 0.0f));
    modelMatrixTop = glm::rotate(modelMatrixTop, rotationAngleRad, glm::vec3(0.0f, 1.0f, 0.0f));
    modelMatrixTop = glm::scale(modelMatrixTop, glm::vec3(roofTopSize, roofTopSize, roofTopSize));
    mainProgram["matrices.modelMatrix"] = modelMatrixTop;
    glDrawArrays(GL_TRIANGLES, 40, 12);
}

Firstly, we have predefined some sizes - size of the bottom part of the house, and size of the roof. Roof is slightly larger, because it should reach beyond the house and cover simply a bit more space. Later on, we first calculate the foundation translation matrix, that gets our house to the right position - modelMatrixHouse = glm::translate(modelMatrixHouse, glm::vec3(-40.0f, 0.0f, -125.0f + i * 25.0f)). Now, we can translate up by calling glm::mat4 modelMatrixBottom = glm::translate(modelMatrixHouse, glm::vec3(0.0f, houseBottomSize / 2.0f, 0.0f)) - we are moving by half of the size of the bottom part of the house. This way, house will be lying directly on the ground. After this, we rotate and scale it.

The top part of the house (roof) is our well known pyramid, which is translated along Y axis by houseBottomSize + roofTopSize / 2.0f - 1.0f - this means, we align pyramid, so that it is directly on top of the cube (whole bottom part size plus half of pyramid size), but we move it by minus one, so that the roof stretches a bit under the top of the house .

The right row of houses (more like skyscrapers ) uses the same principle - we have base position and we build upon it to create the three parts of the skyscraper. Try to examine the code by yourself, play around with it and you will get the point .

Vertical Synchronization

There is one last thing implemented in this tutorial - vertical synchronization. It's a really simple concept actually - the thing is, your screen can only display so much images in one second, that means, if we try to render anything more than that, it's lost anyway. Typical computer screen has a refresh rate of 60 Hz (displays 60 images per second), so if your graphics card is really powerful and it can generate let's say 2000 frames per second, only a fraction of them is actually displayed. Using vertical synchronization, you will limit the amount of frames generated, depending on the refresh rate of the screen

The code to turn it on / off is very simple actually:

void OpenGLWindow::setVerticalSynchronization(bool enable)
{
   glfwSwapInterval(enable ? 1 : 0);
   _isVerticalSynchronizationEnabled = enable;
}

As you can see, it's just a matter of calling one glfw function - glfwSwapInterval. If you pass number 1, you turn the synchronization on, and if you pass 0, you turn it off.

Result

The result looks very nice indeed!

I thought, that this tutorial will be a super short one, but in the end, it's actually pretty long . But I hope that you have enjoyed and understood it . In the next tutorial, we will improve our camera by adding the mouse, so that we have a typical FPS (first person shooter) camera!

Download 133 KB (43 downloads)