版本記錄
| 版本號 | 時(shí)間 |
|---|---|
| V1.0 | 2017.09.05 |
前言
OpenGL 圖形庫項(xiàng)目中一直也沒用過,最近也想學(xué)著使用這個(gè)圖形庫,感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對大家能有所幫助。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴(kuò)展和狀態(tài)機(jī)
3. OpenGL 圖形庫使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標(biāo)系統(tǒng)之五種不同的坐標(biāo)系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標(biāo)系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(jī)(一)
歐拉角
歐拉角(Euler Angle)是可以表示3D空間中任何旋轉(zhuǎn)的3個(gè)值,由萊昂哈德·歐拉(Leonhard Euler)在18世紀(jì)提出。一共有3種歐拉角:俯仰角(Pitch)、偏航角(Yaw)和滾轉(zhuǎn)角(Roll),下面的圖片展示了它們的含義:

俯仰角是描述我們?nèi)绾瓮匣蛲驴吹慕?,可以在第一張圖中看到。第二張圖展示了偏航角,偏航角表示我們往左和往右看的程度。滾轉(zhuǎn)角代表我們?nèi)绾畏瓭L攝像機(jī),通常在太空飛船的攝像機(jī)中使用。每個(gè)歐拉角都有一個(gè)值來表示,把三個(gè)角結(jié)合起來我們就能夠計(jì)算3D空間中任何的旋轉(zhuǎn)向量了。
對于我們的攝像機(jī)系統(tǒng)來說,我們只關(guān)心俯仰角和偏航角,所以我們不會討論滾轉(zhuǎn)角。給定一個(gè)俯仰角和偏航角,我們可以把它們轉(zhuǎn)換為一個(gè)代表新的方向向量的3D向量。俯仰角和偏航角轉(zhuǎn)換為方向向量的處理需要一些三角學(xué)知識,我們先從最基本的情況開始:

如果我們把斜邊邊長定義為1,我們就能知道鄰邊的長度是cos x/h=cos x/1=cos x,它的對邊是sin y/h=sin y/1=sin y。這樣我們獲得了能夠得到x和y方向長度的通用公式,它們?nèi)Q于所給的角度。我們使用它來計(jì)算方向向量的分量:

這個(gè)三角形看起來和前面的三角形很像,所以如果我們想象自己在xz平面上,看向y軸,我們可以基于第一個(gè)三角形計(jì)算來計(jì)算它的長度/y方向的強(qiáng)度(Strength)(我們往上或往下看多少)。從圖中我們可以看到對于一個(gè)給定俯仰角的y值等于sin θ。
direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉(zhuǎn)為弧度
這里我們只更新了y值,仔細(xì)觀察x和z分量也被影響了。從三角形中我們可以看到它們的值等于:
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
看看我們是否能夠?yàn)槠浇钦业叫枰姆至浚?/p>

