【Modern OpenGL】坐标系统 Coordinate Systems
說明:跟著learnopengl的內容學習,不是純翻譯,只是自己整理記錄。
強烈推薦原文,無論是內容還是排版。 原文鏈接
本文地址: http://blog.csdn.net/aganlengzi/article/details/50448453
坐標系統 Coordinate Systems
在上一次教程中,我們學習了怎樣利用轉換矩陣來幫助我們完成基于點的轉換(縮放、平移和旋轉)。前面已經說過,OpenGL只對在標準化設備坐標系中的點進行處理。標準化設備坐標系,也就是x,y和z軸上的坐標都在-1到1之間的一個立方體中。超出標準化設備坐標系范圍內的坐標都是不能顯示的。通常來說,我們都是在vertex shader中將我們的坐標轉換到標準化坐標系中的坐標的。然后符合標準化坐標取值范圍的坐標才會被傳遞給光柵化等階段,最終將它們轉換成我們屏幕上的像素點顯示出來。
上面說的轉換過程:坐標–>標準換坐標系–>屏幕坐標系的轉換過程是一步一步完成的。其中,在最終將對象上的點轉換到屏幕坐標系之前,我們需要將它們逐步轉換到一些中間的坐標系。之所以要進行這樣的轉換,是因為在特定的坐標系中,相應的操作或者計算能夠得到簡化。在整個過程中,主要有5種不同的坐標系需要我們著重理解:
- 局部坐標系或者叫做對象坐標系
- 世界坐標系
- 視口坐標系
- 裁剪坐標系
- 屏幕坐標系
我們繪制對象上的點在最終被顯示到屏幕上之前都會被轉換到這些坐標系中。
你現在可能不知道它們具體是什么,不要緊,下面我們將采用一種便于理解的方式對它們進行解釋。首先我們先看一張總體流程的圖片:
在不同的坐標系之間轉換的方法是通過不同的轉換矩陣進行作用(就像前一個教程中講的那樣),齊總最重要的是以下三個轉換矩陣:模型矩陣,視口矩陣和投影矩陣(如上圖中所示)。上面的整個過程可以描述為:頂點坐標首先是在局部坐標系中的,然后通過模型矩陣轉換到世界坐標系中,談后通過視口矩陣轉換到視口坐標系中,談后又通過投影矩陣作用后轉換到裁剪坐標系統,最終轉換到屏幕坐標系中。
-
局部坐標系是以方便描述對象本身為目的的坐標系,原點選擇可能是對象的中心。完全是對象本身的表示。與其它外界因素全都沒有關系。局部坐標系(實際叫做對象坐標系更好理解)是為了方便利用坐標描述對象本身。
-
世界坐標系就是將所有的對象都放到一個空間中進行描述,所有的對象有公共的原點。那么從世界坐標系中的每個對象來說,處在世界坐標系中應該以世界坐標系的原點為原點,那么相應的描述自身點的坐標值都會有相應的變化(主要是局部坐標系或者說是對象坐標系的原點與世界坐標系原點之間的相對位置)。世界坐標系是為了統一描述更多的對象。對象的相對位置。
-
視口坐標系主要為了將世界坐標系中的對象呈現給二維視口做準備。比如空間中的物體有其相對位置,但是我們看到的只可能是視覺范圍內的物體和我們所處的位置和看的角度決定的一個范圍。并不是其精確相對位置關系。
-
裁剪坐標系緊接著視口坐標系,因為在視口中我們已經知道我們所在的位置和角度能夠看到什么,裁剪坐標系主要將我們看不到的內容從要渲染的畫面中裁剪掉,裁剪坐標系將坐標歸一化到標準化設備坐標系,即每個維度的坐標取值都是[-1,1]。
-
最終將裁剪坐標系中的坐標轉換到實際的屏幕坐標系中。在這個轉換的過程中,要依賴glViewport的參數設置。轉換后的坐標被發送到光柵化器將它們轉換成一個個片段。
通過上圖和以上的解釋,你可能能夠有一點點理解整個過程了。之所以要進行這樣的轉換,主要是相關的操作在特定的坐標系中更好操作。另外采用分階段的方法,這個過程也變得更加靈活。比如,當我們只是對我們繪制的對象進行修改的時候我們并不像考慮其它的對象。所以在局部坐標系中就很方便進行操作。
下面,我們將對每個坐標系進行更加細致地討論。
局部坐標系(對象坐標系)
上面說過,局部坐標系是單獨服務于一個對象的。比如,你想創建一個立方體,可能它的中心是原點坐標(0,0,0),你想創建一個球體,它的中心也是原點坐標(0,0,0)。這是兩個完全不需要知道彼此的對象。之所以將它們的原點定在其中心,是為了創建的時候方便。當然,最終這兩個對象顯示的位置可能是不確定的。而這并不是我們建立模型應該考慮的問題,當我們將它們放到世界坐標系的時候再考慮它們的相對位置就好了嘛!
實際上,在前面的教程中,我們創建的矩形和三角形等等對象的坐標都是局部坐標系中的坐標,也就是局部坐標。
世界坐標系
在局部坐標系中我們創建的各個對象(如上面所說的三角形或者矩形)都有相同的坐標原點,當它們都顯示在屏幕上的時候會是什么樣的情況呢?是的,它們都會重疊堆放到一起。這樣肯定不是我們需要的結果。世界坐標系中的坐標就是來指定其中不同對象之間的相對位置的。它通過模型矩陣來完成這項工作。
模型矩陣是一個轉換矩陣,它就是我們上次教程中講到的不同的轉換(縮放、平移和旋轉)的組合,它的作用是將不同的對象放到它們應該在的位置上。
視口坐標系
視口坐標系就是將空間中的對象轉換成你觀察范圍內所見的情形。它以設定的觀察點和觀察范圍(角度)以及與具體對象的位置來決定可見的場景。這主要是通過視口矩陣來完成的,其中也主要涉及的是平移和旋轉操作。在下一次教程中將會對視口矩陣做進一步說明。
實際上,視口坐標系又叫做照相機坐標系或者人眼坐標系。完成的功能就是模仿人眼的功能,將世界坐標系中的對象轉換到人眼的視覺范圍。
裁剪坐標系
在頂點處理程序完成時,OpenGL都會將不在能夠顯示范圍內的坐標丟棄掉(因為即使做了渲染也不會顯示出來)。這也正是裁剪坐標系名稱的由來。剩下的坐標最終都是要轉換成在屏幕上要顯示的像素的片段。
OpenGL處理標準化設備坐標系,但是坐標值都在[-1,1]的三維空間對我們來說并不是太直觀。實際上我們可以自己指定我們的坐標范圍,只要在OpenGL處理的時候將其轉換成[-1,1]范圍內就好了。
為了將頂點坐標從視口轉換到裁剪坐標系,我們定義了一個投影矩陣將指定坐標的范圍,比如說在每個維度上都是從-1000到1000,內的坐標轉換到標準化設備坐標系規定的范圍(-1,1),所有在指定范圍外的坐標都將會被裁剪掉。比如說在我們指定的范圍內,坐標(1250,500,750)都不會被顯式出來,因為x坐標超出了設定的坐標值范圍。
需要注意的是,如果對象的一部分在裁剪域中而另一部分不在裁剪域中,那么OpenGL就會將這個對象,比如說三角形,重新劃分成更小的三角形來保證將外部的裁剪而將內部的保留。
投影矩陣作用產生的矩形體叫做視口截頭椎體,如下圖所示。所有在這個截頭椎體中的坐標最終都會顯示在用戶屏幕上。所有將指定范圍內的坐標轉換為標準化設備坐標系范圍內二維平面坐標值的操作叫做投影。而投影矩陣就是完成這個功能的。
?
當所有的頂點都被轉換到裁剪坐標系,一種叫做透視除法點的操作就會被執行。也就是用前面講過的齊次坐標w來對位置向量的x,y和z分量進行操作。這個操作實際上是將四維的裁剪坐標系轉換成三維的標準化設備坐標系,而且這個步驟是在每個頂點處理程序結束時自動完成的(不需要我們做任何操作)。
在這個階段之后,生成的坐標才會被映射到屏幕坐標系中(需要用到glViewport函數的參數設置)并且生成最后的片段(還記得片段是什么嗎?就是要生成顯示在屏幕上的一個像素點的所有相關數據)。
投影矩陣實際上有兩種投影方式,一種是正交投影,另一種是透視投影,我們可以選擇兩種中的任意一種來完成我們的投影操作(將視口坐標轉換成裁剪坐標)。以下將對這兩種投影方式進行介紹:
正交投影
正交投影矩陣產生的截頭椎體實際上是一個類似于立方體的截頭椎體,即其前面和后面是相同大小的,它的同方向的線時平行的,如下圖所示。同樣的,超出這個立方體的所有的坐標都會被裁剪掉。為了定義一個正交裁剪矩陣,我們需要指定這個截頭椎體的寬度、高度和長度。實際上就是指定了可見的三維空間范圍。
?
在正交矩陣中,因為我們并不需要做透視除法,所以w的值是無關緊要的。如果w的值是1.0,那么轉換后的坐標和轉換前的坐標值實際上是一致的。
我們可以通過GLM內置的函數glm::ortho來創建一個正交矩陣:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);前兩個參數指定了這個椎體的左邊和右邊的坐標;第三個和第四個參數指定了椎體的上表面和下表面;最后兩個參數指定了前面(near)和后面(far)的位置。這個正交矩陣將所有在指定范圍內的對象的坐標全部轉換成標準化設備坐標系中的坐標值。
實際上正交投影產生的效果是失真的,因為它直接將在椎體內的坐標值都投影到屏幕上。對于人的視覺來說,為了得到更逼真的效果,需要考慮透視。
透視投影
在實際生活中,我們應該有這樣的體會:離我們遠的物體看上去更小,離我們近的物體看上去更大。實際上這種奇怪的效果就是透視。以下這張圖實際上很好體現了透視的效果:一張火車道的延伸圖:
?
如你所見,因為透視,火車道原本平行的線在遠處好像交匯了。透視投影想要模仿的功能就是這樣。像正交投影以下,透視投影也是規定了一個椎體,但是它還對齊次坐標w進行操作:離觀察點越遠的點坐標中,w值越大。當坐標值轉換到裁剪坐標系中的時候,它們的坐標值實際上是在[-w,w]之間的,所有不在這個范圍內的坐標都會被裁剪掉。當然為了將最終的坐標值都轉換成標準化設備坐標系中的坐標值,在最后生成OpenGL能夠處理的坐標值的時候,所有的坐標都要做如下的處理:
頂點的每一個分量都被其次坐標w除。因為w的值是越遠越大,所以造成的結果是越遠的對象的坐標值就越小。這也是齊次坐標w比較重要的一個原因,因為它幫助我們處理透視投影。所有轉換和計算的結果得到的是標準化設備坐標系中的坐標(和正交投影一樣)。
投影矩陣可以利用GLM通過以下方式生成:
glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);它的樣子是這樣的:
?
函數glm::perspective的作用還是創建一個錐體,所有不在其中的坐標都會被裁剪掉而不會在屏幕上顯示。第一個參數指定了視角,為了得到逼真的效果,這個值通常被設置為45.0f。第二個參數設定了縱橫比,它是寬度和高度的比值。第三個和第四個參數設置了近處距離和遠處距離為0.1f和100.0f。這樣就指定了可見空間內的所有坐標,這些坐標將會被處理和渲染。
當近處距離值設置太高的話,比如說10.0f,OpenGL就會將所有在0.0f到10.0f范圍內的坐標都裁剪掉,它們可能是一個對象的表面,表面被裁剪掉了,我們就可能看到對象的內部構造了。
當使用正交投影的時候,每個頂點的坐標都會被直接映射到裁剪坐標系,不會有任何的透視除法操作(實際上是進行了透視除法,只是w值并沒有備操作,所以并不會產生透視的效果)。因為沒有透視效果,遠處的對象實際上也不會顯得比較小,和近處的對象一樣按照原來的大小進行顯示。所以就會造成失真的現象。因此,正交投影是不適合用在三維對象上的,而用在二維平面圖形的生成或者一些結構或者工程應用中,因為在其中,我們不想讓透視效果破壞我們的圖形的形狀。但是在三維圖形、模型或者場景中,我們通常都是要使用透視投影來增加其真實效果的。兩者之間的對比可以看一下下面這兩張圖:
?
把所有的過程串聯起來!
我們為上述提到的每個過程都定義了一個轉換矩陣:模型矩陣、視口矩陣和投影矩陣。頂點坐標轉換成裁剪坐標的過程如下所示:
?
需要注意的是上面的順序不能改變,同時需要注意的是對向量的操作是從右向左來看的。生成的頂點將會被傳遞到頂點處理程序中的gl_Position,OpenGL將會自動進行透視除法和裁剪操作。
接下來呢?
頂點處理程序的輸出應該是在裁剪坐標系中的坐標,這正是我們上面做的事情。然后OpenGL對裁剪坐標進行透視除法操作,把這些坐標轉換成標準化設備坐標系中的坐標。再然后,OpenGL利用glViewport函數的參數來將標準化設備坐標系中的坐標映射到屏幕坐標系中。在屏幕坐標系中,每一個坐標對應的是屏幕中的一個像素點(比如說你的屏幕分辨率是800x600)。這個過程就叫做視口轉換。
3D!3D!
到目前為止,我們知道了怎樣將3D坐標轉換成2D坐標。我們可以使用3D而不是平面的2D來繪制和展示我們創建的圖形了!
首先我們需要做的是創建一個模型矩陣,模型矩陣包含縮放和/或旋轉等操作以完成我們想要對我們創建的對象進行的操作,以使得它們在世界坐標系中的合適位置。讓我們先來把我們的平面繞著x軸旋轉一下使它看上去像是在平鋪在地上的,那么模型矩陣應該是向下面這個樣子的:
glm::mat4 model; model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));接下來我們需要創建一個視口矩陣。我們想要把我們的平面稍微向后移動一下,以便于我們能夠在視口中看到這個看上去像平鋪在地面上的平面。我們可以采用下面的兩種方式來進行移動:
- 把攝像機向后移就相當于把整個場景向前移。
這就是一個視口矩陣的任務,將整個場景而不是將攝像機進行移動。
因為我們想要向后移動,而OpenGL是一個右手系統,所以我們應該把所有的物體向z軸正方向移動,這樣產生的效果是:我們正在把物體向后移動。
右手系統
根據約定,OpenGL是一個右手系統。也就是說x軸正方向是右,y軸正方向是上,z軸正方向是后。可以伸出右手,想象右手的中心是原點,下圖這樣,拇指,食指和中指分別制定的就是坐標軸方向。
?
我們將在下一次教程中詳細介紹怎樣在屏幕上移動對象,在本次教程中,視口矩陣像下面設置的樣子:
glm::mat4 view; // Note that we're translating the scene in the reverse direction of where we want to move view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));最后,我們還需要定義一個投影矩陣。我們使用的投影方式是透視投影,我們的投影矩陣定義如下:
glm::mat4 projection; projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f);像之前一樣,在利用glm時,傳遞參數的時候可能會遇到角度和弧度使用約定不一致的情況。比如說45度角度如果轉換成弧度是1/4 PI。
好的,現在我們已經創建了各個轉換矩陣。現在我們應該把它們傳遞給我們的處理程序(shader)。首先讓我們在vertex shader中聲明uniform類型的這些轉換矩陣。并且按照上面所示的矩陣和向量相乘的順序完成相應的計算:
#version 330 core layout (location = 0) in vec3 position; ... uniform mat4 model; uniform mat4 view; uniform mat4 projection;void main() {// Note that we read the multiplication from right to leftgl_Position = projection * view * model * vec4(position, 1.0f);... }當然在vertex中只是聲明了相關的矩陣,為了使用我們真正創建的矩陣,我們還需要將這些矩陣值傳遞到vertex shader中:
GLint modelLoc = glGetUniformLocation(ourShader.Program, "model")); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); ... // Same for View Matrix and Projection Matrix經過上面的所有的模型、視圖和投影矩陣的轉換后,我們最終的對象應該是:
- 向地板平面傾斜的
- 稍微離我們應該遠一點
- 以透視方式呈現在我們面前(更遠處顯得更小)
讓我們來看一下效果吧:
基本上是我們想象的樣子吧,只是這里因為沒有做將OpenGL坐標軸和屏幕坐標軸相統一,還記得怎么統一吧^_^,所以頭像感覺是倒過來的。源碼main.cpp和vertex shader。源碼中需要注意的是上面提到的glm關于角度和弧度坐標的規定,我的版本中是以弧度為準的。
更加3D一點!
到目前為止,我們還只是對二維的平面進行操作,雖然我們的操作是在三維空間中的。下面就然我們更冒險意一點!讓我們從二維平面中跳出來,跳到真正的三維空間中!讓我們來創建一個三維的立方體。為了創建一個立方體,我們需要總共36個點(6個面 * 2個三角形 * 3個頂點),這是一個難題啊,不過原教程中給我們提供了已經寫好的坐標數組。有了坐標實際上就有了立方體的骨架,這樣我們就可以專心于創建3D立方體的其它方面了。
為了更加有趣,我們還要讓這個立方體旋轉!這個旋轉矩陣應該不陌生了。
model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f));因為已經寫好的數組指定了36個頂點的坐標(包括重復的)所以我們將采用glDrawArray來渲染這些點,而不是glDrawElement。
glDrawArrays(GL_TRIANGLES, 0, 36);利用上面的頂點數組和旋轉矩陣以及繪制函數,當然還需要將OpenGL解讀數據的方式進行重新的格式指定。我們會得到下面的樣子的效果:
看上去好像是一個立方體但又好像不是,很奇怪的一個東西。這個“立方體”的某些面會覆蓋其它的面。這種現象的原因是,OpenGL在繪制三角形的時候是一個接一個繪制的,雖然前面已經繪制了三角形,在輪到某個三角形繪制的時候還是會在那個地方再繪制一次,所以就會有重復繪制進而覆蓋的現象。
所以問題已經知道了——重復繪制造成的覆蓋。那么怎么解決這個問題呢?幸運的是。OpenGL的Z-buffer(深度緩存)為我們提供了很好的解決方法。它記錄了繪制的深度信息并且允許OpenGL根據這些深度信息來決定是否在一個像素上進行繪制(深度檢測)。所以,我們可以啟用OpenGL的深度緩存來進行深度檢測,在不該繪制的時候不要繪制就好了。
深度緩存 Z-buffer
OpenGL利用Z-buffer保存了所有的深度信息,也叫作深度緩存。GLFW自動創建了這么一個緩存(就像顏色緩存保存要輸出圖像的顏色值一樣)。深度緩存保存了每個片段的深度信息。每當輪到一個片段進行處理的時候,OpenGL就會比較它的深度值。如果當前的片段是在其他片段的后面,那么它就會被丟棄(不進行渲染,因為渲染了也不會顯示出來),否則就會將當前的片段覆蓋掉。這個過程就叫做深度檢測,如果啟用的話,它由OpenGL自動完成。
為了使OpenGL能夠運行深度檢測,我們應首先啟用深度檢測,因為默認情況下它是不進行深度檢測的。我們利用狀態設置函數glEnable來使能OpenGL的深度檢測,這個狀態的宏名稱是GL_DEPTH_TEST:
glEnable(GL_DEPTH_TEST);因為我們使用了深度緩存,所以我們需要在每次繪制之前都應該清空深度緩存,否則深度緩存中還保留著可能過時的深度信息。像清空顏色緩存一樣,我們利用clear函數來清空深度緩存,通過下面方式完成:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);讓我們來測試一下我們的程序并且檢查OpenGL是否已經啟用了深度檢測(如果啟用了深度檢測,我們應該能夠看到一個正常的立方體在旋轉了):
就是這樣,能夠得到一個正常的立方體!源碼main.cpp。(但是好像立方體旋轉越來越快好像)。
更多立方體!
加入我們想要在屏幕上顯示10個立方體。每一個立方體都是我們上面已經創建出來的樣子,但是在屏幕中的位置不同,而且有不同的旋轉角度,應該怎么做呢?每一個立方體的樣子都已經固定了,就是我們上面創建出來的樣子。我們實際只需要改變這些立方體在世界坐標系中的排布和數量就好了!讓我們來完成它。
首先,我們來為每一個立方體定義一個變換向量,來指定其在世界坐標系中的位置。因為我們要顯示10個立方體,我們將定義一個包含10個位置坐標向量的數組:
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) };現在,在game loop的循環中,我們可以調用glDrawArrays函數10次,但是每次傳給頂點處理程序的模型矩陣都是不同的。這樣我們就能夠得到分布在我們設定位置處的10個立方體。我們通過一個循環來完成調用:
glBindVertexArray(VAO); for(GLuint i = 0; i < 10; i++) {glm::mat4 model;model = glm::translate(model, cubePositions[i]);GLfloat angle = 20.0f * i; model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));glDrawArrays(GL_TRIANGLES, 0, 36); } glBindVertexArray(0);通過上面的繪制,我們能夠得到有不同的旋轉角度的10個立方體。如下面的圖所示:
總結
以上是生活随笔為你收集整理的【Modern OpenGL】坐标系统 Coordinate Systems的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新手怎么买债券?个人投资债券的方式
- 下一篇: 上海银行结构性存款相关介绍,从这些方面了