從0開始的OpenGL學(xué)習(xí)(九)-FPS攝像機(jī)

本文主要解決一個問題:

如何創(chuàng)建一個FPS攝像機(jī)?

引言

在前一章中,我們討論了觀察矩陣以及如何使用變換矩陣移動場景(雖然僅僅是往后移了一點點)。本章中,我們要創(chuàng)建一個類似FPS的攝像機(jī),它可以移動,可以轉(zhuǎn)頭,可以變焦(狙擊槍里開放大鏡效果)。

在這章中,你會看到

  • 觀察空間變換的內(nèi)部原理
  • 鍵盤操縱攝像機(jī)前后左右移動的方法
  • 鼠標(biāo)操縱攝像機(jī)上下左右轉(zhuǎn)動的方法
  • 實現(xiàn)變焦的方式
  • 將攝像機(jī)功能封裝成類(該死,好久沒這么有創(chuàng)造性的封裝一個類了,碼農(nóng)當(dāng)太久腦子都秀逗了。)

觀察(攝像機(jī))空間

就像前一章說的那樣,觀察空間其實是以攝像機(jī)為原點,以攝像機(jī)觀察的方向為-z軸方向的坐標(biāo)系統(tǒng)。而觀察矩陣的作用,就是將場景中的物體從世界坐標(biāo)轉(zhuǎn)換到觀察坐標(biāo)。要定義一個攝像機(jī)系統(tǒng),我們需要它在世界空間中的位置,它的朝向,以及一個向上方向的向量。

觀察坐標(biāo)系統(tǒng)原理
1、相機(jī)位置

相機(jī)位置就是一個簡單的向量,表示其在世界空間中的位置。我們把它設(shè)置成和前一章一樣的位置。

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

別忘了OpenGL是右手坐標(biāo)系,攝像機(jī)是往-z軸方向看的

2、光線方向

作為朝向的反方向,我稱它為光線方向(物體反射光攝入觀察者眼睛的方向)。計算的方式很簡單,將相機(jī)位置向量和觀察目標(biāo)點向量做減法就可以了。我們使用世界坐標(biāo)原點(默認(rèn)點)作為我們的觀察目標(biāo)點。

glm::vec3 cameraTarget  glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
3、Right軸

我們下一個需要的向量是Right向量,它表示坐標(biāo)系統(tǒng)中的x軸正方向。要計算這個Right向量,我們要用到之前學(xué)的一點小技巧:向量叉乘。Right向量必須要垂直于光線方向,因此,它必須要和光線方向與世界坐標(biāo)系統(tǒng)的y軸組成的平面垂直。這就幫了我們的大忙,根據(jù)叉乘規(guī)則,我們只需要將y軸的單位向量與光線方向向量做叉乘就可以了。

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

現(xiàn)在,我們有了x軸和z軸,y軸已經(jīng)呼之欲出了。沒錯,只需要用z軸向量叉乘x軸向量就可以了!

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

叉乘真是個好東西!

好,坐標(biāo)系統(tǒng)的三個軸都有了,馬上開始生成觀察矩陣。

觀察矩陣

用矩陣的最大好處就是當(dāng)你有了坐標(biāo)空間的3個軸之后,再加上一個位置向量就可以創(chuàng)造一個變換矩陣。用這個矩陣乘上任何向量都可以將這個向量轉(zhuǎn)換到觀察坐標(biāo)系中。我們集齊了這些條件,可以召喚神龍了:

觀察矩陣

R表示Right向量,U表示Up向量,D表示光線方向,P表示位置向量。注意,位置向量取的是它的反方向,因為物體需要朝著攝像機(jī)相反的方向移動才行。

總結(jié)一下我們需要用到的數(shù)據(jù):攝像機(jī)的位置,攝像機(jī)的觀察目標(biāo)(可以生成光線方向),還有世界空間的Up向量。使用這些數(shù)據(jù),通過計算,我們就可以生成任意的觀察矩陣。非常幸運的是,glm已經(jīng)幫我們封裝好了一個函數(shù),調(diào)用它,我們可以直接獲取到觀察矩陣(而且不用擔(dān)心出錯?。?。

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

驗證一下函數(shù)的效果。我們把攝像機(jī)的位置放在半徑為10的圓上,讓它的觀察點始終在世界空間原點上,并且,攝像機(jī)會不斷地在圓上移動。

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
運行效果截圖

是不是很贊?

移動相機(jī)

讓相機(jī)在場景中轉(zhuǎn)圈是挺有趣的,不過更有趣的還是我們自己來控制相機(jī)的移動。第一步,我們要來創(chuàng)建一個相機(jī)系統(tǒng),這需要我們在程序開始的時候定義一些關(guān)于相機(jī)的變量。

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

觀察矩陣就會變成這個樣子:

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

我們希望攝像機(jī)的朝向不變而不是觀察目標(biāo)不變,所以觀察點就變成cameraPos+cameraFront?,F(xiàn)在,我們就要用鍵盤操作移動!

在我們之前定義的processInput函數(shù)的最后添加一些代碼

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);