就像俯仰角的三角形一樣,我們可以看到x分量取決于cos(yaw)的值,z值同樣取決于偏航角的正弦值。把這個(gè)加到前面的值中,會得到基于俯仰角和偏航角的方向向量:
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 譯注:direction代表攝像機(jī)的前軸(Front),這個(gè)前軸是和本文第一幅圖片的第二個(gè)攝像機(jī)的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
這樣我們就有了一個(gè)可以把俯仰角和偏航角轉(zhuǎn)化為用來自由旋轉(zhuǎn)視角的攝像機(jī)的3維方向向量了。你可能會奇怪:我們怎么得到俯仰角和偏航角?
鼠標(biāo)輸入
偏航角和俯仰角是通過鼠標(biāo)(或手柄)移動獲得的,水平的移動影響偏航角,豎直的移動影響俯仰角。它的原理就是,儲存上一幀鼠標(biāo)的位置,在當(dāng)前幀中我們當(dāng)前計(jì)算鼠標(biāo)位置與上一幀的位置相差多少。如果水平/豎直差別越大那么俯仰角或偏航角就改變越大,也就是攝像機(jī)需要移動更多的距離。
首先我們要告訴GLFW,它應(yīng)該隱藏光標(biāo),并捕捉(Capture)它。捕捉光標(biāo)表示的是,如果焦點(diǎn)在你的程序上(譯注:即表示你正在操作這個(gè)程序,Windows中擁有焦點(diǎn)的程序標(biāo)題欄通常是有顏色的那個(gè),而失去焦點(diǎn)的程序標(biāo)題欄則是灰色的),光標(biāo)應(yīng)該停留在窗口中(除非程序失去焦點(diǎn)或者退出)。我們可以用一個(gè)簡單地配置調(diào)用來完成:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
在調(diào)用這個(gè)函數(shù)之后,無論我們怎么去移動鼠標(biāo),光標(biāo)都不會顯示了,它也不會離開窗口。對于FPS攝像機(jī)系統(tǒng)來說非常完美。
為了計(jì)算俯仰角和偏航角,我們需要讓GLFW監(jiān)聽鼠標(biāo)移動事件。(和鍵盤輸入相似)我們會用一個(gè)回調(diào)函數(shù)來完成,函數(shù)的原型如下:
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
這里的xpos和ypos代表當(dāng)前鼠標(biāo)的位置。當(dāng)我們用GLFW注冊了回調(diào)函數(shù)之后,鼠標(biāo)一移動mouse_callback函數(shù)就會被調(diào)用:
glfwSetCursorPosCallback(window, mouse_callback);
在處理FPS風(fēng)格攝像機(jī)的鼠標(biāo)輸入的時(shí)候,我們必須在最終獲取方向向量之前做下面這幾步:
- 計(jì)算鼠標(biāo)距上一幀的偏移量。
- 把偏移量添加到攝像機(jī)的俯仰角和偏航角中。
- 對偏航角和俯仰角進(jìn)行最大和最小值的限制。
- 計(jì)算方向向量。
第一步是計(jì)算鼠標(biāo)自上一幀的偏移量。我們必須先在程序中儲存上一幀的鼠標(biāo)位置,我們把它的初始值設(shè)置為屏幕的中心(屏幕的尺寸是800x600):
float lastX = 400, lastY = 300;
然后在鼠標(biāo)的回調(diào)函數(shù)中我們計(jì)算當(dāng)前幀和上一幀鼠標(biāo)位置的偏移量:
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意這里是相反的,因?yàn)閥坐標(biāo)是從底部往頂部依次增大的
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
注意我們把偏移量乘以了sensitivity(靈敏度)值。如果我們忽略這個(gè)值,鼠標(biāo)移動就會太大了;你可以自己實(shí)驗(yàn)一下,找到適合自己的靈敏度值。
接下來我們把偏移量加到全局變量pitch和yaw上:
yaw += xoffset;
pitch += yoffset;
第三步,我們需要給攝像機(jī)添加一些限制,這樣攝像機(jī)就不會發(fā)生奇怪的移動了(這樣也會避免一些奇怪的問題)。對于俯仰角,要讓用戶不能看向高于89度的地方(在90度時(shí)視角會發(fā)生逆轉(zhuǎn),所以我們把89度作為極限),同樣也不允許小于-89度。這樣能夠保證用戶只能看到天空或腳下,但是不能超越這個(gè)限制。我們可以在值超過限制的時(shí)候?qū)⑵涓臑闃O限值來實(shí)現(xiàn):
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
注意我們沒有給偏航角設(shè)置限制,這是因?yàn)槲覀儾幌M拗朴脩舻乃叫D(zhuǎn)。當(dāng)然,給偏航角設(shè)置限制也很容易,如果你愿意可以自己實(shí)現(xiàn)。
第四也是最后一步,就是通過俯仰角和偏航角來計(jì)算以得到真正的方向向量:
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);
計(jì)算出來的方向向量就會包含根據(jù)鼠標(biāo)移動計(jì)算出來的所有旋轉(zhuǎn)了。由于cameraFront向量已經(jīng)包含在GLM的lookAt函數(shù)中,我們這就沒什么問題了。
如果你現(xiàn)在運(yùn)行代碼,你會發(fā)現(xiàn)在窗口第一次獲取焦點(diǎn)的時(shí)候攝像機(jī)會突然跳一下。這個(gè)問題產(chǎn)生的原因是,在你的鼠標(biāo)移動進(jìn)窗口的那一刻,鼠標(biāo)回調(diào)函數(shù)就會被調(diào)用,這時(shí)候的xpos和ypos會等于鼠標(biāo)剛剛進(jìn)入屏幕的那個(gè)位置。這通常是一個(gè)距離屏幕中心很遠(yuǎn)的地方,因而產(chǎn)生一個(gè)很大的偏移量,所以就會跳了。我們可以簡單的使用一個(gè)bool變量檢驗(yàn)我們是否是第一次獲取鼠標(biāo)輸入,如果是,那么我們先把鼠標(biāo)的初始位置更新為xpos和ypos值,這樣就能解決這個(gè)問題;接下來的鼠標(biāo)移動就會使用剛進(jìn)入的鼠標(biāo)位置坐標(biāo)來計(jì)算偏移量了:
if(firstMouse) // 這個(gè)bool變量初始時(shí)是設(shè)定為true的
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
最后的代碼應(yīng)該是這樣的:
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 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}
現(xiàn)在我們就可以自由地在3D場景中移動了!
縮放
作為我們攝像機(jī)系統(tǒng)的一個(gè)附加內(nèi)容,我們還會來實(shí)現(xiàn)一個(gè)縮放(Zoom)接口。在之前的教程中我們說視野(Field of View)或fov定義了我們可以看到場景中多大的范圍。當(dāng)視野變小時(shí),場景投影出來的空間就會減小,產(chǎn)生放大(Zoom In)了的感覺。我們會使用鼠標(biāo)的滾輪來放大。與鼠標(biāo)移動、鍵盤輸入一樣,我們需要一個(gè)鼠標(biāo)滾輪的回調(diào)函數(shù):
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;
}
當(dāng)滾動鼠標(biāo)滾輪的時(shí)候,yoffset值代表我們豎直滾動的大小。當(dāng)scroll_callback函數(shù)被調(diào)用后,我們改變?nèi)肿兞縡ov變量的內(nèi)容。因?yàn)?5.0f是默認(rèn)的視野值,我們將會把縮放級別(Zoom Level)限制在1.0f到45.0f。
我們現(xiàn)在在每一幀都必須把透視投影矩陣上傳到GPU,但現(xiàn)在使用fov變量作為它的視野:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
最后不要忘記注冊鼠標(biāo)滾輪的回調(diào)函數(shù):
glfwSetScrollCallback(window, scroll_callback);
現(xiàn)在,我們就實(shí)現(xiàn)了一個(gè)簡單的攝像機(jī)系統(tǒng)了,它能夠讓我們在3D環(huán)境中自由移動。

