... 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 .
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
#include
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(const std::function& keyInputFunc,
const std::function& 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:
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 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 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 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 SimpleWalkingCamera::update(std::function keyInputFunc, std::function 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() 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!
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 .
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 .
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.
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 (1342 downloads)