OpenGL(四)坐標(biāo)系

前言

在前兩章,總結(jié)有頂點(diǎn)坐標(biāo),紋理坐標(biāo)。實(shí)際上在這之上還有更多的坐標(biāo)。作者經(jīng)過學(xué)習(xí)后,在本文總結(jié)一番。

上一篇:OpenGL矩陣

正文

OpenGL希望每一次運(yùn)行頂點(diǎn)著色器之后,我們所見到的頂點(diǎn)坐標(biāo)都轉(zhuǎn)化為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(NDC)。

也就是說,每個頂點(diǎn)的x,y,z都應(yīng)該在[-1,1]之間,超出這個范圍都應(yīng)該看不見。我們通常會自己設(shè)定一個坐標(biāo)范圍,之后頂點(diǎn)著色器中所有的坐標(biāo)都會轉(zhuǎn)化為標(biāo)準(zhǔn)化設(shè)備坐標(biāo),然后這些坐標(biāo)經(jīng)歷光柵器,轉(zhuǎn)化為屏幕上的二維坐標(biāo)或者像素。

將坐標(biāo)轉(zhuǎn)化標(biāo)準(zhǔn)化坐標(biāo),在轉(zhuǎn)化為屏幕上的坐標(biāo)都是分步驟來的,中間經(jīng)歷多個坐標(biāo)系。把物體的坐標(biāo)變換到幾個過渡坐標(biāo)系,優(yōu)點(diǎn)在于一些操作就會就會很簡單,大致分為如下5個坐標(biāo)系統(tǒng):

  • 1.局部空間
  • 2.世界空間
  • 3.觀察空間
  • 4.裁剪空間
  • 5.屏幕空間

為了將坐標(biāo)從一個坐標(biāo)系變換到另一個坐標(biāo)系,我們需要幾個變換矩陣來完成這個過程。

分別是模型,觀察和投影。我們的頂點(diǎn)坐標(biāo)起始于局部空間,也稱為局部坐標(biāo)。經(jīng)過模型變換之后,就變成了世界空間。經(jīng)過觀察變換之后就變成了觀察空間,經(jīng)過投影變換之后就變化為裁剪坐標(biāo),最后輸出到設(shè)備,變成屏幕坐標(biāo)。

過程如下圖:


image.png

稍微說明一下,對各個變換的理解。

局部坐標(biāo)就是沒有處理,最初的我們傳入頂點(diǎn)著色器的坐標(biāo)。在這個過程我們,可能會存在兩種坐標(biāo)一種叫做頂點(diǎn)坐標(biāo),一種是紋理坐標(biāo)。這個過程我理解為把一個物體構(gòu)造起來。

世界坐標(biāo)是經(jīng)過模型矩陣變化之后的坐標(biāo)。這過程我是這么理解的,假如有這么一個3D的世界,我們物體已經(jīng)構(gòu)建好了,那么接下來就是把這個物體要擺在這個世界的哪里,需要從原點(diǎn)位移多少,旋轉(zhuǎn)的角度等等。

觀察坐標(biāo),是上面的坐標(biāo)再一次經(jīng)過觀察矩陣進(jìn)行變換得來。實(shí)際上這個過程,我們可以形象的比喻成,我們的眼睛從哪個方向去看這個世界中的物體。

最后一個裁剪坐標(biāo),是經(jīng)過投影矩陣變換得來。我們可以形象比喻為如下這種狀況,我們通過攝像機(jī)或者眼睛去觀察,往往投影在我們的樣子是經(jīng)過類似投影機(jī)生成的圖像,生成一個用2d圖像代表的3d圖像。

如下圖,一個無限延伸的鐵路,在現(xiàn)實(shí)世界中是一個平行的鐵軌,在我們的眼睛中是一個在地平線初相交的兩條直線。


image.png

由投影矩陣創(chuàng)建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個出現(xiàn)在平截頭體范圍內(nèi)的坐標(biāo)都會最終出現(xiàn)在用戶的屏幕上。將特定范圍內(nèi)的坐標(biāo)轉(zhuǎn)化到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系的過程(而且它很容易被映射到2D觀察空間坐標(biāo))被稱之為投影(Projection),因?yàn)槭褂猛队熬仃嚹軐?D坐標(biāo)投影(Project)到很容易映射到2D的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系中。

