要把3D物體投射到電腦2D的螢幕上,要辦到這件事情就是要靠我們的投影矩陣來達成,基本上就是把原本在觀察座標(View coordinate)的頂點轉換到裁剪座標(Clip coordinate)。
投影矩陣分為兩種,一種是正交投影(Orthogonal)和透視投影(Perspective)兩種,差別在於正交投影裡面的平行線就是平行線永不相交;而透視投影比較貼近現實,越遠的東西會越小,且也會逐漸聚在中心成一個點。
因為裁剪坐標是齊次座標(Homogeneous coordinate)的一種,所以除了x, y, z之外還會多一個 w 分量,接著要轉回常見的三維座標,這時會做透視除法(Perspective division),將 x, y 與 z 都除以 w 分量,才會變成標準設備座標(Normalize Device Coordinate),然後再經過所謂的視口變換(Viewport transform),才會是最後的螢幕座標(Screen Coordinate)。
投影矩陣通常都會需要6個參數,分別是: left, right, bottom, top, near, far。
特別注意,我們假設在裁剪空間的一點 ,如果 任數值大於 或小於 都會被裁剪掉,所以就不會呈現在畫面上了。
那至於投影矩陣是如何生成的,這裡將講解其原理過程:
以下投影矩陣將是以 OpenGL 為範例說明,想了解 DirectX 的話請看這裡。
先來探討透視投影,因為這比較難。
我們假設6個參數分別為:,這些參數的意思就是告訴我們要把在x軸 映射到 ;y軸 映射到 ;z軸 映射到 的意思。
特別注意觀察空間是屬於右手座標系統,也就是說Z軸是朝攝影機(攝影機看向Z負軸),而NDC則是屬於左手座標系統,所以Z軸會是相反的,所以 near 和 far 要記得變負數。
可以知道的是說我們要把在觀察座標上的點映射到我們的近平面上,我們先不要管 y 的部分先看 z 軸,它的部分很簡單,就是統一變成 ,但 x 怎麼處理呢?我們可以利用相似三角形(Similar Triangles),下圖中有咖啡色和紫色框起來的三角形,這兩個三角形的三個角度都一樣,只是長度不一樣,所以根據性質這兩個三角形的三個邊彼此是有比例關係的,所以算法如下:
所以說y軸的話同理,你只需要從不同角度去看(由x正軸向x負軸看去即可),得:
這時你會發現, 和 都是依賴於 ,因為它們都有除以 ,我們也知道當裁剪空間要轉換成 NDC 時也會除以 ,所以我們可以讓 ,這樣也可推導出投影矩陣的第四列:
而接下來換求 和 ,因為 和 都是要映射至 NDC 的 到 之間,我們就採用線性轉換的方式(y=ax+b),記住 軸是 變成 ; 軸是 變成 :
帶入 進去 化簡得:
之後求:
所以我們得出式子:
y軸基本上也同理:
帶入 進去 化簡得:
之後求:
所以我們得出式子:
接著,代入我們剛剛算好的 和 :
如此一來我們就可以推導出我們的投影矩陣的第一二列了:
現在我們只要找出第三列即可,但與先前不一樣的是,因為我們把觀察空間中的 值統一映射到近平面 上,但我們還是需要獨一的 值來進行後續的深度測試(Depth test)。這邊 值並不依賴於 和 值,所以我們只能借助 來取得 與 之間的關係:
在觀察空間,等於1,所以說等式會變成這樣:
現在我們只需要找出A跟B的值就好,我們代入,我們會得到以下方程式:
我們藉由等式1可知:
代入等式2後可得:
然後再把A代回等式1求B:
恭喜,我們將整個透視矩陣求出來啦:
然而我們可以再化簡,通常投射矩陣的方錐體(Frustum)通常是對稱的,所以說, ,所以我們可以得出以下結論:
所以最終的透視矩陣為:
特別注意 和 的關係,它們彼此之間是一個非線性的關係,所以說當一個物體很近時它的 精度會很高,但距離一拉長精度卻會大幅降低。
而現今的透視投影需要的是四個參數:視野廣度(FOV)、畫面比例(Aspect Ratio)、近平面 以及遠平面 。視野廣度可以代表人眼睛的可見範圍,通常指的是【垂直視野的夾角】,通常設定為,因為看起來比較正常,FOV越小可見範圍就會越小,所以畫面看起來就像是放大(Zoom In)的感覺,而數值越大看起來畫面就會開始變形。注意:FOV不可大於等於180度。
人的垂直視野大概是,最大可到。
所以說知道了FOV我們就可以求出投影矩陣需要的 與 ,我們使用三角函數的方式來推算:
我們如果要求 值,就需要知道 長度為何,而我們可以發現,它是 的對邊(Opposite),所以 為鄰邊(Adjacent)、 為斜邊(Hypotenuse);而我們知道 是對邊除以鄰邊,那我們只要將 與鄰邊相乘幾可求出對邊長,至於鄰邊長為何,我們可以知道它就是 ,公式如下:
因為是相呼對稱,所以 ,如此一來我們垂直高度已經求出了!。
再來,就是要求水平寬度,如果你的畫面(攝影機、螢幕、視窗大小)是正方形的,那恭喜你很簡單,寬度等於高度,所以:
但現今的螢幕通常是長方形的,比例也不完全相同,所以我們需要長寬比(Aspect Ratio),透過它來推算我們的寬應該要是多少:
注意我們這邊所講的長寬比,預設都是寬除以高哦!
所以說,我們的透視矩陣變為:
然後化簡最終就變成:
因為 FOV 度數會除以 2,所以當 FOV 為 時,會變成 ,注意這在數學是無意義的,而當度數大於180時, 出來的數值都會是負的,這也不合邏輯。
程式碼如下(Implement on C++):
注意要此程式要 include glm,另外 OpenGL 採用的矩陣是 Column-Major Order(指的是記憶體的 Layout)。
正交投影相對來說就簡單了許多,它就像是一個長方體你直接擷取,裡面所有東西都要投射到近平面上,但完全不會產生越遠的物體越小這種視覺效果,平行線就是平行,原理如下:
所以說三軸基本上都是簡單的線性轉換, 軸是 變成 ; 軸是 變成 ; 軸是 變成 :
基本上就是各別帶入 、、 去計算,這邊過程就不特別贅述了,最後求出的式子為:
而 值對於正交投影也不用有什麼特殊計算,就繼續等於 1 即可,所以正交投影矩陣為:
那如果說是相互對稱的話,, ,所以我們可以得出以下結論:
所以最終的正交投影矩陣為:
程式碼如下(Implement on C++):
我們可以在片段著色器中獲取深度值,該值介於0到1之間:
一般來說我們的投影矩陣就會把物品的 值映射到近平面與遠平面之間,預設就是非線性的,如果想要將深度值改為線性模式,我們就必須要先反轉掉投影矩陣的轉換。
首先我們先將深度值從 Screen coordinates 轉為 NDC,這邊只是做做簡單的域值轉換, 到 :
目前我們只是從 Window Coordinates 轉換成 Normalized Device Coordinates,而我們必須逆轉換投影矩陣對 的非線性轉換,我們知道它的公式為:
化簡後得:
接著開始逆向推倒:
最後,我們所求出來的 是在觀察世界,相機面向負 軸,所以 為負,但用於計算深度時,我們要將其乘上 ,所以實作於 code 時語法如下:
http://www.songho.ca/opengl/gl_projectionmatrix.html
https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix
https://learnopengl.com/Advanced-OpenGL/Depth-testing
OpenGL