1. 程式人生 > >模型檢視矩陣與投影矩陣

模型檢視矩陣與投影矩陣

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

模型矩陣

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

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

⎡⎣xyz⎤⎦→⎡⎣⎢xyz′⎤⎦⎥

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

⎡⎣⎢xyz′⎤⎦⎥=⎡⎣xyz⎤⎦+⎡⎣txtytz⎤⎦

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

Untitled-1

很容易得到:

x′=xcosθysinθy′=xsinθ+ysinθ

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

[xy′]=[cosθsinθ−sinθcosθ][xy]

推廣到三維空間中: 
點繞z軸旋轉:

⎡⎣⎢xyz′⎤⎦⎥=⎡⎣cosθsinθ0−sinθcosθ0001⎤⎦⎡⎣xyz⎤⎦

點繞x軸旋轉:

⎡⎣⎢xyz′⎤⎦⎥=⎡⎣1000cosθsinθ0−sinθcosθ⎤⎦⎡⎣xyz⎤⎦

點繞y軸旋轉:

⎡⎣⎢xyz′⎤⎦⎥=⎡⎣cosθ0sinθ010−sinθ0cosθ⎤⎦⎡⎣xyz⎤⎦

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

縮放變換也比較簡單:

⎡⎣⎢xyz′⎤⎦⎥=⎡⎣sx000sy000sz⎤⎦⎡⎣xyz⎤⎦

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

P′=P+T,P′=RP,P′=SP

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

⎡⎣xyz⎤⎦→⎡⎣⎢⎢xyz1⎤⎦⎥⎥

平移變換巧妙地表示為:

⎡⎣⎢⎢⎢xyz′1⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢100001000010tx

tytz1⎤⎦⎥⎥⎥⎡⎣⎢⎢xyz1⎤⎦⎥⎥

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

⎡⎣⎢⎢⎢xyz′1⎤⎦⎥⎥⎥=⎡⎣⎢⎢cosθsinθ00−sinθcosθ0000100001⎤⎦⎥⎥⎡⎣⎢⎢xyz1⎤⎦⎥⎥

⎡⎣⎢⎢⎢xyz′1⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢sx00 00sy0000sz0001⎤⎦⎥⎥⎥⎡⎣⎢⎢xyz1⎤⎦⎥⎥

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

P′=TP,P′=RP,P′=SP

矩陣有一個性質:

M⋅(AB)=(MA)⋅B

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

P=R⋅(TP)=(RT)⋅P

旋轉矩陣和平移矩陣的乘積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)決定。綜上,變換矩陣為:

R⎛⎝⎡⎣xyz⎤⎦⎡⎣abc⎤⎦θ⎞⎠=T⎛⎝xyz⎞⎠⋅Rx(−p)⋅Ry(−q)⋅Rz(θ)⋅Ry(q)⋅Rx(p)⋅T⎛⎝−xyz⎞⎠

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

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

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

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

tz=tvt,θz=tωt

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

mMatrix=Rz(θz)⋅T(0,0,tz)=⎡⎣⎢⎢cosθzsinθz00−sinθzcosθz0000100001⎤⎦⎥⎥⎡⎣⎢⎢10000100001000tz1⎤⎦⎥⎥

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

檢視矩陣

在模型矩陣中,我們關心的是空間中的點在經歷變換後在世界座標系下的位置。事實上,我們更加關心空間中的點相對於觀察者的位置。最簡單的方案是將觀察者置於原點處,面向z軸(或x軸、y軸)正半軸,那麼空間中的點在世界座標系下的位置就是其相對於觀察者的位置。 
觀察者的位置和方向會變化,看上去就好像整個世界的位置和方向發生變化了一樣,所以解決的方案很簡單,將世界裡的所有模型看作一個大模型,在所有模型矩陣的左側再乘以一個表示整個世界變換的模型矩陣,就可以了。這個表示整個世界變換的矩陣又稱為“檢視矩陣”,因為他們經常一起工作,所以將檢視矩陣乘以模型矩陣得到的矩陣稱為“模型檢視矩陣”。模型檢視矩陣的作用是:乘以一個點座標,獲得一個新的點座標,獲得的點座標表示點在世界裡變換,觀察者也變換後,點相對於觀察者的位置。

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

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

⎡⎣⎢⎢⎢100001000010txtytz1⎤⎦⎥⎥⎥−1=⎡⎣⎢⎢⎢100001000010−txtytz1⎤⎦⎥⎥⎥

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

⎡⎣⎢⎢cosθsinθ00−sinθcosθ0000100001⎤⎦⎥⎥−1=⎡⎣⎢⎢cosθ−sinθ0 0sinθcosθ0000100001⎤⎦⎥⎥

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

⎡⎣⎢⎢⎢sx0000sy0000sz00001⎤⎦⎥⎥⎥−1=⎡⎣⎢⎢⎢1/sx00001/sy00001/sz00001⎤⎦⎥⎥⎥

觀察者縮小的情形可能會引起困惑:如果人和貓咪的眼睛在同一個位置,人看到的世界和一隻貓咪看到的世界應當是一樣尺寸的,這和上述檢視矩陣的情形矛盾;但是直覺告訴我,如果你喝了縮小藥水,你應該會覺得整個世界在膨脹,就像檢視矩陣所表現的那樣。解答是這樣:如果在計算機上模擬觀察者喝了縮小藥水的情形,在螢幕上看到整個世界是膨脹的,因為在那個虛擬的三維空間中,計算機螢幕這個“視窗”也隨你(觀察者)縮小。 
檢視矩陣實際上就是整個世界的模型矩陣,這給我一點啟發:一個模型可能由多個較小的子模型組成,模型自身有其模型矩陣,而子模型也有自己的區域性模型矩陣。考慮一輛行駛中的汽車的輪胎,其模型檢視矩陣是區域性模型矩陣(描述輪胎的旋轉)左乘汽車的模型矩陣(描述汽車的行駛)再左乘檢視矩陣得到的。

投影矩陣

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

⎡⎣⎢⎢xyz1⎤⎦⎥⎥→[xy′]

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

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

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

三維世界的顯示中,螢幕模擬了一個視窗,你透過這個視窗觀察“外面”的世界。你的螢幕是有邊緣的(除非你有一個球形的房間,內壁全是螢幕),因此你僅僅能觀察到那個世界的一部分,即“相機空間”。相機空間的左、右、上、下邊界是受限於螢幕的邊緣,同時也設定前、後邊界,因為你很難看清太近或太遠的東西。在正射投影中,相機空間是一個規則的立方體,而在透視投影中則是一個方臺體。 
三維模型可能在不同的顯示器上展現,因此投影的過程中不該將顯示器引數加入進來,而是將空間中的點投影到一個規範的顯示器中。另外,透視投影中的z值並不是毫無用處,它可以用來表示頂點的“深度”:如果三維空間中的兩個不同頂點投影到平面上時重合了,那麼將顯示深度較淺的頂點。

定義一個規範的視窗區域(CCV),為x,y,z都處在區間[-1,1]之間的邊長為2的立方體。x和y座標值用來線形拉伸到到實際螢幕上,而z值儲存了“深度”。而投影的過程就是將三維空間中的點從相機空間對映到CCV中。 
正射投影非常簡單,直接將矩形的相機空間線形壓縮到CCV中即可。採取頂檢視,相機空間的左右邊界為 xleft 和 xright :

Untitled-21

簡單的線形成比例關係:

xxleftxrightxleft=x′−(−1)1−(−1)

x′=2xrightxleftxxright+xleftxrightxleft

推廣到y軸和z軸:

⎡⎣⎢⎢⎢xyz′1⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢2xrightxleft00002ytopybottom00002zbackzfront0−xright+xleftxrightxleftytop+ybottomytopybottomzback+zfrontzbackzfront1⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⋅⎡⎣⎢⎢xyz1⎤⎦⎥⎥

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

−1<x′<1−1<y′<1−1<z′<1

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

xph=xz

xpxleftxrightxleft=x′−(−1)1−(−1)

x′=1z⋅2hxrightxleftxright+xleftxrightxleft

Untitled-122

轉化為齊次的方式:

zx′=x⋅2hxrightxleftxright+xleftxrightxleft

推廣到y軸:

⎡⎣⎢⎢⎢zxzyzzz⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢2xrightxleft00002ytopybottom00−xright+xleftxrightxleftytop+ybottomytopybottomtz100sz0⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⋅⎡⎣⎢⎢xyz1⎤⎦⎥⎥

透視投影矩陣的第三行不是我們關心的內容,只要保證不同頂點投影前後的點座標的第三個分量z和z’的大小關係不變就可以。 
透視投影矩陣尾行是(0,0,1,0),這樣就將計算得到的座標的第四個分量賦值為z而不是1。將相機空間左乘投影矩陣後的結果不是一個CCV空間,如果你將這個空間畫出來,會發現其仍然是一個方臺形。這時進行“透視除法”,將上一步得到的點座標化為第四個分量為1的標準齊次座標:

⎡⎣⎢⎢⎢xyz′1⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢zxzyzzz′⎤⎦⎥⎥⎥⋅1z

然後我們直接取齊次座標中的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圖形程式設計的必須,但是至少能夠幫助我們更好地瞭解那些庫函式在做些什麼,或者自己直接操作矩陣物件。

其他

最後歡迎大家訪問我的個人網站: 1024s