一旦所有頂點(diǎn)被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會執(zhí)行,在這個過程中我們將位置向量的x,y,z分量分別除以向量的齊次w分量;透視除法是將4D裁剪空間坐標(biāo)變換為3D標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的過程。這一步會在每一個頂點(diǎn)著色器運(yùn)行的最后被自動執(zhí)行。

在這一階段之后,最終的坐標(biāo)將會被映射到屏幕空間中(使用glViewport中的設(shè)定),并被變換成片段。

將觀察坐標(biāo)變換為裁剪坐標(biāo)的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。我們可以選擇創(chuàng)建一個正射投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)。

投影矩陣

從上面的小結(jié),我們能夠輕松的理解到,世界空間,觀察坐標(biāo),實(shí)際上就是對原來的頂點(diǎn)坐標(biāo)做一次矩陣左乘操作。但是這個投影矩陣比較特殊,分為 正射投影矩陣以及透視矩陣,這些有研究的價值。

正射投影矩陣

正射投影矩陣構(gòu)成一個立方體平截頭,它定義了一個裁剪空間,在這個空間之外的所有頂點(diǎn)都會被裁剪掉。


image.png

我們定義這個裁剪的空間的時候,能夠定義寬高和遠(yuǎn)平面以及近平面。任何超出近平面和遠(yuǎn)平面的坐標(biāo)都會被裁剪掉。正射平截頭把平截頭體內(nèi)部的坐標(biāo)最后都轉(zhuǎn)化為屏幕坐標(biāo),因?yàn)槊總€向量的w分量都沒有進(jìn)行改變;如果w分量等于1.0,透視除法則不會改變這個坐標(biāo)。

舉個形象的例子,我們通過一個筆直的鏡頭,通過正方形隧道看到遠(yuǎn)方的物體的正視圖,左視圖,俯視圖等情況。

當(dāng)初我看到這里的第一反應(yīng)是,正射投影矩陣是這樣的,但是像素點(diǎn)究竟經(jīng)過什么變換,最后變到哪里,投射到哪里去,對著一切的一切都是蒙蔽。為此,為了弄懂它,實(shí)際上我需要對正射投影矩陣證明有一定的了解。

正射投影矩陣證明

我們從上面的定義能夠理解到,正射投影矩陣的作用就是把一個物體能夠在一個[-1,1]范圍內(nèi)顯示出來,我們可以把這個過程看成一次縮放和位移的過程。


image.png

如果灰色的物體就是就是原來的世界坐標(biāo)中的物體。我們要經(jīng)過一定的變換的得到把視圖容納到下面這個x,y,z坐標(biāo)系。

假如我們?nèi)×⒎襟w其中一面,如果面中的邊緣點(diǎn)都能投影到[-1,1]之間。那么這個立方體就能容納到我們的NDC坐標(biāo)中。假設(shè)立方體一面中如上圖有三個如此的點(diǎn):(l,t,-n),(r,t,-n),(r,b,-n).

為什么我們倒著z軸過來設(shè)置點(diǎn)呢?因?yàn)镺penGL和DirectX不一樣,是一個右手坐標(biāo)系。

按照慣例,OpenGL是一個右手坐標(biāo)系。簡單來說,就是正x軸在你的右手邊,正y軸朝上,而正z軸是朝向后方的。想象你的屏幕處于三個軸的中心,則正z軸穿過你的屏幕朝向你。

定義這個立方體中的某個點(diǎn)(x_{p},y_{p},z_{p})投影到到未知的NDC坐標(biāo)系的x_{n},y_{n},z_{n}

我們以X軸上的x_{p}為例子進(jìn)行推導(dǎo)。

對于x軸上的點(diǎn),已知:l\leq x_{p} \leq r
我們稍微做一下變化,為了讓立方體內(nèi)x的點(diǎn)位于[-1,1]:
0\leq x_{p} - l \leq r - l = 0\leq \frac{x_{p} - l}{r - l}\leq 1

但是這樣還是不夠符合我們的范圍,大致上只有一半,因此我們還進(jìn)一步對這個范圍做一次變換,先乘以2,再減1:
(0 \leq \frac{2x_{p} - 2l}{2r - 2l} \leq 2) - 1 = -1\leq \frac{2x_{p} - 2l}{2r - 2l} - 1\leq 1

= -1\leq \frac{2x_{p} }{r - l} + \frac{r+l}{r-l} \leq 1

這樣就能通過乘法和減法,簡單的把這個x_{p}壓縮到[-1,1]之間,也就是說對應(yīng)到了x_{n}

