OpenGL 圖形庫的使用(九)—— 攝像機(一)

版本記錄

版本號 時間
V1.0 2017.09.05

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)

攝像機

以下內容來自LearnOpenGL CN

OpenGL本身沒有攝像機(Camera)的概念,但我們可以通過把場景中的所有物體往相反方向移動的方式來模擬出攝像機,產生一種我們在移動的感覺,而不是場景在移動。

本節我們將會討論如何在OpenGL中配置一個攝像機,并且將會討論FPS風格的攝像機,讓你能夠在3D場景中自由移動。我們也會討論鍵盤和鼠標輸入,最終完成一個自定義的攝像機類。


攝像機/觀察空間

當我們討論攝像機/觀察空間(Camera/View Space)的時候,是在討論以攝像機的視角作為場景原點時場景中所有的頂點坐標:觀察矩陣把所有的世界坐標變換為相對于攝像機位置與方向的觀察坐標。要定義一個攝像機,我們需要它在世界空間中的位置、觀察的方向、一個指向它右測的向量以及一個指向它上方的向量。細心的讀者可能已經注意到我們實際上創建了一個三個單位軸相互垂直的、以攝像機的位置為原點的坐標系。

1. 攝像機位置

獲取攝像機位置很簡單。攝像機位置簡單來說就是世界空間中一個指向攝像機位置的向量。我們把攝像機位置設置為上一節中的那個相同的位置:

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);

不要忘記正z軸是從屏幕指向你的,如果我們希望攝像機向后移動,我們就沿著z軸的正方向移動。

2. 攝像機方向

下一個需要的向量是攝像機的方向,這里指的是攝像機指向哪個方向。現在我們讓攝像機指向場景原點:(0, 0, 0)。還記得如果將兩個矢量相減,我們就能得到這兩個矢量的差嗎?用場景原點向量減去攝像機位置向量的結果就是攝像機的指向向量。由于我們知道攝像機指向z軸負方向,但我們希望方向向量(Direction Vector)指向攝像機的z軸正方向。如果我們交換相減的順序,我們就會獲得一個指向攝像機正z軸方向的向量:

glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

方向向量(Direction Vector)并不是最好的名字,因為它實際上指向從它到目標向量的相反方向(譯注:注意看前面的那個圖,藍色的方向向量大概指向z軸的正方向,與攝像機實際指向的方向是正好相反的)。

3. 右軸

我們需要的另一個向量是一個右向量(Right Vector),它代表攝像機空間的x軸的正方向。為獲取右向量我們需要先使用一個小技巧:先定義一個上向量(Up Vector)。接下來把上向量和第二步得到的方向向量進行叉乘。兩個向量叉乘的結果會同時垂直于兩向量,因此我們會得到指向x軸正方向的那個向量(如果我們交換兩個向量叉乘的順序就會得到相反的指向x軸負方向的向量):

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));

4. 上軸

現在我們已經有了x軸向量和z軸向量,獲取一個指向攝像機的正y軸向量就相對簡單了:我們把右向量和方向向量進行叉乘:

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

Look At

使用矩陣的好處之一是如果你使用3個相互垂直(或非線性)的軸定義了一個坐標空間,你可以用這3個軸外加一個平移向量來創建一個矩陣,并且你可以用這個矩陣乘以任何向量來將其變換到那個坐標空間。這正是LookAt矩陣所做的,現在我們有了3個相互垂直的軸和一個定義攝像機空間的位置坐標,我們可以創建我們自己的LookAt矩陣

其中R是右向量,U是上向量,D是方向向量P是攝像機位置向量。注意,位置向量是相反的,因為我們最終希望把世界平移到與我們自身移動的相反方向。把這個LookAt矩陣作為觀察矩陣可以很高效地把所有世界坐標變換到剛剛定義的觀察空間。LookAt矩陣就像它的名字表達的那樣:它會創建一個看著(Look at)給定目標的觀察矩陣。

幸運的是,GLM已經提供了這些支持。我們要做的只是定義一個攝像機位置,一個目標位置和一個表示世界空間中的上向量的向量(我們計算右向量使用的那個上向量)。接著GLM就會創建一個LookAt矩陣,我們可以把它當作我們的觀察矩陣:

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 
           glm::vec3(0.0f, 0.0f, 0.0f), 
           glm::vec3(0.0f, 1.0f, 0.0f));

glm::LookAt函數需要一個位置、目標和上向量。它會創建一個和在上一節使用的一樣的觀察矩陣。

