日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

【Modern OpenGL】摄像机系统 Camera

發布時間:2023/12/10 windows 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【Modern OpenGL】摄像机系统 Camera 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

說明:跟著learnopengl的內容學習,不是純翻譯,只是自己整理記錄。?
強烈推薦原文,無論是內容還是排版。?原文鏈接?
本文地址:http://blog.csdn.net/aganlengzi/article/details/50448469

攝像機 Camera

在前面的教程中,我們討論了視口矩陣和我們可以怎樣利用視口矩陣讓繪制的場景移動(我們在上次教程中成功將那個二維平面稍稍向后移動了一點)。OpenGL本身對攝像機這個概念并不熟悉,但是我們可以通過移動場景中所有的對象(就好像反方向移動一個攝像機一樣)來模擬一個。

在本次教程中,我們將會討論我們怎樣在OpenGL中建立攝像機。我們將會創建一個幀率攝像機,允許你在三維場景中自由移動。在這個教程中,我們還會討論一些關于鍵盤和鼠標輸入的只是。最終會形成一個定制的攝像機類。

攝像機/視口坐標系

三維場景中的一個攝像機主要由它在世界坐標系中的位置、它的朝向一個指向右側和一個指向上方的向量來做決定。細心的你可能已經發現了:我們實際上在利用這些量創建一個三個坐標軸相互垂直以所在位置為原點的坐標系。

1.攝像機的位置

得到一個攝像機的位置是十分簡單的。攝像機的位置從根本上來說就是一個指向攝像機位置的向量。我們通過如下代碼將攝像機的位置,也就是我們想要創建的坐標系的原點設置在和我們上次教程相同的位置。

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

不要忘記,z軸的正方向是垂直于屏幕并且指向我們的。所以,當我們想把攝像機向后移動,實際上就是把攝像機向z軸的正方向移動,也就是增大攝像機z軸的坐標值。

2.攝像機的方向

確定了攝像機的位置(即坐標系的原點),接下來需要確定的是攝像機的朝向。目前,我們先默認攝像機指向我們世界坐標系的原點,也就是(0,0,0)。實際上我們是知道攝像機應該指向z軸的負方向的,那么它的相反方向實際上就是其位置坐標和世界坐標系原點的插值(兩個向量之差決定了其方向),我們定義這個向量值為方向向量。如下面的代碼所示:

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

實際上方向向量這個說法是不太合適的,因為這個方向向量命名是攝像機正面朝向的相反方向!

3.向右的坐標軸

按照一開始的說法,接下來需要的向量是向右的方向向量。它實際上代表的是我們想要創建的攝像機坐標系的x軸正方向。怎樣獲得向右指向的方向向量呢?還記的前面教程中關于向量的叉乘的內容嗎?兩個向量的叉乘得到的是垂直于這兩個向量決定的平面的向量(滿足右手坐標系)。所以我們利用2中得到的z軸正向向量和世界坐標系中的向上的方向向量做叉乘,便得到了指向右側的方向向量,它就是我們想創建的攝像機坐標系的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軸方向的向量了,我們通過將x軸和z軸的方向向量做叉乘的方式得到向上的坐標軸方向(也就是我們的y軸方向向量)。如下面的代碼所示:

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

利用上面得到的攝像機坐標系,我們現在可以創建我們的LookAt矩陣了,這個矩陣對于創建一個攝像機是十分有用和必要的。

Look At

使用矩陣的一大好處是:當你利用三個相互垂直的坐標軸建立了一個坐標系,你可以通過這三個坐標軸加上一個轉換向量創建一個矩陣,這個矩陣可以將任何向量轉換到你所定義的那個坐標系空間中。而具體轉換的方式就是左乘這個矩陣。這也正是我們的Look At矩陣將要做的事情。我們現在就用上面得到的三個相互垂直的坐標軸和一個位置向量來定義一個攝像機空間(攝像機坐標系)。并通過定義一個矩陣(Look At矩陣)來將世界坐標系中的坐標值轉換到這個空間中。如下所示:

?

