攝像機

最近在學習OpenGL,把學習的一些過程寫在這里,希望與大家共同分享討論。歡迎光臨我的個人網站Orient一起討論學習。這里是我的GitHub,如果您喜歡,不妨點個贊??

在OpenGL中并沒有攝像機(Camera)的概念,因此引入一個虛擬的攝像機,并自定義一個攝像機類。

攝像機/觀察空間

在討論攝像機/觀察空間(Camera/View Space),其實就討論以攝像機的視角作為場景原點時場景中所有頂點的坐標。使用觀察矩陣可以把所有的世界坐標變幻為相對于攝像機位置與方向的觀察坐標。因此定義一個攝像機,我們需要它在世界中的位置、觀察方向、一個指向它右側的向量以及一個指向它上方的向量。其實就是以攝像機為原點創建了一個如下圖所示的坐標系:

點擊顯示更清晰

1、攝像機位置

獲取攝像機位置很簡單,就是世界空間中一個指向攝像機位置的向量。我們仍然設置為之前的攝像機位置:

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

注意Z軸的方向

2、攝像機方向

我們先讓攝像機指向世界場景原點: (0, 0, 0)。攝像機方向由攝像機的坐標減去它指向點的坐標得到:

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

注意:其實攝像機方向與攝像機的指向剛好相反

3、右軸

我們需要獲取一個右向量(Right Vector),它代表攝像機空間的X軸正方向。右向量的獲取:先定義一個世界空間坐標中的上向量(Up Vector),然后將其與上步得到的攝像機方向向量進行叉乘:

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

如果兩向量的叉乘順序交換將會得到方向相反的一個X軸負方向向量

4、上軸

將攝像機方向向量與右向量叉乘即可得到:

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

我們已經創建了所有構成觀察/攝像機空間的向量。接下來我們使用這些向量就可以構建一個LookAt矩陣

LookAt

使用矩陣的好處之一:如果使用了3個相互垂直的軸定義了一個坐標空間,那么可以用這3個軸外加一個平移向量來創建一個矩陣,并且可以用這個矩陣乘以任何向量來將其變換到那個坐標空間。LookAt矩陣正是如此。

點擊顯示更清晰

其中R是右向量,U是上向量,D是方向向量,P是攝像機位置向量。(注意:位置向量是相反的,因為我們最終希望把世界平移到與我們自身移動的相反方向)這個LookAt矩陣可以很高效地把所有世界坐標變幻到剛定義的觀察空間。

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::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,方向是當前位置加上剛定義的方向向量。這樣就能保證無論怎么移動,攝像機都會注視著目標方向。

在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;
}

這里對右向量進行了標準化。如果沒有對這個向量進行標準化,最后的叉乘結果很根據cameraFront變量返回大小不同的變量。攝像機移動的速度將會發生改變。標準化后,攝像機移動就是勻速的

移動速度

上面的移動速度是個常量,講道理是會一直勻速移動沒有問題。但實際情況是每個處理器的能力不同,有些人每秒繪制的幀數可能要比別人多,或者少,也就是以更高/低的頻率調用processInput函數。結果就是有些人可能移動很快,有些很慢。

圖形程序和游戲通常會跟蹤一個時間差(Deltatime)變量,它存儲了渲染上一幀所用時間。我們把所有速度都去乘以deltaTime值,結果就是如果我們的deltaTime很大,就意味著上一幀的渲染花費了更多時間,所以這一幀的速度需要更高去平衡渲染所花去的時間。因此我們得到一個相對穩定的移動速度:

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

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

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

因此我們的得到了一個更流暢點的攝像機系統。

視角移動

我們根據鼠標的輸入改變cameraFront向量,從而可以改變攝像機的視角。

攝像機視角的改變可以通過改變歐拉角:俯仰角(Pitch)、偏航角(Yaw)和滾轉角(Roll):

點擊顯示更清晰