這樣,我們可以使用WASD鍵來控制前后左右的移動了。

等等,是不是還露了點什么?對了,時間!這段代碼純粹是基于按鍵和代碼運行速度來控制的,如果機(jī)子不好,代碼運行慢點移動的速度也會變慢,這就不太科學(xué)了。因此,我們引入時間來計算移動的距離。

先定義兩個全局的變量,用來保存上一幀繪制的時間以及兩幀之間的間隔時間。

float deltaTime = 0.0f;  //兩幀之間的間隔時間
float lastFrame = 0.0f;  //上一幀繪制的時間

然后,每一幀都更新這兩個數(shù)值:

float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

最后,在processInput中使用這個數(shù)值

float cameraSpeed = 2.5f * deltaTime; //移動速度

編譯運行。

運行效果

在左右方向上移動地非???,筆者也試過調(diào)小2.5f這個數(shù)值,但是經(jīng)過嘗試,即便是將2.5調(diào)成0.01在左右方向上移動地還是很快,而前后方向上就太慢了。

環(huán)顧四周

只用WASD控制移動還不算一個完整的FPS攝像機(jī),我們還要能轉(zhuǎn)頭才行!

要實現(xiàn)轉(zhuǎn)頭的功能呢,我們就要對cameraFront向量進(jìn)行改變了。不過對方向向量的改變比較復(fù)雜,還涉及要一些三角學(xué)的知識。如果你不了解三角學(xué),跳過下面這一段也無妨,直接到代碼的地方,等你想了解原理的時候再回來。

歐拉角

歐拉角是繞著三條軸旋轉(zhuǎn)的一個值(歐拉這個名字應(yīng)該很熟悉吧)。一共有3中歐拉角,分別是:pitch、yaw和roll。(避免歧義,直接用英文。)


歐拉角

pitch表示我們平時抬頭低頭的動作,yaw表示左看右看,roll表示,嗯,二哈打滾就是這種效果,咱不適合。每個歐拉角組合起來之后,我們可以表示任何旋轉(zhuǎn)。

作為一個FPS攝像機(jī),我們只需要pitch和yaw兩種旋轉(zhuǎn)就行了。通過三角計算,將方向向量設(shè)置成新值。

pitch計算

上圖就是pitch旋轉(zhuǎn)的計算方法。我們的初始方向為(0, 0, -1)。當(dāng)我們想要轉(zhuǎn)動pitch角度時,z坐標(biāo)就等于-cos(pitch),y坐標(biāo)就等于sin(pitch),因為我們假定了斜邊長度為1,只考慮其方向。

yaw計算

類似的,計算yaw的方法也是如此,z坐標(biāo)等于-cos(yaw),x坐標(biāo)等于-sin(yaw)。

將兩個旋轉(zhuǎn)整合起來:
x = -sin(yaw)*cos(pitch)
y = sin(pitch)
z = -cos(pitch) * cos(yaw)

鼠標(biāo)輸入

pitch和yaw的值是通過鼠標(biāo)的移動得到的,水平方向上的移動代表了yaw的值,垂直方向上的移動代表了pitch的值。我們需要保存上一次鼠標(biāo)的位置,這樣可以通過計算和這次鼠標(biāo)位置的差值算出轉(zhuǎn)動的角度。不過首先,我們我們需要把鼠標(biāo)的光標(biāo)隱藏起來,并且捕獲鼠標(biāo)消息。

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);  
glfwSetCursorPosCallback(window, mouse_callback);  

mouse_callback是響應(yīng)鼠標(biāo)消息的回調(diào)函數(shù),原型如下:

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

window表示捕獲的窗口,xpos表示x坐標(biāo),ypos表示y坐標(biāo)。

為了計算一個方向向量,我們需要做這么幾件事:

  1. 計算鼠標(biāo)相對于上一次的位置偏移。
  2. 將偏移值累加到攝像機(jī)的yaw和pitch值中去。
  3. 添加一些旋轉(zhuǎn)的限制
  4. 計算方向向量

