1. 程式人生 > >Learn OpenGL (七):攝像機

Learn OpenGL (七):攝像機

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矩陣了:

其中RR是右向量,UU是上向量,DD是方向向量PP是攝像機位置向量。注意,位置向量是相反的,因為我們最終希望把世界平移到與我們自身移動的相反方向。把這個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,在每次渲染迭代中使用GLFW的glfwGetTime函式重新建立觀察矩陣,來擴大這個圓。

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)); 
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變數返回大小不同的向量。如果我們不對向量進行標準化,我們就得根據攝像機的朝向不同加速或減速移動了,但如果進行了標準化移動就是勻速的。

移動速度

目前我們的移動速度是個常量。理論上沒什麼問題,但是實際情況下根據處理器的能力不同,有些人可能會比其他人每秒繪製更多幀,也就是以更高的頻率呼叫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;
  ...
}

尤拉角

尤拉角(Euler Angle)是可以表示3D空間中任何旋轉的3個值,由萊昂哈德·尤拉(Leonhard Euler)在18世紀提出。一共有3種尤拉角:俯仰角(Pitch)、偏航角(Yaw)和滾轉角(Roll),下面的圖片展示了它們的含義:

俯仰角是描述我們如何往上或往下看的角,可以在第一張圖中看到。第二張圖展示了偏航角,偏航角表示我們往左和往右看的程度。滾轉角代表我們如何翻滾攝像機,通常在太空飛船的攝像機中使用。每個尤拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了。

俯仰角是描述我們如何往上或往下看的角,可以在第一張圖中看到。第二張圖展示了偏航角,偏航角表示我們往左和往右看的程度。滾轉角代表我們如何翻滾攝像機,通常在太空飛船的攝像機中使用。每個尤拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了。

對於我們的攝像機系統來說,我們只關心俯仰角和偏航角,所以我們不會討論滾轉角。給定一個俯仰角和偏航角,我們可以把它們轉換為一個代表新的方向向量的3D向量。俯仰角和偏航角轉換為方向向量的處理需要一些三角學知識,我們先從最基本的情況開始:

如果我們把斜邊邊長定義為1,我們就能知道鄰邊的長度是cos x/h=cos x/1=cos xcos⁡ x/h=cos⁡ x/1=cos⁡ x,它的對邊是sin y/h=sin y/1=sin ysin⁡ y/h=sin⁡ y/1=sin⁡ y。這樣我們獲得了能夠得到x和y方向長度的通用公式,它們取決於所給的角度。我們使用它來計算方向向量的分量:

這個三角形看起來和前面的三角形很像,所以如果我們想象自己在xz平面上,看向y軸,我們可以基於第一個三角形計算來計算它的長度/y方向的強度(Strength)(我們往上或往下看多少)。從圖中我們可以看到對於一個給定俯仰角的y值等於sin θsin⁡ θ:

direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉為弧度

這裡我們只更新了y值,仔細觀察x和z分量也被影響了。從三角形中我們可以看到它們的值等於:

direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));

看看我們是否能夠為偏航角找到需要的分量:

就像俯仰角的三角形一樣,我們可以看到x分量取決於cos(yaw)的值,z值同樣取決於偏航角的正弦值。把這個加到前面的值中,會得到基於俯仰角和偏航角的方向向量:

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 譯註:direction代表攝像機的前軸(Front),這個前軸是和本文第一幅圖片的第二個攝像機的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

這樣我們就有了一個可以把俯仰角和偏航角轉化為用來自由旋轉視角的攝像機的3維方向向量了。你可能會奇怪:我們怎麼得到俯仰角和偏航角?

滑鼠輸入

偏航角和俯仰角是通過滑鼠(或手柄)移動獲得的,水平的移動影響偏航角,豎直的移動影響俯仰角。它的原理就是,儲存上一幀滑鼠的位置,在當前幀中我們當前計算滑鼠位置與上一幀的位置相差多少。如果水平/豎直差別越大那麼俯仰角或偏航角就改變越大,也就是攝像機需要移動更多的距離。

首先我們要告訴GLFW,它應該隱藏游標,並捕捉(Capture)它。捕捉光標表示的是,如果焦點在你的程式上(譯註:即表示你正在操作這個程式,Windows中擁有焦點的程式標題欄通常是有顏色的那個,而失去焦點的程式標題欄則是灰色的),游標應該停留在視窗中(除非程式失去焦點或者退出)。我們可以用一個簡單地配置呼叫來完成:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

在呼叫這個函式之後,無論我們怎麼去移動滑鼠,游標都不會顯示了,它也不會離開視窗。對於FPS攝像機系統來說非常完美。

為了計算俯仰角和偏航角,我們需要讓GLFW監聽滑鼠移動事件。(和鍵盤輸入相似)我們會用一個回撥函式來完成,函式的原型如下:

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

這裡的xpos和ypos代表當前滑鼠的位置。當我們用GLFW註冊了回撥函式之後,滑鼠一移動mouse_callback函式就會被呼叫:

glfwSetCursorPosCallback(window, mouse_callback);

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

  1. 計算滑鼠距上一幀的偏移量。
  2. 把偏移量新增到攝像機的俯仰角和偏航角中。
  3. 對偏航角和俯仰角進行最大和最小值的限制。
  4. 計算方向向量。

