OpenGL 學習實錄4: 坐标系統 & 錄影機
文章目錄
- OpenGL 學習實錄4: 坐标系統 & 錄影機
- 系列文章
- 正文
-
- 1. 坐标系統變換
- 2. 矩陣運算庫 glm
- 3. 建構錄影機
-
- 3.1 更多盒子(模型矩陣)
- 3.2 錄影機封裝
-
- 3.2.0 屬性解析
- 3.2.1 錄影機移動(鍵盤響應)
- 3.2.2 視角旋轉(滑鼠響應)
- 3.2.3 視角縮放(滑鼠滾輪響應)
- 3.2.4 建構觀察矩陣
- 3.3 投影矩陣裁切可見空間
- 3.4 更新頂點着色器
- 4. 最終效果
- 其他資源
-
- 參考連接配接
- 完整代碼示例
系列文章
- OpenGL 學習實錄1: 基于 MacOS + Clion 配置 OpenGL 運作環境
- OpenGL 學習實錄2: 基礎繪制初試
- OpenGL 學習實錄3: 深入着色器 - 紋理
正文
前一篇我們制作了一個盒子,并塗上兩張圖檔作為紋理,本篇将要介紹的則是繪制 3D 場景時通用的坐标系統變換方法,建構更多的盒子,并利用坐标變換來模拟錄影機的運轉、也就是模拟使用者視角的移動
1. 坐标系統變換
首先是坐标系統,預設的坐标系統為
- x 方向向
為正右
- y 方向向
為正上
- z 方向指向
為正螢幕外
但是我們要建構 3D 場景的時候就需要對整個模型進行映射,坐标系統分為以下幾個
-
(物體空間 Object Space)局部空間 Local Space
- 描述物體本身坐标系,通常以
為原點(0,0,0)
- 描述物體本身坐标系,通常以
-
世界空間 World Space
- 模拟物體在 3D 世界中的坐标,也就是改把我們的物體放到該放的地方(偏移),進行适當的變形(旋轉、縮放)
-
世界空間 = 模型矩陣 Model Matrix * Local Space
-
(錄影機空間 Camera Space / 視覺空間 Eye Space)觀察空間 View Space
- 使用者觀察世界的角度,也就是所謂的錄影機看向世界的角度(透過對整個場景進行變形來模拟錄影機的運作)
-
觀察空間 = 觀察矩陣 View Matrix * World Space
-
剪裁空間 Clip Space
- 使用者可見的視野範圍,也就是指定使用者可見的最近、最遠距離,将剪裁空間以外的物體舍棄
-
剪裁空間 = 投影矩陣 Projection Matrix * View Space
-
螢幕空間 Screen Space
- 最後螢幕空間就是投影到螢幕上的樣子,這個 OpenGL 會自動幫我們完成
2. 矩陣運算庫 glm
前面提到一堆坐标的變換,而這些坐标都是一些向量,變換都是一堆矩陣,我們将使用的向量/矩陣等運算的數學庫
glm
,下面是我們後續會用到的幾個常見函數
- 向量
glm::vecX
- 矩陣
glm::matX
- 平移
glm::translate(matrix, vec)
- 旋轉
glm::rotate(matrix, radian, vec)
- 縮放
glm::scale(matrix, vec)
- 正射投影
glm::ortho(sx, tx, sy, ty, sz, tz)
- 透視投影
glm::perspective(fov, w/h, sz, tz);
3. 建構錄影機
接下來我們會經曆幾個階段
- 定義物體模型,也就是每個方塊的原始坐标、顔色等
- 定義物體位置,使用
将每個物體映射到目标位置 = 世界空間模型矩陣
- 接下來模拟錄影機的運作,使用
将整個場景進行變形 = 觀察空間觀察矩陣
- 最後根據觀察空間,我們使用
對觀察空間的物體進行裁切 = 剪裁空間投影矩陣
- 接下來 OpenGL 會負責将剪裁空間内的物體繪制到我們的 2D 螢幕上啦
3.1 更多盒子(模型矩陣)
首先前面我們已經定義過一個基礎的盒子對象了,這時候我們擴充一下,變出十個盒子
- 頂點坐标 & 索引數組
float vertices[] = {
// position // texture
-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, 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, 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, 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, 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, 0.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, 0.0f, 0.0f,
};
unsigned int indices[] = {
0, 1, 2,
2, 3, 0,
4, 5, 6,
6, 7, 4,
8, 9, 10,
10, 11, 8,
12, 13, 14,
14, 15, 12,
16, 17, 18,
18, 19, 16,
20, 21, 22,
22, 23, 20,
};
- 建構緩沖對象
unsigned int VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
- 循環内渲染盒子對象
上面的所有頂點和索引隻能建構一個盒子,接下來我們定義十個盒子,并分别指定每個的實際坐标偏移
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)
};
然後渲染時構模組化型矩陣進行映射
for (unsigned int i = 0; i < 10; i++) {
glm::mat4 model = glm::mat4(1.0f);
// 偏移
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i; // 起始旋轉角度 20 * i
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
GLint modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}
這樣應該就能看到畫面上有 10 個盒子了
3.2 錄影機封裝
完成了局部空間到世界空間的映射,也就是初步建構好我們的世界空間場景了,接下來要進行觀察空間的映射,也就是創造一個錄影機
這裡我們寫的是一種 FPS 錄影機,存在一些限制,代碼裡面會提到
首先建構一個
camera.h
頭檔案
-
camera.h
//
// Created by 超悠閒 on 2021/10/20.
//
#ifndef OPEN_GL_CAMERA_COORDINATE_CAMERA_H
#define OPEN_GL_CAMERA_COORDINATE_CAMERA_H
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
enum Camera_Movement {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
// Default camera values
const float DEFAULT_YAW = -90.0f;
const float DEFAULT_PITCH = 0.0f;
const float DEFAULT_SPEED = 5.0f;
const float DEFAULT_SENSITIVITY = 0.1f;
const float DEFAULT_ZOOM = 45.0f;
class Camera {
public:
glm::vec3 position; // 相機位置
glm::vec3 front; // 相機前景中心
glm::vec3 up; // 相機上向量
glm::vec3 right; // 相機右向量
glm::vec3 worldUp; // 世界空間上向量
float yaw; // 水準旋轉角
float pitch; // 鏡頭仰角
float moveSpeed; // 移動速度
float mouseSensitivity; // 滑鼠靈敏度
float zoom; // 鏡頭縮放
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = DEFAULT_YAW,
float pitch = DEFAULT_PITCH);
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch);
glm::mat4 GetViewMatrix();
void ProcessKeyboard(Camera_Movement direction, float deltaTime);
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true);
void ProcessMouseScroll(float yoffset);
private:
void updateCameraVectors();
};
#endif //OPEN_GL_CAMERA_COORDINATE_CAMERA_H
後面則是這個檔案的實作
3.2.0 屬性解析
在開始之前我們先來仔細看一下錄影機類都有哪些屬性要用
glm::vec3 position; // 相機位置
glm::vec3 front; // 相機前景中心
glm::vec3 up; // 相機上向量
glm::vec3 right; // 相機右向量
glm::vec3 worldUp; // 世界空間上向量
float yaw; // 水準旋轉角
float pitch; // 鏡頭仰角
float moveSpeed; // 移動速度
float mouseSensitivity; // 滑鼠靈敏度
float zoom; // 鏡頭縮放
首先我們會使用一個錄影機的坐标
position
,然後定義一個錄影機前方機關長度的坐标
front
,來定位錄影機的朝向,接下來我們可以根據世界空間的向上向量
worldUp
與相機法線向量
position - front
叉乘得到右向量
right
,最後再用方向向量與右向量叉乘得到上向量,透過使用這三個互相垂直的向量,我們就能夠表現任意角度的視角
而下面幾個變量則是表示一些基礎量,并且後續将根據使用者操作來改變值
3.2.1 錄影機移動(鍵盤響應)
第一種操作是錄影機本身進行前後左右的移動
void Camera::ProcessKeyboard(Camera_Movement direction, float deltaTime) {
float velocity = this->moveSpeed * deltaTime;
if (direction == FORWARD) {
this->position += this->front * velocity;
} else if (direction == BACKWARD) {
this->position -= this->front * velocity;
} else if (direction == LEFT) {
this->position -= this->right * velocity;
} else if (direction == RIGHT) {
this->position += this->right * velocity;
}
}
本質上就是根據移動方向改變相機位置
position
3.2.2 視角旋轉(滑鼠響應)
第二種是視角的旋轉,我們透過檢查滑鼠的移動來模拟,上下移動改變仰角,左右移動改變視角
void Camera::ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch) {
xoffset *= this->mouseSensitivity;
yoffset *= this->mouseSensitivity;
this->yaw += xoffset;
this->pitch += yoffset;
// make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch) {
if (this->pitch > 89.0f) {
this->pitch = 89.0f;
} else if (this->pitch < -89.0f) {
this->pitch = -89.0f;
}
}
// update Front, Right and Up Vectors using the updated Euler angles
this->updateCameraVectors();
}
-
表示垂直仰角yaw
-
表示水準視角pitch
由這兩個向量我們可以計算出更新後的方向向量
void Camera::updateCameraVectors() {
glm::vec3 front;
front.x = cos(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
front.y = sin(glm::radians(this->pitch));
front.z = sin(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
this->front = glm::normalize(front);
this->right = glm::normalize(glm::cross(this->front, this->worldUp));
this->up = glm::normalize(glm::cross(this->right, this->front));
}
3.2.3 視角縮放(滑鼠滾輪響應)
視角的縮放則要放到投影矩陣的部分才會用到,這裡僅僅隻是記錄使用者的操作
void Camera::ProcessMouseScroll(float yoffset) {
float zoom = this->zoom -= (float) yoffset;
if (zoom < 1.0f) {
this->zoom = 1.0f;
} else if (zoom > 45.0f) {
this->zoom = 45.0f;
}
}
3.2.4 建構觀察矩陣
到此觀察矩陣所需的要素都備齊了,接下來使用
lookAt
來生成觀察矩陣
glm::mat4 Camera::GetViewMatrix() {
return glm::lookAt(this->position, this->position + this->front, this->up);
}
并且修改一下主入口的代碼
-
/src/main.cpp
首先建構一個錄影機
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
然後加上幾個操作監聽函數
int main() {
// ...
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // 鎖定滑鼠
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// ...
while (!glfwWindowShouldClose(window)) {
processInput(window);
先使用
glfwSetInputMode
隐藏使用者滑鼠,滑鼠移動使用
glfwSetCursorPosCallback
設定監聽函數,滾輪使用
glfwSetScrollCallback
響應,使用者輸入跟之前一樣使用
processInput
進行響應
對于滑鼠移動,我們計算好偏移量之後更新錄影機
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;
camera.ProcessMouseMovement(xOffset, yOffset);
}
縮放效果一樣
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset) {
camera.ProcessMouseScroll(yoffset);
}
最後在渲染過程中建立使用
觀察矩陣
,将世界空間映射到觀察空間當中
glm::mat4 view = camera.GetViewMatrix();
GLint viewLoc = glGetUniformLocation(ourShader.ID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
3.3 投影矩陣裁切可見空間
最後一部分是前面我們使用滾輪操作和錄影機記錄了使用者的縮放行為,最後我們隻需要建構一個投影矩陣就能夠看到正确的剪裁空間了
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(camera.zoom), (float) SCR_WIDTH / (float) SCR_HEIGHT, 0.5f, 100.0f);
GLint projectionLoc = glGetUniformLocation(ourShader.ID, "projection");
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
3.4 更新頂點着色器
模型搭建好了,坐标系統映射也全部準備好了,最後就在頂點着色器裡面加入一半常見的通用坐标變換系統啦
-
vertex.glsl
#version 410 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0f);
TexCoord = aTexCoord;
}
4. 最終效果
最終效果我們可以使用
WSAD
進行前後左右移動,使用滑鼠移動模拟視角的旋轉,最後使用滑鼠的滾輪模拟縮放效果,給出幾個效果圖如下
- 從正面看
- 從右側看
- 從背面看
其他資源
參考連接配接
Title | Link |
---|---|
錄影機 - LearnOpenGL CN | https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/ |
完整代碼示例
https://github.com/superfreeeee/Blog-code/tree/main/others/open_gl/open_gl_camera_coordinate