1. 程式人生 > >【轉】模型檢視矩陣和投影矩陣(webgl筆記)

【轉】模型檢視矩陣和投影矩陣(webgl筆記)

這段時間在做遊戲3D場景生成和2D/3D切換的時候對模型檢視矩陣和投影矩陣很頭疼,直到看到這篇文章才豁然開朗,這篇文章講的非常詳細易懂,特地轉來。

可以關注我的個人主頁:http://alanzjl.sinaapp.com

最近在學習WebGL技術的過程中,我補充了一些原本瞭解甚少的計算機圖形學知識。如果有同學和我一樣,沒有系統學過計算機圖形學就接觸了3D圖形程式設計,而對不少略為艱深的概念有困惑,希望這些筆記能夠幫助你。

模型矩陣

我們必須考慮,當空間中點的位置會發生變化的時候,其座標如何變化。考慮三種基本的變換:平移、旋轉和縮放。

“變換”的含義就是,將點的初始位置的座標P對映到平移、旋轉、縮放後的位置座標P’,即:

clip_image002

平移變換是最簡單的變換:

clip_image004

旋轉變換有一些複雜,先看在二維平面上的旋轉變換:

clip_image006

很容易得到:

clip_image008

矩陣形式的表達更加簡潔,後面大多使用這種形式:

clip_image010

推廣到三維空間中:

點繞z軸旋轉:

clip_image012

點繞x軸旋轉:

clip_image014

點繞y軸旋轉:

clip_image016

繞指定的任意軸旋轉變換是由幾個繞座標軸旋轉變換和平移變換效果疊加而成的,後文會有詳細敘述。

縮放變換也比較簡單:

clip_image018

總結一下:平移變換,變換後點座標等於初始位置點座標加上一個平移向量;而旋轉變換和縮放變換,變換後點座標等於初始位置點座標乘以一個變換矩陣。

clip_image020

clip_image022

clip_image024

齊次座標這天才的發明,允許平移變換也表示成初始位置點座標左乘一個變換矩陣的形式。齊次座標使用4個分量來表示三維空間中的點,前三個分量和普通座標一樣,第四個分量為1。

clip_image026

平移變換巧妙地表示為:

clip_image028

旋轉變換(以繞z軸旋轉為例)和縮放變換相應為:

clip_image030

clip_image032

綜上,在齊次座標下三種基本變換實現了形式上的統一,這種形式的統一意義重大。

clip_image034

clip_image022[1]

clip_image024[1]

矩陣有一個性質:

clip_image036

考慮一個點, 先進行了一次平移變換,又進行了一次旋轉變換,結合上面矩陣的性質,可知變換後的點P’為:

clip_image038

旋轉矩陣和平移矩陣的乘積R·T也是一個4×4的矩陣,這個矩陣代表了一次平移變換和一次旋轉變換效果的疊加;如果這個點還要進行變換,只要將新的變換矩陣按照順序左乘這個矩陣,得到的新矩陣能夠表示之前所有變換效果的疊加,將最初的點座標左乘這個矩陣就能得到一系列變換後最終的點座標,這個矩陣稱為“模型矩陣”。一個模型矩陣乘以另一個模型矩陣得到的還是一個模型矩陣,表示先進行右側模型矩陣代表的變換,再進行左側模型矩陣代表的變換這一過程的效果之和,因此模型矩陣的乘法又可以認為是閉合的。

模型矩陣之所以稱之為“模型矩陣”,是因為該矩陣與點的位置沒有關係,僅僅包含了一系列變換的資訊。而在三維世界中,一個模型裡所有的頂點往往共享同一個變換,對應同一個模型矩陣,比如拋在空中的一個木塊,運轉機器的一個齒輪。

之前說到,考慮一個物體繞指定軸旋轉,如以下這個變換:繞著過頂點(x,y,z)方向為(a,b,c)的軸旋轉角度θ,利用多個變換的疊加構建繞任意軸旋轉的變換矩陣。