我們可以得到第一個點(diǎn):x_{n} = \frac{2x_{p} }{r - l} - \frac{r+l}{r-l}

同理我們可以得到y(tǒng)軸上的點(diǎn)關(guān)系:
y_{n} = \frac{2y_{p} }{t - b} - \frac{t+b}{t-b}

假如立方體,z軸方向,最近是n的距離,最遠(yuǎn)處是一個f的距離。
n\leq z_{p} \leq f

根據(jù)OpenGL這個右手坐標(biāo),我們需要把物體的z軸投影到NDC的z軸的[-1,1].因此我們可以按照上面的z軸一樣做變換。
0<z_{p} - n < f - n

(0 < \frac{z_{p} - n}{f - n} < 1) * 2 - 1
= 0 < \frac{2z_{p} - 2n}{f - n} < 2 - 1
= -1 < \frac{2z_{p} - n - f}{f - n} < 1
= -1 < \frac{2z_{p} }{f - n} - \frac{n + f}{f - n} < 1

同理可以得到下面一個等式:
z_{n} =\frac{2z_{p} }{f - n} - \frac{n + f}{f - n}

我們得到幾個對應(yīng)關(guān)系之后,我們就能寫出如下的正射投影矩陣

\begin{matrix} \frac{2 }{r - l} & 0 & 0 & - \frac{r+l}{r-l} \\ 0 & \frac{2}{t - b} & 0 & - \frac{t+b}{t-b} \\ 0 & 0 & \frac{2}{f - n} & - \frac{n + f}{f - n} \\ 0 & 0 & 0 & 1 \end{matrix}

但是實(shí)際上,如果我們的視體是對稱的(r = -l,t = -b),那么我們可以通過這條件,得到一個更加簡單的矩陣。

\begin{matrix} \frac{1 }{r} & 0 & 0 & 0 \\ 0 & \frac{1}{t } & 0 & 0 \\ 0 & 0 & \frac{2}{f - n} & - \frac{n + f}{f - n} \\ 0 & 0 & 0 & 1 \end{matrix}

這就是正射投影矩陣來源。這是十分簡單的證明過程,接下來透視矩陣會稍微復(fù)雜一點(diǎn)點(diǎn),需要適當(dāng)?shù)氖褂靡稽c(diǎn)三角函數(shù)。

透視投影矩陣

透視投影,實(shí)際上就是模擬我們的人眼,能夠把3d的映像轉(zhuǎn)化為了2d像素投影在屏幕上,就像上面我舉的鐵軌例子。

這個投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個頂點(diǎn)坐標(biāo)的w值,從而使得離觀察者越遠(yuǎn)的頂點(diǎn)坐標(biāo)w分量越大。被變換到裁剪空間的坐標(biāo)都會在-w到w的范圍之間(任何大于這個范圍的坐標(biāo)都會被裁剪掉)。

OpenGL要求所有可見的坐標(biāo)都落在-1.0到1.0范圍內(nèi),作為頂點(diǎn)著色器最后的輸出,因此,一旦坐標(biāo)在裁剪空間內(nèi)之后,透視除法就會被應(yīng)用到裁剪空間坐標(biāo)上:
out = (\begin{matrix} \frac{x}{w} \\ \frac{y}{w} \\ \frac{z}{w} \end {matrix} )

頂點(diǎn)坐標(biāo)的每個分量都會除以它的w分量,距離觀察者越遠(yuǎn)頂點(diǎn)坐標(biāo)就會越小。這是也是w分量非常重要的另一個原因,它能夠幫助我們進(jìn)行透視投影。最后的結(jié)果坐標(biāo)就是處于標(biāo)準(zhǔn)化設(shè)備空間中的。

齊次坐標(biāo)系

這里面涉及到一個新的概念,齊次坐標(biāo)系。
我們先來理解以下齊次性:

一般地,在數(shù)學(xué)里面,如果一個函數(shù)的自變量乘以一個系數(shù),那么這個函數(shù)將乘以這個系數(shù)的k次方,我們稱這個函數(shù)為k次齊次函數(shù),也就是:
如果函數(shù) f(v)滿足
f(ax)=a^k f(x),
其中,x是輸入變量,k是整數(shù),a是非零的實(shí)數(shù),則稱f(x)是k次齊次函數(shù)。

比如:一次齊次函數(shù)就是線性函數(shù)2.多項式函數(shù) f(x,y)=x2+y2
因?yàn)閒(ax,ay)=a^2f(x,y),所以f(x,y)是2次齊次函數(shù)。

