天天看點

深入探索透視投影變換

透視投影是3D固定流水線的重要組成部分,是将相機空間中的點從視錐體(frustum)變換到規則觀察體(Canonical View Volume)中,待裁剪完畢後進行透視除法的行為。在算法中它是通過透視矩陣乘法和透視除法兩步完成的。

透視投影變換是令很多剛剛進入3D圖形領域的開發人員感到迷惑乃至神秘的一個圖形技術。其中的了解困難在于步驟繁瑣,對一些基礎知識過分依賴,一旦對它們中的任何地方感到陌生,立刻導緻了解停止不前。

沒錯,主流的3D APIs如OpenGL、D3D的确把具體的透視投影細節封裝起來,比如

gluPerspective(…)就可以根據輸入生成一個透視投影矩陣。而且在大多數情況下不需要了解具體的内幕算法也可以完成任務。但是你不覺得,如果想要成為一個職業的圖形程式員或遊戲開發者,就應該真正降伏透視投影這個家夥麼?我們先從必需的基礎知識着手,一步一步深入下去(這些知識在很多地方可以單獨找到,但我從來沒有在同一個地方全部找到,但是你現在找到了J)。

我們首先介紹兩個必須掌握的知識。有了它們,我們才不至于在了解透視投影變換的過程中迷失方向(這裡會使用到向量幾何、矩陣的部分知識,如果你對此不是很熟悉,可以參考《向量幾何在遊戲程式設計中的使用》系列文章)。

齊次坐标表示

透視投影變換是在齊次坐标下進行的,而齊次坐标本身就是一個令人迷惑的概念,這裡我們先把它了解清楚。

根據《向量幾何在遊戲程式設計中的使用6》中關于基的概念。對于一個向量v以及基oabc,

深入探索透視投影變換

可以找到一組坐标(v1,v2,v3),使得

v = v1 a + v2 b + v3 c (1)

而對于一個點p,則可以找到一組坐标(p1,p2,p3),使得

p – o = p1 a + p2 b + p3 c (2)

從上面對向量和點的表達,我們可以看出為了在坐标系中表示一個點(如p),我們把點的位置看作是對這個基的原點o所進行的一個位移,即一個向量——p – o(有的書中把這樣的向量叫做位置向量——起始于坐标原點的特殊向量),我們在表達這個向量的同時用等價的方式表達出了點p:

p = o + p1 a + p2 b + p3 c  (3)

(1)(3)是坐标系下表達一個向量和點的不同表達方式。這裡可以看出,雖然都是用代數分量的形式表達向量和點,但表達一個點比一個向量需要額外的資訊。如果我寫出一個代數分量表達(1, 4, 7),誰知道它是個向量還是個點!

我們現在把(1)(3)寫成矩陣的形式:

這裡(a,b,c,o)是坐标基矩陣,右邊的列向量分别是向量v和點p在基下的坐标。這樣,向量和點在同一個基下就有了不同的表達:3D向量的第4個代數分量是0,而3D點的第4個代數分量是1。像這種用4個代數分量表示3D幾何概念的方式是一種齊次坐标表示。

“齊次坐标表示是計算機圖形學的重要手段之一,它既能夠用來明确區分向量和點,同時也更易用于進行仿射(線性)幾何變換。”—— F.S. Hill, JR

這樣,上面的(1, 4, 7)如果寫成(1,4,7,0),它就是個向量;如果是(1,4,7,1),它就是個點。

下面是如何在普通坐标(Ordinary Coordinate)和齊次坐标(Homogeneous Coordinate)之間進行轉換:

從普通坐标轉換成齊次坐标時,

如果(x,y,z)是個點,則變為(x,y,z,1);

如果(x,y,z)是個向量,則變為(x,y,z,0)

從齊次坐标轉換成普通坐标時,

如果是(x,y,z,1),則知道它是個點,變成(x,y,z);

如果是(x,y,z,0),則知道它是個向量,仍然變成(x,y,z)

以上是通過齊次坐标來區分向量和點的方式。從中可以思考得知,對于平移T、旋轉R、縮放S這3個最常見的仿射變換,平移變換隻對于點才有意義,因為普通向量沒有位置概念,隻有大小和方向,這可以通過下面的式子清楚地看出:

深入探索透視投影變換

而旋轉和縮放對于向量和點都有意義,你可以用類似上面齊次表示來檢測。從中可以看出,齊次坐标用于仿射變換非常友善。

此外,對于一個普通坐标的點P=(Px, Py, Pz),有對應的一族齊次坐标(wPx, wPy, wPz, w),其中w不等于零。比如,P(1, 4, 7)的齊次坐标有(1, 4, 7, 1)、(2, 8, 14, 2)、(-0.1, -0.4, -0.7, -0.1)等等。是以,如果把一個點從普通坐标變成齊次坐标,給x,y,z乘上同一個非零數w,然後增加第4個分量w;如果把一個齊次坐标轉換成普通坐标,把前三個坐标同時除以第4個坐标,然後去掉第4個分量。

