# 第六課:鍵盤和鼠標
# 第六課:鍵盤和鼠標
歡迎來到第六課!
我們將學習如何通過鼠標和鍵盤來移動相機,就像在第一人稱射擊游戲中一樣。
## 接口
這段代碼在整個課程中多次被使用,因此把它單獨放在一個文件中:common/controls.cpp,然后在common/controls.hpp中聲明函數接口,這樣tutorial06.cpp就能使用它們了。
和前節課比,tutorial06.cpp里的代碼變動很小。主要的變化是:每一幀都計算MVP(投影視圖矩陣)矩陣,而不像之前那樣只算一次。現在把這段代碼加到主循環中:
```
<pre class="calibre16">```
<span class="token4">do</span><span class="token1">{</span>
<span class="token2">// ...</span>
<span class="token2">// Compute the MVP matrix from keyboard and mouse input</span>
<span class="token3">computeMatricesFromInputs</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 ProjectionMatrix <span class="token">=</span> <span class="token3">getProjectionMatrix</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 ViewMatrix <span class="token">=</span> <span class="token3">getViewMatrix</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 ModelMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">mat4</span><span class="token1">(</span><span class="token6">1.0</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 MVP <span class="token">=</span> ProjectionMatrix <span class="token">*</span> ViewMatrix <span class="token">*</span> ModelMatrix<span class="token1">;</span>
<span class="token2">// ...</span>
<span class="token1">}</span>
```
```
這段代碼需要3個新函數:
- computeMatricesFromInputs()讀鍵盤和鼠標操作,然后計算投影視圖矩陣。這就是奇妙所在。
- getProjectionMatrix()返回計算好的投影矩陣。
- getViewMatrix()返回計算好的視圖矩陣。
這只是一種實現方式,當然,如果你不喜歡這些函數,勇敢地去改寫它們。
來看看controls.cpp在做什么。
## 實際代碼
我們需要幾個變量。
```
<pre class="calibre16">```
<span class="token2">// position</span>
glm<span class="token1">:</span><span class="token1">:</span>vec3 position <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span> <span class="token6">0</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> <span class="token6">5</span> <span class="token1">)</span><span class="token1">;</span>
<span class="token2">// horizontal angle : toward -Z</span>
float horizontalAngle <span class="token">=</span> <span class="token6">3.14</span>f<span class="token1">;</span>
<span class="token2">// vertical angle : 0, look at the horizon</span>
float verticalAngle <span class="token">=</span> <span class="token6">0.0</span>f<span class="token1">;</span>
<span class="token2">// Initial Field of View</span>
float initialFoV <span class="token">=</span> <span class="token6">45.0</span>f<span class="token1">;</span>
float speed <span class="token">=</span> <span class="token6">3.0</span>f<span class="token1">;</span> <span class="token2">// 3 units / second</span>
float mouseSpeed <span class="token">=</span> <span class="token6">0.005</span>f<span class="token1">;</span>
FoV is the level of zoom<span class="token1">.</span> <span class="token6">80</span>° <span class="token">=</span> very wide angle<span class="token1">,</span> huge deformations<span class="token1">.</span> <span class="token6">60</span>° – <span class="token6">45</span>° <span class="token1">:</span> standard<span class="token1">.</span> <span class="token6">20</span>° <span class="token1">:</span> big zoom<span class="token1">.</span>
```
```
首先根據輸入,重新計算位置,水平角,豎直角和視場角(FoV);再由它們算出視圖和投影矩陣。
### 方向
讀取鼠標位置是容易的:
```
<pre class="calibre16">```
<span class="token2">// Get mouse position</span>
int xpos<span class="token1">,</span> ypos<span class="token1">;</span>
<span class="token3">glfwGetMousePos</span><span class="token1">(</span><span class="token">&</span>xpos<span class="token1">,</span> <span class="token">&</span>ypos<span class="token1">)</span><span class="token1">;</span>
```
```
我們需要把光標放到屏幕中心,否則它將很快移到屏幕外,導致無法響應。
```
<pre class="calibre16">```
<span class="token2">// Reset mouse position for next frame</span>
<span class="token3">glfwSetMousePos</span><span class="token1">(</span><span class="token6">1024</span><span class="token">/</span><span class="token6">2</span><span class="token1">,</span> <span class="token6">768</span><span class="token">/</span><span class="token6">2</span><span class="token1">)</span><span class="token1">;</span>
```
```
注意:這段代碼假設窗口大小是1024\*768,這不是必須的。你可以用glfwGetWindowSize來設定窗口大小。
計算觀察角度:
```
<pre class="calibre16">```
<span class="token2">// Compute new orientation</span>
horizontalAngle <span class="token">+</span><span class="token">=</span> mouseSpeed <span class="token">*</span> deltaTime <span class="token">*</span> <span class="token3">float</span><span class="token1">(</span><span class="token6">1024</span><span class="token">/</span><span class="token6">2</span> <span class="token">-</span> xpos <span class="token1">)</span><span class="token1">;</span>
verticalAngle <span class="token">+</span><span class="token">=</span> mouseSpeed <span class="token">*</span> deltaTime <span class="token">*</span> <span class="token3">float</span><span class="token1">(</span> <span class="token6">768</span><span class="token">/</span><span class="token6">2</span> <span class="token">-</span> ypos <span class="token1">)</span><span class="token1">;</span>
```
```
從右往左閱讀這幾行代碼:
- 1024/2 – xpos表示鼠標離窗口中心點的距離。這個值越大,轉動角越大。
- float(…)是浮點數轉換,使乘法順利進行
- mouseSpeed用來加速或減慢旋轉,可以隨你調整或讓用戶選擇。
- += : 如果你沒移動鼠標,1024/2-xpos的值為零,horizontalAngle+=0不改變horizontalAngle的值。如果你用的是”=”,每幀視角都被強制轉回到原始方向,這就不好了。
現在,在世界坐標系下計算一個向量,代表視線方向。
```
<pre class="calibre16">```
<span class="token2">// Direction : Spherical coordinates to Cartesian coordinates conversion</span>
glm<span class="token1">:</span><span class="token1">:</span>vec3 <span class="token3">direction</span><span class="token1">(</span>
<span class="token3">cos</span><span class="token1">(</span>verticalAngle<span class="token1">)</span> <span class="token">*</span> <span class="token3">sin</span><span class="token1">(</span>horizontalAngle<span class="token1">)</span><span class="token1">,</span>
<span class="token3">sin</span><span class="token1">(</span>verticalAngle<span class="token1">)</span><span class="token1">,</span>
<span class="token3">cos</span><span class="token1">(</span>verticalAngle<span class="token1">)</span> <span class="token">*</span> <span class="token3">cos</span><span class="token1">(</span>horizontalAngle<span class="token1">)</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
這是一種標準計算,如果你不了解余弦和正弦,下面有一個簡短的解釋:

上面的公式,只是上圖在三維空間下的推廣。
我們想算出相機的『上方向』。『上方向』不一定是Y軸正方向:你俯視時,『上方向』實際上是水平的。這里有一個例子,位置相同,視點相同的相機,卻有不同的『上方向』。
本例中,唯一不變的是,『相機的右邊』這個方向始終取水平方向。你可以試試:保持手臂水平伸直,向正上方看、向下看;向這之間的任何方向看(譯注:『看』立刻產生視線方向)。現在定義『右方向』向量:因為是水平的,故Y坐標為零,X和Z值就像上圖中的一樣,只是角度旋轉了90度,或Pi/2弧度。
```
<pre class="calibre16">```
<span class="token2">// Right vector</span>
glm<span class="token1">:</span><span class="token1">:</span>vec3 right <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span>
<span class="token3">sin</span><span class="token1">(</span>horizontalAngle <span class="token">-</span> <span class="token6">3.14</span>f<span class="token">/</span><span class="token6">2.0</span>f<span class="token1">)</span><span class="token1">,</span>
<span class="token6">0</span><span class="token1">,</span>
<span class="token3">cos</span><span class="token1">(</span>horizontalAngle <span class="token">-</span> <span class="token6">3.14</span>f<span class="token">/</span><span class="token6">2.0</span>f<span class="token1">)</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
我們有一個『右方向』和一個視線方向,或者說是『前方向』。『上方向』垂直于這兩者。一個很有用的數學工具可以讓三者的聯系變得簡單:叉乘。
```
<pre class="calibre16">```
<span class="token2">// Up vector : perpendicular to both direction and right</span>
glm<span class="token1">:</span><span class="token1">:</span>vec3 up <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">cross</span><span class="token1">(</span> right<span class="token1">,</span> direction <span class="token1">)</span><span class="token1">;</span>
```
```
叉乘是在做什么呢?很簡單,回憶第三課講到的右手定則。第一個向量是大拇指;第二個是食指;叉乘的結果就是中指。十分方便。
### 位置
代碼十分直觀。順便說下,我用上/下/右/左鍵而不用wsad;是因為我的azerty鍵盤中,美式鍵盤的awsd鍵位處實際上是zqsd。qwerZ鍵盤其實又不一樣了,更別提韓國鍵盤了。我甚至不知道韓國人民用的鍵盤是什么布局,但我猜想肯定很不一樣。
```
<pre class="calibre16">```
<span class="token2">// Move forward</span>
<span class="token4">if</span> <span class="token1">(</span><span class="token3">glfwGetKey</span><span class="token1">(</span> GLFW_KEY_UP <span class="token1">)</span> <span class="token">==</span> GLFW_PRESS<span class="token1">)</span><span class="token1">{</span>
position <span class="token">+</span><span class="token">=</span> direction <span class="token">*</span> deltaTime <span class="token">*</span> speed<span class="token1">;</span>
<span class="token1">}</span>
<span class="token2">// Move backward</span>
<span class="token4">if</span> <span class="token1">(</span><span class="token3">glfwGetKey</span><span class="token1">(</span> GLFW_KEY_DOWN <span class="token1">)</span> <span class="token">==</span> GLFW_PRESS<span class="token1">)</span><span class="token1">{</span>
position <span class="token">-</span><span class="token">=</span> direction <span class="token">*</span> deltaTime <span class="token">*</span> speed<span class="token1">;</span>
<span class="token1">}</span>
<span class="token2">// Strafe right</span>
<span class="token4">if</span> <span class="token1">(</span><span class="token3">glfwGetKey</span><span class="token1">(</span> GLFW_KEY_RIGHT <span class="token1">)</span> <span class="token">==</span> GLFW_PRESS<span class="token1">)</span><span class="token1">{</span>
position <span class="token">+</span><span class="token">=</span> right <span class="token">*</span> deltaTime <span class="token">*</span> speed<span class="token1">;</span>
<span class="token1">}</span>
<span class="token2">// Strafe left</span>
<span class="token4">if</span> <span class="token1">(</span><span class="token3">glfwGetKey</span><span class="token1">(</span> GLFW_KEY_LEFT <span class="token1">)</span> <span class="token">==</span> GLFW_PRESS<span class="token1">)</span><span class="token1">{</span>
position <span class="token">-</span><span class="token">=</span> right <span class="token">*</span> deltaTime <span class="token">*</span> speed<span class="token1">;</span>
<span class="token1">}</span>
```
```
這里唯一特別的是deltaTime。你不會希望每幀偏移1單元的,原因很簡單:
- 如果你有一臺快電腦,每秒能跑60幀,你每秒移動60\*speed個單位。
- 如果你有一臺慢電腦,每秒能跑20幀,你每秒移動20\*speed個單位。
電腦性能不能成為速度不穩的借口;你需要通過“前一幀到現在的時間”或“時間間隔(deltaTime)”來控制移動步長。
- 如果你有一臺快電腦,每秒能跑60幀,你每幀移動1/60*speed個單位,每秒移動1*speed個單位。
- 如果你有一臺慢電腦,每秒能跑20幀,你每幀移動1/20*speed個單位,每秒移動1*speed個單位。
這就好多了。deltaTime很容易算:
```
<pre class="calibre16">```
double currentTime <span class="token">=</span> <span class="token3">glfwGetTime</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
float deltaTime <span class="token">=</span> <span class="token3">float</span><span class="token1">(</span>currentTime <span class="token">-</span> lastTime<span class="token1">)</span><span class="token1">;</span>
```
```
### 視場角
為了好玩,我們可以把視場角綁定到鼠標滾輪,作為簡陋的縮放功能:
```
<pre class="calibre16">```
float FoV <span class="token">=</span> initialFoV <span class="token">-</span> <span class="token6">5</span> <span class="token">*</span> <span class="token3">glfwGetMouseWheel</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
```
```
### 計算矩陣
計算矩陣已經很直觀了。使用和前面幾乎一樣的函數,僅參數不同。
```
<pre class="calibre16">```
<span class="token2">// Projection matrix : 45° Field of View, 4:3 ratio, display range : 0.1 unit <-> 100 units</span>
ProjectionMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">perspective</span><span class="token1">(</span>FoV<span class="token1">,</span> <span class="token6">4.0</span>f <span class="token">/</span> <span class="token6">3.0</span>f<span class="token1">,</span> <span class="token6">0.1</span>f<span class="token1">,</span> <span class="token6">100.0</span>f<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Camera matrix</span>
ViewMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">lookAt</span><span class="token1">(</span>
position<span class="token1">,</span> <span class="token2">// Camera is here</span>
position<span class="token">+</span>direction<span class="token1">,</span> <span class="token2">// and looks here : at the same position, plus "direction"</span>
up <span class="token2">// Head is up (set to 0,-1,0 to look upside-down)</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
## 結果

### 隱藏面消除
現在可以自由移動鼠標,你會注意到:如果鼠標移動到立方體里面,多邊形仍然會被顯示。這看起來理所當然,實則可以優化。事實上,在常見應用中,你從來不會處于立方體內。
有一個思路是讓GPU檢查相機在三角形的后面還是前面。如果在前面,顯示該三角形;如果相機在三角形后面,且不在網格(網格必須是封閉的)內部,那么必有其他三角形在相機前面,故不顯示該三角形。沒有人會注意到什么,除了一切都會變快:三角形平均少了兩倍!
更妙的是,檢查起來還很簡單:GPU計算三角形的法向(用叉乘,記得吧?),然后檢查這個法向是否朝向相機。
不幸的是這樣做有代價:三角形的方向是隱式的。這意味著如果你在緩沖區中交換兩個頂點,可能會產生洞。但一般來說,它值得做一點額外工作。一般你只要在三維建模軟件中點擊“反轉法向”(實際是交換兩個頂點,從而反轉法向),一切就正常了。
開啟隱藏面消除是很輕松的:
```
<pre class="calibre16">```
<span class="token2">// Cull triangles which normal is not towards the camera</span>
<span class="token3">glEnable</span><span class="token1">(</span>GL_CULL_FACE<span class="token1">)</span><span class="token1">;</span>
```
```
## 練習
- 限制verticalAngle,使之不能顛倒方向
- 創建一個相機,使它繞著物體旋轉 ( position = ObjectCenter + ( radius \* cos(time), height, radius \* sin(time) ) );然后將半徑/高度/時間的變化綁定到鍵盤/鼠標上,諸如此類。
- 玩得開心!