投影矩陣推導(翻譯)
原網址:http://www.codeguru.com/cpp/misc/misc/graphics/article.php/c10123/Deriving-Projection-Matrices.htm
3D矩陣變換中,投影矩陣是最復雜的。位移和縮放變換一目了然,旋轉變換只要基本的三角函數就能想象出來,投影矩陣則很難憑借直覺想象出來。
總述:什么是投影
計算機顯示屏是二維平面,所以如果你想顯示三維物體,需要找到把三維物體渲染成二維圖像的方法。這正是投影要做的。最簡單的做法:直接丟掉三維物體各頂點的Z坐標。對於一個立方體,看起來像圖1:
圖1 通過丟掉Z坐標方法投影到XY平面
這種投影簡單且不實用。所以,一開始就不應該投影到“面”(plane)上,而應該投影到一個“體”(volume)內,即所謂的“規范視域體”(canonical view volume)。規范視域體的頂點坐標在不同的API(DirectX/OpenGL)中有所不同。這里就用D3D的標准,從 (-1,-1,0)到(1,1,1)。當所有的頂點映射到規范視域體中后,XY坐標用來再映射到屏幕上。Z坐標看起來無用,不過通常用來表示深度信息。這也是為什么會投影到一個“體”,而不是“面”的原因。
下面將講述兩種常見變換:正交變換、透視變換。
正交變換
“正交”的由來是投影線與顯示平面垂直,是一種相對簡單的投影技術。“視體”——眼睛可見的所有幾何體的空間——是個各邊與坐標軸平行的盒子,現在要把這個盒子映射到規范視域體中,如圖2所示:
圖2 正交投影
正如你所見到的,視體(view volume)可以由6個面定義:
Left: x = l
Right: x = r
Bottom: y = b
Top: y = t
Near: z = n
Far: z = f
因為視體(view volume)和規范視域體(canonical view volume)的軸都與坐標軸平行,所以在這種投影中,對距離沒有修正(correction)(這里指的是近大遠小)。實際上,它非常像 圖1 那種,直接扔掉各點Z坐標的做法。在3D空間中,同樣大小的物體,投影后,在投影空間也是一樣大,即使其中一個距離攝像機比另一個遠很多也不會有不同。3D空間中平行的線在投影后依然平行。這種投影不可能用於第一人稱射擊游戲,因為它不能區分出物體的遠近。但是它確實有自己的用途。你可以把它用在基於格子的游戲中,特別是當攝像機處於固定角度時,正如 圖3所示的那樣:
圖3 正交投影的例子
下面來看看它(正交投影)是怎么工作的。簡單地,可以將三個坐標軸分開考慮,分別計算如何將各點(在某個軸上)的坐標(3D空間中),映射到規范視域體中(投影空間)。先算X軸。視體中的坐標處於 [l, r] 區間,現在要把它映射到 [-1, 1] 區間:
在進行縮放前,要將區間的左邊歸 0。簡單地,將 X 坐標減去l:
現在區間的左邊已經是 0 了,可以將它縮放到需要的尺寸。我們想要的投影后的區間長度為 2(-1 到 1),所以將 X 坐標乘 2/(r-l)。因為 r – l 就是視體的寬度,所以它總為正數,不用擔心它會改變不等號方向:
接下來,將結果減去 1,讓它落在 [-1, 1] 區間:
整理一下:
好了,下面要把得到的結果分解成 px + q 這樣的形式,因為這樣方便轉換成矩陣形式:
上面式子的中間部分,就是將3D空間中的 X 轉換到規范視域體的算式:
對於Y軸也應用同樣的過程:
類似的過程也應用於Z軸。這會有一點點的不同,因為Z的范圍是 [0, 1],而不是 [-1, 1]。但它們看起來會很相似。
開始:
接着:
再接着:
再接着:
最后:
現在,我們已經做好准備,能寫出正交變換矩陣了。這里有三個算式:
把它們寫成矩陣:
這就是我們要得到的結果!
D3D提供了一個叫 D3DXMatrixOrthoOffCenterLH() 的函數來構建正交變換矩陣。LH表示這個函數使用的是左手坐標系。那么,OffCenter 是什么鬼?
這個問題涉及一個形式更簡單的正交變換矩陣。
考慮這樣一些點:
1. 在眼睛所在的空間里,攝像機處於原點;
2. 通常,我們希望視場(Field of View)相對於Z軸左右對稱,上下對稱。
如此的話,Z軸就直接從視體(View Volume)的中間穿過。所以有 r = -l,t = -b。換句話說,可以不去管 r,l,t和b,只需要視體(view volume)的寬w和高h,加上裁剪平面f和n,就可以定義視體了。如果使用新的定義,將得到一個更簡單的投影矩陣版本:
D3DXMatrixOrthoLH() 方法實現了這個矩陣的算法。大多數情況下可以直接使用這個版本,而不是那個更通用的“off center”版本。
在結束這個小節前,我們再深入一點點。這個正交變換矩陣可以表示為兩個稍微簡單點的矩陣的乘積:平移矩陣和縮放矩陣。不難理解,因為正交變換相當於把點從一個坐標軸平行的盒子里變換到另一個坐標軸平行的盒子里。視體(Viewing Volume)並沒有改變物體的形狀,只改變了位置和大小。更准確地說:
上面的乘積更直觀。首先,視體沿着Z軸平移,讓它的近平面與原點重合;接着,對它進行縮放。讓它的大小與規范視域體(Canonical View Volume)相同。這個很好理解吧?Off Center版本的投影矩陣也可以被表示為一個平移和縮放矩陣的乘積。
以上是正交變換矩陣的所有內容,接下來試試點挑戰。
透視投影
透視投影相對復雜一點,也更常用。因為它給人一種近大遠小的錯覺,所以感覺上更真實。
從幾何上說,這種投影與正交投影的不同在於:透視投影的視域(View Volume)是一個截頭錐體,就是一個截掉頭部的金字塔形狀,而不是一個各邊與坐標軸平行的盒子。如圖4:
圖 4 透視投影
如你所見,視體的近平面是一個左下角(l,b,n)到右上角(r,t,n)的長方形。遠平面則是從原點到近平面的四個頂點作射線,與平面z = f相交,得到的四個點構成的長方形。因為視錐(View Frustum)距離原點越遠越寬,又因為要把這個視錐體變換到規范視域體(canonical view volume)中。而規范視域體是一個各邊與坐標軸平行的盒子。所以視錐的遠端(比較粗的一頭),相對於近端(比較細的一頭)壓縮的程序更大。這也是“近大遠小”的原因。
因為視錐在變換的過程中形狀發生了變化,所以透視投影不能簡單地分解成平移和綻放變換。不過這並不意味着在正交投影下的工作白費。數學上常用的方法就是把一個復雜問題分解成簡單問題嘛。上次(正交變換),我們一次考慮一個坐標軸。而這次,將把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 近平面上
如圖 5,從點(x,y,z)向原點作直線,直線與 z = n平面(近平面)相交於圖中黑色的點。從這些點向z軸作兩條垂線,突然你就得到了兩個相似三角形:D。如果想不起相似三角形是啥,翻翻高中課本就知道了。
根據勾股定理,從(x,y,z)向z軸作的垂線長度為:
從近平面上的交點(黑色點)到z軸作的垂線的長度:
投影點的x坐標為 x * n / z,y坐標為 y * n / z。(其實不用勾股定理,直接由相似三角形就得到結果了。)
這就是第1步的內容。
第2步類似正交變換中的,將x,y坐標映射到規范視域體:
公式是一樣的,只是要把x用x * n / z代替,y用 y * n / z 代替:
兩邊同乘z:
這個結果有點奇怪。想要把它寫成矩陣的話,需要寫成這種格式:
不過上面的式子不是這樣的,看起來似乎不對:p。
怎么辦呢?我們已經得到了x’z,y’z的表達式,如果同樣能獲得類似的z’z的表達式,就能獲得將(x,y,z)映射到(x’z,y’z,z’z)的矩陣。將它們同除z,就能獲得(x’,y’,z’)了。
Z到z’的變換不依賴x,y。因為是線性變換,我們知道大概的變換公式是這樣的:z’z = pz + q,其中p,q是常數。要確定這兩個常數也容易,因為有兩個特殊點。要映射 [n,f] 到 [0, 1],所以當 z = n時,z’ = 0,當 z = f 時 z’ = 1。將這兩組值代入 z’z = pz + q,可以解出 p,q。
第 1 組值:
第 2 組值:
結果:
這樣就獲得了z’z的表達式:
快完成了。不過因為結果需要用齊次坐標來表示。原來,我們直接把齊次坐標的第四個分量寫成 1,即 w = 1,現在我們要的是(x’z,y’z,z’z,w’z),w’ = 1,所以,可以寫成 w’z = z。
將它寫成矩陣形式:
當我們把這個矩陣應用於點(x,y,z,1)時,我們獲得的是(x’z,y’z,z’z,z)。接着我們會做齊次坐標常做的處理:同除第四個分量。這樣就得到了(x’,y’,z’,1)。
這就是投影矩陣了。D3DXMatrixPerspectiveOffCenterLH()函數實現了上面的公式。
如果視錐是以z軸為中心對稱的(r = -1,t = -b)。投影矩陣可以用視錐的寬和高來表示:
D3D也有一個這種形式的函數:D3DXMatrixPerspectiveLH()。
最后,還有一種表示也很方便。在這種形式中,並不考慮視錐參數,而是使用攝像機的參數。圖6展示了這個概念:
圖6 用相機張角表示視錐的高
視錐的高度用相機的視角來表示。相機的視角用α表示,則α被z軸平分。α與視錐的寬和高的關系如下:
這樣可以替換掉公式中的高度。更進一步,還可以將寬度替換為(屏幕的)高寬比r。
這樣就獲得了一個用視角和高寬比表示的投影矩陣:
D3D中,可以用D3DXMatrixPerspectiveFovLH()來獲得這個形式的投影矩陣。這種形式的矩陣比較方便,因為只要設置好屏幕高寬比,和相機視角(一般90度),剩下的就只要關心視錐在z軸上的位置就行了。
這就是投影矩陣的推導過程。如果你用的是右手坐標系,可能會有所不同。