由于齊次坐标使用了4個分量來表達3D概念,使得平移變換可以使用矩陣進行,進而如F.S. Hill, JR所說,仿射(線性)變換的進行更加友善。由于圖形硬體已經普遍地支援齊次坐标與矩陣乘法,是以更加促進了齊次坐标使用,使得它似乎成為圖形學中的一個标準。

簡單的線性插值

這是在圖形學中普遍使用的基本技巧,我們在很多地方都會用到,比如2D位圖的放大、縮小,Tweening變換,以及我們即将看到的透視投影變換等等。基本思想是:給一個x屬于[a, b],找到y屬于[c, d],使得x與a的距離比上ab長度所得到的比例,等于y與c的距離比上cd長度所得到的比例,用數學表達式描述很容易了解:

這樣,從a到b的每一個點都與c到d上的唯一一個點對應。有一個x,就可以求得一個y。

此外,如果x不在[a, b]内,比如x < a或者x > b,則得到的y也是符合y < c或者y > d,比例仍然不變,插值同樣适用。

透視投影變換

好,有了上面兩個理論知識,我們開始分析這次的主角——透視投影變換。這裡我們選擇OpenGL的透視投影變換進行分析,其他的APIs會存在一些差異,但主體思想是相似的,可以類似地推導。經過相機矩陣的變換,頂點被變換到了相機空間。這個時候的多邊形也許會被視錐體裁剪,但在這個不規則的體中進行裁剪并非那麼容易的事情,是以經過圖形學前輩們的精心分析,裁剪被安排到規則觀察體(Canonical View Volume, CVV)中進行,CVV是一個正方體,x, y, z的範圍都是[-1,1](DX裡面z的範圍是0到1),多邊形裁剪就是用這個規則體完成的。是以,事實上是透視投影變換由兩步組成:

1)  用透視變換矩陣把頂點從視錐體中變換到裁剪空間的CVV中。

2)  CVV裁剪完成後進行透視除法(一會進行解釋)。

深入探索透視投影變換

我們一步一步來,我們先從一個方向考察投影關系。

深入探索透視投影變換

上圖是右手坐标系中頂點在相機空間中的情形。設P(x,z)是經過相機變換之後的點,視錐體由eye——眼睛位置,np——近裁剪平面,fp——遠裁剪平面組成。N是眼睛到近裁剪平面的距離,F是眼睛到遠裁剪平面的距離。投影面可以選擇任何平行于近裁剪平面的平面,這裡我們選擇近裁剪平面作為投影平面。設P’(x’,z’)是投影之後的點,則有z’ = -N(注意DX是左手坐标系,Opengl是右手坐标系,是以Z軸方向不一樣)。通過相似三角形性質,我們有關系:

深入探索透視投影變換

同理,有

深入探索透視投影變換

這樣,我們便得到了P投影後的點P’

深入探索透視投影變換

從上面可以看出,投影的結果z’始終等于-N,在投影面上。實際上,z’對于投影後的P’已經沒有意義了,這個資訊點已經沒用了。但對于3D圖形管線來說,為了便于進行後面的片元操作,例如z緩沖消隐算法,有必要把投影之前的z儲存下來,友善後面使用。是以,我們利用這個沒用的資訊點存儲z,處理成:

深入探索透視投影變換

這個形式最大化地使用了3個資訊點,達到了最原始的投影變換的目的,但是它太直白了,有一點蠻幹的意味,我感覺我們最終的結果不應該是它,你說呢?我們開始結合CVV進行思考,把它寫得在數學上更優雅一緻,更易于程式處理。假入能夠把上面寫成這個形式:

深入探索透視投影變換

那麼我們就可以非常友善的用矩陣以及齊次坐标理論來表達投影變換:

深入探索透視投影變換

其中

深入探索透視投影變換

哈,看到了齊次坐标的使用,這對于你來說已經不陌生了吧?這個新的形式不僅達到了上面原始投影變換的目的,而且使用了齊次坐标理論,使得處理更加規範化。注意在把

深入探索透視投影變換

變成

深入探索透視投影變換

的一步我們是使用齊次坐标變普通坐标的規則完成的。這一步在透視投影過程中稱為透視除法(Perspective Division),這是透視投影變換的第2步,經過這一步,就丢棄了原始的z值(得到了CVV中對應的z值,後面解釋),頂點才算完成了投影。而在這兩步之間的就是CVV裁剪過程,是以裁剪空間使用的是齊次坐标

深入探索透視投影變換

,主要原因在于透視除法會損失一些必要的資訊(如原始z,第4個-z保留的)進而使裁剪變得更加難以處理,這裡我們不讨論CVV裁剪的細節,隻關注透視投影變換的兩步。

矩陣

深入探索透視投影變換

就是我們投影矩陣的第一個版本。你一定會問為什麼要把z寫成

深入探索透視投影變換