俯仰角描述我們如何往上或往下看的角。偏航角表示我們往左和往右看的程度。滾轉角代表我們如何翻滾攝像機。在這里我們不涉及翻滾角

我們通過三角函數來將角度轉換為方向向量:

基于俯仰角:

direction.y = sin(glm::radians(pitch)); // 先把角度轉為弧度
direction.x = cos(glm::radians(pitch)); 
direction.z = cos(glm::radians(pitch)); /

基于俯仰角與偏航角:

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

鼠標輸入

偏航角與俯仰角是通過鼠標/手柄移動獲得的。水平移動影響偏航角,豎直移動影響俯仰角。原理就是:存儲上一幀鼠標的位置,在當前幀中計算鼠標位置與上一幀的位置相差多少。如果水平/豎直差別越大那么俯仰角或偏航角就改變越大。

首先告訴GLFW隱藏光標并捕捉它:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

接下來申明一個鼠標監聽函數:

/*
 *p2、p3:鼠標當前位置
 */
void mouse_callback(GLFWwindow *window, double xpos, double ypos)

當用GLFW注冊了回調函數后,鼠標一移動mouse_callback函數就會被調用:

glfwSetCursorPosCallback(window, mouse_callback);

在處理FPS風格攝像機的鼠標輸入時,必須在最終獲取方向向量之前做下面幾步:

  • 計算鼠標距上一幀的偏移量
  • 把偏移量添加到攝像機的俯仰角和偏航角中
  • 對偏航角和俯仰角進行最大和最小值的限制
  • 計算方向向量

第一步是計算鼠標自上一幀的偏移量。須先在程序中存儲上一幀鼠標的位置,這里將其設置在屏幕中心(屏幕尺寸:800 x 600):

float lastX = 400, lastY = 300;

然后在鼠標回調函數中計算當前幀和上一幀鼠標位置的偏移量:

float xoffset = xpos - lastX;
float yoffset = lastY - ypos;   // y坐標是從底部往頂部依次增大
lastX = xpos;
lastY = ypos;

float sensitivity = 0.05f;  //靈敏度
xoffset *= sensitivity;
yoffset *= sensitivity;

接下來把偏移量加到全局變量pitchyaw上:

yaw += xoffset;
pitch += yoffset;

第三部給攝像機添加一些限制,防止其發生奇怪的移動(同時避免一些奇怪的問題)。對于俯仰角,要讓用戶不能看向高于89度的地方(在90度時視角發生逆轉,所以把89度作為極限),同樣也不允許小于-89度。在值超過極限值的時候將其改為極限值來實現:

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

最后一步,通過俯仰角和偏航角計算得到真正的方向向量:

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

為了防止在運行代碼開始,窗口第一次獲取焦點時攝像機的抖動,設置一個bool變量檢驗是否第一次獲取鼠標輸入,若是,則把鼠標初始位置更新為xposypos值:

if (firstMouse) //這個變量初始值是true
{
    lastX = xpos;
    lastY = ypos;
    firstMouse = false;
}

最后整理代碼如下:

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if(firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

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

縮放

視野(Field of View)或fov定義了我們能夠看到的場景范圍。當視野變小時,場景投影出來的空間就會見效,產生放大(Zoom In)的感覺,這里使用鼠標的滾輪來放大。同樣申明一個鼠標滾輪的回調函數:

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

以上設置了縮放范圍限制在1.0f45.0f之間

現在須每一幀都必須把透視矩陣上傳到GPU,但現在使用fov變量作為它的視野:

projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);

記得注冊鼠標滾輪回調函數:

glfwSetScrollCallback(window, scroll_callback);

攝像機類

最后的最后,我們把這個攝像機進行一次封裝,以便以后調用。攝像機類可以在這里找到。

攝像機完整的項目文件可以在這里找到。

如果本項目對您有所幫助,希望能夠獲得您的 star。萬分感謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容