OpenGL------三維變換


我們生活在一個三維的世界——如果要觀察一個物體,我們可以:
1、從不同的位置去觀察它。(視圖變換
2、移動或者旋轉它,當然了,如果它只是計算機里面的物體,我們還可以放大或縮小它。(模型變換
3、如果把物體畫下來,我們可以選擇:是否需要一種“近大遠小”的透視效果。另外,我們可能只希望看到物體的一部分,而不是全部(剪裁)。(投影變換
4、我們可能希望把整個看到的圖形畫下來,但它只占據紙張的一部分,而不是全部。(視口變換
這些,都可以在OpenGL中實現。

OpenGL變換實際上是通過矩陣乘法來實現。無論是移動、旋轉還是縮放大小,都是通過在當前矩陣的基礎上乘以一個新的矩陣來達到目的。OpenGL可以在最底層直接操作矩陣,不過作為初學,這樣做的意義並不大。這里就不做介紹了。
1、模型變換和視圖變換
從“相對移動”的觀點來看,改變觀察點的位置與方向和改變物體本身的位置與方向具有等效性。在OpenGL中,實現這兩種功能甚至使用的是同樣的函數。
由於模型和視圖的變換都通過矩陣運算來實現,在進行變換前,應先設置當前操作的矩陣為“模型視圖矩陣”。設置的方法是以GL_MODELVIEW為參數調用glMatrixMode函數,像這樣:
glMatrixMode(GL_MODELVIEW); //需要修改的是模型視圖矩陣、投影矩陣還是紋理矩陣。mode的值可以為:GL_MODELVIEW、GL_PROJECTION或GL_TEXTURE。
通常,我們需要在進行變換前把當前矩陣設置為單位矩陣。這也只需要一行代碼:
glLoadIdentity();

然后,就可以進行模型變換和視圖變換了。進行模型和視圖變換,主要涉及到三個函數:

glTranslate*,把當前矩陣和一個表示移動物體的矩陣相乘。三個參數分別表示了在三個坐標上的位移值。
glRotate*,把當前矩陣和一個表示旋轉物體的矩陣相乘。物體將繞着(0,0,0)到(x,y,z)的直線以逆時針旋轉,參數angle表示旋轉的角度。
glScale*,把當前矩陣和一個表示縮放物體的矩陣相乘。x,y,z分別表示在該方向上的縮放比例。


注意我都是說“與XX相乘”,而不是直接說“這個函數就是旋轉”或者“這個函數就是移動”,這是有原因的,馬上就會講到。
假設當前矩陣為單位矩陣,然后先乘以一個表示旋轉的矩陣R,再乘以一個表示移動的矩陣T,最后得到的矩陣再乘上每一個頂點的坐標矩陣v。所以,經過變換得到的頂點坐標就是((RT)v)。由於矩陣乘法的結合率,((RT)v) = (R(Tv)),換句話說,實際上是先進行移動,然后進行旋轉。即:實際變換的順序與代碼中寫的順序是相反的。由於“先移動后旋轉”和“先旋轉后移動”得到的結果很可能不同,初學的時候需要特別注意這一點。
OpenGL之所以這樣設計,是為了得到更高的效率。但在繪制復雜的三維圖形時,如果每次都去考慮如何把變換倒過來,也是很痛苦的事情。這里介紹另一種思路,可以讓代碼看起來更自然(寫出的代碼其實完全一樣,只是考慮問題時用的方法不同了)。
讓我們想象,坐標並不是固定不變的。旋轉的時候,坐標系統隨着物體旋轉。移動的時候,坐標系統隨着物體移動。如此一來,就不需要考慮代碼的順序反轉的問題了。

以上都是針對改變物體的位置和方向來介紹的。如果要改變觀察點的位置,除了配合使用glRotate*和glTranslate*函數以外,還可以使用這個函數:gluLookAt。它的參數比較多,前三個參數表示了觀察點的位置,中間三個參數表示了觀察目標的位置,最后三個參數代表從(0,0,0)到 (x,y,z)的直線,它表示了觀察者認為的“上”方向。

2、投影變換

投影變換就是定義一個可視空間,可視空間以外的物體不會被繪制到屏幕上。(注意,從現在起,坐標可以不再是-1.0到1.0了!)
OpenGL支持兩種類型的投影變換,即透視投影和正投影。投影也是使用矩陣來實現的。如果需要操作投影矩陣,需要以GL_PROJECTION為參數調用glMatrixMode函數。
glMatrixMode(GL_PROJECTION);
通常,我們需要在進行變換前把當前矩陣設置為單位矩陣。
glLoadIdentity();

透視投影所產生的結果類似於照片,有近大遠小的效果,比如在火車頭內向前照一個鐵軌的照片,兩條鐵軌似乎在遠處相交了。
使用glFrustum函數可以將當前的可視空間設置為透視投影空間。其參數的意義如下圖:

這個函數原型為:
  void glFrustum(GLdouble left, GLdouble Right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far);
創建一個透視型的視景體。其操作是創建一個透視投影的矩陣,並且用這個矩陣乘以當前矩陣。這個函數的參數只定義近裁剪平面的左下角點和右上角點的三維空間坐標,即(left,bottom,-near)和(right,top,-near);最后一個參數far是遠裁剪平面的離視點的距離值,其左下角點和右上角點空間坐標由函數根據透視投影原理自動生成。near和far表示離視點的遠近,它們總為正值(near/far 必須>0)。

void mydisplay (void)
{
     ......
    glMatrixMode (GL_PROJECTION);
    LoadIdentity ();
    Frustum (left, right, bottom, top, near, far);
    ......
}

2.gluPerspective():也可以使用更常用的gluPerspective函數



這個函數原型為:
void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);
  創建一個對稱的透視型視景體,但它的參數定義於前面的不同,如圖。其操作是創建一個對稱的透視投影矩陣,並且用這個矩陣乘以當前矩陣。參數fovy定義視野在Y-Z平面的角度,范圍是[0.0, 180.0];參數aspect是投影平面寬度與高度的比率;參數Near和Far分別是近遠裁剪面到視點(沿Z負軸)的距離,它們總為正值。
  以上兩個函數缺省時,視點都在原點,視線沿Z軸指向負方向。

3.glOrtho():正投影相當於在無限遠處觀察得到的結果,它只是一種理想狀態。但對於計算機來說,使用正投影有可能獲得更好的運行速度。
使用glOrtho函數可以將當前的可視空間設置為正投影空間


這個函數的原型為:
glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far) 

六個參數, 前兩個是x軸最小坐標和最大坐標,中間兩個是y軸,最后兩個是z軸值
它創建一個平行視景體(就是一個長方體空間區域)。
實際上這個函數的操作是創建一個正射投影矩陣,並且用這個矩陣乘以當前矩陣。
其中近裁剪平面是一個矩形,矩形左下角點三維空間坐標是(left,bottom,-near),
右上角點是(right,top,-near);遠裁剪平面也是一個矩形,左下角點空間坐標是(left,bottom,-far),右上角點是(right,top,-far)。
注意,所有的near和far值同時為正或同時為負, 值不能相同。如果沒有其他變換,正射投影的方向平行於Z軸,且視點朝向Z負軸。這意味着物體在視點前面時far和near都為負值,物體在視點后面時far和near都為正值。
只有在視景體里的物體才能顯示出來。
如果最后兩個值是(0,0),也就是near和far值相同了,視景體深度沒有了,整個視景體都被壓成個平面了,就會顯示不正確。

gluLookAt 函數詳解

 void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,

                                   GLdouble centerx,GLdouble centery,GLdouble centerz,
                                   GLdouble upx,GLdouble upy,GLdouble upz);
函數定義一個視圖 矩陣,並與當前矩陣相乘。
第一組eyex, eyey,eyez 相機在世界坐標的位置
第二組centerx,centery,centerz 相機鏡頭對准的物體在世界坐標的位置
第三組upx,upy,upz 相機向上的方向在世界坐標中的方向
你把相機想象成為你自己的腦袋:
第一組數據就是腦袋的位置
第二組數據就是眼睛看的物體的位置
第三組就是頭頂朝向的方向(因為你可以歪着頭看同一個物體)。
#include <GL/glut.h>  
#include <stdlib.h>  
  
void init(void)   
{  
   glClearColor (0.0, 0.0, 0.0, 0.0); //背景黑色 
}  
  
void display(void)  
{  
   glClear (GL_COLOR_BUFFER_BIT);  
   glColor3f (1.0, 1.0, 1.0); //畫筆白色  
  
   glLoadIdentity();  //加載單位矩陣  
  
   gluLookAt(0.0,0.0,5.0,  0.0,0.0,0.0,  0.0,1.0,0.0);  
   glutWireTeapot(2);  
   glutSwapBuffers();  
}  
  
void reshape (int w, int h)  
{  
  glViewport (0, 0, (GLsizei) w, (GLsizei) h);   
   glMatrixMode (GL_PROJECTION);  
   glLoadIdentity ();  
   gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);  
   glMatrixMode(GL_MODELVIEW);  
   glLoadIdentity();  
   gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);  
}  
int main(int argc, char** argv)  
{  
   glutInit(&argc, argv);  
   glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB);  
   glutInitWindowSize (500, 500);   
   glutInitWindowPosition (100, 100);  
   glutCreateWindow (argv[0]);  
   init ();  
   glutDisplayFunc(display);   
   glutReshapeFunc(reshape);  
   glutMainLoop();  
   return 0;  
}  

 一、上面的display()函數中:gluLookAt(0.0,0.0,5.0, 0.0,0.0,0.0, 0.0,1.0,0.0); 相當於我們的腦袋位置在(0.0,0.0,5.0)處,眼睛望向(0.0,0.0,0.0),即原點。后面的三個參數(0.0,1.0,0.0),y軸為1,其余為0,表示腦袋朝上,就是正常的情況。看到的情況如下圖:

壺嘴在右,壺柄在坐,壺底在下,壺蓋在上。
 
二、若將gluLookAt的后三個參數設置為(0.0,-1.0,0.0),即y軸為-1,其余為0。這樣表示腦袋向下,即人眼倒着看,看到的效果如下圖:
 
三、再次修改gluLookAt的后三個參數為(1.0,0.0,0.0);x軸為1,其余為0.即人的腦袋像右歪90度來看,即順時針轉90度(換個角度思考就是壺逆時針轉90度),猜想看到的結果應該是壺嘴在上,壺蓋在右,壺底在左,壺柄在下。如下圖:
         如果並沒有調用gluLookAt(),那么照相機就被設置為默認的位置和方向。在默認情況下,照相機位於原點,指向z軸的負方向,朝上向量為(0,1,0)。
 
       可以修改原來的代碼。把視圖變換函數gluLookAt()函數,改為模型變換函數glTranslatef(),並使用參數(0.0,0.0,-5.0)。這個函數的效果和使用gluLookAt()函數的效果是完全相同的,原因:gluLookAt()函數是通過移動照相機(使用試圖變換)來觀察這個立方體,而glTranslatef()函數是通過移動茶壺(使用模型變換)。另外注意:視圖變換要在模型變換之前進行。

3、視口變換
當一切工作已經就緒,只需要把像素繪制到屏幕上了。這時候還剩最后一個問題:應該把像素繪制到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半。(即:把整個圖象填充到一半的窗口內)

