iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
0
自我挑戰組

寫遊戲初體驗系列 第 14

Day 14 [OpenGL] Coordinate

  • 分享至 

  • xImage
  •  

Coordinate

OpenGL 座標在經過 Shader 轉換之後,所有的座標都會變標準化設備座標 Normalized Device Coordinates (NDC),也就是所有的x y z都在$[-1.0, 1.0]$之間。超出此範圍的座標就不會顯示。

座標在被轉換成螢幕座標(Screen-Space)時還會經過多次轉換

  • 座標系統
    • 局部空間 Local Space 或是 物體空間(Object Space)
    • 世界空間 World Space
    • 觀察空間 View Space 或 Eye Space
    • 裁剪空間 Clip Space
    • 螢幕空間 Screen Space


為了將坐標從一個坐標系變換到另一個坐標系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣

  • Local Space
    • 物體相對於局部原點的座標
  • World Space
    • 相對於世界的原點,會和其他物體相對於原點擺放
  • View Space
    • 從 Camera (觀察者) 的角度進行觀察的頂點座標
  • Clip Space
    • View Space 經過投影後,裁剪座標會被處理到 NDC 空間 $[-1.0, 1.0]$
  • Screen Space
    • 經過 Viewport Transform,將 $[-1.0, 1.0]$ 內的座標變換到 glViewport 所定義的座標範圍內
    • 出來的座標會送到 Rasterizer 變成 Fragment (Pixel)

之所以把頂點在不同坐標系中轉換,是因為有些操作在特定的坐標系中才有意義且方便。例如,需要對物體修改時,在 Local Space 比較方便;如果需要相對其他物體時,在 World Space 比較方便。

局部空間

局部空間是指物件所在的坐標空間,一個物體的原點$(0,0,0)$
但最後可能出現在世界的不同地方(座標),它的所有頂點都是相對於 Local Space 的原點,這些座標都是局部(Local)的。

世界空間

頂點相對於遊戲世界原點的座標。從 Local Space 變換到 World Space 是由 Model Matrix 來完成的
把物體從 Local Space 經過位移、縮放、旋轉來把物體擺在世界的位置

View Space 觀察空間

觀察空間是將世界座標轉成使用者視野(攝影機 Camera)前方的座標。經由位移和旋轉場景,使得特定的物體變換到攝影機(Camera)的前方,此種變換的矩陣稱作 View Matrix

Clip Space 裁剪空間

在經過 Vertex Shader 後 OpenGL 希望所有的座標都在 $[-1.0, 1.0]$ 內,超出的會被裁剪(Clip) 掉(忽略掉),在此座標內的頂點最後會被光柵化成 Fragment

為了將頂點座標變換到 NDC 空間,要經由 Projection Matrix 投影矩陣,它指定了一個範圍的座標 e.g. [-1000, 1000],投影矩陣會將在範圍內的座標轉成 NDC。超出範圍的座標,轉出的座標會超出 -1.0 或 1.0,最後會被剪裁掉。

因此會有個範圍內的頂點都會被轉換,這個範圍叫做 觀察箱Viewing Box又稱作 Frustum

這個轉換的過程稱作 投影 Projection

有兩種不同的投影方式:正射投影、透視投影

正射投影 Orthographic Projection

正射投影定義了一個像長方體的觀察箱,超出這個觀察箱外的頂點會被剪裁掉,在此觀察箱內的頂點會被轉成 NDC 座標。
由寬、高及近(Near)平面和遠(Far)平面定義

// glm::ortho(left, right, bottom, top, zNear, zFar)
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

:::spoiler Details

出來的矩陣是:
$\begin{pmatrix}
\frac{2}{\text{right}-\text{left}} & 0 & 0 & t_x \
0 & \frac{2}{\text{top} - \text{bottom}} & 0 & t_y \
0 & 0 & \frac{2}{\text{zFar} - \text{zNear}} & t_z \
0 & 0 & 0 & 1
\end{pmatrix}$
$t_x = - \frac{\text{right}+\text{left}}{\text{right}-\text{left}} \
t_y = - \frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} \
t_z = - \frac{\text{zFar} + \text{zNear}}{\text{zFar} - \text{zNear}}$

TODO 理解

:::

透視投影 Perspective Projection

現實中,離你越遠的東西看起來更小。這個奇怪的效果稱之為透視(Perspective)。透視的效果在我們看一條無限長的高速公路或鐵路時尤其明顯,正如下面圖片顯示的那樣:

正如你看到的那樣,由於透視,這兩條線在很遠的地方看起來會相交。這正是透視投影想要模仿的效果,它是使用透視投影矩陣來完成的。這個投影矩陣將給定的平截頭體範圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大。被變換到裁剪空間的坐標都會在-w到w的範圍之間(任何大於這個範圍的坐標都會被裁剪掉)。

  • Perspective Division 透視除法
    • 在轉換到 Clip Space 的頂點,每個座標的 $(x, y, z)$ 會都除上 $w$ 分量
    • 也就是 $(\frac{x}{w}, \frac{y}{w}, \frac{z}{w})$
    • 將 4D 的 Clip Space 座標轉換成 3D NDC 座標
    • 在 Vertex Shader 的最後會被 自動執行

glm::mat4 proj = glm::perspective(
    glm::radians(45.0f),       // Field Of View
    (float)width/(float)height // 寬高比
    , 0.1f, 100.0f);           // 近平面, 遠平面

比較

正射投影(Orthographic)不會產生透視(w分量是1.0)所以看起來遠處的物體跟近處是一樣的大小,主要用在渲染 2D 或是建築、工程、或建模的應用。

透視投影(Perspective)遠處的的物體就看起來較小。

組合在一起

一個頂點座標會經過下列算式轉成 Clip Space。注意矩陣運算式從右往左。

3D

隨然我們是要做2D的東西,但我們還是來嘗試一下3D的物品

在開始進行3D繪圖時,我們首先創建一個模型矩陣。這個模型矩陣包含了位移、縮放與旋轉操作,它們會被應用到所有物體的頂點上,以變換它們到全局的世界空間。讓我們變換一下我們的平面,將其繞著x軸旋轉,使它看起來像放在地上一樣。這個模型矩陣看起來是這樣的:

glm::mat4 model(1.f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec(1.0f, 0.0f, 0.0f));
// 繞 x 軸旋轉 -55 度

OpenGL 的座標是右手坐標系

我們先假設我們的 Camera,也就是看出去的座標是固定不動的。我們希望看到 Camera 放在 ${0, 0, 3}$看出去的效果,我們就將整個物體往反方向(0, 0, -3)移動就好了

glm::mat4 view(1.f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

最後我們希望的透視效果

glm::mat4 projection(1.0f);
projection = glm::perspective(glm::radians(45.0f), width / height, 0.1f, 100.f);

接著傳入 Shader 中

#version 330 core
layout (location = 0) in vec3 aPos;
// ...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 注意矩陣乘法
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ...
}

最終的結果:

立方體

要畫一個3D立方體要36個點,所以改用glDrawArrays
並且我們讓他隨著時間旋轉。

這的確有點像是一個立方體,但又有種說不出的奇怪。立方體的某些本應被遮擋住的面被繪製在了這個立方體其他面之上。之所以這樣是因為OpenGL是一個三角形一個三角形地來繪製你的立方體的,所以即便之前那裡有東西它也會覆蓋之前的像素。因為這個原因,有些三角形會被繪製在其它三角形上面,雖然它們本不應該是被覆蓋的。

幸運的是,OpenGL存儲深度信息在一個叫做Z緩衝(Z-buffer)的緩衝中,它允許OpenGL決定何時覆蓋一個像素而何時不覆蓋。通過使用Z緩衝,我們可以配置OpenGL來進行深度測試。

Z-Buffer

OpenGL 的 Z-Buffer (Z 緩衝)讓 OpenGL 知道一個像素的深度 (Depth)(可以想像成圖層那樣),進行深度測試(Depth Test)。

  • 啟用 Depth Test

    glEnable(GL_DEPTH_TEST);
    
  • 每一 frame 都要清掉 Z-Buffer 的內容

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    

這樣看起來就這正常了吧

程式碼在這裡

多個立方體

如果想要在螢幕上畫出10個立方體,首先我們要定義每個立方體的位移量。

std::vector<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)
};

接在遊戲循環的時候,調用glDrawArrays10次

for(int i = 0; i < cubePositions.size(); i++)
{
    float angle = 20.0f * i;
    model = glm::translate(model, glm::vec3(cubePositions[i]));
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 3.0f, 5.0f));
    UploadMat4("vModel", model);
    model = glm::mat4(1.f);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

就會長這樣

參考資料

https://learnopengl.com/Getting-started/Coordinate-Systems


上一篇
Day 13 [OpenGL] Transformations
下一篇
Day 15 [OpenGL] Camera
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言