你可以去自由地實(shí)驗(yàn),如果遇到困難,可以對比源代碼。
注意,使用歐拉角的攝像機(jī)系統(tǒng)并不完美。根據(jù)你的視角限制或者是配置,你仍然可能引入萬向節(jié)死鎖問題。最好的攝像機(jī)系統(tǒng)是使用四元數(shù)(Quaternions)的,但我們將會把這個(gè)留到后面討論。(譯注:這里可以查看四元數(shù)攝像機(jī)的實(shí)現(xiàn))
攝像機(jī)類
接下來的教程中,我們將會一直使用一個(gè)攝像機(jī)來瀏覽場景,從各個(gè)角度觀察結(jié)果。然而,由于一個(gè)攝像機(jī)會占用每篇教程很大的篇幅,我們將會從細(xì)節(jié)抽象出來,創(chuàng)建我們自己的攝像機(jī)對象,它會完成大多數(shù)的工作,而且還會提供一些附加的功能。與著色器教程不同,我們不會帶你一步一步創(chuàng)建攝像機(jī)類,我們只會提供你一份(有完整注釋的)代碼,如果你想知道它的內(nèi)部構(gòu)造的話可以自己去閱讀。
和著色器對象一樣,我們把攝像機(jī)類寫在一個(gè)單獨(dú)的頭文件中。你可以在這里找到它,你現(xiàn)在應(yīng)該能夠理解所有的代碼了。我們建議您至少看一看這個(gè)類,看看如何創(chuàng)建一個(gè)自己的攝像機(jī)類。
我們介紹的攝像機(jī)系統(tǒng)是一個(gè)FPS風(fēng)格的攝像機(jī),它能夠滿足大多數(shù)情況需要,而且與歐拉角兼容,但是在創(chuàng)建不同的攝像機(jī)系統(tǒng),比如飛行模擬攝像機(jī),時(shí)就要當(dāng)心。每個(gè)攝像機(jī)系統(tǒng)都有自己的優(yōu)點(diǎn)和不足,所以確保對它們進(jìn)行了詳細(xì)研究。比如,這個(gè)FPS攝像機(jī)不允許俯仰角大于90度,而且我們使用了一個(gè)固定的上向量(0, 1, 0),這在需要考慮滾轉(zhuǎn)角的時(shí)候就不能用了。
使用新攝像機(jī)對象,更新后版本的源碼可以在這里找到。
后記
未完,待續(xù)~~~
