OpenGL basic (17) camera

OpenGL itself does not have the concept of camera, but we can simulate the camera by moving all objects in the scene in the opposite direction. It feels like we are moving, not the scene is moving.

1. Establishment of camera LookAt matrix

To define a camera, we need a camera's position in world space, observation direction, a right measurement vector pointing to it and a vector pointing above it. Based on these four information, we can construct a LookAt matrix.

1.1 matrix basic information acquisition

@1. Define camera

Define the position of the camera, a three-dimensional vector (0, 0, 3), as follows:

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

Note here: the z axis is pointing from the screen to you in front of the screen.

@2 camera direction (z-axis)

Camera position vector - scene origin vector = camera pointing vector, but this points to the negative direction of the z-axis, and we want the direction vector to point to the positive direction of the z-axis of the camera. If we change the order of subtraction, we will get a vector pointing to the positive Z axis of the camera. As follows:

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

@3 right axis (x axis)

The right vector represents the positive direction of the x-axis of the camera space. Find the right axis direction of the camera?

Because it is the right axis, it must be perpendicular to the upper axis (0.0f, 1.0f, 0.0f) and the direction of the camera. The idea is to cross multiply the up vector and the camera direction vector obtained in the previous step to get the vector pointing to the positive direction of the x-axis, that is, the right vector. Convert to code as follows:

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

In this way, the right axis is obtained.

@4 upper axis (y-axis)

With the x-axis and z-axis, the y-axis is perpendicular to the x-z plane, so finding the right axis direction only requires the cross multiplication of the x-axis vector and the z-axis vector. The code is as follows:

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

In this way, the upper axis is obtained.

To sum up, we only need a camera position, a target position and a vector in the world space representing the upper vector to get the above four information.

1.2 # GLM builds LookAt matrix

To build a LookAt matrix through GLM, we only need to define a camera position, a target origin position and a vector in world space representing the upper vector (we use the upper vector to calculate the right vector). GLM will create a LookAt matrix, which we can use as our observation matrix:

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

2 LookAt matrix application

2.1 camera rotation

Rotate our camera around the scene. Our gaze remains at (0, 0, 0). We create x and z coordinates in each frame. x and z represent a point on a circle, which we will use as the position of the camera. By traversing the points on all circles, the camera rotates around the scene. That is, a new observation matrix is created for each rendering iteration. The relevant codes are as follows:

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

2.2 free movement (key control)

Define some basic variables of the camera, as follows:

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

Now the LookAt matrix looks like this:

//Here, the target position is adjusted to (cameraPos + cameraFront)
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

We first set the camera position to the previously defined cameraPos. The direction is the current position + direction vector (so that no matter how we move, the camera will look at the target direction). Next, write a program to update the cameraPos vector by pressing the key. We have defined a processInput function for GLFW keyboard input. Next, we add several key commands to check:

void processInput(GLFWwindow *window)
{
    ...
    float cameraSpeed = 0.05f; // adjust accordingly
    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;
}

@1 Key Optimization

The camera system can't move in both directions at the same time. When you press a button, it will pause first before moving. This is because most event input systems can only handle one keyboard input at a time, and their functions are called only when we activate a key. Here we do some optimization to track which key is pressed / released in the callback function. In the rendering loop, we read these values, check which key is activated, and then react accordingly. We only store the state information of which key is pressed / released, and respond to the state in the game cycle. We create a Boolean array to represent the pressed / released key, and then we must set the key in the key_ Set the press / release key to true or false in the callback function:

bool keys[1024];

// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
    if (key >= 0 && key < 1024)
    {
        if (action == GLFW_PRESS)
            keys[key] = true;
        else if (action == GLFW_RELEASE)
            keys[key] = false;
    }
}

We continue to improve the processInput function to update the camera value according to the pressed key:

void processInput()
{
  //...
  // Camera control
  GLfloat cameraSpeed = 0.01f;
  if(keys[GLFW_KEY_W])
    cameraPos += cameraSpeed * cameraFront;
  if(keys[GLFW_KEY_S])
    cameraPos -= cameraSpeed * cameraFront;
  if(keys[GLFW_KEY_A])
    cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
  if(keys[GLFW_KEY_D])
    cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}

@2. Speed optimization

Moving speed is a constant. There is no problem in theory, but in practice, the capabilities of various hardware processors are different. We should try our best to ensure that the rendering effect} is the same on all hardware. We adopt this strategy:

Because the renderer tracks a deltaTime variable, it stores the time taken to render the last frame. Multiply all the speeds by the delta time value. (if our delta time is large, it means that the rendering of the previous frame takes more time, so the speed of this frame needs to be higher to balance the time spent in rendering). When using this method, whether your computer is fast or slow, the speed of the camera will be balanced accordingly, so that the experience of each user is the same.

//Initializes the time of the previous frame and the current frame
float deltaTime = 0.0f; // The time difference between the current frame and the previous frame
float lastFrame = 0.0f; // Time of last frame

//Continuous iteration
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

void processInput(GLFWwindow *window)
{
  //calculation speed
  float cameraSpeed = 2.5f * deltaTime;
  ...
}

After optimization, the effect of free movement is much better.

2.3 mouse input and viewing angle movement

@1 view movement

In order to change the direction, we must change the cameraFront vector according to the input of the mouse. However, changing the direction vector according to the mouse rotation requires Euler angle knowledge (refer to the video: Khan college linear algebra course (free) )In fact, you have a certain understanding of Euler angle, and then through some function operations of GLM library, you can get the direction vectors of pitch angle (pitch, x axis) and yaw angle (yaw, y axis), as shown below:

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

In this way, the pitch angle and yaw angle can be transformed into three-dimensional direction vectors of the camera for free rotation.

@2 mouse input

The mouse movement is connected with the pitch angle and yaw angle: the horizontal movement of the mouse affects the yaw angle, and the vertical movement of the mouse affects the pitch angle. Store the mouse position of the previous frame. In the current frame, we calculate the difference between the current mouse position and the position of the previous frame. The greater the difference, the greater the change in pitch or yaw angle.

@3 code writing and process sorting

@@3.1 GLFW advance operation:

We want to hide the mouse in the window (press esc to exit when leaving the window). The configuration code of the hidden mouse is as follows:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

Create function mouse_ Callback (the glfwSetCursorPosCallback function is called when the mouse moves the mouse_callback function). The code is as follows:

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

Register the callback function. The relevant codes in GLFW are as follows:

glfwSetCursorPosCallback(window, mouse_callback);

@@3.2 processing mouse input

It is summarized as 4 steps (in the code comments), and the relevant codes are as follows:

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if(firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
	
	//@1 calculate the offset of the mouse position between the current frame and the previous frame
    GLfloat xoffset = xpos - lastX;
    GLfloat yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

    GLfloat sensitivity = 0.05;
    xoffset *= sensitivity;
    yoffset *= sensitivity;
	
	//@2 add the offset to the global variables pitch and yaw:
    yaw   += xoffset;
    pitch += yoffset;

	//@3 add some restrictions to the camera (for the pitch angle, the user should not look higher than 89 degrees, because the viewing angle will be reversed at 90 degrees, so we take 89 degrees as the limit)
    if(pitch > 89.0f)
        pitch = 89.0f;
    if(pitch < -89.0f)
        pitch = -89.0f;

	//@4 calculate the pitch angle and yaw angle to obtain the actual direction vector mentioned above
    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}  

2.4 zoom

In the previous chapter, FOV defines how wide we can see in the scene. When the field of vision becomes smaller, the visible area will decrease, resulting in the feeling of magnification. We use the mouse wheel to zoom in. Like mouse movement and keyboard input, we need a callback function of mouse wheel:

//yoffset indicates the size of scrolling, xoffset is not used
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
  //The zoom level is limited to 1.0f to 45.0f, and the default is 45.0f
  if(aspect >= 1.0f && aspect <= 45.0f)
    aspect -= yoffset;
  if(aspect <= 1.0f)
    aspect = 1.0f;
  if(aspect >= 45.0f)
    aspect = 45.0f;
}

Register the callback function. The relevant codes in GLFW are as follows:

glfwSetScrollCallback(window, scroll_callback);

Now we have implemented a simple camera system that allows us to move freely in a 3D environment. We must upload the perspective projection matrix to the GPU at each frame, but this time make the aspect variable as its fov, that is:

projection = glm::perspective(aspect, (GLfloat)WIDTH/(GLfloat)HEIGHT, 0.1f, 100.0f);

2.5 , integrated into camera class

The header file contents of the sorted camera related operations are as follows:

#pragma once
#include <vector>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
enum Camera_Movement {
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

// Default camera values
const GLfloat YAW        = -90.0f;
const GLfloat PITCH      =  0.0f;
const GLfloat SPEED      =  3.0f;
const GLfloat SENSITIVTY =  0.25f;
const GLfloat ZOOM       =  45.0f;

// An abstract camera class that processes input and calculates the corresponding Eular Angles, Vectors and Matrices for use in OpenGL
class Camera
{
public:
    // Camera Attributes
    glm::vec3 Position;
    glm::vec3 Front;
    glm::vec3 Up;
    glm::vec3 Right;
    glm::vec3 WorldUp;
    // Eular Angles
    GLfloat Yaw;
    GLfloat Pitch;
    // Camera options
    GLfloat MovementSpeed;
    GLfloat MouseSensitivity;
    GLfloat Zoom;

    // 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 = YAW, GLfloat pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
    {
        this->Position = position;
        this->WorldUp = up;
        this->Yaw = yaw;
        this->Pitch = pitch;
        this->updateCameraVectors();
    }
    // Constructor with scalar values
    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(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
    {
        this->Position = glm::vec3(posX, posY, posZ);
        this->WorldUp = glm::vec3(upX, upY, upZ);
        this->Yaw = yaw;
        this->Pitch = pitch;
        this->updateCameraVectors();
    }

    // Returns the view matrix calculated using Eular Angles and the LookAt Matrix
    glm::mat4 GetViewMatrix()
    {
        return glm::lookAt(this->Position, this->Position + this->Front, this->Up);
    }

    // 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 ProcessKeyboard(Camera_Movement direction, GLfloat deltaTime)
    {
        GLfloat velocity = this->MovementSpeed * deltaTime;
        if (direction == FORWARD)
            this->Position += this->Front * velocity;
        if (direction == BACKWARD)
            this->Position -= this->Front * velocity;
        if (direction == LEFT)
            this->Position -= this->Right * velocity;
        if (direction == RIGHT)
            this->Position += this->Right * velocity;
    }

    // Processes input received from a mouse input system. Expects the offset value in both the x and y direction.
    void ProcessMouseMovement(GLfloat xoffset, GLfloat yoffset, GLboolean constrainPitch = true)
    {
        xoffset *= this->MouseSensitivity;
        yoffset *= this->MouseSensitivity;

        this->Yaw   += xoffset;
        this->Pitch += 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 Eular angles
        this->updateCameraVectors();
    }

    // Processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis
    void ProcessMouseScroll(GLfloat yoffset)
    {
        if (this->Zoom >= 1.0f && this->Zoom <= 45.0f)
            this->Zoom -= yoffset;
        if (this->Zoom <= 1.0f)
            this->Zoom = 1.0f;
        if (this->Zoom >= 45.0f)
            this->Zoom = 45.0f;
    }

private:
    // Calculates the front vector from the Camera's (updated) Eular Angles
    void 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));
    }
};

Keywords: OpenGL

Added by flforlife on Wed, 19 Jan 2022 23:35:33 +0200