有三個原因:

 0)後面投影之後的光栅化階段,要通過x'和y'對z進行線性插值,以求出三角形内部片元的z,進行z緩沖深度測試。在數學上,投影後的x'和y',與z不是線性關系,與1/z才是線性關系。而

深入探索透視投影變換

正是1/z的線性關系,即-a+b/z。用這個1/z的線性組合值和x'、y'進行插值才是正确的。(2013年11月補充條目。對此感到迷惑的讀者可以參考《深入探索透視紋理映射》,裡面從細節上說明了這個問題。)

  P’的3個代數分量統一地除以分母-z,易于使用齊次坐标變為普通坐标來完成,使得處理更加一緻、高效。後面的CVV是一個x,y,z的範圍都為[-1,1]的規則體,便于進行多邊形裁剪。而我們可以适當的選擇系數a和b,使得

深入探索透視投影變換

這個式子在z = -N的時候值為-1,而在z = -F的時候值為1,進而在z方向上建構CVV。

接下來我們就求出a和b:

深入探索透視投影變換

這樣我們就得到了透視投影矩陣的第一個版本:

深入探索透視投影變換

使用這個版本的透視投影矩陣可以從z方向上建構CVV,但是x和y方向仍然沒有限制在[-1,1]中,我們的透視投影矩陣的下一個版本就要解決這個問題。

深入探索透視投影變換

為了能在x和y方向把頂點從Frustum情形變成CVV情形,我們開始對x和y進行處理。先來觀察我們目前得到的最終變換結果:

我們知道-Nx / z的有效範圍是投影平面的左邊界值(記為left)和右邊界值(記為right),即[left, right],-Ny / z則為[bottom, top]。而現在我們想把-Nx / z屬于[left, right]映射到x屬于[-1, 1]中,-Ny / z屬于[bottom, top]映射到y屬于[-1, 1]中。你想到了什麼?哈,就是我們簡單的線性插值,你都已經掌握了!我們解決掉它:

深入探索透視投影變換

則我們得到了最終的投影點:

深入探索透視投影變換

下面要做的就是從這個新形式出發反推出下一個版本的透視投影矩陣。注意到

深入探索透視投影變換

深入探索透視投影變換

經過透視除法的形式,而P’隻變化了x和y分量的形式,az+b和-z是不變的,則我們做透視除法的逆處理——給P’每個分量乘上-z,得到

深入探索透視投影變換

而這個結果又是這麼來的:

深入探索透視投影變換

則我們最終得到:

深入探索透視投影變換

M就是最終的透視變換矩陣。相機空間中的頂點,如果在視錐體中,則變換後就在CVV中。如果在視錐體外,變換後就在CVV外。而CVV本身的規則性對于多邊形的裁剪很有利。OpenGL在建構透視投影矩陣的時候就使用了M的形式。注意到M的最後一行不是(0 0 0 1)而是(0 0 -1 0),是以可以看出透視變換不是一種仿射變換,它是非線性的。另外一點你可能已經想到,對于投影面來說,它的寬和高大多數情況下不同,即寬高比不為1,比如640/480。而CVV的寬高是相同的,即寬高比永遠是1。這就造成了多邊形的失真現象,比如一個投影面上的正方形在CVV的面上可能變成了一個長方形。解決這個問題的方法就是在對多變形進行透視變換、裁剪、透視除法之後,在歸一化的裝置坐标(Normalized Device Coordinates)上進行的視口(viewport)變換中進行校正,它會把歸一化的頂點之間按照和投影面上相同的比例變換到視口中,進而解除透視投影變換帶來的失真現象。進行校正前提就是要使投影平面的寬高比和視口的寬高比相同。

便利的投影矩陣生成函數

3D APIs都提供了諸如gluPerspective(fov, aspect, near, far)或者D3DXMatrixPerspectiveFovLH(pOut, fovY, Aspect, zn, zf)這樣的函數為使用者提供快捷的透視矩陣生成方法。我們還是用OpenGL的相應方法來分析它是如何運作的。

gluPerspective(fov, aspect, near, far)

fov即視野,是視錐體在xz平面或者yz平面的開角角度,具體哪個平面都可以。OpenGL和D3D都使用yz平面。

aspect即投影平面的寬高比。

near是近裁剪平面的距離

far是遠裁剪平面的距離。

深入探索透視投影變換

上圖中左邊是在xz平面計算視錐體,右邊是在yz平面計算視錐體。可以看到左邊的第3步top = right / aspect使用了除法(圖形程式員讨厭的東西),而右邊第3步right = top x aspect使用了乘法,這也許就是為什麼圖形APIs采用yz平面的原因吧!

到目前為止已經完成了對透視投影變換的闡述,我想如果你一直跟着我的思路下來,應該能夠對透視投影變換有一個細節層次上的認識。當然,很有可能你已經是一個透視投影變換專家,如果是這樣的話,一定給我寫信,指出我認識上的不足,我會非常感激J。Bye!