OpenGL是個大的狀態機器。我們就是通過改變它的狀態來實現情境的渲染的。
1.最基本的矩陣變換:
模型變換:移動和變換情境中的模型;
投影變換 :對視見空間進行裁剪和扭曲;
視角變換: 對最終輸出進行縮放。
矩陣變換
通過前三章的學習,我們知道了如何使用OpenGL在3D空間中繪製基本圖元,並把使用圖元組成模型。然而,在我們繪製完一個物體或一個情境之後,我們總希望從多個角度觀察這個物體,或者在情境中走動。這時,我們需要OpenGL的另一個功能:變換。
OpenGL為我們提供了許多方面和類型的變換。你可以對投影方式進行變換,也可以對物體/模型 進行變換。你可以改變自己的位置和方向,也可以改變物體的大小和角度。學習本章內容,你將瞭解:
OpenGL中變換的種類
使用矩陣描述一個變換
基本變換
定義和使用自己的變換
4.1 OpenGL中的變換
變換(Transform),可以使3D空間中的物體投影到2D平面上。使用變換,你可以移動、旋轉、縮放甚至彎曲一個物體。然而變換並沒有直接修改頂點資料,取而代之,變換修改了座標系。如果旋轉一個座標系,然後再在旋轉後的座標系裡繪圖,繪製後的圖形就好像被旋轉了。在基本OpenGL渲染流程中,將進行以下變換:
視圖變換 :用於指定觀察者的位置和方向;
模型視圖變換:移動和變換情境中的模型;
投影變換 :對視見空間進行裁剪和扭曲;
視見區變換:對最終輸出進行縮放。
4.1.1 視圖變換
在一個情境中,我們希望改變觀察者的位置和觀察角度。用於改變觀察者方位和角度的變換,就是視圖變換。預設情況下(沒有執行任何變換時),觀察者位於點(0,0,0),且視線朝著-Z方向。也就是說,只有在z<0的地方繪圖,才有可能被觀察到。
4.1.2 模型視圖變換
此變換用於移動和旋轉情境中的物體。使用模型視圖變換完全可以代替視圖變換。道理是很簡單的:比如你想使用視圖變換將觀察者向-Z軸移動10個單位,此時情境中所有的物體都向+Z軸移動了10個單位。這跟你直接使用模型視圖變換將情境中所有物體向+Z方向移動10個單位的效果是完全一樣的。
4.1.3 投影變換
要把3D情境投影到2D平面上,就必須執行投影變換。投影變換有兩種形式,即平行投影變換和透視投影變換。關於平行投影和透視投影,在第3中已進行了具體的介紹,這裡不在複述。現在要強調的是,投影也是一種變換,實現投影,本質上是對情境中所有物體進行特殊的變換,使得它們能夠被畫在一個平面上。比如透視投影變換會將情境中所有物體按照遠近不同進行縮放和扭曲,使它們看起來具有立體感。
4.1.4 視見區變換
這裡又回到了第二章中的主題。視見區變換就是對投影后的2D圖象進行縮放和剪裁,使它能夠被正確地顯示在視窗上。你可以回到第二章瞭解視見區的具體概念。
4.2 矩陣
矩陣(Matrix)是那樣的強大以至於幾乎所有的變換都可以由矩陣來表達。矩陣是又n行m列的數組成的一個陣列(m、n≥1)。通過矩陣的乘法運算就可以運用各種變換。在OpenGL中,統一使用大小為4×4的矩陣。
由於矩陣的運演算法則和具體數學內容,和OpenGL這一主題並沒有太大關係(使用OpenGL並不需要瞭解矩陣是怎樣運算的,因為OpenGL會幫你完成一切),所以這裡不再介紹。但這並不代表這些數學知識是不重要的,靈活地運用矩陣,可以自己創造出許多OpenGL沒有提供的變換,並提高運算速度。你可以參看《線性代數》瞭解更多內容。
在OpenGL進行變換操作時,會首先把頂點轉換為1×4的矩陣(第1-3行分別存放頂點的x、y、z座標,第4行存放w座標,即縮放因子,一般總為1.0),然後將這個點依次乘以模型視圖變換矩陣、投影矩陣、視見區變換矩陣......最後得到因出現在螢幕上的2D螢幕座標,完成變換。幸運的是,你不需要任何數學基礎,哪怕你對矩陣一無所知,也能順利地完成這一流程。因為OpenGL已經封裝了進階函數,這使得你不用自己動手寫矩陣,就能完成所有的基本變換。稍後就將介紹這些基本函數。
4.3 基本變換
對於每一種變換,OpenGL都有自己的函數用來產生這些變換的矩陣並應用它們。下面將一一介紹。
4.3.1 模型變換矩陣
這是本章最重要的內容。使用模型變換,你就可以完成物體的旋轉和移動,併產生移動觀察者的效果。這正是本章的主題。為了能夠完成我們的樣本,我們定義以下函數在原點繪製一個球體。以下函數涉及到二次曲面的內容,這是OpenGL的另一個進階主題,我們將在今後的章節中具體講解,現在我們只用它來繪製球體:
procedure DrawSphere(R:Single); //R代表球體的半徑
var SpObj:GLUQuadricObj;
begin
spObj:=gluNewQuadric;
gluQuadricNormals(SpObj,GLU_SMOOTH);
gluQuadricOrientation(SpObj,GLU_OUTSIDE);
gluSphere(SpObj,R,50,50);
gluDeleteQuadric(spObj);
end;
4.3.1.1 平移
當我們調用DrawSphere時,會在原點繪製一個球體。現在我們想在點(0,10,0)上繪製這個球體,就必須在繪製之前將座標系沿+Y方向平移10個單位。於是我們會寫出這樣的代碼:
//建立一個將座標系沿+Y方向平移10個單位的矩陣:
....
//用當前模型視圖矩陣乘以這個矩陣:
...
DrawSphere(5);//繪製一個半徑為5的球體
但事實上,我們不需要這麼麻煩。OpenGL為我們提供了這樣一個函數:
glTranslatef(x,y,z:Single);
其中,x,y,z分別表示在X、Y、Z軸上平移的量。調用這個函數之後,OpenGL會自動產生一個平移矩陣,然後應用這個矩陣。因此,我們可以這樣寫代碼:
glTranslatef(0,10,0);
DrawSphere(5);
這樣就能在(0,10,0)上繪製一個球體了。
4.3.1.2 旋轉
與平移類似,OpenGL也為我們提供了一個進階函數用於旋轉物體:
glRotatef(Angle,x,y,z:Single);
這個函數將產生並應用一個將座標系以向量(x,y,z)為軸,旋轉angle個角度的矩陣。如果我們想將一個球體以Y軸自轉50度,就可以調用:
glRotatef(50,0,1,0);
DrawSphere(5);
4.3.1.3 縮放
縮放變換其實是將座標系的x、y、z軸按不同的縮放因子展寬,從而實現縮放效果。函數
glScalef(x,y,z:Single);
把座標系的X、Y、Z軸分別縮放x、y、z倍。例如:
glScalef(2,2,2);
DrawSphere(5);
將繪製一個半徑為10的球體。
4.3.1.4 變換的疊加性質
使用變換時,我們應該注意的是,變換是疊加在上次變換的基礎上的。也就是說,變換的效果會累積。每次調用變換函數時,會產生一個新的函數來乘以當前的模型視圖矩陣,隨後,新的矩陣將成為當前的模型變換矩陣,在下次執行變換時,會被新的矩陣相乘,因此作用效果將不斷累積。舉個例子就能很明白地說明這一點。
例如,你想在(0,10,0)上繪製一個球體,完後在(10,0,0)上繪製另一個,得到4.3-1所示的圖形:
圖4.3-1
你可能會寫出如下代碼:
//沿Y軸向上平移10個單位
glTranslatef(0,10,0);
//畫第一個球體
DrawSphere(5);
//沿X軸向左平移10個單位
glTranslatef(10,0,0);
//畫第二個球體
DrawSphere(5);
然而,你不應該忘記,變換的作用效果是累積的。在繪製第二個球體時,由於此時座標系已經向Y軸移動了10個單位,再向X方向移動10個單位之後,新的座標系的原點應是絕對座標系中的點(10,10)。因此,上述程式將繪製出4.3-2所示的圖形。
圖4.3-2
你可能會在繪製第二個球體之前調用glTranslatef(0,-10,0);把座標系往回移動10個單位。但這樣會降低代碼的可讀性,還會給CPU增加額外的運算。這個時候,我們可以使用單位矩陣。
我們可以調用glLoadIdentity();函數將當前模型視圖變換矩陣重設到初始狀態,再進行新的繪製:
procedure RenderScene();
begin
glMatrixMode(GL_MODELVIEW);
//沿Y軸向上平移10個單位
glTranslatef(0,10,0);
//畫第一個球體
DrawSphere(5);
//載入單位矩陣
glLoadIdentity;
//沿X軸向上平移10個單位
glTranslatef(10,0,0);
//畫第二個球體
DrawSphere(5);
end;
請看第一行代碼。這裡調用了glMatrixMode函數。這個函數的作用是通知OpenGL我們將對模型視圖變換矩陣進行操作。也就是要進行模型視圖變換。glMatrixMode可用參數如下:
GL_PROJECTION :用於修改投影矩陣
GL_MODELVIEW :用於修改模型視圖變換矩陣
4.3.1.5 矩陣堆棧
如果每次變換前都把當前矩陣恢複到單位矩陣,也比較麻煩。更多時候,我們希望儲存當前矩陣,執行一些變換之後,把當前矩陣恢複到上次儲存時的狀態。
OpenGL為我們提供了一個"矩陣堆棧"滿足我們的這種要求。我們可以把當前矩陣壓入堆棧中,然後執行一些變換,再彈出剛才壓入的矩陣,從而把當前矩陣恢複到上次變換之前的狀態。我們調用
glPushMatrix();
把當前矩陣壓入矩陣堆棧,調用
glPopMatrix();
彈出矩陣。我們還可以分別調用
glGet(GL_MAX_MODELVIEW_STACK_DEPTH);
glGet(GL_MAX_PROJECTION_STACK_DEPTH);
來擷取模型視圖矩陣堆棧和投影矩陣堆棧的最大堆棧深度。一般情況下(在Windows平台上),模型視圖的最大堆棧深度是32,而投影堆棧的最大深度是2。
使用矩陣堆棧,4.3.1.4節中的程式可以改寫為:
procedure RenderScene();
begin
glMatrixMode(GL_MODELVIEW);
//推入矩陣堆棧
glPushMatrix;
//沿Y軸向上平移10個單位
glTranslatef(0,10,0);
//畫第一個球體
DrawSphere(5);
//恢複到上次儲存時的狀態
glPopMatrix;
//沿X軸向左平移10個單位
glTranslatef(10,0,0);
//畫第二個球體
DrawSphere(5);
end;
4.3.2 投影矩陣
設定投影矩陣往往在OpenGL繪圖和模型視圖變換之前。一般情況下,我們調用
glMatrixMode(GL_PROJECTION);
將當前矩陣設定為投影矩陣。再調用
glOrtho 或 gluPerspective 來建立平行或透視投影。建立完後,再調用
glMatrixMode(GL_MODELVIEW);
將當前變換矩陣設定為模型視圖變換矩陣。
至此,你應該能夠理解前面章節的樣本程式中的 SetView 過程的意義了吧。請再看一次SetView過程:
procedure TfrmMain.SetView;
begin
glClearColor(0,0,0,0);//設定背景顏色為黑色
glViewPort(0,0,ClientWidth,ClientHeight);//指定OpenGL在此地區內繪圖。
glMatrixMode(GL_PROJECTION);//設定視圖投影變換矩陣
glLoadIdentity;//載入單位矩陣。
glOrtho(0,ClientWidth,ClientHeight,0,1,-1);//建立平行投影。
glMatrixMode(GL_MODELVIEW);//將矩陣變換對象切換為模型視圖變換。
end;
4.4 定義和使用自己的變換
除了使用OpenGL為我們提供的幾個進階變換函數之外,我們還可以自己建立一個矩陣,並使用當前矩陣乘以該矩陣來進行特殊的變換。你可以建立一個4×4的二維數組用於描述一個矩陣。如:
M:array[1..4] of array[1..4] of Single;
其中M[j,i]表示矩陣M的第j行,第i列的資料。
你也可以建立一個一維數組:
M:array[1..16]of Single;
無論是2維數組還是一維數組,都是按照列優先的順序儲存的。
矩陣定義完後,調用
glLoadMatrix(M);
可以用矩陣M替換當前矩陣,調用
glMultMatrix(M);
用當前矩陣乘以矩陣M。
要說明的是,使用glLoadMatrix或glMultMatrix的速度沒有OpenGL的進階變換函數快。所以如果不是進階變換函數完成不了的變換,就不要使用glLoadMatrix或者glMultMatrix。
4.5 樣本程式
這是一個經典的樣本程式。它示範了太陽系中地月系與太陽之間的運動關係:月球饒地球轉,整個地月系饒太陽轉,所有的星球都自轉。這個例子很好地展示了矩陣變換的性質和矩陣堆棧的作用。為了增加視覺效果,本程式中加入了光照渲染。同時,我們也加入了紋理貼圖,這是為了能看星球自轉的景象。有關光照和紋理貼圖的詳細內容,我們都將會在今後的章節中具體講解。
以下是渲染過程的代碼:
procedure TfrmMain.RenderScene;
begin
glEnable(GL_CULL_FACE);
glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
glTranslatef(0,0,-110);
glRotatef(yDeg,0,1,0);
glRotatef(xDeg,1,0,1);
RenderLights;//光照處理
//繪製太陽
glColor3ub(255,100,64);
glPushMatrix;
glRotate(SunSelfAng,0,1,0);
DrawSphere(10);
glPopMatrix;
glPushMatrix;//推入當前矩陣
//繪製地月系
glRotatef(EarthCommonAng,0,1,0);
glTranslatef(50,0,0);
glPushMatrix;//繪製地球
glRotatef(EarthSelfAng,0,1,0);
glColor3ub(20,50,255);
DrawSphere(5);
glPopMatrix;
glPushMatrix;//繪製月球
glColor3ub(200,200,200);
glRotatef(MoonAng,0,1,0);
glTranslatef(10,0,0);
glRotatef(MoonSelfAng,0,1,0);
DrawSphere(2);
glPopMatrix;
glPopMatrix;//彈出矩陣
SwapBuffers(wglGetCurrentDC);
end;
2. OpenGL中分為3種材質:環境光線,漫反射光,鏡面反射光
設定環境光線強度:
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };
為光源GL_LIGHT1賦予環境光線屬性:
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);
為光源GL_LIGHT1設定位置:
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);