OpenGL learning from 0 - FPS camera

This paper mainly solves one problem:

How to create an FPS camera?

1. Introduction

In the previous chapter, we only discussed how to move the matrix back a little. In this chapter, we want to create a camera similar to FPS, which can move, turn around and zoom (open big mirror effect in sniper gun).

In this chapter, you will see

  • Observe the internal principle of spatial transformation
  • Method of keyboard controlling camera to move back and forth, left and right
  • Method of mouse controlling camera to rotate up, down, left and right
  • How to achieve zoom
  • Encapsulate the camera functions into classes (damn it, it's been a long time since we have so creatively encapsulated a class, and the coder has been amused for too long.)

2. Observation (camera) space

As mentioned in the previous chapter, the observation space is actually a coordinate system with the camera as the origin and the viewing direction of the camera as the - z axis. The function of observation matrix is to convert the objects in the scene from world coordinates to observation coordinates. To define a camera system, we need its position in world space, its orientation, and an upward vector.

2.1 camera position

Camera position is a simple vector that represents its position in world space. We set it in the same position as in the previous chapter.

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 4.0f);

Don't forget that OpenGL is a right-hand coordinate system, and the camera looks towards the - z axis

2.2 light direction

As the opposite direction of orientation, I call it the direction of light (the direction in which the light reflected by the object enters the observer's eye). The calculation method is very simple. Just subtract the camera position vector and the observation target point vector. We use the world coordinate origin (the default point) as our observation target point.

glm::vec3 cameraTarget  glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

2.3 Right axis

The next vector we need is the Right vector, which represents the positive direction of the x axis in the coordinate system. To calculate this Right vector, we need to use a little skill we learned before: vector cross multiplication. The Right vector must be perpendicular to the light direction, so it must be perpendicular to the plane composed of the light direction and the y axis of the world coordinate system. This helps us a lot. According to the cross multiplication rule, we only need to cross multiply the unit vector of the y-axis and the light direction vector.

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight  = glm::normalize(glm::cross(up, cameraDirection));

2.4 Up axis

Now, we have the x-axis and the z-axis, and the y-axis is ready to come out. Yes, just cross multiply the z-axis vector by the x-axis vector!

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

Cross riding is a good thing!

OK, now that you have all three axes of the coordinate system, start generating the observation matrix immediately.

3. Observation matrix

When you add the coordinates of the three axes of the matrix, you can create another one. Multiplying this matrix by any vector can transform this vector into the observation coordinate system. With these conditions, we can summon the Dragon:

R represents the Right vector, U represents the Up vector, D represents the light direction, and P represents the position vector. Note that the position vector takes its opposite direction, because the object needs to move in the opposite direction of the camera.

Summarize the data we need: the position of the camera, the observation target of the camera (which can generate the direction of light), and the Up vector in world space. Using these data, we can generate any observation matrix through calculation. Fortunately, glm has encapsulated a function for us. By calling it, we can get the observation matrix directly (and don't worry about making mistakes!).

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 4.0f),
                              glm::vec3(0.0f, 0.0f, 0.0f),
                              glm::vec3(0.0f, 1.0f, 0.0f));

Verify the effect of the function. We put the position of the camera on a circle with a radius of 10, so that its observation point is always at the origin of world space, and the camera will continue to move on the circle.

Reference source code:

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

Isn't it great?

Reference source code:

4. Mobile camera

It's fun to let the camera circle in the scene, but what's more interesting is that we control the movement of the camera ourselves. The first step is to create a camera system, which requires us to define some variables about the camera at the beginning of the program.

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 4.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

The observation matrix will look like this:

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

We want the orientation of the camera to remain unchanged rather than the observation target, so the observation point becomes cameraPos+cameraFront. Now, we have to use the keyboard to operate the mobile!

Add some code at the end of the processInput function we defined earlier:

float cameraSpeed = 0.05f; //Moving speed
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
    cameraPos += cameraSpeed * cameraFront;

if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
    cameraPos -= cameraSpeed * cameraFront;

if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
    cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);

if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
    cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);

In this way, we can use the WASD key to control the front, back, left and right movement.

Wait, is there something else? By the way, time! This code is purely controlled based on the keys and the running speed of the code. If the machine is not good, the code will run slowly and the moving speed will slow down, which is not very scientific. Therefore, we introduce time to calculate the moving distance.

First, two global variables are defined to save the drawing time of the previous frame and the interval between two frames.

float deltaTime = 0.0f;  //Interval between two frames
float lastFrame = 0.0f;  //Time of last frame drawing

These two values are then updated for each frame:

float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

Finally, use this value in processInput

float cameraSpeed = 2.5f * deltaTime; //Moving speed

Compile and run.

It moves very fast in the left and right directions. The author also tried to reduce the value of 2.5f, but after trying, even if 2.5 is adjusted to 0.01, it moves very fast in the left and right directions, and it is too slow in the front and rear directions.

Reference source code:

5. Look around

Just using WASD to control the movement is not a complete FPS camera, we have to be able to turn around!

To realize the function of turning head, we need to change the cameraFront vector. However, the change of direction vector is more complex and involves some knowledge of trigonometry. If you don't understand trigonometry, it's OK to skip the following paragraph, go directly to the code, and come back when you want to understand the principle.

Euler angle
Euler angle is a value that rotates around three axes (the name Euler should be familiar). There are three Euler angles: pitch, yaw and roll. (avoid ambiguity and use English directly.)

pitch indicates that we usually look up and down, yaw indicates that we look left and right, and roll indicates that, uh, erha rolling is this effect, which is not suitable for us. When each Euler angle is combined, we can represent any rotation.

As an FPS camera, we only need two kinds of rotation: pitch and yaw. Set the direction vector to a new value through triangulation.

The figure above shows the calculation method of pitch rotation. Our initial direction is (0, 0, - 1). When we want to rotate the pitch angle, the z coordinate is equal to - cos(pitch) and the y coordinate is equal to sin(pitch), because we assume that the bevel length is 1 and only consider its direction.

Similarly, the same is true for the calculation of yaw. The z coordinate is equal to - cos(yaw) and the x coordinate is equal to - sin(yaw).

Integrate the two rotations:
x = -sin(yaw)*cos(pitch)
y = sin(pitch)
z = -cos(pitch) * cos(yaw)

6. Mouse input

The values of pitch and yaw are obtained by moving the mouse. The horizontal movement represents the value of yaw, and the vertical movement represents the value of pitch. We need to save the last mouse position, so that we can calculate the rotation angle by calculating the difference between the last mouse position and this mouse position. But first, we need to hide the mouse cursor and capture the mouse message.

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);  
glfwSetCursorPosCallback(window, mouse_callback);  

mouse_callback is a callback function that responds to mouse messages. The prototype is as follows:

void mouse_callback(GLFWwindow* window, double xpos, double ypos);

Window represents the captured window, xpos represents the x coordinate and ypos represents the y coordinate.

In order to calculate a direction vector, we need to do several things:

  • Calculates the position offset of the mouse relative to the previous.
  • Add the offset value to the yaw and pitch values of the camera.
  • Add some rotation restrictions
  • Calculate direction vector
    Look at the code first
if (firstMouse) {  //Set the initial position to prevent sudden jumping in a certain direction
    lastX = xPos;
    lastY = yPos;
    firstMouse = false;

float xoffset = lastX - xPos;   //Don't forget that in the window, the coordinates on the left are smaller than those on the right, and we need a positive angle
float yoffset = lastY - yPos;   //Similarly, in the window, the lower coordinate is greater than the upper coordinate, and we need a positive angle when we look up
lastX = xPos;
lastY = yPos;

float sensitivity = 0.05f;  //Rotation accuracy
xoffset *= sensitivity;
yoffset *= sensitivity;

yaw += xoffset;
pitch += yoffset;

if (pitch > 89.0f)  //You can't look up more than 90 degrees
    pitch = 89.0f;
if (pitch < -89.0f)  //You can't look down more than 90 degrees
    pitch = -89.0f;

glm::vec3 front;
front.x = -sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw));
cameraFront = glm::normalize(front);

In order to prevent jumping to a certain direction suddenly, we set its position at the beginning of the mouse.
Next, calculate the offset from the last position, and then multiply it by the rotation accuracy to obtain the rotation angle value.
Then, the rotation angle is accumulated into the pitch and yaw values, and the maximum and minimum values of pitch are set.
Finally, according to the formula we pushed down above, the direction vector is calculated and normalized.

Write this code to mouse_ In the callback function, compile and run!

Reference source code:

7. Zoom

The zoom function is the magnifying lens of the sniper gun. By changing the field of view value to achieve the effect, reducing the fov value, we can see a finer picture in the distance. Increasing the fov value, we can see a wider picture. Of course, we also lose the advantage of accuracy.

So how do we get the change value of fov? The answer is through the mouse wheel message to simulate!

//Mouse wheel message callback
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    if (fov >= 1.0 && fov <= 45.0)
        fov -= yoffset;
    if (fov <= 1.0)
        fov = 1.0;
    if (fov >= 45.0)
        fov = 45.0;

When the roller moves forward, yoffset is positive, which makes the fov value smaller, and the object larger and finer. On the contrary, when the roller moves backward, yoffset is negative, which makes the fov value larger, the object smaller and the field of vision wider.

Of course, it is essential to register this roller callback function before.

glfwSetScrollCallback(window, scroll_callback); 

So our projection matrix becomes:

projection = glm::perspective(glm::radians((float)fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);

It's simple! Compile and run, and you can zoom through the scroll wheel.

Reference source code:

8. Encapsulation

In the following examples, we will often use this camera to observe the display effect, so it is smart to encapsulate it into classes. Limited to space, I will not list the detailed code, but the source code will be given later. Interested children's shoes can see the internal implementation by themselves.

It is a good habit to check whether the class is available. The source code of the camera class is here and the source code of the main file is here.

The class we now encapsulate can meet most requirements, but it is not without defects. An important problem is universal joint deadlock. To solve this problem, we can use the quaternion method later. Now let's sell it first.

Reference source code:

9. Summary

In this chapter, we learned the internal principle of observation matrix and realized a simple FPS camera through some trigonometry knowledge. The results are remarkable! The next article will summarize and sort out the contents learned so far. After all, there is not much knowledge but mastery.

Source code of this chapter:

Author: Lightning blue Panda
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

Tags: OpenGL

Posted by the_NEWBIE_ON_THE_BLOCK on Mon, 11 Apr 2022 00:38:30 +0300