在討論用戶輸入之前,我們先來做些有意思的事,把我們的攝像機在場景中旋轉。我們會將攝像機的注視點保持在(0, 0, 0)。

我們需要用到一點三角學的知識來在每一幀創建一個x和z坐標,它會代表圓上的一點,我們將會使用它作為攝像機的位置。通過重新計算x和y坐標,我們會遍歷圓上的所有點,這樣攝像機就會繞著場景旋轉了。我們預先定義這個圓的半徑radius,在每次渲染迭代中使用GLFWglfwGetTime函數重新創建觀察矩陣,來擴大這個圓。

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0)); 

運行代碼,應該會得到下面的結果:

通過這一小段代碼,攝像機現在會隨著時間流逝圍繞場景轉動了。自己試試改變半徑和位置/方向參數,看看LookAt矩陣是如何工作的。同時,如果你在哪卡住的話,這里有源碼。


自由移動

讓攝像機繞著場景轉的確很有趣,但是讓我們自己移動攝像機會更有趣!首先我們必須設置一個攝像機系統,所以在我們的程序前面定義一些攝像機變量很有用:

glm::vec3 cameraPos   = glm::vec3(0.0f, 0.0f,  3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp    = glm::vec3(0.0f, 1.0f,  0.0f);

LookAt函數現在成了:

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

我們首先將攝像機位置設置為之前定義的cameraPos。方向是當前的位置加上我們剛剛定義的方向向量。這樣能保證無論我們怎么移動,攝像機都會注視著目標方向。讓我們擺弄一下這些向量,在按下某些按鈕時更新cameraPos向量。

我們已經為GLFW的鍵盤輸入定義過一個processInput函數了,我們來新添加幾個需要檢查的按鍵命令:

void processInput(GLFWwindow *window)
{
    ...
    float cameraSpeed = 0.05f; // adjust accordingly
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        cameraPos += cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        cameraPos -= cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}

當我們按下WASD鍵的任意一個,攝像機的位置都會相應更新。如果我們希望向前或向后移動,我們就把位置向量加上或減去方向向量。如果我們希望向左右移動,我們使用叉乘來創建一個右向量(Right Vector),并沿著它相應移動就可以了。這樣就創建了使用攝像機時熟悉的橫移(Strafe)效果。

注意,我們對右向量進行了標準化。如果我們沒對這個向量進行標準化,最后的叉乘結果會根據cameraFront變量返回大小不同的向量。如果我們不對向量進行標準化,我們就得根據攝像機的朝向不同加速或減速移動了,但如果進行了標準化移動就是勻速的。

現在你就應該能夠移動攝像機了,雖然移動速度和系統有關,你可能會需要調整一下cameraSpeed


移動速度

目前我們的移動速度是個常量。理論上沒什么問題,但是實際情況下根據處理器的能力不同,有些人可能會比其他人每秒繪制更多幀,也就是以更高的頻率調用processInput函數。結果就是,根據配置的不同,有些人可能移動很快,而有些人會移動很慢。當你發布你的程序的時候,你必須確保它在所有硬件上移動速度都一樣。

圖形程序和游戲通常會跟蹤一個時間差(Deltatime)變量,它儲存了渲染上一幀所用的時間。我們把所有速度都去乘以deltaTime值。結果就是,如果我們的deltaTime很大,就意味著上一幀的渲染花費了更多時間,所以這一幀的速度需要變得更高來平衡渲染所花去的時間。使用這種方法時,無論你的電腦快還是慢,攝像機的速度都會相應平衡,這樣每個用戶的體驗就都一樣了。

我們跟蹤兩個全局變量來計算出deltaTime值:

float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間

在每一幀中我們計算出新的deltaTime以備后用。

float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

現在我們有了deltaTime,在計算速度的時候可以將其考慮進去了:

void processInput(GLFWwindow *window)
{
  float cameraSpeed = 2.5f * deltaTime;
  ...
}

與前面的部分結合在一起,我們有了一個更流暢點的攝像機系統:

現在我們有了一個在任何系統上移動速度都一樣的攝像機。同樣,如果你卡住了,查看一下源碼。我們可以看到任何移動都會影響返回的deltaTime值。


視角移動

只用鍵盤移動沒什么意思。特別是我們還不能轉向,移動很受限制。是時候加入鼠標了!

為了能夠改變視角,我們需要根據鼠標的輸入改變cameraFront向量。然而,根據鼠標移動改變方向向量有點復雜,需要一些三角學知識。如果你對三角學知之甚少,別擔心,你可以跳過這一部分,直接復制粘貼我們的代碼;當你想了解更多的時候再回來看。

后記

未完,待續~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容