攝像機(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中攝像機章節的介紹。