版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2017.09.05 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
歐拉角
歐拉角(Euler Angle)
是可以表示3D空間中任何旋轉的3個值,由萊昂哈德·歐拉(Leonhard Euler)在18世紀提出。一共有3種歐拉角:俯仰角(Pitch)、偏航角(Yaw)和滾轉角(Roll)
,下面的圖片展示了它們的含義:
俯仰角是描述我們如何往上或往下看的角,可以在第一張圖中看到。第二張圖展示了偏航角,偏航角表示我們往左和往右看的程度。滾轉角代表我們如何翻滾攝像機,通常在太空飛船的攝像機中使用。每個歐拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了。
對于我們的攝像機系統來說,我們只關心俯仰角和偏航角,所以我們不會討論滾轉角。給定一個俯仰角和偏航角,我們可以把它們轉換為一個代表新的方向向量的3D向量。俯仰角和偏航角轉換為方向向量的處理需要一些三角學知識,我們先從最基本的情況開始:
如果我們把斜邊邊長定義為1,我們就能知道鄰邊的長度是cos x/h=cos x/1=cos x
,它的對邊是sin y/h=sin y/1=sin y
。這樣我們獲得了能夠得到x和y方向長度的通用公式,它們取決于所給的角度。我們使用它來計算方向向量的分量:
這個三角形看起來和前面的三角形很像,所以如果我們想象自己在xz平面上,看向y軸,我們可以基于第一個三角形計算來計算它的長度/y方向的強度(Strength)(我們往上或往下看多少)。從圖中我們可以看到對于一個給定俯仰角的y值等于sin θ。
direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉為弧度
這里我們只更新了y值,仔細觀察x和z分量也被影響了。從三角形中我們可以看到它們的值等于:
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
看看我們是否能夠為偏航角找到需要的分量:
就像俯仰角的三角形一樣,我們可以看到x分量取決于cos(yaw)
的值,z值同樣取決于偏航角的正弦值。把這個加到前面的值中,會得到基于俯仰角和偏航角的方向向量:
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 譯注:direction代表攝像機的前軸(Front),這個前軸是和本文第一幅圖片的第二個攝像機的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
這樣我們就有了一個可以把俯仰角和偏航角轉化為用來自由旋轉視角的攝像機的3維方向向量了。你可能會奇怪:我們怎么得到俯仰角和偏航角?
鼠標輸入
偏航角和俯仰角是通過鼠標(或手柄)移動獲得的,水平的移動影響偏航角,豎直的移動影響俯仰角。它的原理就是,儲存上一幀鼠標的位置,在當前幀中我們當前計算鼠標位置與上一幀的位置相差多少。如果水平/豎直差別越大那么俯仰角或偏航角就改變越大,也就是攝像機需要移動更多的距離。
首先我們要告訴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);
在處理FPS風格攝像機的鼠標輸入的時候,我們必須在最終獲取方向向量之前做下面這幾步:
- 計算鼠標距上一幀的偏移量。
- 把偏移量添加到攝像機的俯仰角和偏航角中。
- 對偏航角和俯仰角進行最大和最小值的限制。
- 計算方向向量。
第一步是計算鼠標自上一幀的偏移量。我們必須先在程序中儲存上一幀的鼠標位置,我們把它的初始值設置為屏幕的中心(屏幕的尺寸是800x600):
float lastX = 400, lastY = 300;
然后在鼠標的回調函數中我們計算當前幀和上一幀鼠標位置的偏移量:
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意這里是相反的,因為y坐標是從底部往頂部依次增大的
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
注意我們把偏移量乘以了sensitivity(靈敏度)值。如果我們忽略這個值,鼠標移動就會太大了;你可以自己實驗一下,找到適合自己的靈敏度值。
接下來我們把偏移量加到全局變量pitch
和yaw
上:
yaw += xoffset;
pitch += yoffset;
第三步,我們需要給攝像機添加一些限制,這樣攝像機就不會發生奇怪的移動了(這樣也會避免一些奇怪的問題)。對于俯仰角,要讓用戶不能看向高于89
度的地方(在90度時視角會發生逆轉,所以我們把89度作為極限),同樣也不允許小于-89
度。這樣能夠保證用戶只能看到天空或腳下,但是不能超越這個限制。我們可以在值超過限制的時候將其改為極限值來實現:
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
注意我們沒有給偏航角設置限制,這是因為我們不希望限制用戶的水平旋轉。當然,給偏航角設置限制也很容易,如果你愿意可以自己實現。
第四也是最后一步,就是通過俯仰角和偏航角來計算以得到真正的方向向量:
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);
計算出來的方向向量就會包含根據鼠標移動計算出來的所有旋轉了。由于cameraFront
向量已經包含在GLM
的lookAt
函數中,我們這就沒什么問題了。
如果你現在運行代碼,你會發現在窗口第一次獲取焦點的時候攝像機會突然跳一下。這個問題產生的原因是,在你的鼠標移動進窗口的那一刻,鼠標回調函數就會被調用,這時候的xpos和ypos會等于鼠標剛剛進入屏幕的那個位置。這通常是一個距離屏幕中心很遠的地方,因而產生一個很大的偏移量,所以就會跳了。我們可以簡單的使用一個bool變量檢驗我們是否是第一次獲取鼠標輸入,如果是,那么我們先把鼠標的初始位置更新為xpos和ypos值,這樣就能解決這個問題;接下來的鼠標移動就會使用剛進入的鼠標位置坐標來計算偏移量了:
if(firstMouse) // 這個bool變量初始時是設定為true的
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
最后的代碼應該是這樣的:
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);
}
現在我們就可以自由地在3D場景中移動了!
縮放
作為我們攝像機系統的一個附加內容,我們還會來實現一個縮放(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)
限制在1.0f到45.0f。
我們現在在每一幀都必須把透視投影矩陣上傳到GPU,但現在使用fov變量作為它的視野:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
最后不要忘記注冊鼠標滾輪的回調函數:
glfwSetScrollCallback(window, scroll_callback);
現在,我們就實現了一個簡單的攝像機系統了,它能夠讓我們在3D環境中自由移動。
你可以去自由地實驗,如果遇到困難,可以對比源代碼。
注意,使用歐拉角的攝像機系統并不完美。根據你的視角限制或者是配置,你仍然可能引入萬向節死鎖問題。最好的攝像機系統是使用四元數(Quaternions)的,但我們將會把這個留到后面討論。(譯注:這里可以查看四元數攝像機的實現)
攝像機類
接下來的教程中,我們將會一直使用一個攝像機來瀏覽場景,從各個角度觀察結果。然而,由于一個攝像機會占用每篇教程很大的篇幅,我們將會從細節抽象出來,創建我們自己的攝像機對象,它會完成大多數的工作,而且還會提供一些附加的功能。與著色器教程不同,我們不會帶你一步一步創建攝像機類,我們只會提供你一份(有完整注釋的)代碼,如果你想知道它的內部構造的話可以自己去閱讀。
和著色器對象一樣,我們把攝像機類寫在一個單獨的頭文件中。你可以在這里找到它,你現在應該能夠理解所有的代碼了。我們建議您至少看一看這個類,看看如何創建一個自己的攝像機類。
我們介紹的攝像機系統是一個FPS風格的攝像機,它能夠滿足大多數情況需要,而且與歐拉角兼容,但是在創建不同的攝像機系統,比如飛行模擬攝像機,時就要當心。每個攝像機系統都有自己的優點和不足,所以確保對它們進行了詳細研究。比如,這個FPS攝像機不允許俯仰角大于90度,而且我們使用了一個固定的上向量(0, 1, 0),這在需要考慮滾轉角的時候就不能用了。
使用新攝像機對象,更新后版本的源碼可以在這里找到。
后記
未完,待續~~~