其中,R表示右向向量,U表示向上的向量,D代表方向向量,P是攝像機的位置向量。需要注意的是,這里的位置向量是上面講的偽“方向向量”的反方向,也就是攝像機朝向的方向向量。為什么要這么做?因為我們向左移動攝像機那么世界坐標系中的對象看上去實際上是向右移動的。使用這個Look At矩陣作為我們的視口矩陣能夠高效地將世界坐標系中的坐標轉換到我們剛剛定義的視口坐標系中。這樣,Look At矩陣也就名符其實了:它創建了一個朝向給定目標的視口矩陣。

幸運的是,GLM早就為我們封裝了以上的過程。我們只需要指定一個攝像機的位置,一個目標位置和一個代表世界坐標系中向上方向的向量作為參數就可以創建這個Look At矩陣了。我們使用這個創建好的Look At矩陣作為我們的視口矩陣:

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的三個參數分別就是上面提到的三個參數,得到的view和我們上面通過手工一步步創建得到的矩陣是一模一樣的。

在深入研究用戶輸入之前,讓我們首先利用攝像機的旋轉來產生一些酷炫的效果。我們設置場景的目標向量是(0,0,0)。

我們將我們的攝像機放在一個半徑為10.0f的圓周上(這個圓周在世界坐標系的z軸和x軸決定的平面上),并且隨著時間的推移,攝像機的位置不斷改變(通過三角函數實現,主要是要保證攝像機總是在圓周上)。這樣在每次改變后得到的矩陣作為我們的視口矩陣。然后來看效果。

GLfloat radius = 10.0f; GLfloat camX = sin(glfwGetTime()) * radius; GLfloat 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));

得到的效果應該是這個樣子的(顯示不了動畫,應該看上起是圍繞中心旋轉的樣子):

可以通過設置不同圓的半徑值來得到不同的效果。

漫游 Walk around

轉動攝像機來得到變化角度的場景是有趣的,但是我們還可以讓它更加有趣!那就是我們自己來移動攝像機(想什么時候移動就什么時候移動,想移動到哪兒就移動到哪兒)。不過我們還是像上面一樣,應該建立攝像機系統。為了靈活性,先來定義一些必要的變量吧:

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);

現在我們上面說到的Look At矩陣編程了這個樣子:

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

還是和上面一樣。我們首先向glm::lookAt函數傳遞進事先定義的攝像機位置向量,方向是當前的位置與事先定義的方向向量的和,這保證了無論我們怎樣移動,這個攝像機都會指向目標。下面讓我們通過將攝像機的位置變量cameraPos和我們的鍵盤輸入相關聯,以達到按鍵改變相機位置的目的。

在很早的教程中我們就已經知道了怎樣使用鍵盤響應函數。不過之前只是實現了我們的程序應該如何相應”ESC”鍵,現在讓我們為這個鍵盤響應函數添加更多的鍵值處理:

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {...GLfloat cameraSpeed = 0.05f;if(key == GLFW_KEY_W)cameraPos += cameraSpeed * cameraFront;if(key == GLFW_KEY_S)cameraPos -= cameraSpeed * cameraFront;if(key == GLFW_KEY_A)cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;if(key == GLFW_KEY_D)cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; }

每當我們按下WASD鍵的時候,cameraPos就會被相應改變。當我們想要前后移動的時候,我們將攝像機的位置在指向目標的連線上進行加減。當我們想要左右移動的時候,我們通過叉乘的方式獲得要移動方向的方向向量,然后也是做相應的加減操作。

需要注意的是,我們在代碼中對叉乘的結果進行了歸一化處理,如果不這樣做的話,因為我們叉乘得到的向量的模可能不一致,可能造成按一下鍵移動的距離不一樣的效果,這不是我們想要的。

如果我們完成上面講的相關代碼,我們應該得到的效果應該是下面這個樣子(額,沒法呈現出來。下圖是我按了幾下w鍵的效果):

就是鍵盤上的WASD分別代表向前向左向后向右,如果按下這些鍵的話,可以看到可見場景內的所有對象向前向左向后向右。

在初步試驗了攝像機系統你可能已經注意到了:我們不能同時在兩個方向上移動(也就是在對角線上移動)攝像機!而且,當我們按下某個方向上的按鍵的時候,畫面會“遲疑”一下,然后才會按照應該移動的方向進行移動,好奇怪有沒有。原因是:大多數的事件輸入系統每次只能處理一個按鍵,并且它們的函數也只能在我們激活一個按鍵的時候才會被調用。我們的系統雖然也是這樣,但是我們可以通過一些小技巧來克服這個缺點。

