OpenGL希望在每次頂點(diǎn)著色器運(yùn)行后,我們可見(jiàn)的所有頂點(diǎn)都為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinate, NDC)。
我們通常會(huì)自己設(shè)定一個(gè)坐標(biāo)的范圍,之后再在頂點(diǎn)著色器中將這些坐標(biāo)變換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。然后將這些標(biāo)準(zhǔn)化設(shè)備坐標(biāo)傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標(biāo)或像素。
在流水線中,物體的頂點(diǎn)在最終轉(zhuǎn)化為屏幕坐標(biāo)之前還會(huì)被變換到多個(gè)坐標(biāo)系統(tǒng)(Coordinate System)。
對(duì)我們來(lái)說(shuō)比較重要的總共有5個(gè)不同的坐標(biāo)系統(tǒng):
- 局部空間(Local Space,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺(jué)空間(Eye Space))
- 裁剪空間(Clip Space)
- 屏幕空間(Screen Space)
這就是一個(gè)頂點(diǎn)在最終被轉(zhuǎn)化為片段之前需要經(jīng)歷的所有不同狀態(tài)。
概述
為了將坐標(biāo)從一個(gè)坐標(biāo)系變換到另一個(gè)坐標(biāo)系,我們需要用到幾個(gè)變換矩陣。
最重要的幾個(gè)分別是模型(Model)、觀察(View)、投影(Projection)三個(gè)矩陣。
我們的頂點(diǎn)坐標(biāo)起始于局部空間(Local Space),在這里它稱為局部坐標(biāo)(Local Coordinate),它在之后會(huì)變?yōu)?strong>世界坐標(biāo)(World Coordinate),觀察坐標(biāo)(View Coordinate),裁剪坐標(biāo)(Clip Coordinate),并最后以屏幕坐標(biāo)(Screen Coordinate)的形式結(jié)束。下面的這張圖展示了整個(gè)流程以及各個(gè)變換過(guò)程做了什么:
- 局部坐標(biāo)是對(duì)象相對(duì)于局部原點(diǎn)的坐標(biāo),也是物體起始的坐標(biāo)。
- 下一步是將局部坐標(biāo)變換為世界空間坐標(biāo),世界空間坐標(biāo)是處于一個(gè)更大的空間范圍的。這些坐標(biāo)相對(duì)于世界的全局原點(diǎn),它們會(huì)和其它物體一起相對(duì)于世界的原點(diǎn)進(jìn)行擺放。
- 接下來(lái)我們將世界坐標(biāo)變換為觀察空間坐標(biāo),使得每個(gè)坐標(biāo)都是從攝像機(jī)或者說(shuō)觀察者的角度進(jìn)行觀察的。
- 坐標(biāo)到達(dá)觀察空間之后,我們需要將其投影到裁剪坐標(biāo)。裁剪坐標(biāo)會(huì)被處理至-1.0到1.0的范圍內(nèi),并判斷哪些頂點(diǎn)將會(huì)出現(xiàn)在屏幕上。
- 最后,我們將裁剪坐標(biāo)變換為屏幕坐標(biāo),我們將使用一個(gè)叫做視口變換(Viewport Transform)的過(guò)程。視口變換將位于-1.0到1.0范圍的坐標(biāo)變換到由
glViewport
函數(shù)所定義的坐標(biāo)范圍內(nèi)。最后變換出來(lái)的坐標(biāo)將會(huì)送到光柵器,將其轉(zhuǎn)化為片段。
我們之所以將頂點(diǎn)變換到各個(gè)不同的空間的原因是有些操作在特定的坐標(biāo)系統(tǒng)中才有意義且更方便。例如,當(dāng)需要對(duì)物體進(jìn)行修改的時(shí)候,在局部空間中來(lái)操作會(huì)更說(shuō)得通;如果要對(duì)一個(gè)物體做出一個(gè)相對(duì)于其它物體位置的操作時(shí),在世界坐標(biāo)系中來(lái)做這個(gè)才更說(shuō)得通,等等。如果我們?cè)敢?,我們也可以定義一個(gè)直接從局部空間變換到裁剪空間的變換矩陣,但那樣會(huì)失去很多靈活性。
局部空間
局部空間是指物體所在的坐標(biāo)空間,即對(duì)象最開(kāi)始所在的地方。
世界空間
世界空間中的坐標(biāo)正如其名:是指頂點(diǎn)相對(duì)于(游戲)世界的坐標(biāo)。
如果你希望將物體分散在世界上擺放(特別是非常真實(shí)的那樣),這就是你希望物體變換到的空間。物體的坐標(biāo)將會(huì)從局部變換到世界空間;該變換是由模型矩陣(Model Matrix)實(shí)現(xiàn)的。
模型矩陣是一種變換矩陣,它能通過(guò)對(duì)物體進(jìn)行位移、縮放、旋轉(zhuǎn)來(lái)將它置于它本應(yīng)該在的位置或朝向。
觀察空間
觀察空間經(jīng)常被人們稱之OpenGL的攝像機(jī)(Camera)(所以有時(shí)也稱為攝像機(jī)空間(Camera Space)或視覺(jué)空間(Eye Space))。
觀察空間是將世界空間坐標(biāo)轉(zhuǎn)化為用戶視野前方的坐標(biāo)而產(chǎn)生的結(jié)果。因此觀察空間就是從攝像機(jī)的視角所觀察到的空間。
這通常是由一系列的位移和旋轉(zhuǎn)的組合來(lái)完成,平移/旋轉(zhuǎn)場(chǎng)景從而使得特定的對(duì)象被變換到攝像機(jī)的前方。
這些組合在一起的變換通常存儲(chǔ)在一個(gè)觀察矩陣(View Matrix)里,它被用來(lái)將世界坐標(biāo)變換到觀察空間。
裁剪空間
在一個(gè)頂點(diǎn)著色器運(yùn)行的最后,OpenGL期望所有的坐標(biāo)都能落在一個(gè)特定的范圍內(nèi),且任何在這個(gè)范圍之外的點(diǎn)都應(yīng)該被裁剪掉(Clipped)。被裁剪掉的坐標(biāo)就會(huì)被忽略,所以剩下的坐標(biāo)就將變?yōu)槠聊簧峡梢?jiàn)的片段。這也就是裁剪空間(Clip Space)名字的由來(lái)。
因?yàn)閷⑺锌梢?jiàn)的坐標(biāo)都指定在-1.0到1.0的范圍內(nèi)不是很直觀,所以我們會(huì)指定自己的坐標(biāo)集(Coordinate Set)并將它變換回標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系,就像OpenGL期望的那樣。
為了將頂點(diǎn)坐標(biāo)從觀察變換到裁剪空間,我們需要定義一個(gè)投影矩陣(Projection Matrix),它指定了一個(gè)范圍的坐標(biāo),比如在每個(gè)維度上的-1000到1000。投影矩陣接著會(huì)將在這個(gè)指定的范圍內(nèi)的坐標(biāo)變換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的范圍(-1.0, 1.0)。
所有在范圍外的坐標(biāo)不會(huì)被映射到在-1.0到1.0的范圍之間,所以會(huì)被裁剪掉。在上面這個(gè)投影矩陣所指定的范圍內(nèi),坐標(biāo)(1250, 500, 750)將是不可見(jiàn)的,這是由于它的x坐標(biāo)超出了范圍,它被轉(zhuǎn)化為一個(gè)大于1.0的標(biāo)準(zhǔn)化設(shè)備坐標(biāo),所以被裁剪掉了。
如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積(Clipping Volume),則OpenGL會(huì)重新構(gòu)建這個(gè)三角形為一個(gè)或多個(gè)三角形讓其能夠適合這個(gè)裁剪范圍。
由投影矩陣創(chuàng)建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個(gè)出現(xiàn)在平截頭體范圍內(nèi)的坐標(biāo)都會(huì)最終出現(xiàn)在用戶的屏幕上。
將特定范圍內(nèi)的坐標(biāo)轉(zhuǎn)化到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系的過(guò)程(而且它很容易被映射到2D觀察空間坐標(biāo))被稱之為投影(Projection)
一旦所有頂點(diǎn)被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會(huì)執(zhí)行,在這個(gè)過(guò)程中我們將位置向量的x
,y
,z
分量分別除以向量的齊次w
分量;透視除法是將4D裁剪空間坐標(biāo)變換為3D標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的過(guò)程。這一步會(huì)在每一個(gè)頂點(diǎn)著色器運(yùn)行的最后被自動(dòng)執(zhí)行。
在這一階段之后,最終的坐標(biāo)將會(huì)被映射到屏幕空間中(使用glViewport
中的設(shè)定),并被變換成片段。
將觀察坐標(biāo)變換為裁剪坐標(biāo)的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。
我們可以選擇創(chuàng)建一個(gè)正射投影矩陣(Orthographic Projection Matrix)或一個(gè)透視投影矩陣(Perspective Projection Matrix)。
正射投影
正射投影矩陣定義了一個(gè)類似立方體的平截頭箱,它定義了一個(gè)裁剪空間,在這空間之外的頂點(diǎn)都會(huì)被裁剪掉。創(chuàng)建一個(gè)正射投影矩陣需要指定可見(jiàn)平截頭體的寬、高和長(zhǎng)度。在使用正射投影矩陣變換至裁剪空間之后處于這個(gè)平截頭體內(nèi)的所有坐標(biāo)將不會(huì)被裁剪掉。它的平截頭體看起來(lái)像一個(gè)容器:
上面的平截頭體定義了可見(jiàn)的坐標(biāo),它由由寬、高、近(Near)平面和遠(yuǎn)(Far)平面所指定。任何出現(xiàn)在近平面之前或遠(yuǎn)平面之后的坐標(biāo)都會(huì)被裁剪掉。正射平截頭體直接將平截頭體內(nèi)部的所有坐標(biāo)映射為標(biāo)準(zhǔn)化設(shè)備坐標(biāo),因?yàn)槊總€(gè)向量的w分量都沒(méi)有進(jìn)行改變;如果w分量等于1.0,透視除法則不會(huì)改變這個(gè)坐標(biāo)。
要?jiǎng)?chuàng)建一個(gè)正射投影矩陣,我們可以使用GLM的內(nèi)置函數(shù)glm::ortho
:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前兩個(gè)參數(shù)指定了平截頭體的左右坐標(biāo),第三和第四參數(shù)指定了平截頭體的底部和頂部。通過(guò)這四個(gè)參數(shù)我們定義了近平面和遠(yuǎn)平面的大小,然后第五和第六個(gè)參數(shù)則定義了近平面和遠(yuǎn)平面的距離。這個(gè)投影矩陣會(huì)將處于這些x,y,z值范圍內(nèi)的坐標(biāo)變換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。
正射投影矩陣直接將坐標(biāo)映射到2D平面中,即你的屏幕,但實(shí)際上一個(gè)直接的投影矩陣會(huì)產(chǎn)生不真實(shí)的結(jié)果,因?yàn)檫@個(gè)投影沒(méi)有將透視(Perspective)考慮進(jìn)去。所以我們需要透視投影矩陣來(lái)解決這個(gè)問(wèn)題。
透視投影
如果你曾經(jīng)體驗(yàn)過(guò)實(shí)際生活給你帶來(lái)的景象,你就會(huì)注意到離你越遠(yuǎn)的東西看起來(lái)更小。這個(gè)奇怪的效果稱之為透視(Perspective)。
透視的效果在我們看一條無(wú)限長(zhǎng)的高速公路或鐵路時(shí)尤其明顯,正如下面圖片顯示的那樣:
正如你看到的那樣,由于透視,這兩條線在很遠(yuǎn)的地方看起來(lái)會(huì)相交。這正是透視投影想要模仿的效果,它是使用透視投影矩陣來(lái)完成的。
這個(gè)投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個(gè)頂點(diǎn)坐標(biāo)的w值,從而使得離觀察者越遠(yuǎn)的頂點(diǎn)坐標(biāo)w分量越大。
被變換到裁剪空間的坐標(biāo)都會(huì)在-w到w的范圍之間(任何大于這個(gè)范圍的坐標(biāo)都會(huì)被裁剪掉)。OpenGL要求所有可見(jiàn)的坐標(biāo)都落在-1.0到1.0范圍內(nèi),作為頂點(diǎn)著色器最后的輸出,因此,一旦坐標(biāo)在裁剪空間內(nèi)之后,透視除法就會(huì)被應(yīng)用到裁剪空間坐標(biāo)上:
頂點(diǎn)坐標(biāo)的每個(gè)分量都會(huì)除以它的w分量,距離觀察者越遠(yuǎn)頂點(diǎn)坐標(biāo)就會(huì)越小。這是也是w分量非常重要的另一個(gè)原因,它能夠幫助我們進(jìn)行透視投影。最后的結(jié)果坐標(biāo)就是處于標(biāo)準(zhǔn)化設(shè)備空間中的。
在GLM中可以這樣創(chuàng)建一個(gè)透視投影矩陣:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
同樣,glm::perspective所做的其實(shí)就是創(chuàng)建了一個(gè)定義了可視空間的大平截頭體,任何在這個(gè)平截頭體以外的東西最后都不會(huì)出現(xiàn)在裁剪空間體積內(nèi),并且將會(huì)受到裁剪。一個(gè)透視平截頭體可以被看作一個(gè)不均勻形狀的箱子,在這個(gè)箱子內(nèi)部的每個(gè)坐標(biāo)都會(huì)被映射到裁剪空間上的一個(gè)點(diǎn)。下面是一張透視平截頭體的圖片:
它的第一個(gè)參數(shù)定義了fov的值,它表示的是視野(Field of View),并且設(shè)置了觀察空間的大小。如果想要一個(gè)真實(shí)的觀察效果,它的值通常設(shè)置為45.0f,但想要一個(gè)末日風(fēng)格的結(jié)果你可以將其設(shè)置一個(gè)更大的值。第二個(gè)參數(shù)設(shè)置了寬高比,由視口的寬除以高所得。第三和第四個(gè)參數(shù)設(shè)置了平截頭體的近和遠(yuǎn)平面。我們通常設(shè)置近距離為0.1f,而遠(yuǎn)距離設(shè)為100.0f。所有在近平面和遠(yuǎn)平面內(nèi)且處于平截頭體內(nèi)的頂點(diǎn)都會(huì)被渲染。
把它們都組合到一起
我們?yōu)樯鲜龅拿恳粋€(gè)步驟都創(chuàng)建了一個(gè)變換矩陣:模型矩陣、觀察矩陣和投影矩陣。
一個(gè)頂點(diǎn)坐標(biāo)將會(huì)根據(jù)以下過(guò)程被變換到裁剪坐標(biāo):
注意矩陣運(yùn)算的順序是相反的(記住我們需要從右往左閱讀矩陣的乘法)。最后的頂點(diǎn)應(yīng)該被賦值到頂點(diǎn)著色器中的gl_Position,OpenGL將會(huì)自動(dòng)進(jìn)行透視除法和裁剪。
然后呢?
頂點(diǎn)著色器的輸出要求所有的頂點(diǎn)都在裁剪空間內(nèi),這正是我們剛才使用變換矩陣所做的。OpenGL然后對(duì)裁剪坐標(biāo)執(zhí)行透視除法從而將它們變換到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。OpenGL會(huì)使用glViewPort
內(nèi)部的參數(shù)來(lái)將標(biāo)準(zhǔn)化設(shè)備坐標(biāo)映射到屏幕坐標(biāo),每個(gè)坐標(biāo)都關(guān)聯(lián)了一個(gè)屏幕上的點(diǎn)(在我們的例子中是一個(gè)800x600的屏幕)。這個(gè)過(guò)程稱為視口變換。