第一步是計算滑鼠自上一幀的偏移量。我們必須先在程式中儲存上一幀的滑鼠位置,我們把它的初始值設定為螢幕的中心(螢幕的尺寸是800x600):

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;

注意我們把偏移量乘以了sensitivity(靈敏度)值。如果我們忽略這個值,滑鼠移動就會太大了;你可以自己實驗一下,找到適合自己的靈敏度值。

接下來我們把偏移量加到全域性變數pitch和yaw上:

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

計算出來的方向向量就會包含根據滑鼠移動計算出來的所有旋轉了。由於cameraFront向量已經包含在GLM的lookAt函式中,我們這就沒什麼問題了。

如果你現在執行程式碼,你會發現在視窗第一次獲取焦點的時候攝像機會突然跳一下。這個問題產生的原因是,在你的滑鼠移動進視窗的那一刻,滑鼠回撥函式就會被呼叫,這時候的xpos和ypos會等於滑鼠剛剛進入螢幕的那個位置。這通常是一個距離螢幕中心很遠的地方,因而產生一個很大的偏移量,所以就會跳了。我們可以簡單的使用一個bool變數檢驗我們是否是第一次獲取滑鼠輸入,如果是,那麼我們先把滑鼠的初始位置更新為xpos和ypos值,這樣就能解決這個問題;接下來的滑鼠移動就會使用剛進入的滑鼠位置座標來計算偏移量了:

if(firstMouse) // 這個bool變數初始時是設定為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);
}

縮放

作為我們攝像機系統的一個附加內容,我們還會來實現一個縮放(Zoom)介面。在之前的教程中我們說視野(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;
}

當滾動滑鼠滾輪的時候,yoffset值代表我們豎直滾動的大小。當scroll_callback函式被呼叫後,我們改變全域性變數fov變數的內容。因為45.0f是預設的視野值,我們將會把縮放級別(Zoom Level)限制在1.0f45.0f

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

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

最後不要忘記註冊滑鼠滾輪的回撥函式:

glfwSetScrollCallback(window, scroll_callback);

現在,我們就實現了一個簡單的攝像機系統了,它能夠讓我們在3D環境中自由移動。

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <learnopengl/filesystem.h>
#include <learnopengl/shader_m.h>

#include <iostream>
using namespace std;
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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

bool firstMouse = true;
float yaw   = -90.0f;	// yaw is initialized to -90.0 degrees since a yaw of 0.0 results in a direction vector pointing to the right so we initially rotate a bit to the left.
float pitch =  0.0f;
float lastX =  800.0f / 2.0;
float lastY =  600.0 / 2.0;
float fov   =  45.0f;

// timing
float deltaTime = 0.0f;	// time between current frame and last frame
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    //fwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile our shader zprogram
    // ------------------------------------
    Shader ourShader("7.3.camera.vs", "7.3.camera.fs");

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    // world space positions of our cubes
    glm::vec3 cubePositions[] = {
        glm::vec3( 0.0f,  0.0f,  0.0f),
        glm::vec3( 2.0f,  5.0f, -15.0f),
        glm::vec3(-1.5f, -2.2f, -2.5f),
        glm::vec3(-3.8f, -2.0f, -12.3f),
        glm::vec3( 2.4f, -0.4f, -3.5f),
        glm::vec3(-1.7f,  3.0f, -7.5f),
        glm::vec3( 1.3f, -2.0f, -2.5f),
        glm::vec3( 1.5f,  2.0f, -2.5f),
        glm::vec3( 1.5f,  0.2f, -1.5f),
        glm::vec3(-1.3f,  1.0f, -1.5f)
    };
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // texture coord attribute
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);


    // load and create a texture 
    // -------------------------
    unsigned int texture1, texture2;
    // texture 1
    // ---------
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
    unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);
    // texture 2
    // ---------
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0);
    if (data)
    {
        // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    // tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
    // -------------------------------------------------------------------------------------------
    ourShader.use();
    ourShader.setInt("texture1", 0);
    ourShader.setInt("texture2", 1);


    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

        // bind textures on corresponding texture units
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);

        // activate shader
        ourShader.use();

        // pass projection matrix to shader (note that in this case it could change every frame)
        glm::mat4 projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        ourShader.setMat4("projection", projection);

        // camera/view transformation
        glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        ourShader.setMat4("view", view);

        // render boxes
        glBindVertexArray(VAO);
        for (unsigned int i = 0; i < 10; i++)
        {
            // calculate the model matrix for each object and pass it to shader before drawing
            glm::mat4 model;
            model = glm::translate(model, cubePositions[i]);
            float angle = 20.0f * i;
            model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
            ourShader.setMat4("model", model);

            glDrawArrays(GL_TRIANGLES, 0, 36);
        }

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    float cameraSpeed = 2.5 * deltaTime;
    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;
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
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; // opengl視窗左上角是原點
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.1f; // change this value to your liking
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw += xoffset;
    pitch += yoffset;

    // make sure that when pitch is out of bounds, screen doesn't get flipped
    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);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
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;
}