一、坐標系統概述
本文類容見LearnOpenGL CN。直接copy過來留個存檔。
OpenGL希望每次頂點着色后,我們的可見頂點都為標准化設備坐標(Normalized Device Coordinate,NDC)。也就是說每個頂點的\(z,y,z\)都應該在\(-1\)到\(1\)之間,超出這個范圍的頂點將是不可見的。通常情況下我們會自己設定一個坐標范圍,之后再在頂點着色器中將這些坐標變換為表轉化設備坐標。然后這些標化設備坐標傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標和像素。
將坐標變換為標准化設備坐標,接着再轉化為屏幕坐標的過程通常是分步進行的,也就是類似於流水線那樣子。在流水線中,物體的頂點在最終轉化為屏幕坐標之前還會被變換到多個坐標系統(Coordinate System)。將物體的坐標變換到幾個過渡坐標系(Intermediate Coordinate System)的優點在於,在這些特定的坐標系統中,一些操作或運算更加方便和容易,這一點很快就會變得很明顯。對我們來說比較重要的總共有5個不同的坐標系統
- 局部空間(Local Space,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺空間(Eye Space))
- 裁剪空間(Clip Space)
- 屏幕空間(Screen Space)
這就是一個頂點在最終被轉化為片段之前需要經歷的所有不同狀態。為了將坐標從一個坐標系變換到另一個坐標系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣。物體頂點的起始坐標再局部空間(Local Space),這里稱它為局部坐標(Local Coordinate),它在之后會變成世界坐標(world Coordinate),觀測坐標(View Coordinate),裁剪坐標(Clip Coordinate),並最后以屏幕坐標(Screen Corrdinate)的形式結束。下面這張圖闡釋了流程以及各個變換在做什么
有以下幾點需要注意
- 局部坐標是對象相對於原點的坐標,也是物體的起始坐標。
- 下一步將局部坐標轉化為世界空間坐標,世界空間坐標是一個處於更大空間范圍內的。這些坐標相對於世界的全局原點,它們會和其他物體一起相對於世界原點進行擺放。
- 接下來將世界坐標轉化為觀測坐標,使得每個坐標都是從攝像機或者說觀察者角度進行觀察的。
- 坐標到達觀測空間后,我們需要將其投影到裁剪坐標。裁剪坐標會被處理到\(-1.0\)到\(1.0\)范圍內,並判斷哪些點將會出現在屏幕上。
- 最后,我們將裁剪坐標變換為屏幕坐標,我們將使用一個叫做視口變換(Viewport Transform)的過程。視口變換將位於\(-1.0\)到\(1.0\)范圍的坐標變換到由
glViewport
函數所定義的坐標范圍內。最后變換出來的坐標將會送到光柵器,將其轉化為片段。
你可能已經大致了解了每個坐標空間的作用。我們之所以將頂點變換到各個不同的空間的原因是有些操作在特定的坐標系統中才有意義且更方便。例如,當需要對物體進行修改的時候,在局部空間中來操作會更說得通;如果要對一個物體做出一個相對於其它物體位置的操作時,在世界坐標系中來做這個才更說得通,等等。如果我們願意,我們也可以定義一個直接從局部空間變換到裁剪空間的變換矩陣,但那樣會失去很多靈活性。
二、坐標空間
2.1、局部空間
局部空間是指物體所在的坐標空間,即對象最開始所在的地方。想象你在一個建模軟件(比如說Blender)中創建了一個立方體。你創建的立方體的原點有可能位於(0, 0, 0),即便它有可能最后在程序中處於完全不同的位置。甚至有可能你創建的所有模型都以(0, 0, 0)為初始位置(譯注:然而它們會最終出現在世界的不同位置)。所以,你的模型的所有頂點都是在局部空間中:它們相對於你的物體來說都是局部的。
2.2、世界空間
如果我們將我們所有的物體導入到程序當中,它們有可能會全擠在世界的原點(0, 0, 0)上,這並不是我們想要的結果。我們想為每一個物體定義一個位置,從而能在更大的世界當中放置它們。世界空間中的坐標正如其名:是指頂點相對於世界的坐標。如果你希望將物體分散在世界上擺放(特別是非常真實的那樣),這就是你希望物體變換到的空間。物體的坐標將會從局部變換到世界空間;該變換是由模型矩陣(Model Matrix)實現的。
模型矩陣是一種變換矩陣,它能通過對物體進行位移、縮放、旋轉來將它置於它本應該在的位置或朝向。你可以將它想像為變換一個房子,你需要先將它縮小(它在局部空間中太大了),並將其位移至郊區的一個小鎮,然后在y軸上往左旋轉一點以搭配附近的房子。你也可以把上一節將箱子到處擺放在場景中用的那個矩陣大致看作一個模型矩陣;我們將箱子的局部坐標變換到場景/世界中的不同位置。
2.3 、觀測空間
觀察空間經常被人們稱之OpenGL的攝像機(Camera)(所以有時也稱為攝像機空間(Camera Space)或視覺空間(Eye Space))。觀察空間是將世界空間坐標轉化為用戶視野前方的坐標而產生的結果。因此觀察空間就是從攝像機的視角所觀察到的空間。而這通常是由一系列的位移和旋轉的組合來完成,平移/旋轉場景從而使得特定的對象被變換到攝像機的前方。這些組合在一起的變換通常存儲在一個觀察矩陣(View Matrix)里,它被用來將世界坐標變換到觀察空間。在下一節中我們將深入討論如何創建一個這樣的觀察矩陣來模擬一個攝像機。
2.4 、裁剪空間
2.4.1 過程
在一個頂點着色器運行的最后,OpenGL期望所有的坐標都能落在一個特定的范圍內,且任何在這個范圍之外的點都應該被裁剪掉(Clipped)。被裁剪掉的坐標就會被忽略,所以剩下的坐標就將變為屏幕上可見的片段。這也就是裁剪空間(Clip Space)名字的由來。
因為將所有可見的坐標都指定在\(-1.0\)到\(1.0\)的范圍內不是很直觀,所以我們會指定自己的坐標集(Coordinate Set)並將它變換回標准化設備坐標系,就像OpenGL期望的那樣。
為了將頂點坐標從觀察變換到裁剪空間,我們需要定義一個投影矩陣(Projection Matrix),它指定了一個范圍的坐標,比如在每個維度上的\(-1000\)到\(1000\)。投影矩陣接着會將在這個指定的范圍內的坐標變換為標准化設備坐標的范圍\((-1.0, 1.0)\)。所有在范圍外的坐標不會被映射到在\(-1.0\)到\(1.0\)的范圍之間,所以會被裁剪掉。在上面這個投影矩陣所指定的范圍內,坐標\((1250, 500, 750)\)將是不可見的,這是由於它的\(x\)坐標超出了范圍,它被轉化為一個大於\(1.0\)的標准化設備坐標,所以被裁剪掉了。
如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積(Clipping Volume),則OpenGL會重新構建這個三角形為一個或多個三角形讓其能夠適合這個裁剪范圍。
由投影矩陣創建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個出現在平截頭體范圍內的坐標都會最終出現在用戶的屏幕上。將特定范圍內的坐標轉化到標准化設備坐標系的過程(而且它很容易被映射到2D觀察空間坐標)被稱之為投影(Projection),因為使用投影矩陣能將3D坐標投影(Project)到很容易映射到2D的標准化設備坐標系中。
一旦所有頂點被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會執行,在這個過程中我們將位置向量的x,y,z分量分別除以向量的齊次w分量;透視除法是將4D裁剪空間坐標變換為3D標准化設備坐標的過程。這一步會在每一個頂點着色器運行的最后被自動執行。
在這一階段之后,最終的坐標將會被映射到屏幕空間中(使用glViewport中的設定),並被變換成片段。將觀察坐標變換為裁剪坐標的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。我們可以選擇創建一個正射投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)。
2.4.2 正射投影
正射投影矩陣定義了一個類似立方體的平截頭箱,它定義了一個裁剪空間,在這空間之外的頂點都會被裁剪掉。創建一個正射投影矩陣需要指定可見平截頭體的寬、高和長度。在使用正射投影矩陣變換至裁剪空間之后處於這個平截頭體內的所有坐標將不會被裁剪掉。它的平截頭體看起來像一個容器:![屏幕快照
上面的平截頭體定義了可見的坐標,它由由寬、高、近(Near)平面和遠(Far)平面所指定。任何出現在近平面之前或遠平面之后的坐標都會被裁剪掉。正射平截頭體直接將平截頭體內部的所有坐標映射為標准化設備坐標,因為每個向量的\(w\)分量都沒有進行改變;如果\(w\)分量等於\(1.0\),透視除法則不會改變這個坐標。
要創建一個正射投影矩陣,我們可以使用GLM的內置函數glm::ortho
:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前兩個參數指定了平截頭體的左右坐標,第三和第四參數指定了平截頭體的底部和頂部。通過這四個參數我們定義了近平面和遠平面的大小,然后第五和第六個參數則定義了近平面和遠平面的距離。這個投影矩陣會將處於這些\(x,y,z\)值范圍內的坐標變換為標准化設備坐標。
正射投影矩陣直接將坐標映射到2D平面中,即你的屏幕,但實際上一個直接的投影矩陣會產生不真實的結果,因為這個投影沒有將透視(Perspective)考慮進去。所以我們需要透視投影矩陣來解決這個問題。
2.4.3 透視投影
如果你曾經體驗過實際生活給你帶來的景象,你就會注意到離你越遠的東西看起來更小。這個奇怪的效果稱之為透視(Perspective)。透視的效果在我們看一條無限長的高速公路或鐵路時尤其明顯,正如下面圖片顯示的那樣:
正如你看到的那樣,由於透視,這兩條線在很遠的地方看起來會相交。這正是透視投影想要模仿的效果,它是使用透視投影矩陣來完成的。這個投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大。被變換到裁剪空間的坐標都會在-w到w的范圍之間(任何大於這個范圍的坐標都會被裁剪掉)。OpenGL要求所有可見的坐標都落在-1.0到1.0范圍內,作為頂點着色器最后的輸出,因此,一旦坐標在裁剪空間內之后,透視除法就會被應用到裁剪空間坐標上:
頂點坐標的每個分量都會除以它的\(w\)分量,距離觀察者越遠頂點坐標就會越小。這是也是\(w\)分量非常重要的另一個原因,它能夠幫助我們進行透視投影。最后的結果坐標就是處於標准化設備空間中的。在GLM中可以這樣創建一個透視投影矩陣:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
同樣,glm::perspective
所做的其實就是創建了一個定義了可視空間的大平截頭體,任何在這個平截頭體以外的東西最后都不會出現在裁剪空間體積內,並且將會受到裁剪。一個透視平截頭體可以被看作一個不均勻形狀的箱子,在這個箱子內部的每個坐標都會被映射到裁剪空間上的一個點。下面是一張透視平截頭體的圖片
它的第一個參數定義了fov的值,它表示的是視野(Field of View),並且設置了觀察空間的大小。如果想要一個真實的觀察效果,它的值通常設置為45.0f,但想要一個末日風格的結果你可以將其設置一個更大的值。第二個參數設置了寬高比,由視口的寬除以高所得。第三和第四個參數設置了平截頭體的近和遠平面。我們通常設置近距離為0.1f,而遠距離設為100.0f。所有在近平面和遠平面內且處於平截頭體內的頂點都會被渲染。
當使用正射投影時,每一個頂點坐標都會直接映射到裁剪空間中而不經過任何精細的透視除法(它仍然會進行透視除法,只是w分量沒有被改變(它保持為1),因此沒有起作用)。因為正射投影沒有使用透視,遠處的物體不會顯得更小,所以產生奇怪的視覺效果。由於這個原因,正射投影主要用於二維渲染以及一些建築或工程的程序,在這些場景中我們更希望頂點不會被透視所干擾。某些如 Blender 等進行三維建模的軟件有時在建模時也會使用正射投影,因為它在各個維度下都更准確地描繪了每個物體。下面你能夠看到在Blender里面使用兩種投影方式的對比:
你可以看到,使用透視投影的話,遠處的頂點看起來比較小,而在正射投影中每個頂點距離觀察者的距離都是一樣的。
三、將坐標系統組合在一起
我們為上述的每一個步驟都創建了一個變換矩陣:模型矩陣、觀察矩陣和投影矩陣。一個頂點坐標將會根據以下過程被變換到裁剪坐標:
這一系列的矩陣變換需要從右往左讀。最后的頂點應該被賦值到頂點着色器中的gl_Position,OpenGL將會自動進行透視除法和裁剪。
然后呢?
頂點着色器的輸出要求所有的頂點都在裁剪空間內,這正是我們剛才使用變換矩陣所做的。OpenGL然后對裁剪坐標執行透視除法從而將它們變換到標准化設備坐標。OpenGL會使用glViewPort內部的參數來將標准化設備坐標映射到屏幕坐標,每個坐標都關聯了一個屏幕上的點(在我們的例子中是一個800x600的屏幕)。這個過程稱為視口變換。