先看代碼

if (firstMouse) {  //設(shè)置初始位置,防止突然跳到某個方向上
    lastX = xPos;
    lastY = yPos;
    firstMouse = false;
}

float xoffset = lastX - xPos;   //別忘了,在窗口中,左邊的坐標(biāo)小于右邊的坐標(biāo),而我們需要一個正的角度
float yoffset = lastY - yPos;   //同樣,在窗口中,下面的坐標(biāo)大于上面的坐標(biāo),而我們往上抬頭的時候需要一個正的角度
lastX = xPos;
lastY = yPos;

float sensitivity = 0.05f;  //旋轉(zhuǎn)精度
xoffset *= sensitivity;
yoffset *= sensitivity;

yaw += xoffset;
pitch += yoffset;

if (pitch > 89.0f)  //往上看不能超過90度
    pitch = 89.0f;
if (pitch < -89.0f)  //往下看也不能超過90度
    pitch = -89.0f;

glm::vec3 front;
front.x = -sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw));
cameraFront = glm::normalize(front);

為了防止突然跳到某個方向,我們在鼠標(biāo)剛開始的時候?qū)λ奈恢眠M(jìn)行設(shè)置。
接下來,計算與上次位置的偏移量,然后乘上旋轉(zhuǎn)精度得到旋轉(zhuǎn)的角度值。
然后,將旋轉(zhuǎn)角度累加到pitch和yaw值中去,并且,設(shè)置pitch的最大和最小值。
最后,根據(jù)我們上面推倒的公式,計算方向向量,并將其規(guī)范化。

將這段代碼寫入到mouse_callback函數(shù)中,編譯運行!

運行效果

這正是我們想要的!如果現(xiàn)實不對,可以下載源碼比較。這里只提供本章寫的源碼,資源以及其他代碼可以到上一篇文章中下載。

變焦

變焦功能,就是狙擊槍的放大鏡頭。通過改變視野值來達(dá)到效果,將fov值變小,我們就能看到遠(yuǎn)方更精細(xì)的畫面,將fov值變大,我們就可以看到更廣的畫面,當(dāng)然也失去了精度優(yōu)勢。

那么我們?nèi)绾潍@得fov的改變值呢?答案是通過鼠標(biāo)滾輪消息來模擬!

//鼠標(biāo)滾輪消息回調(diào)
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    if (fov >= 1.0 && fov <= 45.0)
        fov -= yoffset;
    if (fov <= 1.0)
        fov = 1.0;
    if (fov >= 45.0)
        fov = 45.0;
}

當(dāng)滾輪往前的時候,yoffset為正,使得fov值變小,物體變大變精細(xì)。相反,當(dāng)滾輪往后的時候,yoffset為負(fù),使fov值變大,物體變小視野變廣。

當(dāng)然,必不可少的一項在之前注冊這個滾輪回調(diào)函數(shù)。

glfwSetScrollCallback(window, scroll_callback); 

于是,我們的投影矩陣就變成了:

projection = glm::perspective(glm::radians((float)fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);

非常簡單!編譯運行,你就能通過滾輪來變焦了。

變焦操作

如果在顯示上遇到麻煩,請參照源碼。

封裝類

之后的例子中,我們會經(jīng)常用到這個攝像機(jī)來觀察顯示效果,所以,將它封裝成類是聰明的做法。限于篇幅,就不再列出詳細(xì)的代碼了, 不過后面會給出源碼,有興趣的童鞋可以自己看內(nèi)部的實現(xiàn)。

檢查一遍類是否可用是一個非常好的習(xí)慣,攝像機(jī)類的源碼在這里,主文件的源碼在這里。

我們現(xiàn)在封裝的這個類可以滿足大部分需求,但它并不是沒有缺陷的。一個重要的問題就是萬向節(jié)死鎖。要解決這個問題,我們之后可以使用四元數(shù)的方法,現(xiàn)在先賣個關(guān)子。

總結(jié)

本章我們學(xué)了觀察矩陣的內(nèi)部原理,也通過一些三角學(xué)知識實現(xiàn)了一個簡單的FPS攝像機(jī),成果斐然!下一篇文章會對到目前為止所學(xué)到的內(nèi)容進(jìn)行總結(jié)梳理,畢竟知識不在多而在融會貫通。

下一篇
目錄
上一篇

參考資料:
www.learningopengl.com(非常好的網(wǎng)站,建議仔細(xì)學(xué)習(xí))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容