OpenGL 中模擬相機的方法是,把場景(整個世界)照相機想移動的相反方向移動,來模擬出相機的感覺。
View(Camera) Space
View Space 是指把相機當作原點的座標,一個相機可以用以下三個東西定義出來:
- 相機(Camera)
- 位置
- 觀察的方向
- 相機頭頂的方向向量
第三個軸可以用外積求出,於是我們定義出了一個以相機為原點的座標系,接下來就是要構造 View Matrix 來把世界座標轉換過去。
- 位置
1 | glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); |
- 觀察的方向
1 | glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); |
- 右向量(對於相機)
- 這裡會用世界的 $y$ 軸向量(向上)跟 觀察方向做外積
- 拿到的向量會朝向 View Space 的 $+x$ ,也就是相機的右邊
1 | glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); |
- 上向量(對於相機)
- 我們已經有 View Space 的 $x$ 軸和 $z$ 軸的向量
1 | glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight); |
這三個向量構成了 View Space。
- 為什麼要世界的上向量
- 先用外積求出 View Space 的右向量(Right Vector)
- 再算出 View Space 之上向量
- https://stackoverflow.com/questions/5717654/glulookat-explanation
- https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/lookat-function
上面的數學知識是: Gram-Schmidt process
Look At 矩陣
$$
LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix}
$$
$\color{Red}{R}$ 是右向量、$\color{Green}{U}$是上向量、$\color{Blue}{D}$是方向向量,$\color{purple}{P}$ 是相機位置,注意位移是相反方向,是因為前面提到我們希望將世界往相機的相反方向移動(因為 OpenGL 的相機在 $(0, 0, 0)$)
- GLM 提供了直接產生 Look At Matrix 的實用功能,只要提供相機位置、觀察目標、跟世界空間的上向量
1 | glm::mat4 view; |
- 7.1.camera_circle
- 這個範例示範了,把 Camera 繞 $x-z$ 平面旋轉
自由移動
上面只示範了旋轉,如果能自由移動的話會更有趣,但是我們要改一下 Camera 的實作
1 | glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); |
lookAt
變成了:
1 | view = glm::lookAt(cameraPos, cameraPos+cameraFront, cameraUp); |
接下來新增移動功能
1 | if(sf::Keyboard::isKeyPressed(sf::Keyboard::W)) |
當我們按下 WASD 時,Camera 就會在遊戲世界中移動,按 Shift 時 Camera 會加速
除了按鍵移動之外,我們還改了移動速度的 code,根據處理器不同,每個 loop 的時間會不同,通常都會用 delta time 去乘上某個東西的時間變化量(e.g. 速度),來更新當前的狀態。
1 | if(sf::Keyboard::isKeyPressed(sf::Keyboard::LShift)) |
視角移動
歐拉角
歐拉角(Euler Angle)可以表示空間中三軸的旋轉。俯仰角(Pitch)、偏擺角(Yaw)和翻滾角(Roll)
在傳統 FPS 中不會有 Roll 所以這裡不討論,有了歐拉角後就可以算出旋轉後的新的方向向量
1 | glm::vec3 dir; |
有了方向向量,我們就可以把 cameraFront
設定成方向向量,來把相機轉過去了。
滑鼠輸入
通常根據滑鼠輸入變動視角的處理方法是:
- 計算滑鼠位置的偏移量(相對上個 frame)
- 把偏移量加到
pitch
跟yaw
中 - 限制最大跟最小值
- 計算方向向量
1 | // 滑鼠目前為位置 |
把偏移量乘上滑鼠敏感度
1 | off *= sensitivity; |
把角度加上偏移量,並限制最大、最小 (不限制水平旋轉是因為古早 FPS 都是視角轉 = 人轉)
1 | static float pitch = 0.f; |
最後計算 Camera 新的方向向量
1 | glm::vec3 newCameraFront; |
- 隱藏滑鼠游標
1 | window.setMouseCursorVisible(false); // 因為 imgui 的緣故,滑鼠還是會被畫出來 |
視角縮放
把 FOV 變小時,會產生一種放大的感覺,我們使用滑鼠滾輪來進行縮放。SFML 裏頭偵測滑鼠滾輪只能用事件
1 | // Event update |
使用歐拉角的 Camera 會有萬向鎖(Gimbal Lock)的問題
Lab
將 Camera 封裝成 class
試著自己實作看看 lookAt()
- OpenGL 矩陣是 Column-major,所以 LookAt 公式要改寫成
$$
LookAt = \begin{bmatrix}
\color{red}{R_x} & \color{green}{U_x} & \color{blue}{D_x} & 0 \\
\color{red}{R_y} & \color{green}{U_y} & \color{blue}{D_y} & 0 \\
\color{red}{R_z} & \color{green}{U_z} & \color{blue}{D_z} & 0 \\
0 & 0 & 0 & 1
\end{bmatrix} *
\begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
-\color{purple}{P_x} & -\color{purple}{P_y} & -\color{purple}{P_z} & 1
\end{bmatrix}
$$
我卡了一整天幹zz
如果你覺得這篇文章很棒,請你不吝點讚 (゚∀゚)