運用相機模擬方式,我們很容易理解視口變換就是類似於照片的放大與縮小。在計算機圖形學中,它的定義是將經過幾何變換、投影變換和裁剪變換后的物體顯示於屏幕窗口內指定的區域內,這個區域通常為矩形,稱為視口。

    在實際中,視口的長寬比率總是等於視景體裁剪面的長寬比率。如果兩個比率不相等,那么投影后的圖像顯示於視口內時會發生變形,如圖所示。
[轉載]計算機圖形學基礎知識-三維變換
 

使用glViewport來定義視口。其中前兩個參數定義了視口的左下腳(0,0表示最左下方),后兩個參數分別是寬度和高度。

void glViewport(GLint x,GLint y,GLsizei width,GLsizei height);

在窗口中定義一個像素矩形,最終的圖像將映射到這個矩形中。參數x和y指定了窗口內部視口的左下角,width和height指定了視口的大小。附:視口的縱橫比一般和視景體的縱橫比相同,若不同則當圖像投影到視口時就會變形


4、操作矩陣堆棧
介於是入門教程,先簡單介紹一下堆棧。你可以把堆棧想象成一疊盤子。開始的時候一個盤子也沒有,你可以一個一個往上放,也可以一個一個取下來。每次取下的,都是最后一次被放上去的盤子。通常,在計算機實現堆棧時,堆棧的容量是有限的,如果盤子過多,就會出錯。當然,如果沒有盤子了,再要求取一個盤子,也會出錯。
我們在進行矩陣操作時,有可能需要先保存某個矩陣,過一段時間再恢復它。當我們需要保存時,調用glPushMatrix函數,它相當於把矩陣(相當於盤子)放到堆棧上。當需要恢復最近一次的保存時,調用glPopMatrix函數,它相當於把矩陣從堆棧上取下。OpenGL規定堆棧的容量至少可以容納32個矩陣,某些OpenGL實現中,堆棧的容量實際上超過了32個。因此不必過於擔心矩陣的容量問題。
通常,用這種先保存后恢復的措施,比先變換再逆變換要更方便,更快速。
注意:模型視圖矩陣和投影矩陣都有相應的堆棧。使用glMatrixMode來指定當前操作的究竟是模型視圖矩陣還是投影矩陣。

