在3D圖形程序的基本矩陣變換中,投影矩陣是其中比較複雜的。平移和縮放瀏覽一下就能理解,旋轉矩陣只要掌握了三角函數知識也能夠理解,但投影矩陣有點棘手。若是你曾經看過投影矩陣,你會發現你的常識不足以告訴你它是怎麼來的。並且,我在網上還未看到許多關於如何推導投影矩陣的教程資源。本文的話題就是如何推導投影矩陣。程序員
對於剛剛開始接觸3D圖形的人,我應該指出,理解投影矩陣如何推導多是咱們對於數學的好奇心,它不是必須的。你能夠只用公式,而且若是你用像OpenGL那樣的圖形API,你甚至都不須要使用公式,圖形API會爲你構建一個投影矩陣。因此,若是本文看起來有點難,不要懼怕。只要你理解了投影矩陣作了什麼,你不必在你不想的狀況下關注它是怎麼作的。本文是給那些想了解更多的程序員的。函數
概述: 什麼是投影?學習
計算機顯示器是一個二維表面,因此若是你想顯示三維圖像,你須要一種方法把3D幾何體轉換成一種可做爲二維圖像渲染的形式。那也正是投影作的。拿一個簡單的例子來講,一種把3D對象投影到2D表面的方法是簡單的把每一個座標點的z座標丟棄。對立方體來講,看上去可能像圖1:測試
圖1: 經過丟棄Z座標投影到XY平面spa
固然,這過於簡單,而且在大多數狀況下不是特別有用。首先,根本不會投影到一個平面上;相反,投影公式將變換你的幾何體到一個新的空間體中,稱爲規範視域體(canonical view volume),規範視域體的精確座標可能在不一樣的圖形API之間互不相同,但做爲討論起見,把它認爲是從(-1, -1, 0)延伸至(1, 1, 1)的盒子,這也是OpenGL中使用的。一旦全部頂點被映射到規範視域體,只有它們的x和y座標被用於映射到屏幕上。這並不表明z座標是無用的,它一般被深度緩衝用於可見度測試。這就是爲何變換到一個新的空間體中,而不是投影到一個平面上。對象
注意,圖1描述的是左手座標系,攝像機俯視z軸正方向,y軸朝上而且x軸朝右。這是OpenGL中使用的座標系,本文中我都將使用該座標系。對於右手座標系系統來講,在計算方面沒有明顯差別,在規範視域體方面有一點區別,因此一切討論仍將適用即便你的圖形API使用與OpenGL不一樣的規定。教程
如今,能夠進入實際的投影變換了。有許多投影方法,我將介紹最多見的2種:正交和透視。遊戲
正交投影(Orthographic Projection)資源
正交投影,之因此這麼稱呼是由於全部的投影線都與最終的繪圖表面垂直,是一種相對簡單的投影技術。視域體,也就是包含全部你想顯示的幾何體的可視空間——是一個將被變換到規範視域體的軸對齊盒子,見圖2:文檔
圖2: 正交投影
正如你看見的,視域體由6個面定義:
由於視域體和規範視域體都是軸對齊盒子,這種類型的投影沒有距離更正。最終的結果是,事實上,很像圖1那樣每一個座標點只是丟棄了z座標。對象在3D空間中的大小和在投影中的大小相同,即便一個對象比另外一個對象距離攝像機遠不少。在3D空間中平行的直線在最終的圖像上也是平行的。使用這種類型的投影將出現一些問題像第一人稱射擊遊戲——試想一下在不知道任何東西有多遠的狀況下玩!但它也有它的用處。你可能在格子游戲中使用它,例如,特別是攝像機被綁定在一個固定角度的一款格子游戲中,圖3顯示了1個簡單的例子:
圖3: 正交投影的一個簡單例子
因此,事不宜遲,如今開始弄清楚它是如何工做的。最簡單的方法多是3個座標軸分開考慮,而且計算如何沿着每一個座標軸將點從視域體映射到規範視域體。從x軸開始,視域體中的點的x座標範圍在[l, r],想把它變換到範圍在[-1, 1]:
如今,準備把範圍縮小到咱們指望的,各項減去l,這樣,最左邊的項變爲0。另外一種可能考慮的作法是平移範圍使其以0爲中心,而不是一端爲0,但如今這種方式代數式更整潔,因此爲了可讀性起見我將以如今這種方式作:
如今,範圍的一端是0,你能夠縮小到指望的大小。你指望x值的範圍是2個單位寬,從1到-1,因此把各項乘以2/(r-l)。注意r-l是視域體的寬度,所以始終是一個正數,因此不用擔憂不等號會改變方向:
下一步,各項減去1就產生了咱們指望的範圍[-1,1]:
基本代數容許咱們將中間項寫成一個單一的分數:
最後,把中間項分紅兩部分使它形如px+q的形式,咱們須要把項組織成這種形式這樣咱們推導的公式就能夠簡單的轉換成矩陣形式:
這個不等式的中間項告訴了咱們把x轉換到規範視域體的公式:
獲取y的變換公式的步驟是徹底同樣的——只要用y替代x,用t替代r,用b替代l——因此這裏不重複它們了,只是給出結果:
最後,須要推倒z的變換公式。z的推導有點不一樣,由於須要把z映射到範圍[0, 1]而不是[-1, 1],但看上去很類似。z座標最開始在範圍[n,f]:
把各項減去n,這樣的話範圍的下限就變爲了0:
如今剩餘要作的就是除以f-n,這樣就產生了最終的範圍[0,1]。和前面相同,注意f-n是視域體的深度因此絕對不會爲負:
最後,把它分紅兩部分使它形如px+q的形式:
這樣便給出了z的變換公式
如今,能夠準備寫正交投影矩陣了。總結到目前爲止的工做,推導了3個投影公式:
若是寫成矩陣形式,就獲得了:
就是這樣!OpenGL提供了orthoM()()方法構造一個和這個公式相同的正交投影矩陣;你能夠在OpenGL文檔中找到。方法名中的"LH"表明了你正在使用左手座標系。可是,究竟"OffCenter"的意思是什麼呢?
這一問題的答案引導你到一個正交投影矩陣的簡化形式。考慮幾點: 首先,在可見空間中,攝像機定位在原點而且沿着z軸方向觀看。第二,你一般但願你的視野在左右方向上延伸的一樣遠,而且在z軸的上下方向上也延伸的一樣遠。若是是這樣的狀況,那麼z軸正好直接穿過你視域體的的中心,因此獲得了r = -l而且t = -b。換句話說,你能夠把r, l, t和b一塊兒忘掉,簡單的把視域體定義爲1個寬度w和1個高度h,以及裁剪面f和n。若是你在正交投影矩陣中應用上面說的,那麼你將獲得這個至關簡化的版本:
你幾乎能夠一直使用這個矩陣替代上面那個你推導的更通用的"OffCenter"版本,除非你用投影作些奇怪的事情。
在完成這部分以前還有一點。它啓發咱們注意到這個矩陣能夠用兩個簡單的變換串聯替代:平移其次是縮放。若是你思考幾何的話這對你是有意義的,由於全部你在正交投影中作的就是從一個軸對齊盒子轉向另外一個軸對齊盒子;視域體不改變它的形狀,只改變它的位置和大小。具體來講,有:
這種投影方式可能更直觀一點由於它讓你更容易想象發生了什麼。首先,視域體沿着z軸平移使它的近平面和原點重合;而後,應用一個縮放把它縮小到規範視域體大小。很容易理解吧,對不對?一個偏離中心(OffCenter)的正交投影矩陣也能夠用一個變換和一個縮放代替,它和上面的結果很類似因此我在這裏不列出了。
上面就是正交投影,如今能夠去接觸一些更有挑戰性的東西了。
透視投影(Perspective Projection)
透視投影是稍複雜的一種投影方法,而且用的愈來愈平凡,由於它創造了距離感,所以會生成更逼真的圖像。從幾何上說,這種方法與正交投影不一樣的地方在於透視投影的視域體是一個平截頭體——也就是,一個截斷的金字塔,而不是一個軸對稱盒子。見圖4:
圖4: 透視投影
正如你所看見的,視域體的近平面從(l,b, n)延伸至(r, t, n)。遠平面範圍是從原點發射穿過近平面四個點的射線直至與平面z=f相交。因爲視域體從原點進一步延伸,它變得愈來愈寬大;同時你將這個形狀變換到規範視域體盒子;視域體的遠端比視域體的近端壓縮的更厲害。所以,視域體遠端的物體會變得更小,這就給了你距離感。
因爲空間體形狀的這種變換,透視投影不能像正交投影那樣簡單的表達爲一個平移和一個縮放。你必須制定一些不一樣的東西。可是,這並不意味着你在正交投影上作的工做是無用的。一個方便的解決數學問題的方法是把問題減小到你已經知道怎麼解決的那一個。因此,這就是你在這裏能夠作的。上一次,你一次檢查一個座標,但此次,你將把x和y座標合起來一塊兒作,而後再考慮z座標。你對x和y的處理能夠分2個步驟:
第1步: 給定視域體中的點(x,y, z),把它投影到近平面z=n。因爲投影點在近平面上,因此它的x座標範圍在[l, r],y座標範圍在[b, t]。
第2步: 使用你在正交投影中學會推導的公式,把x座標從[l, r]映射到[-1, 1],把y座標範圍從[b, t]映射到[-1, 1]。
聽上去很棒吧?看一看圖5:
圖5: 使用類似三角形投影一個點到z=n平面
在這個圖中,你從點(x, y, z)到原點畫了條直線,注意直線與z=n平面相交的那個點——用黑色標記的那個。經過這些點,你畫了2條相對於z軸的垂線,忽然你獲得了一對類似三角形。若是你可以回想起高中的幾何知識,類似三角形是擁有相同形狀但大小不必定相同的三角形。爲了證實2個三角形是類似的,必須證實它們的同位角相等,在這裏不難作到。角1被兩個三角形共享,顯然它和自身相等。角2和角3是穿越兩條平行線造成的同位角,因此它們是相等的。同時,直角固然是彼此相等的,因此兩個三角形是類似的。
對於類似三角形你應該感興趣的是它們的每對對應邊都是同比例的。你知道沿着z軸的邊的長度,它們是n和z。那意味着其餘對應邊的比例也是n/z。因此,考慮下你知道了什麼。根據勾股定理,從(x, y, z)相對於z軸作的垂線具備如下長度:
若是你知道了從你的投影點到z軸的垂線的長度,那麼你就能夠計算出該點的x和y座標。長度怎麼求?那太簡單了!由於你有了類似三角形,因此長度就是簡單的L乘以n/z:
所以,x座標是x * n/z,y座標是y * n/z。第一步作完了。
第二步只是簡單的執行你上一部分作的一樣的映射,因此是時候回顧下你在正交投影中學習到的推導公式了。回想下把x和y座標映射到規範視域體,像這樣:
如今你能夠再次調用這些公式,除非你要考慮到投影;因此,把x用x * n/z代替,把y用y * n/z代替:
如今,經過乘以z:
這些結果有點奇怪。爲了把這些等式寫進矩陣,你須要把它們寫成這種形式:
但很明顯,如今還作不到,因此如今看起來進入了僵局。應該作什麼呢?若是你能找到個辦法得到z'z的公式就像x'z和y'z那樣,你就能夠寫一個變換矩陣把(x, y, z)映射到(x'z, y'z, z'z)。而後,你只須要把各部分除以點z,你就會獲得你想要的(x', y', z')。
由於你知道z到z'的轉換不依賴於x和y,你知道你想要一個公式形如z'z= pz + q,p和q是常量。而且,你能夠很容易的找到那些常量,由於你知道在兩種特殊狀況下如何獲得z': 由於你要把[n, f]映射到[0, 1],你知道當z=n時z'=0,和z=f時z'=1。當你把第一組值代入z'z = pz + q,你能夠解得:
如今,把第二組值代入,獲得:
把q的值代入等式,你能夠很容易的解得p:
如今你有p的值了,而且剛剛你求得了q= –pn,因此你能夠解得q:
最後,把p和q的表達式代入最原始的公式中,得:
你就快完成了,可是你處理這個問題的不尋常的性質須要你也處理齊次座標w。一般狀況下,只是簡單的設置w' = 1 ——你可能已經注意到在一個基本的變換下最後一行老是[0, 0, 0, 1]---可是如今你在爲點(x'z, y'z, z'z, w'z)寫一個變換。因此取而代之的,把w' = 1寫成w'z = z。所以最後用於透視投影的等式以下:
如今,當你把這個等式寫成矩陣的形式,獲得:
當你把這個矩陣用於點(x, y, z,1),它將產生(x'z, y'z, z'z, w'z)。而後,你應用一般的步驟去除以齊次座標,獲得(x', y', z', 1)。那就是透視投影。OpenGL的perspectiveM()方法也實現了上述公式。正如正交投影,若是你假設視域體是對稱的而且中心是z軸(也就是r = -l,t = -b),你能夠簡單的用視域體的寬w和高h改寫矩陣中的各項:
OpenGL的perspectiveM()方法也生成這個矩陣。
最後,還有個常常用的上的透視投影的表示。在這種表示中,你根據攝像機的可視範圍定義視域體,而不用去擔憂視域體的尺寸。此概念參閱圖6:
圖6: 視域體的高由垂直可視範圍的角度a定義
垂直可視範圍的角度是a。這個角度被z軸一分爲二,因此根據基本的三角函數,你能夠寫下面的方程,關聯a和近平面n以及屏幕高度h:
這個表達式能夠取代投影矩陣中的高度。此外,使用橫縱比r代替寬度,r定義爲顯示區域的寬比高的橫縱比。因此,獲得:
所以,有了用垂直可視範圍角度a和橫縱比r構成的透視投影矩陣:
這種形式特別有用,由於你能夠直接把r設置成渲染窗口的橫縱比,而且可視範圍角度爲p / 4比較好。因此,你真正須要擔憂的事情只是定義視域體沿着z軸的範圍。
下一篇效果圖: