最近在學習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;
接下來把偏移量加到全局變量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);
為了防止在運行代碼開始,窗口第一次獲取焦點時攝像機的抖動,設置一個bool
變量檢驗是否第一次獲取鼠標輸入,若是,則把鼠標初始位置更新為xpos
和ypos
值:
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.0f
和45.0f
之間
現在須每一幀都必須把透視矩陣上傳到GPU,但現在使用fov變量作為它的視野:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
記得注冊鼠標滾輪回調函數:
glfwSetScrollCallback(window, scroll_callback);
攝像機類
最后的最后,我們把這個攝像機進行一次封裝,以便以后調用。攝像機類可以在這里找到。
攝像機完整的項目文件可以在這里找到。
如果本項目對您有所幫助,希望能夠獲得您的 star。萬分感謝!