這個技巧的原理是:我們在按鍵響應函數中只對按鍵的按下和釋放做處理。在game loop中我們檢查哪一個按鍵被按下/釋放了,并且完成相應的動作。所以,我們需要存儲哪一個按鍵被按下或者釋放的狀態信息并且在游戲循環中對這些狀態進行反應。首先,讓我們來創建一個bool類型的數組來保存所有可能按鍵的狀態信息:

bool keys[1024];

然后,我們需要在按鍵響應函數key_callback中根據按鍵的情況來動態改變這些值:

if(action == GLFW_PRESS)keys[key] = true; else if(action == GLFW_RELEASE)keys[key] = false;

還有就是要創建響應函數,當我們檢查到鍵的狀態改變的時候做出相應的動作:

void do_movement() {// Camera controlsGLfloat cameraSpeed = 0.01f;if(keys[GLFW_KEY_W])cameraPos += cameraSpeed * cameraFront;if(keys[GLFW_KEY_S])cameraPos -= cameraSpeed * cameraFront;if(keys[GLFW_KEY_A])cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;if(keys[GLFW_KEY_D])cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; }

最后,我們在game loop中調用我們上面實現的兩個函數來完成之前只用一個函數完成的功能:

while(!glfwWindowShouldClose(window)) {// Check and call eventsglfwPollEvents();do_movement(); // Render stuff... }

現在,試試吧,我們應該能夠同時在兩個方向上移動了,并且也應該沒有”遲疑“了吧。

移動速度 Movement speed

目前看上去我們設置了一個恒定的值來作為我們移動的速度,在理論上來看,這是正確的,但是在實際上,人們的處理器是不相同的,這就造成了了,有些人能夠在一秒內繪制很多幀而有些人卻只能繪制比較少的幀,每秒內繪制的幀的數量叫做幀率。當幀率比較大的時候,那么對do_movement的調用次數也就會變多,最終造成的是性能較好的機器能夠產生的動作要多于或者快于性能較差的機器。這顯然不是我們希望的,我們希望我們的程序能夠在任何配置的機器上都能夠對相同的動作產生相同的響應。

我們通過下面的方法來保證我們的程序在不同性能機器上都能產生相同的體驗效果:?
主要的原理是對每幀中產生的動作的速度和幀渲染時間進行同步關聯。那么移動速度是以幀處理時間為參照的,多以并不會受到機器性能的影響了。

我們記錄下面這兩個量(作為程序的全局變量):

GLfloat deltaTime = 0.0f; // 上一幀開始和當前幀開始的時間間隔 GLfloat lastFrame = 0.0f; // 上一幀開始時刻

在每幀內,我們利用上面這兩個量來計算新的deltaTime,供當前幀使用。

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

現在我們可以利用deltatime來計算渲染速度了:

void Do_Movement() {GLfloat cameraSpeed = 5.0f * deltaTime;... }

當我們上面關于移動速度的代碼加上后,得到的應該就是能夠明顯感覺比較平滑的效果了。我修改的代碼main.cpp。

環視 Look around

只用鍵盤進行四個方向上的移動并不是那么有趣,尤其是我們不能夠讓我們環顧(類似于我們是地球環繞太陽一周那樣)場景而只能夠在某個方向上平移!是時候加入鼠標來達到我們想要的這個效果了!

為了能夠環顧我們的場景,我們需要根據鼠標的輸入來改變攝像機的cameraFront向量。但是這并不是十分簡單的,因為要達到較好的效果,其中需要三角函數相關的計算。

歐拉角度 Euler angles

歐拉角的基本思想是將角位移分解為繞三個互相垂直軸的三個旋轉組成的序列。這聽起來復雜,其實它是非常直觀的。之所以有“角位移”的說法正是因為歐拉角能用來描述任意旋轉,但最有意義的是使用笛卡爾坐標系并按照一定順序所組成的旋轉序列。最常用的約定,即所謂“heading-pitch-bank”約定。在這個系統中,一個方位被定義為一個heading角,一個pitch角,和一個bank角。它的基本思想就是讓物體開始于“標準”方位——就是物體坐標軸和慣性坐標軸對齊。在標準方位上,讓物體作heading,pitch,bank旋轉,最后物體到達我們想要描述的方位。

歐拉角由三個可以表示三維中任意旋轉角度的部分組成,是由Leonhard Euler在1700s左右定義的。這三個歐拉角度分別是pitch,yaw和roll。下圖分別直觀展示了這三個量:

第一個圖中顯示的是pitch,它代表了我們向上或者向下看的角度。第二個圖顯示的是yaw值,它代表了我們向左或者向右看的大小。第三個量是roll代表了我們的轉動量,通常在航天飛機攝像機中使用。每一個歐拉角都是一個數量值,它們三個進行組合就能夠表示三維空間中的任何一個轉動角度。

在我們的攝像機系統中,我們只關心yaw和pitch值,所以我們不會討論roll值的改變。基于任何給定的pitch和yaw值,我們可以將它們轉換到三維空間中得到一個方向向量。這個轉換過程需要相應的三角函數的的知識:

?

如果我們定義直角三角形的斜邊為1,三角函數變得簡單,cos = x/h=cos x/1=cos x;sin y/h=sin y/1=sin y。利用這兩個公式,我們可以根據已知的角度值得到x和y方向的長度。我們下面就是要用這種方法來計算我們方向向量的分量。

?

我們的目標是把歐拉角轉換成我們的三維坐標。實際上看懂了下面這張圖就可以了:

上圖中斜邊是1,pitch和yaw已經給出,那么pitch和yaw以及斜邊決定的點的坐標就是如圖所示的樣子,這樣,我們也就得到了我們的方向向量。它的計算方式可以通過glm的函數來完成:

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); direction.y = sin(glm::radians(pitch)); direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

以上就是我們將給定的歐拉角轉換到三維坐標的方式。但是我們要怎樣獲得歐拉角的值呢?

鼠標輸入 Mouse input

Pitch和yaw值的獲取就是通過鼠標啊~鼠標的水平移動改變yaw值,垂直移動改變pitch值,就是這么簡單。我們的想法是保存上一幀鼠標的位置并且在當前幀中,我們計算鼠標值的該表。根據其在水平和豎直方向上改變大小來更新我們的pitch值和yaw值。然后也就決定了我們的攝像機的移動。

那么試一下吧~?
首先我們需要設置GLFW設置我們的程序能夠在鼠標進入我們的程序窗口的時候捕獲它,并且使它隱藏起來。我們可以通過以下簡單的方式來設置:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

在這個函數調用之后,我們在程序窗口中移動鼠標的時候,它不可見并且不會超出我們的程序窗口。

為了按照上述方式計算鼠標的pitch和yaw值,我們需要設置GLFW使其監聽鼠標移動事件,這和前面我們已經使用的鍵盤監聽函數的實現實際上是差不多的。我們只要先實現一個函數,然后把這個函數通過注冊函數注冊到程序中就可以了。1

void mouse_callback(GLFWwindow* window, double xpos, double ypos);

其中 xpos和ypos表示鼠標的當前位置。一旦我們注冊了這個函數,這個函數就會在鼠標移動的時候被調用。

glfwSetCursorPosCallback(window, mouse_callback);

上面的過程完成了鼠標數據的輸入。接下來還有一些步驟要做:

  • 計算鼠標從上一幀到這一幀的位移
  • 根據位移更新攝像機的yaw和pitch值
  • 限定yaw和pitch的最大最小值
  • 計算方向向量
  • 為了完成步驟1,我們設置了兩個坐標值用于表示坐標在程序窗口的位置,實際上是一個二維坐標,初始化為窗口中心的位置。

    GLfloat lastX = 400, lastY = 300;

    然后,在鼠標的回調函數中我們來計算鼠標兩幀之間的位移:

    GLfloat xoffset = xpos - lastX; GLfloat yoffset = lastY - ypos; // Reversed since y-coordinates range from bottom to top lastX = xpos; lastY = ypos;GLfloat sensitivity = 0.05f; xoffset *= sensitivity; yoffset *= sensitivity;

    需要注意的是我們對位移量做了一個靈敏度加權的操作,如果我們不使用這個權值的話,鼠標的移動可能會太大,這不是我們想要的效果。我們可以根據程序的實際運行結果對這個靈敏度權值進行調整。

    接下來,我們將偏移量更新到歐拉角pitch和yaw上:

    yaw += xoffset; pitch += yoffset;

    下面,我們想要對這個攝像機系統加上必要的限制,以防止用戶得到奇怪的坐標值和奇怪的效果。下面設置的限定是:向上看不能看到大于89度或者小于-89度的情況(就是不能把脖子仰斷或者頭向下看到屁股):

    if(pitch > 89.0f)pitch = 89.0f; if(pitch < -89.0f)pitch = -89.0f;

    我們并沒有對yaw值進行限制,如果加上,也是十分方便的。原理同上。

    最后一步是根據yaw和pitch計算作用后的方向向量:

    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變量中,這是一個向量,我們在前面已經在函數中進行調用傳參。

    如果直接運行上面的代碼,我們得到的結果是:在每次鼠標最開始進入到我們程序的窗口的時候,我們窗口中的場景會有一個比較大的跳躍。產生這種現象的原因是我們將鼠標的位置初始化為我們窗口的中心點了(還記得前面的初始化吧),但是我們鼠標進入到窗口的時候一般不會恰好是窗口的中心店,這樣就會造成在第一次計算偏移量的時候的一大步跳躍。改進的方法就是將鼠標最開始進入到我們程序窗口的坐標作為鼠標的初始坐標:

    if(firstMouse) // this bool variable is initially set to true {lastX = xpos;lastY = ypos;firstMouse = false; }

    最終的鼠標響應函數為:

    void mouse_callback(GLFWwindow* window, double xpos, double ypos) {if(firstMouse){lastX = xpos;lastY = ypos;firstMouse = false;}GLfloat xoffset = xpos - lastX;GLfloat yoffset = lastY - ypos; lastX = xpos;lastY = ypos;GLfloat 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); }

    Ok!這樣就好了!用我們的鼠標就可以讓我們的場景不斷地旋轉了!代碼.

    縮放 Zoom

    我們還想要為我們的攝像機系統再加入一點縮放功能的接口。前面的教程中我們講到可以利用fov來定義我們可見程序窗口的尺寸。當視口變小,那么投影出來的對象也就變小了,看上去就是縮小的效果。我們想要利用鼠標轉動滾輪來達到縮放的功能。像上面的鍵盤函數和鼠標移動函數,我們首先定義一個鼠標滾輪滾動的響應函數:

    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代表了我們垂直滾動量。我們借用這個值來修改我們全局設定的fov值,因為45.0ffov的默認值。我們設定縮放范圍是1.0f到45.0f。

    我們將fov作為參數傳遞進我們的透視投影矩陣生成函數中:

    projection = glm::perspective(fov, (GLfloat)WIDTH/(GLfloat)HEIGHT, 0.1f, 100.0f);

    最后別忘了將鼠標滾輪函數注冊上。

    glfwSetScrollCallback(window, scroll_callback);

    這樣應該就能夠實現鼠標滾輪滾動來縮放場景的效果了。

    原作者指出利用歐拉角實現攝像機系統還是比較low的,他還有更好的方法,好吧,我原來用的方法還沒有歐拉角好。只能先膜拜了!好消息是后面他會講到更好的方法。

    攝像機類 Camera class

    在后面的教程中,我們會經常使用改變攝像機的參數來達到我們想要的效果。但是正像這次教程講的這整個過程,攝像機系統的建立還是比較復雜的。所以原作者封裝了一個攝像機類以方便后面的使用。這是代碼。

    像前面的shader類一樣,這個攝像機類也是只有一個頭文件,我們想要使用的時候,直接包含這個頭文件就好了,其中的代碼相信完整看過上面步驟的人都能夠很輕松看懂。

    利用這個攝像機類(頭文件)完成的上面相同效果的整個代碼在這兒。

    總結

    以上是生活随笔為你收集整理的【Modern OpenGL】摄像机系统 Camera的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。