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

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

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