摄像机(Camera)
前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景(我们向后移动了一点)。Vulkan本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。
本节我们将会讨论如何在Vulkan中配置一个摄像机,并且将会讨论FPS风格的摄像机,让你能够在3D场景中自由移动。我们也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。
首先贴出摄像机类,后续我们逐一解释其内容:
Camera.h
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class Camera
{
public:
//常规构造
Camera(glm::vec3 position, glm::vec3 target, glm::vec3 worldup);
//欧拉角构造
Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup);
glm::vec3 Position;//摄像机位置
glm::vec3 Forward;//摄像机方向
glm::vec3 Right;//右轴
glm::vec3 Up;//上轴
glm::vec3 WorldUp;//世界上轴
float Pitch;//欧拉_俯仰角
float Yaw;//欧拉_偏航角
float SenceX = 0.001f;//鼠标移动X速率
float SenceY = 0.001f;//鼠标移动Y速率
float speedX = 0.0f;//键盘X移动速率
float speedY = 0.0f;//键盘Y移动速率
float speedZ = 0.0f;//键盘Z移动速率
//获取观察矩阵
glm::mat4 GetViewMatrix();
//鼠标移动
void ProcessMouseMovement(float deltaX, float deltaY);
//更新摄像机位置
void UpdataCameraPosition();
private:
//更新摄像机角度
void UpdataCameraVectors();
};
Camera.cpp
#include "Camera.h"
//常规构造
Camera::Camera(glm::vec3 position, glm::vec3 target, glm::vec3 worldup)
{
Position = position;
WorldUp = worldup;
Forward = glm::normalize(target - position);
Right = glm::normalize(glm::cross(Forward, WorldUp));
Up = glm::normalize(glm::cross(Right, Forward));
}
//欧拉角构造
Camera::Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup)
{
Position = position;
WorldUp = worldup;
Pitch = pitch;
Yaw = yaw;
Forward.x = glm::cos(Pitch)*glm::sin(Yaw);
Forward.y = glm::sin(Pitch);
Forward.z = glm::cos(Pitch)*glm::cos(Yaw);
Right = glm::normalize(glm::cross(Forward, WorldUp));
Up = glm::normalize(glm::cross(Right, Forward));
}
//获取视图观察矩阵
glm::mat4 Camera::GetViewMatrix()
{
return glm::lookAt(Position, Position + Forward, WorldUp);
}
//更新摄像机角度
void Camera::UpdataCameraVectors()
{
Forward.x = glm::cos(Pitch)*glm::sin(Yaw);
Forward.y = glm::sin(Pitch);
Forward.z = glm::cos(Pitch)*glm::cos(Yaw);
Right = glm::normalize(glm::cross(Forward, WorldUp));
Up = glm::normalize(glm::cross(Right, Forward));
}
//鼠标移动
void Camera::ProcessMouseMovement(float deltaX, float deltaY)
{
Pitch += deltaY * SenceX;
Yaw += deltaX * SenceY;
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
UpdataCameraVectors();
}
//更新摄像机位置
void Camera::UpdataCameraPosition()
{
//Position += glm::vec3(speedX, speedY,-speedZ) * 0.3f;
Position += Forward * speedZ * 0.001f + Right * speedX * 0.001f + Up * speedY * 0.001f;
}
一、摄像机/观察空间
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点座标:观察矩阵把所有的世界座标变换为相对于摄像机位置与方向的观察座标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的座标系。
此处Camera类存在两个构造函数:正常构造方式和欧拉角构造方式,并在全局进行定义。
...
const int WIDTH = 800;
const int HEIGHT = 600;
//Camera camera(glm::vec3(-0.5f, -2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
Camera camera(glm::vec3(0.0f, -2.0f, 2.0f), glm::radians(45.0f), glm::radians(180.0f), glm::vec3(0.0f, 1.0f, 0.0f));
...
在updateUniformBuffer函数中改变原有硬编码赋值观察矩阵的方式,采用Camera中的获取方式
void updateUniformBuffer() {
...
//ubo.view = glm::lookAt(glm::vec3(-0.5f, -2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
ubo.view = camera.GetViewMatrix();
...
此时运行程序,将会出现下图所示模型,说明此上修改生效:
二、鼠标移动
偏航角和俯仰角是通过鼠标(或手柄)移动获得的,水平的移动影响偏航角,竖直的移动影响俯仰角。它的原理就是,储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大,也就是摄像机需要移动更多的距离。
首先我们要告诉GLFW,它应该隐藏光标,并捕捉(Capture)它。捕捉光标表示的是,如果焦点在你的程序上(译注:即表示你正在操作这个程序,Windows中拥有焦点的程序标题栏通常是有颜色的那个,而失去焦点的程序标题栏则是灰色的),光标应该停留在窗口中(除非程序失去焦点或者退出)。我们可以用一个简单地配置调用来完成:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
在调用这个函数之后,无论我们怎么去移动鼠标,光标都不会显示了,它也不会离开窗口。对于FPS摄像机系统来说非常完美。
为了计算俯仰角和偏航角,我们需要让GLFW监听鼠标移动事件。(和键盘输入相似)我们会用一个回调函数来完成,函数的原型如下:
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
这里的xpos和ypos代表当前鼠标的位置。当我们用GLFW注册了回调函数之后,鼠标一移动mouse_callback函数就会被调用:
glfwSetCursorPosCallback(window, mouse_callback);
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan_Day28_Camera", nullptr, nullptr);
glfwSetWindowUserPointer(window, this);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);//禁用鼠标悬浮
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetWindowSizeCallback(window, HelloTriangleApplication::onWindowResized);
}
在处理FPS风格摄像机的鼠标输入的时候,我们必须在最终获取方向向量之前做下面这几步:
- 计算鼠标距上一帧的偏移量。
- 把偏移量添加到摄像机的俯仰角和偏航角中。
- 对偏航角和俯仰角进行最大和最小值的限制。
- 计算方向向量。
第一步是计算鼠标自上一帧的偏移量。我们必须先在程序中储存上一帧的鼠标位置,我们把它的初始值设置为屏幕的中心:
//鼠标移动位置记录
float lastX= WIDTH / 2, lastY = HEIGHT / 2;
//缩放视野
float fov = 45.0f;
bool firstMouse = true;
然后在鼠标的回调函数中我们计算当前帧和上一帧鼠标位置的偏移量,最后调用camera.ProcessMouseMovement()方法,实时更新视角。
void mouse_callback(GLFWwindow* window, double xPos, double yPos) {
if (firstMouse){
lastX = xPos;
lastY = yPos;
firstMouse = false;
}
float deltaX, deltaY;
deltaX = xPos - lastX;
deltaY = yPos - lastY;
lastX = xPos;
lastY = yPos;
camera.ProcessMouseMovement(deltaX, deltaY);
std::cout<< deltaX <<";"<<deltaY<<std::endl;
}
现在运行程序,我们就可以随着鼠标移动俯视场景中模型了!
三、键盘移动
我们为GLFW的键盘输入定义一个processInput函数,并在mainLoop中循环监听及调用UpdataCameraPosition方法实时更新相机位置。
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
processInput(window);
updateUniformBuffer();
drawFrame();
camera.UpdataCameraPosition();
}
vkDeviceWaitIdle(device);
}
接下来我们来新添加几个需要检查的按键命令:
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
camera.speedZ = 1.0f;
}
else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
camera.speedZ = -1.0f;
}
else {
camera.speedZ = 0.0f;
}
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
camera.speedX = -1.0f;
}
else if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
camera.speedX = 1.0f;
}
else {
camera.speedX = 0.0f;
}
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
camera.speedY = 1.0f;
}
else if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) {
camera.speedY = -1.0f;
}
else {
camera.speedY = 0.0f;
}
}
当我们按下WASDQE键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个右向量(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的横移(Strafe)效果。
- 注意,我们对右向量进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据cameraFront变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如果进行了标准化移动就是匀速的。
现在你就应该能够移动摄像机了,虽然移动速度和系统有关,你可能会需要调整一下Camera类中位置运算中乘上的参数即可。
四、缩放
作为我们摄像机系统的一个附加内容,我们还会来实现一个缩放(Zoom)接口。在之前的教程中我们说视野(Field of View)或fov定义了我们可以看到场景中多大的范围。当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)了的感觉。我们会使用鼠标的滚轮来放大。与鼠标移动、键盘输入一样,我们需要一个鼠标滚轮的回调函数:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if(fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if(fov <= 1.0f)
fov = 1.0f;
if(fov >= 45.0f)
fov = 45.0f;
}
当滚动鼠标滚轮的时候,yoffset值代表我们竖直滚动的大小。当scroll_callback函数被调用后,我们改变全局变量fov变量的内容。因为45.0f是默认的视野值,我们将会把缩放级别(Zoom Level)限制在0.1f到10.0f。
我们现在在每一帧都必须把透视投影矩阵上传到GPU,但现在使用fov变量作为它的视野:
ubo.proj = glm::perspective(glm::radians(fov), swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 10.0f);
最后不要忘记注册鼠标滚轮的回调函数:
glfwSetScrollCallback(window, scroll_callback);
现在我们就可以自由地在3D场景中旋转、移动以及缩放查看模型了!
关于相机相关知识,不太了解的可以观看LearnOpenGL中摄像机章节的介绍。