齊次性在數(shù)學(xué)中描述的是函數(shù)的一個倍數(shù)的性質(zhì)。

在理解了齊次性之后,我們就能比較好的理解齊次坐標(biāo)系。

齊次坐標(biāo)

在數(shù)學(xué)里,齊次坐標(biāo)(homogeneous coordinates),或投影坐標(biāo)(projective coordinates)是指一個用于投影幾何里的坐標(biāo)系統(tǒng),如同用于歐氏幾何里的笛卡兒坐標(biāo)一般。

實(shí)投影平面可以看作是一個具有額外點(diǎn)的歐氏平面,這些點(diǎn)稱之為無窮遠(yuǎn)點(diǎn),并被認(rèn)為是位于一條新的線上(該線稱之為無窮遠(yuǎn)線)。每一個無窮遠(yuǎn)點(diǎn)對應(yīng)至一個方向(由一條線之斜率給出),可非正式地定義為一個點(diǎn)自原點(diǎn)朝該方向移動之極限。在歐氏平面里的平行線可看成會在對應(yīng)其共同方向之無窮遠(yuǎn)點(diǎn)上相交。給定歐氏平面上的一點(diǎn) (x, y),對任意非零實(shí)數(shù) Z,三元組 (xZ, yZ, Z) 即稱之為該點(diǎn)的齊次坐標(biāo)。依據(jù)定義,將齊次坐標(biāo)內(nèi)的數(shù)值乘上同一個非零實(shí)數(shù),可得到同一點(diǎn)的另一組齊次坐標(biāo)。例如,笛卡兒坐標(biāo)上的點(diǎn) (1,2) 在齊次坐標(biāo)中即可標(biāo)示成 (1,2,1) 或 (2,4,2)。原來的笛卡兒坐標(biāo)可透過將前兩個數(shù)值除以第三個數(shù)值取回。因此,與笛卡兒坐標(biāo)不同,一個點(diǎn)可以有無限多個齊次坐標(biāo)表示法。

一條通過原點(diǎn) (0, 0) 的線之方程可寫作 nx + my = 0,其中 n 及 m 不能同時為 0。以參數(shù)表示,則能寫成 x = mt, y = ? nt。令 Z=1/t,則線上的點(diǎn)之笛卡兒坐標(biāo)可寫作 (m/Z, ? n/Z)。在齊次坐標(biāo)下,則寫成 (m, ? n, Z)。當(dāng) t 趨向無限大,亦即點(diǎn)遠(yuǎn)離原點(diǎn)時,Z 會趨近于 0,而該點(diǎn)的齊次坐標(biāo)則會變成 (m, ?n, 0)。因此,可定義 (m, ?n, 0) 為對應(yīng) nx + my = 0 這條線之方向的無窮遠(yuǎn)點(diǎn)之齊次坐標(biāo)。因?yàn)闅W氏平面上的每條線都會與透過原點(diǎn)的某一條線平行,且因?yàn)槠叫芯€會有相同的無窮遠(yuǎn)點(diǎn),歐氏平面每條線上的無窮遠(yuǎn)點(diǎn)都有其齊次坐標(biāo)。

概括來說:

  • 投影平面上的任何點(diǎn)都可以表示成一三元組 (X, Y, Z),稱之為該點(diǎn)的'齊次坐標(biāo)或投影坐標(biāo),其中 X、Y 及 Z 不全為 0。
  • 以齊次坐標(biāo)表表示的點(diǎn),若該坐標(biāo)內(nèi)的數(shù)值全乘上一相同非零實(shí)數(shù),仍會表示該點(diǎn)。
  • 相反地,兩個齊次坐標(biāo)表示同一點(diǎn),當(dāng)且僅當(dāng)其中一個齊次坐標(biāo)可由另一個齊次坐標(biāo)乘上一相同非零常數(shù)得取得。
  • 當(dāng) Z 不為 0,則該點(diǎn)表示歐氏平面上的該 (X/Z, Y/Z)。
  • 當(dāng) Z 為 0,則該點(diǎn)表示一無窮遠(yuǎn)點(diǎn)。
  • 注意,三元組 (0, 0, 0) 不表示任何點(diǎn)。原點(diǎn)表示為 (0, 0, 1)[3]。

齊次坐標(biāo)系為什么在計算機(jī)視覺中運(yùn)用廣泛?

