本文同時發(fā)布在我的個人博客上:https://dragon_boy.gitee.io
攝像機
??在上一章節(jié)中我們討論了視圖(view)矩陣的用法,我們用這種方式來移動場景。在OpenGL中并沒有攝像機的概念,但我們可以通過反向移動我們的場景來模擬攝像機。
視圖空間
??view矩陣將世界空間的坐標轉(zhuǎn)化到視圖空間,也就是攝像機觀察到的空間。為了定義一個攝像機,我們需要它在世界空間的位置,它指向的觀察方向,它的右側(cè)方向的向量,它指向上方的向量,且這些向量都是單位向量。

1.攝像機的位置
??攝像機的位置向量是一個在世界空間中由原點指向該攝像機位置的向量:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
2.攝像機的指向
??這個向量代表攝像機所指向的方向,在這里我們讓攝像機指向原點。我們通過攝像機的位置向量減去目標位置的向量來獲取指向攝像機+z軸的方向向量:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normaliza(cameraPos - cameraTarget);
3.右軸向
??我們需要一個攝像機的右方向向量來代表攝像機的+x軸軸向。為了獲得這個向量,我們先定義攝像機的yz平面,+z方向的向量即上面攝像機的指向,我們定義一個世界空間的+y單位向量,這兩個向量構(gòu)成yz平面,將這兩個向量叉乘即獲得攝像機的右軸向向量:
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
4.上軸向
??接下來將指向與右軸向叉乘就簡單的獲取了攝像機的上軸向向量:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
??通過上述的方法我們就獲得了幾個描述視圖空間的向量。通過這些向量我們可以構(gòu)造一個LookAt矩陣來創(chuàng)建一個攝像機。
LookAt
??在這里,view矩陣就是這個LookAt矩陣,我們這樣構(gòu)成:
??R是由軸向,U是上軸向,D是指向,P是位置。添加負號是因為我們通過相反方向的場景移動來實現(xiàn)攝像機的移動。
??當然,通過GLM我們可以很簡單地構(gòu)造lookAt矩陣,只需給出攝像機位置,目標位置,代表世界空間的上軸向向量:
glm::mat4 view = glm::mat4(1.0f);
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));
在場景中四處走動
??首先初始化攝像機:
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);
??構(gòu)建view矩陣:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
??cameraPos+cameraFront保證攝像機永遠指向目標方向。接下來設(shè)置一些鍵盤響應(WASD),來前后左右移動攝像機:
void processInput(GLFW* window)
{
...//之前的設(shè)定
const float cameraSpeed = 0.05f;
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;
}
移動速度
??由于每個人的電腦硬件性能不同,每兩幀畫面渲染間隔時間會有不同,這對實際攝像機的移動速度有很大的影響。我們通過計算幀與幀之間的時間來定義攝像機移動速度,這樣可以消除這種影響:
//定義
float deltaTime = 0.0f; //間隔時間
float lastFrame = 0.0f; //上一幀渲染時間
//計算間隔時間
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 修改攝像機速度
void processInput(GLFWwindow* window)
{
float cameraSpeed = 2.5f * deltaTime;
...
}
??運行程序,我們就會發(fā)現(xiàn)可以前后左右移動攝像機。這里給出原文源碼供參考,以及預期結(jié)果。
在場景中四周環(huán)看
??為了轉(zhuǎn)動攝像頭,我們可以通過捕捉鼠標移動并修改之前設(shè)定的cameraFront來實現(xiàn)。下面講解一下原理。
歐拉角
??歐拉角包含三個描述旋轉(zhuǎn)的值,俯仰角(pitch),偏航角(yaw),自旋角(roll):

??在這個案例中,我們不考慮攝像機的自旋。下面來講俯仰角和偏航角轉(zhuǎn)化為我們的向量表示。
??俯仰角是攝像機繞自身的x軸旋轉(zhuǎn)的角度,攝像機坐落在xz平面,平面圖如下:

??我們們可以得到攝像機指向的三個分量為,y軸為sin(pitch),x、z軸為cos(pitch)。
??偏航角是攝像機繞自身y軸旋轉(zhuǎn)的角度,平面圖(沿y軸觀察)如下:

??我們可以得到攝像機指向的x分量:cos(yaw),z分量:sin(yaw)。
??將這兩個角度的影響結(jié)合起來就是:
glm::vec3 direction(1.0f);
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
??如果yaw角初始為0,攝像機將朝向+z軸,為避免這種情況,我們將yaw角初始設(shè)為-90度。
設(shè)置鼠標響應
??我們設(shè)置鼠標的水平移動影響偏航角,垂直移動影響俯仰角。實現(xiàn)方法是存儲上一幀鼠標的位置,并與當前幀的鼠標位置計算差值,獲取差值的x,y分量,并用分量影響俯仰角和偏航角。
??我們通過GLFW的glfwSetInputMode設(shè)置鼠標輸入:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
??設(shè)置后,光標將一直處于窗口的中心位置。接著設(shè)置回調(diào)函數(shù):
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
??通過下面的方法在渲染循環(huán)中使用回調(diào)函數(shù):
glfwSetCursorPosCallback(window, mouse_callback);
??在回調(diào)函數(shù)中,我們先計算鼠標的偏移,我們將上一幀的初始位置設(shè)置在窗口中心,接著設(shè)置降低偏移的影響(太高的話,攝像頭會轉(zhuǎn)的很快),最后計算俯仰角和偏航角:
float lastX = 400, lastY = 300;
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 由于窗口的原點在左上角,Y軸反向
lastX = xpos;
lastY = ypos;
const float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
??最后,為了避免攝像機翻轉(zhuǎn)(如果攝像機的指向與世界空間代表正上的軸向平行會造成LookAt矩陣反轉(zhuǎn)),我們不能讓pitch角超過89度,也不能小于-89度:
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
??最后設(shè)置方向向量:
glm::vec3 direction;
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(direction);
??接著會有一個問題,如果就這么運行程序會發(fā)現(xiàn)剛開始會有一個鏡頭的快速偏移,這是由于一開始xPos和yPos等于你剛進入程序時的位置,可能非常遠離窗口中心。我們可以設(shè)置一個bool變量來判斷是否是第一次設(shè)置鼠標輸入:
if(firstMouse) //初始設(shè)置
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
??最后的回調(diào)函數(shù)如下:
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if(firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
glm::vec3 direction;
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(direction);
}
添加鏡頭縮放
??結(jié)合之前對于透視投影的學習,我們可以通過鼠標滾輪修改透視角來縮放鏡頭。這里設(shè)置滾輪回調(diào)函數(shù):
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if(fov > 1.0f && fov < 45.0f)
fov -= yoffset;
else if(fov <= 1.0f)
fov = 1.0f;
else if(fov >= 45.0f)
fov = 45.0f;
}
??滾輪的y偏移告訴我們垂直方向偏移了多少,減去這段偏移來設(shè)置透視角,并將透視角設(shè)置在我們設(shè)定的45度以內(nèi),同時不能小于1度。
??接著,將修改的透視角應用到投影矩陣中:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
??別忘了在渲染循環(huán)中使用回調(diào)函數(shù):
glfwSetScrollCallback(window, scroll_callback);
??這里給出原文源碼參考:Code,以及效果的實現(xiàn):結(jié)果。
??當然,為了方便之后的學習,我們像Shader.h一樣將攝像機的相關(guān)代碼封裝起來:Camera.h
??最后,請多多參考原文:https://learnopengl.com/Getting-started/Camera。