圖中顯示了OpenGL圖形管線的主要部分,也是我們在進行圖形編程的時候常常要用到的部分。一個頂點數據從圖的左上角(MC)進入管線,最后從圖的右下角(DC)輸出。MC是Model Coordinate的簡寫,表示模型坐標。DC是Device Coordinate的簡寫,表示設備坐標。當然DC有很多了,什么顯示器,打印機等等。這里DC我們就理解成常說的屏幕坐標好了。MC當然就是3D坐標了(注意:我說的3D坐標,而不是世界坐標),這個3D坐標就是模型坐標,也說成本地坐標(相對於世界坐標)。MC要經過模型變換(Modeling Transformation)才變換到世界坐標,圖2:
變換到世界坐標WC(World Coordinate)說簡單點就是如何用世界坐標系來表示本地坐標系中的坐標。為了講得更清楚一些,這里舉個2D的例子。如圖3:
圖中紅色坐標系是世界坐標系WC,綠色的是模型坐標系MC。現在有一個頂點,在模型坐標系中的坐標為(1,1),現在要把這個模型坐標轉換到世界坐標中來表示。從圖中可以看出,點(1,1)在世界坐標系中的坐標為(3,4),現在我們來通過計算得到我們希望的結果。首先我們要把模型坐標系MC在世界坐標系中表示出來,使用齊次坐標(Homogeneous Coordinate )可以表示為矩陣(注意,本教程中使用的矩陣都是以列向量組成):其中,矩陣的第一列為MC中x軸在WC中的向量表示,第二列為MC中y軸WC中的向量表示,第三列為MC中的原點在WC中的坐標。對齊次坐標系不了解的同學,請先學習游戲數學方面的知識。有了這個模型變換矩陣后,用這個矩陣乘以在MC中表示的坐標就可以得到該坐標在世界坐標系中的坐標。所以該矩陣和MC中的坐標(1,1)相乘有:
這也正是我們需要的結果。現在讓我們把相機坐標也加進去,相機坐標也稱為觀測坐標(View Coordinate),如圖4和圖5。
來看看MC坐標中的點(1,1)如何在相機坐標中表示。從圖5中可以直接看出MC中的點(1,1)在相機坐標系VC中為(-2,-2)。和上面同樣的道理,我們可以寫出相機坐標系VC在世界標系WC中可以表示為:
那么世界坐標系中的點轉換為相機坐標系中的點我們就需求VC的逆矩陣:
那么世界坐標系WC中的點(3,4)在相機坐標系VC中坐標為:
上面的變換過程,就是可以把模型坐標變換為相機坐標。在OpenGL中,當我們申明頂點的時候,有時候說的是世界坐標,這是因為初始化的時候世界坐標系、模型坐標系和相機坐標系是一樣的,重合在一起的。所以OpenGL中提供了模型觀測變換,它是把模型坐標系直接轉換為相機坐標系,如圖4。現在我們已經計算得到了VC-1和MC,如果把VC-1和MC相乘,就可以得到模型坐標在相機坐標中的表示。為了得到模型坐標系中的坐標在相機坐標系中的表示,這就是OpenGL中的ModelView變換矩陣。這也是ModelView變換的名字的由來,它是通過了上面兩個步驟得到的。那么這里,ModelView變換矩陣M為:
現在只要用上面的模型觀測矩陣M乘以模型坐標系MC中的坐標就可以得到相機坐標系中的坐標了。模型觀測變換的關鍵就是要得到相機坐標系中的坐標,因為光照等計算都是在這個這個坐標系中完成的。下面我們實際OpenGL程序中檢查一下。在程序中,為了計算方便,我們使用圖6中的模型。
根據圖中的數據,我們分別可以寫出對應MC和VC-1,從而求得觀測變換矩陣M。
現在程序中用glGetFloatv()這個函數來獲得當前矩陣數據來檢查一下。
-
float
如果在上面程序段中最后一個glGetFloatv(GL_MODELVIEW_MATRIX, m)處設定斷點的話,就可以看到圖7所顯示的數據。
到這里,整個ModelView變換就完成了。通過ModelView變換后得到是相機坐標系內的坐標。在這個坐標系內典型的計算就是法線了。現在再來看看后面一個階段。
經過測試,模型視點矩陣計算是正確的!
//////////////////////////////////////////////////////////////
我的理解:ModelView 變換矩陣,就是完成從模型坐標到View坐標的轉換,是坐標系之間的大變換。注意:modelview 既有model,也有view。不只是一個model的矩陣。
只對model進行平移或旋轉的函數為 glTranslatef等函數,稱作模型變換!它的坐標是基於模型本身的,即位於模型坐標系,比如glTranslatef(0, 0, -3)的3個坐標值。
只是針對view進行設置的函數為gluLookAt,它的坐標系是view坐標系,比如
- gluLookAt(0.0, 0.0, 5.0,
它里面的坐標的原點位於相機坐標系的原點。/////////////////////////////////////////////////////////////////
1.2投影變換
先還是復習一下OpenGL的渲染管線。投影變換意味着之前已經變換到相機坐標系VCS的場景再次變換到投影坐標系下。投影坐標系和相機坐標系相同,一般都使用右手坐標系,Z軸正方向指向視點,投影的近裁剪面為Z=-near,遠裁剪平面為Z=-far。圖1中可以看到,在投影變換(Projection Transformation)中也分為兩個部分,第一個部分是將上個階段得到的所有數據從觀察坐標轉換到裁剪坐標,第二個部分是將這些裁剪坐標通過除以w分量的方式轉換到歸一化設備坐標(NDC)。NDC坐標系是一個左手坐標系,其Z軸正方向與投影坐標系正好相反。一般地,將三維坐標轉換為平面坐標有兩種投影方式:正交投影(Orthogonal Projection)和透視投影(Perspective Projection)。
1.2.1 正交投影
正交投影很簡單,如圖8,對於三維空間中的坐標點和一個二維平面,要在對應的平面上投影,只需將非該平面上的點的坐標分量改為該平面上的坐標值,其余坐標不變。
比如將點(1,1,5)正交投影到z=0的平面上,那么投影后的坐標為(1,1,0)。在openGL中,設置正交投影可以使用函數:
-
glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar)
該函數可以設置正交投影的投影空間,在該空間以外的坐標點就不會被投影到投影平面上。函數中的六個參數分是投影空間六個平面,
在圖9中,大的投影空間是根據這六個參數設置的投影空間,OpenGL會自動將該空間歸一化,也就是將該空間或立方體轉化為變長為1的正六面體投影空間,並且該證六面體的中心在相機坐標系的原點。一旦設置使用glortho函數設置投影空間,OpenGL會生成投影矩陣。這個矩陣的作用就是將坐標進行正交投影並且將投影后的坐標正規化(轉換到-1到1之間)。要注意的是,生成該矩陣的時候,OpenGL會把右手坐標系轉換為左手坐標系。原因很簡單,右手坐標系的Z軸向平面外的,這樣不符合我們的習慣。該矩陣的矩陣推導這里就不詳細說明了,不了解的同學可以參考游戲數學方面資料,這里只給出正交投影矩陣。
這里的矩陣好像有問題: 第三行第三列的負號去掉!
可以參考OpenGL投影矩陣的推倒http://blog.sina.com.cn/s/blog_73428e9a0102v920.html
注意NDC和相機坐標系的不同,NDC是左手坐標系,方向和相機坐標系是相反的。下圖是正交投影視景體和NDC坐標系的比較。
osg中計算正交矩陣的代碼如下(需要注意的是osg中矩陣是后乘,而OpenGL中矩陣是前乘,所以OSG中的正交矩陣是openGL中正交矩陣的轉置矩陣)
- void Matrix_implementation::makeOrtho(double left, double right,
- double bottom, double top,
- double zNear, double zFar)
- {
- // note transpose of Matrix_implementation wr.t OpenGL documentation, since the OSG use post multiplication rather than pre.
- double tx = -(right+left)/(right-left);
- double ty = -(top+bottom)/(top-bottom);
- double tz = -(zFar+zNear)/(zFar-zNear);
- SET_ROW(0, 2.0/(right-left), 0.0, 0.0, 0.0 )
- SET_ROW(1, 0.0, 2.0/(top-bottom), 0.0, 0.0 )
- SET_ROW(2, 0.0, 0.0, -2.0/(zFar-zNear), 0.0 )
- SET_ROW(3, tx, ty, tz, 1.0 )
- }
void Matrix_implementation::makeOrtho(double left, double right, double bottom, double top, double zNear, double zFar) { // note transpose of Matrix_implementation wr.t OpenGL documentation, since the OSG use post multiplication rather than pre. double tx = -(right+left)/(right-left); double ty = -(top+bottom)/(top-bottom); double tz = -(zFar+zNear)/(zFar-zNear); SET_ROW(0, 2.0/(right-left), 0.0, 0.0, 0.0 ) SET_ROW(1, 0.0, 2.0/(top-bottom), 0.0, 0.0 ) SET_ROW(2, 0.0, 0.0, -2.0/(zFar-zNear), 0.0 ) SET_ROW(3, tx, ty, tz, 1.0 ) }
現在還是在OpenGL程序中來檢查一下。在OpenGL程序中添加下面代碼段:
-
//投影設置
在glGetFloatv(GL_PROJECTION_MATRIX,m)處設定斷點就可以看到圖10中所顯示的信息。
1.2.2透視投影
透視投影和正交投影最大的區別就是透視投影具有遠近感。
圖11 透視投影
透視投影采用了圖11中的模型,這樣的模型就是保證遠的物體看起來小,近的物體看起來大。 在OpenGL中設置透視投影可以使用函數:
-
void
該函數也會根據給定的參數生成一個投影空間。如圖11中,該投影空間是一個截頭體。同樣地,OpenGL會自動生成透視投影矩陣,該矩陣也會讓3D坐標投影在投影平面上,並且將投影后的坐標也進行正規化。下面也直接給出OpenGL中使用的透視投影矩陣。
這個矩陣有問題:第三行第四列前面的負號需要去掉!
下面在OpenGL中添加下面代碼段:-
//投影設置
設置斷點后,我們可以看到圖12中顯示的數據。
到此為止,整個投影變換就完成了。透過投影變換后得到的是正規化的投影平面坐標。這為下一個階段的視口變換(View port Transformation)做好了准備。
1.3視口變換
視口變換的詳細內容http://blog.csdn.net/wang15061955806/article/details/49995467
現在到了最后一個階段了。這個階段叫做視口變換,它把上個階段得到的正規化的投影坐標轉化為windows 窗口坐標。視口變換會將投影平面上的畫面映射到窗口上。在OpenGL中可以使用函數
-
GLAPI
來進行對窗口的映射,如圖13。
圖13 視口變換glViewport(width/2, 0, width/2, height/2)舉個例子說明,比如上個階段中得到了一個頂點的坐標為(0,0,0.5,1),根據這個坐標,該頂點位於投影平面的正中間。如果將該點映射到大小為50*50的窗口上時,那么它應該位於屏幕的中間,坐標為(25,25, 0.5,1)。當然這里深度值0.5是不會改變的。有的同學肯定有疑問了,既然投影到了窗口上,那么還要深度值0.5干什么?這里要注意的是,雖然在窗口上顯示時只需要x,y坐標就夠了,但是要在2D窗口上顯示3D圖形時深度值是不可少的。這里的深度值不是用於顯示,而是用於在光柵化的時候進行深度測試。
OpenGL也會根據glViewport函數提供的參數值生成一個視口變換矩陣
//這里h/2 應該改為 -h/2
該矩陣把上個階段得到的正規化坐標映射到窗口上,並且將正規化坐標中的深度值在轉換到0到1之間。所以在深度緩沖中最大值為1,最小值為0。視口變換結束后,OpenGL中主要的圖形管線階段就算完成了,后面就是光柵化等等操作。再來回顧一下圖1,現在相信大家對這個渲染管線有了一定的認識了,也明白了每一個階段對應的變換矩陣以及如何進行坐標之間的轉換的。2. 屏幕坐標轉換為世界坐標
通過前面的教程,以及現在大家對OpenGL整個渲染管線理解后,現在要將屏幕上一點坐標轉換為世界坐標就比較容易了。從圖形管線的開始到結束,一個模型坐標系中的坐標被轉化為了屏幕坐標,那么現在把整個過程倒過來的話,屏幕上一點坐標也可以轉為為世界坐標。只要在對應的階段求得對應變換矩陣的逆矩陣,就可以得到前一個階段的坐標。這整個過程可以用圖14表示。
圖中顯示的過程完全就是OpenGL渲染管線的逆過程,通過這個過程,屏幕上的點就可以轉化為世界坐標系中的點了。可能又有的同學要問,當鼠標點擊屏幕上一點的時候並沒有深度信息,轉換的時候要怎么辦呢?這個時候可以使用OpenGL函數
-
void
該函數能夠獲得屏幕上一點對應像素的深度信息。有了這個深度信息,就可以利用上面過程把屏幕上一點轉換為世界坐標了。在OpenGL中,上面的過程其實已經有現成的函數可以使用,那就是
-
int
- GLdouble projMatrix[16],
- GLint viewport[4],
該函數直接將屏幕上一點轉換對應的世界坐標,該函數的內部實現其實還是上面的那么逆過程。下面給出利用該函數獲取世界坐標的代碼段。
-
GVector screen2world(
- {
- )x;
- winY = ()viewport[3] - ()y;
- (winY), 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &winZ);
- gluUnProject(winX, winY, winZ, modelview, projection, viewport, &posX, &posY, &posZ);
- v;
代碼中函數返回類型GVector是用戶定義的向量類,返回的是齊次坐標。
OpenGL的默認屏幕坐標在左下角,X 軸向右,Y 軸向上,Z 軸從屏幕向外。
視圖模型變換(model-view)就是一個將頂點坐標從世界坐標系變換到視覺坐標系的過程。這里很重要的是對兩個坐標系的認識。
OpenGL 中有一個坐標變換矩陣棧(ModelView),棧頂就是當前坐標變換矩陣,進入OpenGL管道(?)的每個坐標(齊次坐標)都會先乘上這個矩陣,結果才是對應點在場景中的世界坐標。OpenGL中的坐標變換都是通過矩陣運算完成的,與圖形學課本的描述完全一致。要注意的是變換中的矩陣乘法是左乘,而矩陣乘法與算術乘法不同,不符合交換律。
opengl中世界坐標系中的旋轉的正方向為逆時針方向,用右手進行旋轉,右手拇指所指方向為旋轉軸方向,四指所指方向即為旋轉正方向(難道旋轉方向的正負就是這么決定的嗎?)
矩陣變換的順序
對於三維空間的變換而言,縮放運算和旋轉運算總是需要參照坐標軸原點來實現,因此無論是基於OpenGL還是DirectX的程序,一般都會遵循SRT(Scale/Rotate/Translate)的運算順序來完成復合矩陣的構建。