主要有兩點(diǎn):

  • 1.區(qū)分向量還是點(diǎn)
  • 2.更加易于仿射(線性)變換
1.區(qū)分向量還是點(diǎn)

(1) 從普通坐標(biāo)轉(zhuǎn)成齊次坐標(biāo)時:
如果(x,y,z)是向量,那么齊次坐標(biāo)為(x,y,z,0)
如果(x,y,z)是 3D 點(diǎn),那么齊次坐標(biāo)為 (x,y,z,1)

(2) 從齊次坐標(biāo)轉(zhuǎn)成普通坐標(biāo)時:
如果 (x,y,z,1)(3D點(diǎn)),在普通坐標(biāo)系下為(x,y,z)
如果 (x,y,z,0)(向量),在普通坐標(biāo)系下為(x,y,z)

這樣就能通過w的分量來察覺到是點(diǎn)還是向量

2.更加易于仿射(線性)變換

對于平移,旋轉(zhuǎn)和縮放,都通過矩陣的乘法完成的。在之前的文章我都是默認(rèn)使用齊次坐標(biāo)系進(jìn)行計算。假如我們放棄了齊次坐標(biāo)系,使用傳統(tǒng)的坐標(biāo)系會如何?

就以最簡單的平移為例子。
首先一個矢量來表示空間中的一個點(diǎn):r=[rx,ry,rz]r=[rx,ry,rz]
如果我們要將其平移, 平移的矢量為:t=[tx,ty,tz]t=[tx,ty,tz]
那么正常的做法就是:r+t=[rx+tx,ry+ty,rz+tz]

假如我們不使用齊次坐標(biāo)系需要一個圖像平移,那么我們就需要一個移動矩陣m乘以原來坐標(biāo)像素矩陣r,辦到以下的情況
t*r = [t_{x}+r_{x},t_{y}+r_{y},t_{z}+r_{z}]
很可惜,我們無法找到這么一個矩陣。

但是如果我們加多一個維度w,就能找到這么一個矩陣
\left[ \begin{matrix} 1 & 0 & 0 & t_{x} \\ 0 & 1 & 0 & t_{y} \\ 0 & 0 & 1 & t_{z} \\ 0 & 0 & 0 & 1 \end {matrix} \right] * [m_{x},m_{y},m_{z},1] = [t_{x}+r_{x},t_{y}+r_{y},t_{z}+r_{z},1]

這就是齊次坐標(biāo)系的便利。我們就在這個基礎(chǔ)上做投影矩陣的推導(dǎo)。

透視投影矩陣的推導(dǎo)

image.png

能看到此時的平截頭像一個梯形。小的那一面叫做近平面,大的那一面叫做遠(yuǎn)平面。這個梯形是怎么來的?實(shí)際上如下圖,就像一個攝像機(jī)以一定大小的視角對這個空間的觀察:


image.png

能看到整個梯形的平截頭,是由一個攝像機(jī),以fov的角度觀察世界,攝像機(jī)距離外面世界的距離就是攝像機(jī)到近平面的距離。遠(yuǎn)平面就是指我們能夠看到最遠(yuǎn)的地方是哪里,物體擺放的位置超出了遠(yuǎn)平面,就看不到了。

我們能夠根據(jù)上面的圖,如果原點(diǎn)是我們的視角,從這個視角看出去,就能構(gòu)成一個三角形。


DirectX的左手坐標(biāo)系透視投影圖解剖圖.png

我們能夠輕松的看到,此時剛好以fov的角度構(gòu)成一個直角三角形。不妨把整個透視投影過程看成:把平截頭內(nèi)的點(diǎn),投射到近平面上。我們要求的透視投影點(diǎn),就是上面黑色的點(diǎn)。

我們拆開了x,y軸拆開觀察,就如下情況


OpenGL右手坐標(biāo)系解剖圖.png

我們就根據(jù)這兩個圖分別對x,y軸的平截頭投影到NDC的證明。

老規(guī)矩,我們收集一下圖中有用的信息。

  • 視角原點(diǎn)到近平面的距離為n,距離遠(yuǎn)平面為f
  • 已知平截頭內(nèi)一點(diǎn)(x_{e},y_{e},z_{e})
  • 已知視角角度\theta
  • 求投影點(diǎn)(x_{n},y_{n},z_{n})

因?yàn)槭莵碜酝粋€角度,同一個原點(diǎn)的兩條射線。因此原點(diǎn),z軸相交的點(diǎn)(0,0,-n),(x_{n},y_{n},z_{n})構(gòu)成的三角形A,和原點(diǎn),(0,0,-z_{e}),(x_{e},y_{e},z_{e})構(gòu)成的三角形B是相似三角形。