首先將頂點(x,y,z)平移到原點,繞x軸旋轉角度p使指定的旋轉軸在x-z平面上,繞y軸旋轉角度q使指定的旋轉軸與z軸重合,繞指定旋轉軸(也就是z軸)旋轉角度θ,繞y軸旋轉角度-q,繞x軸旋轉角度-p,將頂點平移到向量(x,y,z),p和q的值由方向(a,b,c)決定。綜上,變換矩陣為:

clip_image040

因此在處理圍繞非座標軸旋轉的模型時,根據指定的旋轉引數可以直接按照上述公式生成按照指定軸旋轉的旋轉矩陣,參加模型矩陣的構建。

齊次座標還有一個優點,能夠區分點和向量:在普通座標裡,點和向量都是由三個分量組成的,表示位置的點座標(2,3,4)和表示方向的向量(2,3,4)沒有區別。而在齊次座標中,第四個分量可以區分它們,點座標的第四個分量為1,而向量座標第四個分量為0。比如,平移一個點是有意義的,能夠得到平移後的點座標;而平移一個向量是沒有意義的,方向不會因為平移而改變。

clip_image042

clip_image044

以上,我們已經瞭解到模型矩陣可以儲存一個模型空間位置變化的資訊,在生成三維動畫每一幀的過程中,我們首先計算每個模型的模型矩陣,然後將最初的模型的每一頂點座標都左乘該模型矩陣,得到這一幀表示的時刻(模型已經經過多次變換)該模型每一頂點的座標。上面說的“幀”並不狹義地指螢幕的兩次重新整理時間的短暫間隔中螢幕上呈現的影象,而是指在這幅影象所描繪的整個三維空間的這個瞬間的所有頂點的位置。

來看個具體的例子:一個繞z軸勻速螺旋勻速上升的立方體,在某一幀中(即在這一幀對應的時刻t下),其向z軸正方向平移的長度和繞z軸旋轉的角度分別為:

clip_image046

clip_image048

則模型矩陣(注意上文齊次座標下的基本變換矩陣)為:

clip_image050

產生這一幀時,只需要計算一次模型矩陣,再將立方體中8個頂點座標分別左乘該矩陣,就可以得到經過變換後8個頂點的座標。當一個模型頂點數量增加到上百甚至上千個,模型變換的步驟數也增加到幾十步時,模型矩陣的作用就很明顯了:如果沒有齊次座標(也當然沒有模型矩陣),對每個頂點都需要一步一步地變換:平移的時候加上一個向量,旋轉的時候左乘一個矩陣,才能得到變換後的頂點座標;而模型變換隻需要計算一次模型矩陣(當然也是一步一步的),然後每個頂點左乘模型矩陣就可以直接得到變換後的座標了。

檢視矩陣

在模型矩陣中,我們關心的是空間中的點在經歷變換後在世界座標系下的位置。事實上,我們更加關心空間中的點相對於觀察者的位置。最簡單的方案是將觀察者置於原點處,面向z軸(或x軸、y軸)正半軸,那麼空間中的點在世界座標系下的位置就是其相對於觀察者的位置。

觀察者的位置和方向會變化,看上去就好像整個世界的位置和方向發生變化了一樣,所以解決的方案很簡單,將世界裡的所有模型看作一個大模型,在所有模型矩陣的左側再乘以一個表示整個世界變換的模型矩陣,就可以了。這個表示整個世界變換的矩陣又稱為“檢視矩陣”,因為他們經常一起工作,所以將檢視矩陣乘以模型矩陣得到的矩陣稱為模型檢視矩陣。模型檢視矩陣的作用是:乘以一個點座標,獲得一個新的點座標,獲得的點座標表示點在世界裡變換,觀察者也變換後,點相對於觀察者的位置。