5、綜合舉例
我們要制作的是一個三維場景,包括了太陽、地球和月亮。假定一年有12個月,每個月30天。每年,地球繞着太陽轉一圈。每個月,月亮圍着地球轉一圈。即一年有360天。現在給出日期的編號(0~359),要求繪制出太陽、地球、月亮的相對位置示意圖。(這是為了編程方便才這樣設計的。如果需要制作更現實的情況,那也只是一些數值處理而已,與OpenGL關系不大)
首先,讓我們認定這三個天體都是球形,且他們的運動軌跡處於同一水平面,建立以下坐標系:太陽的中心為原點,天體軌跡所在的平面表示了X軸與Y軸決定的平面,且每年第一天,地球在X軸正方向上,月亮在地球的正X軸方向。
下一步是確立可視空間。注意:太陽的半徑要比太陽到地球的距離短得多。如果我們直接使用天文觀測得到的長度比例,則當整個窗口表示地球軌道大小時,太陽的大小將被忽略。因此,我們只能成倍的放大幾個天體的半徑,以適應我們觀察的需要。(百度一下,得到太陽、地球、月亮的大致半徑分別是:696000km, 6378km,1738km。地球到太陽的距離約為1.5億km=150000000km,月亮到地球的距離約為380000km。)
讓我們假想一些數據,將三個天體的半徑分別“修改”為:69600000(放大100倍),15945000(放大2500倍),4345000(放大5000倍)。將地球到月亮的距離“修改”為38000000(放大100倍)。地球到太陽的距離保持不變。
為了讓地球和月亮在離我們很近時,我們仍然不需要變換觀察點和觀察方向就可以觀察它們,我們把觀察點放在這個位置:(0, -200000000, 0) ——因為地球軌道半徑為150000000,咱們就湊個整,取-200000000就可以了。觀察目標設置為原點(即太陽中心),選擇Z軸正方向作為 “上”方。當然我們還可以把觀察點往“上”方移動一些,得到(0, -200000000, 200000000),這樣可以得到45度角的俯視效果。
為了得到透視效果,我們使用gluPerspective來設置可視空間。假定可視角為60度(如果調試時發現該角度不合適,可修改之。我在最后選擇的數值是75。),高寬比為1.0。最近可視距離為1.0,最遠可視距離為200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);

現在我們來看看如何繪制這三個天體。
為了簡單起見,我們把三個天體都想象成規則的球體。而我們所使用的glut實用工具中,正好就有一個繪制球體的現成函數:glutSolidSphere,這個函數在“原點”繪制出一個球體。由於坐標是可以通過glTranslate*和glRotate*兩個函數進行隨意變換的,所以我們就可以在任意位置繪制球體了。函數有三個參數:第一個參數表示球體的半徑,后兩個參數代表了“面”的數目,簡單點說就是球體的精確程度,數值越大越精確,當然代價就是速度越緩慢。這里我們只是簡單的設置后兩個參數為20。
太陽在坐標原點,所以不需要經過任何變換,直接繪制就可以了。
地球則要復雜一點,需要變換坐標。由於今年已經經過的天數已知為day,則地球轉過的角度為day/一年的天數*360度。前面已經假定每年都是360天,因此地球轉過的角度恰好為day。所以可以通過下面的代碼來解決:

glRotatef(day, 0, 0, -1);
/* 注意地球公轉是“自西向東”的,因此是饒着Z軸負方向進行逆時針旋轉 */
glTranslatef(地球軌道半徑, 0, 0);
glutSolidSphere(地球半徑, 20, 20);


月亮是最復雜的。因為它不僅要繞地球轉,還要隨着地球繞太陽轉。但如果我們選擇地球作為參考,則月亮進行的運動就是一個簡單的圓周運動了。如果我們先繪制地球,再繪制月亮,則只需要進行與地球類似的變換:

glRotatef(月亮旋轉的角度, 0, 0, -1);
glTranslatef(月亮軌道半徑, 0, 0);
glutSolidSphere(月亮半徑, 20, 20);


但這個“月亮旋轉的角度”,並不能簡單的理解為day/一個月的天數30*360度。因為我們在繪制地球時,這個坐標已經是旋轉過的。現在的旋轉是在以前的基礎上進行旋轉,因此還需要處理這個“差值”。我們可以寫成:day/30*360 - day,即減去原來已經轉過的角度。這只是一種簡單的處理,當然也可以在繪制地球前用glPushMatrix保存矩陣,繪制地球后用glPopMatrix恢復矩陣。再設計一個跟地球位置無關的月亮位置公式,來繪制月亮。通常后一種方法比前一種要好,因為浮點的運算是不精確的,即是說我們計算地球本身的位置就是不精確的。拿這個不精確的數去計算月亮的位置,會導致 “不精確”的成分累積,過多的“不精確”會造成錯誤。我們這個小程序沒有去考慮這個,但並不是說這個問題不重要。
還有一個需要注意的細節: OpenGL把三維坐標中的物體繪制到二維屏幕,繪制的順序是按照代碼的順序來進行的。因此后繪制的物體會遮住先繪制的物體,即使后繪制的物體在先繪制的物體的“后面”也是如此。使用深度測試可以解決這一問題。使用的方法是:1、以GL_DEPTH_TEST為參數調用glEnable函數,啟動深度測試。2、在必要時(通常是每次繪制畫面開始時),清空深度緩沖,即:glClear(GL_DEPTH_BUFFER_BIT);其中,glClear (GL_COLOR_BUFFER_BIT)與glClear(GL_DEPTH_BUFFER_BIT)可以合並寫為:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
且后者的運行速度可能比前者快。

到此為止,我們終於可以得到整個“太陽,地球和月亮”系統的完整代碼。
Code:

// 太陽、地球和月亮
// 假設每個月都是30天
// 一年12個月,共是360天
static int day = 200; // day的變化:從0到359
void myDisplay(void)
{
     glEnable(GL_DEPTH_TEST);
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 400000000);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

     // 繪制紅色的“太陽”
     glColor3f(1.0f, 0.0f, 0.0f);
     glutSolidSphere(69600000, 20, 20);
     // 繪制藍色的“地球”
     glColor3f(0.0f, 0.0f, 1.0f);
     glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(150000000, 0.0f, 0.0f);
     glutSolidSphere(15945000, 20, 20);
     // 繪制黃色的“月亮”
     glColor3f(1.0f, 1.0f, 0.0f);
     glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
}

試修改day的值,看看畫面有何變化。


小結:本課開始,我們正式進入了三維的OpenGL世界。
OpenGL通過矩陣變換來把三維物體轉變為二維圖象,進而在屏幕上顯示出來。為了指定當前操作的是何種矩陣,我們使用了函數glMatrixMode。
我們可以移動、旋轉觀察點或者移動、旋轉物體,使用的函數是glTranslate*和glRotate*。
我們可以縮放物體,使用的函數是glScale*。
我們可以定義可視空間,這個空間可以是“正投影”的(使用glOrtho或gluOrtho2D),也可以是“透視投影”的(使用glFrustum或gluPerspective)。
我們可以定義繪制到窗口的范圍,使用的函數是glViewport。
矩陣有自己的“堆棧”,方便進行保存和恢復。這在繪制復雜圖形時很有幫助。使用的函數是glPushMatrix和glPopMatrix。

好了,艱苦的一課終於完畢。我知道,本課的內容十分枯燥,就連最后的例子也是。但我也沒有更好的辦法了,希望大家能堅持過去。不必擔心,熟悉本課內容后,以后的一段時間內,都會是比較輕松愉快的了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM