上接:https://blog.csdn.net/weixin_44506615/article/details/150225094?spm=1001.2014.3001.5501
完整代码:https://gitee.com/Duo1J/learn-open-gl
一、摄像机LookAt矩阵与观察空间
摄像机即为我们观察场景的位置与角度,随着我们摄像机的改变,我们的 观察矩阵View Matrix 也会随之改变,接下来我们将要描述相机并计算对应的观察矩阵
为了描述相机,我们定义相机的位置与相机的方向,如下图所示 (图片来自于LearnOpenGL)
这里我们用一个坐标 (0, 0, 2) 来表示相机所处的位置,为相机定义向 前(front / forward)、向上(up) 和 向右(right) 三个轴来描述相机的方向
有了这些信息之后,我们就可以来计算观察矩阵了,在这里我们也称LookAt矩阵,含义为一个看着(Look At)给定目标的观察矩阵,我们可以用这个矩阵来乘以任何向量来将其变换到那个坐标空间,如下图所示 (图片来自于LearnOpenGL)
这里的R表示向右向量,U表示向上向量,D表示向前向量,P则是相机的位置
GLM为我们提供了创建LookAt矩阵的接口
// 观察矩阵 / LookAt矩阵
glm::mat4 view;
// 传入相机位置、目标与向上向量
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
二、自由变换相机
1. 相机位置移动
首先我们来移动相机位置
定义相机的属性,这里我们不用定义cameraRight,我们可以通过cameraFront和cameraUp的 叉乘(cross) 来计算cameraRight
// 相机位置
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);
// 相机速度
float cameraSpeed = 2.5f;
接收键盘输入,改变相机位置
// 获取相机速度
float GetCameraSpeed()
{
return cameraSpeed * deltaTime;
}
void ProcessInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE))
{
glfwSetWindowShouldClose(window, true);
}
// 处理相机的移动
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
{
cameraPos += cameraFront * GetCameraSpeed();
}
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
{
cameraPos -= cameraFront * GetCameraSpeed();
}
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
{
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * GetCameraSpeed();
}
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
{
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * GetCameraSpeed();
}
}
这里我们定义了一个方法来获取相机速度,在获取相机速度的时候我们乘以了一个 deltaTime 值
这是由于用户的计算机性能的差异,会导致在不同设备上的执行频率,即帧数不一样,这会导致我们移动会存在很大的速度差异
因此我们定义一个deltaTime变量,含义为距离上一帧的时间差,它存储了渲染上一帧的所用的时间,如果花费时间少,那么我们这次移动的距离就会更少,反之会变多,这样,无论帧数是多少,我们的相机在每个用户的设备上的表现就一致了
// 当前帧与上一帧的时间差
float deltaTime = 0.0f;
// 上一帧的时间
float lastFrame = 0.0f;
//...
//主循环中
while (!glfwWindowShouldClose(window))
{
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
}
最后,修改我们的观察矩阵
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
编译运行,按下WASD,顺利的话可以左右前后的控制相机 (视频上传不了~,可以在这里看到正确的表现)
2. 相机旋转 (视角移动)
我们使用欧拉角来描述相机的旋转,欧拉角分为 俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll) 如下图所示 (图片来自LearnOpenGL)
我们可以通过简单的三角函数来计算出相机的方向单位向量,如下图所示 (图片来自LearnOpenGL)
对于xy平面,我们可以通过pitch角计算出direction方向向量的
direction.x = cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
接下来转到xz平面
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));
计算方法有了,接下来看我们的代码
首先声明变量,这里加上我们马上要用到的灵敏度
// 相机pitch
float pitch = 0;
// 相机yaw
float yaw = -90;
// 相机roll
float roll = 0;
// 相机旋转灵敏度
float cameraRotateSensitivity = 0.05f;
接着注册鼠标输入回调,并在回调中计算相机的向前向量
void ProcessMouseInput(GLFWwindow* window, double x, double y)
{
static bool firstMouseInput = true;
static float lastX = 0, lastY = 0;
if (firstMouseInput)
{
firstMouseInput = false;
lastX = x;
lastY = y;
}
float xOffset = x - lastX;
// 由于y坐标是从底部往顶部依次增大,所以这里是相反的
float yOffset = lastY - y;
lastX = x;
lastY = y;
xOffset *= cameraRotateSensitivity;
yOffset *= cameraRotateSensitivity;
yaw += xOffset;
pitch += yOffset;
pitch = glm::clamp(pitch, -89.0f, 89.0f);
// 计算相机的向前向量
cameraFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront.y = sin(glm::radians(pitch));
cameraFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(cameraFront);
}
//...
//主循环中
// 隐藏鼠标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 设置鼠标移动回调
glfwSetCursorPosCallback(window, ProcessMouseInput);
编译运行,顺利的话,我们就可以在空间中旋转我们的相机来以不同的角度看向我们的立方体了
3. 相机缩放
接下来我们来添加鼠标滚轮的回调函数,为我们的相机fov添加变化
void ProcessMouseScroll(GLFWwindow* window, double x, double y)
{
fov += -y;
fov = glm::clamp(fov, 1.0f, 90.0f);
}
// 主循环中
// 设置鼠标滚轮回调
glfwSetScrollCallback(window, ProcessMouseScroll);
// 修改投影矩阵
projection = glm::perspective(glm::radians(fov), screenWidth / screenHeight, 0.1f, 100.0f);
编译运行,顺利的话,滚动滚轮,可以看见画面在 “变大变小”
完整代码可在顶部的git仓库找到 (07-01)
三、封装我们的相机类
之后会一直用到相机,我们接下来对相机进行一个简单的封装
直接上代码
Camera.h
#pragma once
#include <GLFW/glfw3.h>
#include <glm.hpp>
#include "Transform.h"
/**
* 摄像机
*/
class Camera
{
public:
/**
* 变换
*/
Transform transform;
/**
* FOV
*/
float fov = 45;
/**
* 移动速度
*/
float moveSpeed = 2.5f;
/**
* 旋转灵敏度
*/
float rotateSensity = 0.05f;
public:
Camera();
Camera(Transform& _transform);
/**
* 获取观察矩阵
*/
glm::mat4 GetViewMatrix();
/**
* 处理键盘输入
*/
void ProcessKeyboardInput(GLFWwindow* window, float deltaTime);
/**
* 处理鼠标输入
*/
void ProcessMouseInput(GLFWwindow* window, double x, double y, float deltaTime);
/**
* 处理滚轮输入
*/
void ProcessMouseWheelInput(GLFWwindow* window, double x, double y, float deltaTime);
/**
* 更新相机向量
*/
void UpdateCameraVector();
};
Camera.cpp
#include "Camera.h"
#include <gtc/matrix_transform.hpp>
Camera::Camera()
{
transform.front = glm::vec3(0, 0, 1);
transform.up = glm::vec3(0, 1, 0);
UpdateCameraVector();
}
Camera::Camera(Transform& _transform)
{
transform = _transform;
UpdateCameraVector();
}
glm::mat4 Camera::GetViewMatrix()
{
return glm::lookAt(transform.position, transform.position + transform.front, transform.up);
}
void Camera::ProcessKeyboardInput(GLFWwindow* window, float deltaTime)
{
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
{
transform.position += transform.front * moveSpeed * deltaTime;
}
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
{
transform.position -= transform.front * moveSpeed * deltaTime;
}
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
{
transform.position -= glm::normalize(glm::cross(transform.front, transform.up)) * moveSpeed * deltaTime;
}
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
{
transform.position += glm::normalize(glm::cross(transform.front, transform.up)) * moveSpeed * deltaTime;
}
}
void Camera::ProcessMouseInput(GLFWwindow* window, double x, double y, float deltaTime)
{
static bool firstMouseInput = true;
static float lastX = 0, lastY = 0;
if (firstMouseInput)
{
firstMouseInput = false;
lastX = x;
lastY = y;
}
float xOffset = x - lastX;
float yOffset = lastY - y;
lastX = x;
lastY = y;
xOffset *= rotateSensity;
yOffset *= rotateSensity;
transform.rotate.yaw += xOffset;
transform.rotate.pitch += yOffset;
UpdateCameraVector();
}
void Camera::ProcessMouseWheelInput(GLFWwindow* window, double x, double y, float deltaTime)
{
fov += -y;
fov = glm::clamp(fov, 1.0f, 90.0f);
}
void Camera::UpdateCameraVector()
{
transform.rotate.pitch = glm::clamp(transform.rotate.pitch, -89.0f, 89.0f);
transform.front.x = cos(glm::radians(transform.rotate.yaw)) * cos(glm::radians(transform.rotate.pitch));
transform.front.y = sin(glm::radians(transform.rotate.pitch));
transform.front.z = sin(glm::radians(transform.rotate.yaw)) * cos(glm::radians(transform.rotate.pitch));
transform.front = glm::normalize(transform.front);
}
Transform.h
#pragma once
#include <glm.hpp>
#include "EulerAngle.h"
/**
* 变换
*/
struct Transform
{
public:
/**
* 位置
*/
glm::vec3 position = glm::vec3(0, 0, 0);
/**
* 向前
*/
glm::vec3 front = glm::vec3(0, 0, 0);
/**
* 向上
*/
glm::vec3 up = glm::vec3(0, 0, 0);
/**
* 向右
*/
glm::vec3 right = glm::vec3(0, 0, 0);
/**
* 旋转欧拉角
*/
EulerAngle rotate;
};
EulerAngle.h
#pragma once
/**
* 欧拉角
*/
struct EulerAngle
{
public:
/**
* Pitch角
*/
float pitch = 0;
/**
* Yaw角
*/
float yaw = 0;
/**
* Roll角
*/
float roll = 0;
EulerAngle() : EulerAngle(0, 0, 0)
{
}
explicit EulerAngle(float _pitch, float _yaw, float _roll)
: pitch(_pitch), yaw(_yaw), roll(_roll)
{
}
};
然后,将 Main.cpp 中的相机相关调用换为Camera对象即可
完整代码见顶部git仓库