檢視矩陣同樣也可以分為平移、旋轉和縮放,檢視矩陣是將觀察者視為一個模型,獲得的觀察者在世界中變換的模型矩陣的逆矩陣。

觀察者平移了(tx,ty,tz),檢視矩陣如下,可以看出如果將檢視矩陣看作整個世界的模型矩陣,相當於整個世界平移了(-tx,-ty,-tz)。

clip_image042[1]

觀察者繞z軸旋轉了角度θ,檢視矩陣如下,相當於整個世界繞z軸旋轉了-θ度。

clip_image044[1]

觀察者在三個方向等比例縮小了s倍,檢視矩陣如下,相當於整個世界放大了s倍。

clip_image052

觀察者縮小的情形可能會引起困惑:如果人和貓咪的眼睛在同一個位置,人看到的世界和一隻貓咪看到的世界應當是一樣尺寸的,這和上述檢視矩陣的情形矛盾;但是直覺告訴我,如果你喝了縮小藥水,你應該會覺得整個世界在膨脹,就像檢視矩陣所表現的那樣。解答是這樣:如果在計算機上模擬觀察者喝了縮小藥水的情形,在螢幕上看到整個世界是膨脹的,因為在那個虛擬的三維空間中,計算機螢幕這個“視窗”也隨你(觀察者)縮小。

檢視矩陣實際上就是整個世界的模型矩陣,這給我一點啟發:一個模型可能由多個較小的子模型組成,模型自身有其模型矩陣,而子模型也有自己的區域性模型矩陣。考慮一輛行駛中的汽車的輪胎,其模型檢視矩陣是區域性模型矩陣(描述輪胎的旋轉)左乘汽車的模型矩陣(描述汽車的行駛)再左乘檢視矩陣得到的。

投影矩陣

模型檢視矩陣的作用是確定某一幀中,空間裡每個頂點的座標,而投影矩陣則將這些頂點座標對映到二維的螢幕上,即:

clip_image054

最主要的有兩種投影方式,正射投影和透視投影。前者用於精確製圖,如工業零件側檢視或建築物頂檢視,從螢幕上就可以量測平行於螢幕的線段長度;後者用於模擬視覺,遠處的物體看上去較小。下圖中,空間中的同一個矩形,正射投影后仍然是矩形,而透視投影后則變成了梯形。

正射投影(投影面和相機空間):

clip_image056

透視投影(投影面和相機空間):

clip_image058

三維世界的顯示中,螢幕模擬了一個視窗,你透過這個視窗觀察“外面”的世界。你的螢幕是有邊緣的(除非你有一個球形的房間,內壁全是螢幕),因此你僅僅能觀察到那個世界的一部分,即相機空間。相機空間的左、右、上、下邊界是受限於螢幕的邊緣,同時也設定前、後邊界,因為你很難看清太近或太遠的東西。在正射投影中,相機空間是一個規則的立方體,而在透視投影中則是一個方臺體。

三維模型可能在不同的顯示器上展現,因此投影的過程中不該將顯示器引數加入進來,而是將空間中的點投影到一個規範的顯示器中。另外,透視投影中的z值並不是毫無用處,它可以用來表示頂點的“深度”:如果三維空間中的兩個不同頂點投影到平面上時重合了,那麼將顯示深度較淺的頂點。

定義一個規範的視窗區域(CCV),為xyz都處在區間[-1,1]之間的邊長為2的立方體。x和y座標值用來線形拉伸到到實際螢幕上,而z值儲存了“深度”。而投影的過程就是將三維空間中的點從相機空間對映到CCV中。

正射投影非常簡單,直接將矩形的相機空間線形壓縮到CCV中即可。採取頂檢視,相機空間的左右邊界為:

clip_image060

clip_image062

簡單的線形成比例關係:

clip_image064

clip_image066

推廣到y軸和z軸:

clip_image068

相機空間中的點經過正射投影矩陣左乘後得到的點都在CCV之中:

clip_image070

透視投影相對較為複雜,同樣用頂檢視考慮x座標的情況:

clip_image072

clip_image074

clip_image076

clip_image078

轉化為齊次的方式:

clip_image080

推廣到y軸:

clip_image082

透視投影矩陣的第三行不是我們關心的內容,只要保證不同頂點投影前後的點座標的第三個分量z和z’的大小關係不變就可以。

透視投影矩陣尾行是(0,0,1,0),這樣就將計算得到的座標的第四個分量賦值為z而不是1。將相機空間左乘投影矩陣後的結果不是一個CCV空間,如果你將這個空間畫出來,會發現其仍然是一個方臺形。這時進行“透視除法”,將上一步得到的點座標化為第四個分量為1的標準齊次座標:

clip_image084

然後我們直接取齊次座標中的x’和y’值,並將其線形對映到螢幕上,比如點(0,0)出現在螢幕中央,點(-1,1)出現在螢幕左上角。

WebGL

WebGL中對於模型檢視矩陣和投影矩陣的操作依賴於第三方庫,比如Oak3D或glMatrix,WebGL本身不支援(或者說不限制)任何對模型檢視矩陣和投影矩陣的操作。

WebGL是在瀏覽器端執行的,所以使用JavaScript程式設計。下面的程式碼來自www.hiwebgl.com翻譯的LearningWebGL.com的WebGL教程。

以glMatrix庫為例:

// 新建空模型檢視矩陣

var mvMatrix = mat4.create();

// 將矩陣設定為單位陣

mat4.identity(mvMatrix);

// 平移和旋轉

mat4.translate(mvMatrix, [-1.5, 0.0, -8.0]);

mat4.rotate(mvMatrix, degToRad(45), [0, 1, 0]);

將矩陣設定為單位陣相當於說:“這個矩陣表示什麼都還沒做(平移、旋轉、縮放)呢”,事實上,任意點座標乘以單位矩陣都只能得到自己,正說明“什麼都沒做”。

平移矩陣的函式mat4.translate()做的僅僅是將mvMatrix左乘一個平移矩陣而已。

旋轉矩陣的函式mat4.rotate()也許比較複雜,它做的是上面我們討論過的“圍繞任意軸旋轉”的問題,這個函式預設使用“本地軸”,即過所有平移效果累加後的那一點的軸,引數向量[0,1,0]是軸的指向,因此上面的函式呼叫處理了一個圍繞本地y軸的旋轉。

// 新建空投影矩陣

var pMatrix = mat4.create();

// 初始化投影矩陣

mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

投影矩陣不會因為場景裡模型的位置變化或觀察者的移動而變化(當然如果你想模擬觀察者戴眼鏡的過程你可能要考慮),故而投影矩陣只需要一次初始化就夠了。初始化需要給出相機空間的前、後、左、右、上、下邊界,很容易從函式呼叫裡傳入的引數推知:包括前、後邊界,相機空間的寬高比和水平視場角。

如果你使用指令碼除錯工具監測矩陣物件mvMatrix和pMatrix,就會發現他們僅僅是有16個元素的Float32Array物件而已,你完全可以親自處理它。

值得一提的是glMatrix庫的函式大多不返回處理後的矩陣,在將矩陣作為引數傳入時已經給了函式修改矩陣的權利,很少的情況下需要會寫這樣的程式碼(但其他的庫不一定這樣):

xMatrix = matX.operate();

使用庫函式或自力更生處理完矩陣後,通過著色器程式傳遞到著色器中(著色器程式是JavaScript腳本里的概念,而著色器是用其他指令碼語言編寫的在顯示卡中執行的邏輯,這些不在本文的討論範圍內):

// 設定著色器程式

……

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");

……

// 將模型檢視矩陣和投影矩陣傳入著色器

gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);

然後看看著色器裡的程式碼,這是用x-shader型別的指令碼語言寫的:

void main(void){

……

gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

……

}

可以看到螢幕上點座標由初始點座標左乘模型檢視矩陣,再左乘投影矩陣得到的。對於較複雜的場景,我猜測可能需要重新編寫著色器,將模型矩陣和檢視矩陣拆開處理。

綜上所述,模型檢視矩陣和投影矩陣是三維計算機圖形學的基石。關於這兩個矩陣的知識雖然不是進行3D圖形程式設計的必須,但是至少能夠幫助我們更好地瞭解那些庫函式在做些什麼,或者自己直接操作矩陣物件。

OpenGL矩陣推導——模型檢視變化

在三維程式設計中,模型檢視變換是從三維世界到二維螢幕中一個很重要的變換,但是這個變換往往很多人都不太理解,要麼是事而非。而這方面的文章不是太少就是講的太淺沒有真正的理解模型檢視變換,本人在這個過程中曾經走過很多歪路,不過好在最終在自己的不懈努力下終於降伏了這隻猛虎。本人就以自己的理解,通過矩陣推導過程一步一步來了解模型檢視變化,最後通過兩個OpenGl的程式來進一步理解模型檢視矩陣。先從一個基本的模型檢視—透視投影變換講起。

透射投影是將相機空間中的點從視錐體(frustum)變換到規則觀察體(Canonical View Volume 以下簡稱CVV)中,待裁剪完畢後進行透視除法的行為。

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

主流的3D APIs 都把透射投影的具體細節進行了封裝,從而只需一個函式便可生成一個透射投影矩陣比如gluPerspective(),使得我們不需要了解其演算法便可實現三維到二維的轉化,然而實事是,一些三維圖形或遊戲開發人員遇到一些檢視矩陣的問題往往會不知所措,比如視景體裁剪。

以下部分內容是從別處那轉過來的,主要感謝Twinsen和一個叫丁歐南的高中生。

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

對於一個向量v以及基oabc clip_image086

可以找到一組座標(v1,v2,v3),使得

v = v1 a + v2 b + v3 c1

而對於一個點p,則可以找到一組座標(p1,p2,p3),使得

po = p1 a + p2 b + p3c 2

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

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

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

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

clip_image088

這裡(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),它就是個點。

下面是如何在普通座標(OrdinaryCoordinate)和齊次座標(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個最常見的仿射變換,平移變換隻對於點才有意義,因為普通向量沒有位置概念,只有大小和方向,這可以通過下面的式子清楚地看出:

clip_image090

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

此外,對於一個普通座標的點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所說,仿射(線性)變換的進行更加方便。由於圖形硬體已經普遍地支援齊次座標與矩陣乘法,因此更加促進了齊次座標使用,使得它似乎成為圖形學中的一個標準。

簡單的線性插值

線性插值我舉的是丁歐南的溫度計的例子:

已知有一破溫度計(何以謂破?刻度之間間距雖平均,但間距或大於或小於標準值,謂之破),當其插入0 0C水裡時顯示為50C,當其插入1000C的沸水中時顯示為900C,問:當實際水溫為500C時此破溫度計顯示的值是多少?

解:因刻度均勻,所以刻度之間的比例與好溫度計相同,由此:設顯示的數為T,

(90-T)/(T-5)=(100-50)/(50-0) 解出T=47.5 0C.

結論:由一個數域(如題目中的好溫度計兩個端點[0,100])對映到另一個數域(如題目中的破溫度計的兩個端點[5,90])時,如果兩個數域都是線性(就如題目中暗示的刻度平均),那麼它們對應點成比例(比如50和47.5這一對端點).

這道題的應用是把一組座標對映到另一個範圍,這將在介紹NDC(Normalized Device Coordinate,歸一化的裝置座標)時用到.

透視投影變換

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

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

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

clip_image092

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

clip_image094

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

clip_image096

同理,有

clip_image098

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

clip_image100

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

clip_image102

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

clip_image104

那麼我們就可以非常方便的用矩陣以及齊次座標理論來表達投影變換:

clip_image106

其中clip_image108

哈,看到了齊次座標的使用,這對於你來說已經不陌生了吧?這個新的形式不僅達到了上面原始投影變換的目的,而且使用了齊次座標理論,使得處理更加規範化。注意在把clip_image110 變成clip_image112 的一步我們是使用齊次座標變普通座標的規則完成的。這一步在透視投影過程中稱為透視除法(Perspective Division),這是透視投影變換的第2步,經過這一步,就丟棄了原始的z值(得到了CVV中對應的z值,後面解釋),頂點才算完成了投影。而在這兩步之間的就是CVV裁剪過程,所以裁剪空間使用的是齊次座標clip_image110[1] ,主要原因在於透視除法會損失一些必要的資訊(如原始z,第4個-z保留的)從而使裁剪變得更加難以處理,這裡我們不討論CVV裁剪的細節,只關注透視投影變換的兩步。

矩陣clip_image114

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

clip_image116

有兩個原因:

1) P’的3個代數分量統一地除以分母-z,易於使用齊次座標變為普通座標來完成,使得處理更加一致、高效。

2) 後面的CVV是一個x,y,z的範圍都為[-1,1]的規則體,便於進行多邊形裁剪。而我們可以適當的選擇係數a和b,使得clip_image116[1] 這個式子在z = -N的時候值為-1,而在z = -F的時候值為1,從而在z方向上構建CVV。

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

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

clip_image120

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

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

clip_image121

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

clip_image123

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

clip_image125

下面要做的就是從這個新形式出發反推出下一個版本的透視投影矩陣。注意到clip_image121[1]clip_image110[2] 經過透視除法的形式,而P’只變化了x和y分量的形式,az+b和-z是不變的,則我們做透視除法的逆處理——給P’每個分量乘上-z,得到

clip_image127

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

clip_image129

則我們最終得到:

clip_image131

上面是一般情況,我們要把它變成特殊性版本,即gluPerspective,它是一種左右對稱的投影形式,因此我們從對x和y進行插值的那一步來看:

那一步來看:

clip_image133

銷掉兩邊的1/2,得到:

clip_image135

則我們反推出透視投影矩陣:

clip_image137

這就是gluPerspective的投影矩陣了。

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

而r-l和t-b可以分別看作是投影平面的寬w和高h。如果我們不知道right、left、top以及bottom這幾個參量,也可以根據視野(FOV – Field Of View)參量來求得。下面是兩個平面的視野關係圖:

clip_image139

clip_image141

其中,兩個fov分別是在x-z以及y-z平面上的視野。如果只給了一個視野,也可以通過投影平面的寬高比計算出來:

clip_image143

用一個視野算出w或者h,然後用寬高比算出h或者w。

我們可以通過一個例子來證明我們矩陣的正確性:

void OpenGlCom::ReSize()

{

RECTrct ;

GetClientRect(m_Hwnd,&rct);

intm_view_width = rct.right- rct.left;

intm_view_height = rct.bottom - rct.top;

glViewport(0,0,m_view_width,m_view_height);

float nearz =5.0;

float farz =80000;

float AspectRatio =float(m_view_height)/ float(m_view_width);

float ViewAngleH =90 * (PI / 180);//90度

float ViewAngleV = atan(tan(ViewAngleH/2) * AspectRatio)* 2;

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

GLfloatm[16];

::ZeroMemory(m,16*sizeof(float));

//////m[8],m[9]=0表示對稱視椎體即gluPerspective///////////

m[0] = 1.0 /tan(ViewAngleV / 2);

m[5] = m[0]*AspectRatio;

m[10] = -(farz +nearz) / (farz - nearz);

m[11] = -1;

m[14] = - 2 * farz *nearz / (farz - nearz);

glMultMatrixf(m);

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

}

void OpenGlCom::draw()

{

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);