OpenGL中的座標變換、矩陣變換

孫群發表於2014-06-01

OpenGL中六種常見座標系:
1. Object or model coordinates(模型座標系)
2. World coordinates(世界座標系)
3. Eye (or Camera) coordinates(視座標系)
4. Clip coordinates(裁剪座標系)

5. Normalized device coordinates(歸一化裝置座標系)

6. Window (or screen) coordinates(螢幕座標系)


當然這六中座標系只是經常在開發中涉及的,還有其他很多座標系,比如在進行法向貼圖的時候的切向座標系等,此處只介紹這六中座標系。


需要注意的是模型座標系、世界座標系、視座標系以及裁剪座標系任何時候都是右手座標系。預設情況下這四個座標系完全重合,預設這四個座標系的原點都在螢幕中心,從原點向右為x軸正半軸,從原點向上為y軸正半軸,從原點垂直於螢幕向外是z軸的正半軸。如果進行了某些矩陣操作變換,那麼這四個座標系就很可能不在重合,不過這四個座標系還是右手座標系。NDC座標系(歸一化裝置座標系)是左手座標系,其z軸方向與前四個座標系的z軸方向相反。



Object or model coordinates(模型座標系)

所謂的模型指的就是一個三維的物體。每個物體都有其自身的模型座標系,也就是說物體A的模型座標系為Coordinate System A,物體B的模型座標系為Coordinate System B,二者的模型座標系不同。模型座標系是一個假想的座標系,該座標系與物體的相對位置始終是不變的。在進行了glLoadIdentity()之後,模型座標系和世界座標系是重合的,但是如果呼叫了glTranslate()或者glRotate(),模型座標系就會進行相應的平移和旋轉。該座標系以物體的中心為座標原點,物體的旋轉、平移等操作都是以模型座標系進行的。這時當物體模型進行旋 轉、平移等操作時,模型座標系也執行相應的旋轉、平移等操作。在OpenGL中繪圖時,都是首先通過glTranslate、glRotate等來改變模型座標系與世界座標系的相對位置,然後在模型座標系中用glVertexf等繪圖,比如繪 制了一個點glVertexf(1,2,3),那麼座標(1,2,3)是模型座標系中的座標,而不是世界座標系中的座標。



 

World coordinates(世界座標系)

這個座標系就是我們生活的真實的3D場景,在OpenGL中有且只有一個世界座標系。模型座標系中的模型座標左乘模型矩陣之後會轉化為世界座標,假設模型座標系中有一點P,其座標為Pmodel(Xmodel,Ymodel,Zmodel),該點左乘模型矩陣Mmodel之後就會得到該點在世界座標系中的座標Pworld(Xworld,Yworld,Zworld),即


上式中的Mmodel就是模型矩陣,是一個4×4的矩陣組成如下圖所示,


上式中的m3,m7,m11均為0,m15為1。 (m12,m13,m14)可以表示一個點P,P點表示的是物體的模型座標系的座標原點在世界座標系中的位置。剩下的9個值可分為三組(m0,m1,m2)、(m4,m5,m6)、(m8,m9,m10),這三組可表示三個向量vector1、vector2、vector3,而且這三個向量都是歸一化向量,也就是說向量長度為1。其中vector1表示的是物體的模型座標系的x軸正半軸在世界座標系中的方向,vector2表示的是物體的模型座標系的y軸的正半軸在世界座標系中的方向,vector3表示的是物體的模型座標系的z軸正半軸在世界座標系中的方向。由此可以看出模型矩陣包含了所有模型座標系的資訊(模型座標系的原點在世界座標系中的位置以及模型座標系的三個座標軸在世界座標系中的方向)。由於模型座標系的三個座標軸互相兩兩垂直,所以vector1、vector2、vector3互相垂直。那麼根據數學定義由其中任意兩個向量相互叉乘即可得到另一個向量,即有如下三個關係式:
vector1 叉乘 vector2 = vector3;
vector2 叉乘 vector3 = vector1;
vector3 叉乘 vector1 = vector2;
又因為vector1、vector2和vector3都是單位向量,那麼可知:
 m0*m0 + m1*m1 + m2*m2 = 1;
 m4*m4 + m5*m5 + m6*m6 = 1;
 m8*m8 + m9*m9 + m10*m10 = 1;
根據上述理論,假設我們已知物體的模型座標的原點在世界座標系中的座標為P,物體的模型座標系的x、y、z三個座標軸的正半軸在世界座標系中的方向分別為vector1、vector2、vector3,且這三個向量都是歸一化向量。那麼根據這些資訊我們就直接可以寫出物體的模型矩陣,如下所示:




Eye (or Camera) coordinates(視座標系)

我們繪製的圖形最終要呈現在視座標系中,視座標系是右手座標系的。那麼什麼是視座標系呢?視座標系就是Camera座標系。現在打個比 方,你站在世界座標系中,你的眼睛就是Camera,你的眼睛平視前方,並且保持視線方向與頭頂的朝向互相垂直。現在你的眼睛就是視座標系的座標原點,你的視線方向就是視座標系Z軸的負半軸方向,你的頭頂的朝向就是視座標系的Y軸正半軸方向,與YOZ相互垂直向右的指向就是X軸正半軸的方向,X軸正半軸可由Y與Z叉乘得到,Camera的視座標系也可以稱為uvn座標系,對應著世界座標系的XYZ三個軸。預設情況下,視座標系與世界座標系是重合的。需要注意的是Camera本身就是一種普通的物體,即Camera就是一種模型,自然就有了前面所述的模型座標系。不過Camera又是一種有點特殊的模型,為什麼這麼說呢?因為正常的模型都會在三維空間中畫出來,但是一般情況下我們不會在三維空間中把Camera畫出來(當然也可以將其在3D空間中畫出來),所以一般情況下我們可以把Camera看作是透明的模型,但是上述模型座標系的一系列理論都適用於Camera。 我們在開頭就說了每次點從某個座標系變換到另一個座標系的時候都要左乘某個變換矩陣,從世界座標系變換到視座標系的時候所要左乘的變換矩陣我們稱之為視點矩陣,需要特別注意的是視座標系並不神祕,其實視座標系就是Camera的模型座標系。在一般情況下,3D空間中只有一個Camera,那麼也就是隻有一個視座標系,視座標系就是Camera的模型座標系。既然視座標系就是Camera的模型座標系,那麼視座標系中某點Pview(Xview,Yview,Zview)就是Camera的模型座標系中點Pcamera_model(Xcamera_model,Ycamera_model,Zcamera_model),即


世界座標系中的點左乘視點矩陣即可得到視點座標系中座標,那麼有如下定義:


當然又因為Pview就是Pcamera_model,所以也有如下定義:


那麼視點矩陣Mview如何計算呢?
我們上面說過Camera也是一種模型,其自身也有模型座標系,假設我們已知Camera的模型座標系的原點在世界座標系中的座標以及Camera的模型座標系的三個座標軸的正半軸在世界座標系中的方向,那麼我們就知道了Camera的模型矩陣Mmodel。
我們已知將世界座標系中的點轉換到視座標系中時有如下定義:
Pview = Mview•Pworld,
我們又根據模型座標系中的點轉換到世界座標系中的定義得知:
Pworld = Mcamera_model•Pcamera_model,所以將Pworld的值帶入上式得
Pview = Mview•Pworld = Mview•(Mcamera_model•Pcamera_model) = (Mview•Mcamera_model)•Pcamera_model
即Pview = (Mview•Mcamera_model) •Pcamera_model,又已知Pview = Pcamra_model,那麼則得到
Pcamera_model = (Mview•Mcamera_model) •Pcamera_model,所以(Mview•Mcamera_model)為單位矩陣,也就是說Mview與Mcamera_model互為逆矩陣,所以

現在視點矩陣就計算完了,就是Camera的模型矩陣的逆矩陣。
現在大家可以回想一下OpenGL中常用的glLookAt(cameraPoint,targetPoint,upDirection)方法,該方法用於設定camera,其中cameraPonint是攝像機在世界座標系中位置,targetPoint是攝像機觀察的世界座標系中的某點,upDirection指的是攝像機的頭頂朝向在世界座標系中的方向。我們可以根據這三個引數完全推算出camera的模型矩陣,計算過程如下:


攝像機的模型矩陣由以上16個資料組成,根據我們之前所述,m3、m7、m11都為0,m15為1。我們根據targetPoint可以得知m12=targetPoint.x, m13= targetPoint.y, m14= targetPoint.z,我們根據targetPoint和cameraPoint這兩個點就能得到camera的視線方向,視線方向的反方向就是Camera的模型座標系的z軸正半軸在世界座標系中的方向,假設該方向的歸一化向量是vector3,那麼m8=vector3.x, m9=vector3.y, m10=vector3.z。upDirection就是Camera的模型座標系的y軸正半軸在世界座標系中的方向,假設該方向的歸一化向量是vector2,那麼m4=vector2.x, m5=vector2.y, m6=vector2.z。由於三個座標軸互相垂直,那麼Camera的模型座標系的x軸正半軸在世界座標系中的歸一化後的方向vector1 = vector3 叉乘 vector2。則m0=vector1.x, m1=vector1.y, m2=vector1.z。這樣我們根據glLookAt(cameraPoint,targetPoint,upDirection)中的三個引數完全推出了Camera的模型矩陣,同時我們便可以計算出視點矩陣Mview為Camera模型矩陣的逆矩陣。也就是說glLookAt方法確定了Camera的模型矩陣,而且確定了3D空間的視點矩陣。




Clip coordinates(裁剪座標系)
現實生活中的場景都滿足“近大遠小”的特點,但是視點座標系中並不滿足“近大遠小”的特點,視點座標系中的物體遠近都一樣大,為了將上一步得到的視點座標轉換為符合“近大遠小”特點的座標,我們需要將視點座標左乘一個變換矩陣,我們稱這個矩陣為投影矩陣,相乘得到的座標稱為投影座標,但一般情況下我們更多地稱其為裁剪座標(為什麼叫這個名字下面會解釋)。Pclip = Mproj•Pview。裁剪座標系也是右手座標系。
首先我們會定義一個視景體,位於視景體內的物體我們才會看到,不在視景體內的物體就會被裁剪拋棄掉。視景體如下所示:


視景體是由六個引數構成的: left、right、top、bottom以及near和far。視景體是存在於視座標系中的,這6個引數也是視座標系中的。near表示的是視座標系的座標原點近裁剪面的距離,far表示的是視座標系的座標原點到遠裁剪面的距離。[left,right]對應近裁剪面的x的值域,[bottom,top]對應近裁剪面的y的值域,一般情況下left=-right、bottom=-top。根據這六個引數就可以構造投影矩陣了。此處我們暫時不討論如何構建該矩陣。
在GLSL的頂點著色器中,我們一般都要計算設定gl_Position的值。一般情況下該值這樣設定: gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition,1.0);
其中aVertexPosition為模型座標,uMVMatrix為模型視點矩陣,即為視點矩陣與模型矩陣的乘積,則uMVMatrix = uViewMatrix * uModelMatrix。uPMatrix是投影矩陣,gl_Position就是經過了投影的裁剪座標,也就是說gl_Position所在的座標系就是投影座標系。現在我們來解釋一下這個座標系為什麼叫做“裁剪座標系”,看看哪裡來的“裁剪”二字:
需要特別注意的是,模型座標系、世界座標系、視點座標系中的第四個分量w都是1,但是經過了投影變換之後的座標的w分量不再為1,這時候就可以發揮w分量的作用了。在我們手動程式設計計算完gl_Position之後,進入GPU自身的流水管線,GPU會根據裁剪座標gl_Position中xyz分量與w分量絕對值的大小進行比較進行裁剪。具體的過程是:GPU依次將gl_Position中x、y、z的絕對值與w的絕對值分別比較,只要有一個分量的絕對值大於w的絕對值,GPU就認為該點不在視景體內,就會被裁減掉,也就是說裁剪的過程是GPU自己進行的,沒有被裁減掉的座標xyz分量的絕對值都小於w的絕對值。所以現在應該知道經過投影之後的座標是為了讓GPU進行裁剪用的,所以才叫做“裁剪座標”。




Normalized device coordinates(歸一化裝置座標系)
裁剪座標在經過GPU自動的裁剪之後會過濾掉那些位於視景體之外的座標,只保留位於視景體內的座標,然後GPU會自動進行投影除法(projective division)。


具體的過程是:會將齊次座標轉換為普通的三元的座標(只有xyz,無w),會讓裁剪座標(裁剪座標也是齊次座標,包含w資訊)中的xyz依次除以w,得到新的xyz,新的xyz就是歸一化後的座標,即歸一化裝置座標(Normalized Device Coordinates),NDC座標存放於一個邊長為2的立方體座標系中,NDC座標是三分量的座標,只包含xyz資訊,不再包含w資訊。歸一化後的NDC座標中的x、y、z的取值範圍都是[-1,1]。x、y的取值範圍是[-1,1],所表達的含義是將螢幕的中心點對應於[0,0],螢幕左下點對應[-1,-1],螢幕右上點對應於[1,1],x和y能夠表示出該點相對於螢幕中心原點的位置。NDC座標中的z表示了深度資訊,取值範圍也是[-1,1]。
需要特別注意的是NDC座標系與裁剪座標系相比其Z軸方向發生了翻轉,也就是說NDC座標系是左手座標系,這個非常重要。而且其xyz的取值都是[-1,1]。Xndc與Yndc比較好計算,就是簡單的線性比例計算而已。那麼Zndc如何計算呢?
Zndc表示的是深度資訊,視景體的近裁剪面的Zndc為-1,視景體的遠裁剪面的Zndc為1。Zndc與Zview並不是線性的關係,兩者的具體關係是:


其中far指的是視座標系中座標原點到遠裁剪面的距離,near指的是視座標系中座標原點到近裁剪面的距離,Zveiw是視座標系中的Z值。這樣就可以根據Zview計算出Zndc。
當位於近裁剪面時,Zview=-near,則計算出的Zndc=-1;
當位於遠裁剪面時,Zview=-far,則計算出的Zndc=1。
那麼Zview為何值時,Zndc為0呢?注意肯定不是近裁剪面與遠裁剪面的中間點。
根據上面的公式我們可以計算出當Zndc=0時,




Window (or screen) coordinates(螢幕座標系)

最後系統需要根據NDC座標將其畫到我們的螢幕上,此處不再通過矩陣來完成了,而是通過呼叫命令glViewPort(x, y, width, height)。
x、y指定了視口矩形的左上角點,預設值是(0,0);
width、height指定了視口的寬度和高度。當OpenGL上下文第一次繫結到window時,width和height被預設設定為window的寬度和高度。
glViewport指定了x、y從NDC座標系(歸一化裝置座標系)到螢幕座標系的仿射變換。如果(Xndc,Yndc)是歸一化座標,那麼螢幕座標(Xwindow,Ywindow)計算如下:

一般情況下,在呼叫glViewPort時,我們一般使用glViewPort(0, 0, width, height),也就是前兩個偏移座標都為0,所以上式簡化為:


在片源著色器中有一個內建的輸入vec4變數gl_FragCoord,gl_FragCoord表示的是片元的座標位置,包含了片元的視窗相對座標值(x, y, z, 1/w)

其中此處的gl_FragCoord.x和gl_FragCoord.y分別就是上述計算的Xwindow和Ywindow,即表示相對於螢幕左上角的螢幕座標位置。螢幕座標系的左上角為座標原點,向右為X軸正半軸,向下為Y軸正半軸。gl_FragCoord.z表示的是片元的深度值,預設情況下,在片源著色器中gl_FragCoord.z的深度取值範圍是[0, 1],注意,NDC座標系中的z的取值範圍是[-1, 1],z值的座標範圍之所以能從NDC座標系中的[-1,1]轉變到螢幕座標系中的[0, 1],是因為OpenGL會根據函式glDepthRange(nearVal, farVal)中nearVal與farVal的值,將NDC座標系中z從[-1, 1]對映到[nearVal, farVal],這一對映就是簡單地線性變換。其中,如果沒有主動呼叫過glDepthRange,那麼OpenGL就認為nearVal就是0,farVal就是1。 在NDC座標系中,近裁剪面的z值為-1,遠裁剪面的z值為1,根據nearVal與farVal,將近、遠裁剪面的z值分別對映到了nearVal與farVal,預設情況下在螢幕座標系中,原近裁剪面的深度z就變成了0,原遠裁剪面的深度z就變成了1。 gl_FragCoord中的第四個分量是儲存了“投影除法”的相關資訊1/w。


至此我們就完成了將一個模型座標轉換成螢幕座標的一系列變換。

相關文章