因此,我們能夠得到如下的比例:
\frac{x_{p}}{x_{e}} = \frac{-n}{z_{e}}
稍微轉(zhuǎn)化一下:
x_{p}= \frac{n*x_{e}}{-z_{e}}

同理:
y_{p}= \frac{n*y_{e}}{-z_{e}}

能看到,此時的投影后的x和y都依賴于-z_{e}.
我們根據(jù)齊次坐標(biāo)系知道如下的等式:

裁剪矩陣 = 投影矩陣 * 空間矩陣
\left[ \begin{matrix} x_{clip} \\ y_{clip} \\ z_{clip} \\ w_{clip} \end{matrix} \right] = M_{projection} * \left[ \begin{matrix} x_{eye} \\ y_{eye} \\ z_{eye} \\ w_{eye} \end {matrix} \right]

NDC 矩陣 = 裁剪矩陣 / w分量
\left[ \begin{matrix} x_{ndc} \\ y_{ndc} \\ z_{ndc} \\ \end{matrix} \right] = \left[ \begin{matrix} x_{clip} / w_{clip}\\ y_{clip} / w_{clip}\\ z_{clip} / w_{clip} \\ \end {matrix} \right]

因此,我們?yōu)榱俗尶臻g矩陣可以正確的除以-z_{e}。因此我們需要構(gòu)造如下的矩陣:

image.png

我們已經(jīng)推導(dǎo)出了投影矩陣的w分量的參數(shù)。讓我們繼續(xù)推導(dǎo)x,y,z上的分量。
根據(jù)我們在正射投影推導(dǎo)出來的結(jié)果,在透視投影也同樣適用。我們同樣要把平截頭內(nèi)所有點(diǎn),都壓縮到NDC的[-1,1]之內(nèi)。因此,正射投影的結(jié)論一樣能夠放到這里來使用。

正射投影矩陣中:
x_{n} = \frac{2x_{p} }{r - l} - \frac{r+l}{r-l}
y_{n} = \frac{2y_{p} }{t - b} - \frac{t+b}{t-b}

不過這只是簡單的做縮放和位移。在透視投影中,平截頭內(nèi)和NDC兩個點(diǎn)之間還需要依賴z_{e}

x_{n} = \frac{2 * \frac{n*y_{e}}{-z_{e}} }{r - l} - \frac{r+l}{r-l}

我們可以把它化簡為除以w的分量
x_{n} = \frac{\left( \frac{2n}{r-l} * x_{e} + \frac{r+l}{r-l} *z_{e} \right)}{-z_{e}}

y_{n} = \frac{\left( \frac{2n}{t-b} * y_{e} + \frac{t+b}{t-b} *z_{e} \right)}{-z_{e}}

這樣我們就能獲得透視投影矩陣x,y的分量
\left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ . & . & . & . \\ 0 & 0 & -1 & 0 \end{matrix} \right]

這樣,我們就還差z軸的分量還沒有求出來。但是我們知道,z軸不依賴x,y軸。因此,我們可以把矩陣寫出如下形式:
\left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & A & B \\ 0 & 0 & -1 & 0 \end{matrix} \right]

就可以寫出如下等式:
z_{n} = \frac{A*z_{e}+B}{-z_{e}}

但是我們根據(jù)定義,能夠知道這個z_{n}坐標(biāo)將會壓縮在[-1,1]的區(qū)間。換句話說我們帶入(0,0,-n),(0,0,-f)必定對應(yīng)上-1和1之間。

2個未知數(shù),兩個連立方程式,必定有解。
\frac{-A*n+B}{n} = -1
\frac{-A*f+B}{f} = 1

求解A,B:
A =- \frac{f+n}{f-n}
B = - \frac{2n}{f-n}

此時我們就完成了透視投影矩陣的推導(dǎo):

\left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & - \frac{2n}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix} \right]

同樣的,如果視體是對稱的,我們一樣可以獲得一個簡化的矩陣

\left[ \begin{matrix} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{t} & 0 & 0 \\ 0 & 0 & - \frac{f+n}{f-n} & - \frac{2n}{f-n} \\ 0 & 0 & -1 & 0 \end {matrix} \right]

這樣就把正射投影矩陣和透視投影矩陣證明完畢。

實(shí)戰(zhàn)演練

從世界空間某個角度觀察圖片

那么我們開發(fā)中是不是要這么麻煩的處理編寫投影矩陣呢?實(shí)際上glm已經(jīng)已經(jīng)提供了相關(guān)的函數(shù)了:
正射投影:

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

前兩個參數(shù)指定了平截頭體的左右坐標(biāo),第三和第四參數(shù)指定了平截頭體的底部和頂部。通過這四個參數(shù)我們定義了近平面和遠(yuǎn)平面的大小,然后第五和第六個參數(shù)則定義了近平面和遠(yuǎn)平面的距離。這個投影矩陣會將處于這些x,y,z值范圍內(nèi)的坐標(biāo)變換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。

透視投影矩陣:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

它的第一個參數(shù)定義了fov的值,它表示的是視野(Field of View),并且設(shè)置了觀察空間的大小。如果想要一個真實(shí)的觀察效果,它的值通常設(shè)置為45.0f,但想要一個末日風(fēng)格的結(jié)果你可以將其設(shè)置一個更大的值。第二個參數(shù)設(shè)置了寬高比,由視口的寬除以高所得。第三和第四個參數(shù)設(shè)置了平截頭體的近和遠(yuǎn)平面。我們通常設(shè)置近距離為0.1f,而遠(yuǎn)距離設(shè)為100.0f。所有在近平面和遠(yuǎn)平面內(nèi)且處于平截頭體內(nèi)的頂點(diǎn)都會被渲染。

如果我們把攝像機(jī)放到z軸上方,看一個在世界坐標(biāo)上,沿著x軸做向上反轉(zhuǎn)-55度的笑臉箱子,該如何處理?

我們繼續(xù)沿用上一期的代碼,做一定的修改

首先編寫對應(yīng)的頂點(diǎn)著色器

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main(){
    gl_Position =  projection * view * model * vec4(aPos,1.0);
    
    TexCoord = vec2(aTexCoord.x,aTexCoord.y);
}

設(shè)定好之后,我們?nèi)ヌ幚硎澜缈臻g,觀察者空間,投影空間的矩陣:

        shader->use();
        //設(shè)置的是紋理單元
        glUniform1i(glGetUniformLocation(shader->ID,"ourTexture"),0);
        shader->setInt("ourTexture2", 1);

        //模型矩陣
        //物體坐標(biāo)變換到世界坐標(biāo)
        //把它從局部坐標(biāo),擺到世界中,是沿著x軸旋轉(zhuǎn)-55度
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f,0.0f,0.0f));
        //觀察矩陣
        //觀察的位置的移動
        //從世界坐標(biāo)到觀察空間
        //模擬我們攝像機(jī),沿著z軸向后移3個單位
        glm::mat4 view = glm::mat4(1.0f);
        view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));

        //投影矩陣
        //觀察到裁剪空間
        glm::mat4 projection = glm::mat4(1.0f);
        projection = glm::perspective(glm::radians(45.0f),
                                      ((float)engine->screenWidth)/((float)engine->screenHeight), 0.1f, 100.0f);

        GLuint modelLoc = glGetUniformLocation(shader->ID,"model");
        glUniformMatrix4fv(modelLoc,1,GL_FALSE,&model[0][0]);

        GLuint viewLoc = glGetUniformLocation(shader->ID,"view");
        glUniformMatrix4fv(viewLoc,1,GL_FALSE,&view[0][0]);

        GLuint projectionLoc = glGetUniformLocation(shader->ID,"projection");
        glUniformMatrix4fv(projectionLoc,1,GL_FALSE,&projection[0][0]);


        engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,
                                                     GLuint* texture,GLFWwindow *window){

            //箱子
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D,texture[0]);

            //笑臉
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D,texture[1]);
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);


        });
    }
image.png

從世界空間觀察某個3d物體。

我們先通過36點(diǎn)頂點(diǎn)坐標(biāo)構(gòu)造一個立方體:

void create3D(GLuint& VAO,GLuint& VBO){
    float vertices[] = {
        -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,  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, 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,  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, 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, 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, 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,  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,  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,  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,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    
    glGenVertexArrays(1,&VAO);
    glBindVertexArray(VAO);
    
    glGenBuffers(1,&VBO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)0);
    glEnableVertexAttribArray(0);
    
    glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    
    //glBindVertexArray(0);
    
}

緊接著繪制:

void showOneCube(Shader *shader){
    //模型矩陣
    //物體坐標(biāo)變換到世界坐標(biāo)
    //把它從局部坐標(biāo),擺到世界中,是沿著x軸旋轉(zhuǎn)90度
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.5f,1.0f,0.0f));
    //觀察矩陣
    //觀察的位置的移動
    //從世界坐標(biāo)到觀察空間
    //模擬我們攝像機(jī),沿著z軸向后移3個單位
    glm::mat4 view = glm::mat4(1.0f);
    view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));
    
    //投影矩陣
    //觀察到裁剪空間
    glm::mat4 projection = glm::mat4(1.0f);
    projection = glm::perspective(glm::radians(45.0f),
                                  (width/height), 0.1f, 100.0f);
    
    //            GLuint modelLoc = glGetUniformLocation(shader->ID,"model");
    //            glUniformMatrix4fv(modelLoc,1,GL_FALSE,&model[0][0]);
    shader->setMat4("model", &model[0][0]);
    
    //            GLuint viewLoc = glGetUniformLocation(shader->ID,"view");
    //            glUniformMatrix4fv(viewLoc,1,GL_FALSE,&view[0][0]);
    
    shader->setMat4("view", &view[0][0]);
    
    shader->setMat4("projection", &projection[0][0]);
    
    //            GLuint projectionLoc = glGetUniformLocation(shader->ID,"projection");
    //            glUniformMatrix4fv(projectionLoc,1,GL_FALSE,&projection[0][0]);
}
if(shader&&shader->isCompileSuccess()){

        shader->use();
        //設(shè)置的是紋理單元
        glUniform1i(glGetUniformLocation(shader->ID,"ourTexture"),0);
        shader->setInt("ourTexture2", 1);


        width = (float)engine->screenWidth;
        height = (float)engine->screenHeight;

        glEnable(GL_DEPTH_TEST);

        engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,
                                                     GLuint* texture,GLFWwindow *window){


            //箱子
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D,texture[0]);

            //笑臉
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D,texture[1]);



            showOneCube(shader);

            //showMoreCube(shader);

            glBindVertexArray(VAO);
            glDrawArrays(GL_TRIANGLES, 0, 36);


        });
    }
image.png

繪制更多的立方體并且轉(zhuǎn)動起來

如果我們需要讓3的倍數(shù)的立方體旋轉(zhuǎn)起來又如何?

我們需要更多的世界坐標(biāo)來設(shè)置立方體:

float width = 0;
float height = 0;
//世界坐標(biāo)
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)
};

循環(huán)繪制:

void showMoreCube(Shader *shader){
    glm::mat4 view = glm::mat4(1.0f);
    glm::mat4 projection = glm::mat4(1.0f);
    
    view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));
    projection = glm::perspective(glm::radians(45.0f),
                                  (width/height), 0.1f, 100.0f);
    
    shader->setMat4("view", glm::value_ptr(view));
    shader->setMat4("projection", glm::value_ptr(projection));
    
    for(int i = 0;i<10;i++){
        glm::mat4 model = glm::mat4(1.0f);
        
        
        model = glm::translate(model, cubePositions[i]);
        float angle = 20.0f * i;
        if(i % 3 == 0){
            model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(1.0f,3.0f,0.5f));
        }else{
            model = glm::rotate(model, angle, glm::vec3(1.0f,3.0f,0.5f));
        }
        
        shader->setMat4("model", glm::value_ptr(model));
        
        glDrawArrays(GL_TRIANGLES,0,36);
    }
}
世界中的旋轉(zhuǎn)立方體.gif

總結(jié)

OpenGL的有5個坐標(biāo)系,局部空間,世界空間,觀察空間,裁剪空間,屏幕空間。

我們能夠通過如下的公式來對一個在局部空間中用頂點(diǎn)坐標(biāo)構(gòu)建的物體做變換到一個從攝像機(jī)角度觀察的世界中的物體:
M_{clip} = M_{projection} * M_{view} * M_{model} *M_{local}

當(dāng)我們要從裁剪坐標(biāo)換算到NDC坐標(biāo),要符合如下公式:
\left[ \begin{matrix} x_{ndc} \\ y_{ndc} \\ z_{ndc} \\ \end {matrix} \right] = \left[ \begin{matrix} x_{clip} / w_{clip} \\ y_{clip} / w_{clip} \\ z_{clip} / w_{clip} \\ \end{matrix}\right]

實(shí)際上這就是所有坐標(biāo)之間的關(guān)系。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容