OpenGL入門學習(五) 【轉】

 今天要講的是三維變換的內容,課程比較枯燥。主要是因爲很多函數在單獨使用時都不好描述其效果,我只好在最後舉一個比較綜合的例子。希望大家能一口氣看到底了。只看一次可能不夠,如果感覺到迷糊,不妨多看兩遍。有疑問可以在下面跟帖提出。
我也使用了若干圖形,希望可以幫助理解。


在前面繪製幾何圖形的時候,大家是否覺得我們繪圖的範圍太狹隘了呢?座標只能從-1到1,還只能是X軸向右,Y軸向上,Z軸垂直屏幕。這些限制給我們的繪圖帶來了很多不便。

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

OpenGL變換實際上是通過矩陣乘法來實現。無論是移動、旋轉還是縮放大小,都是通過在當前矩陣的基礎上乘以一個新的矩陣來達到目的。關於矩陣的知識,這裏不詳細介紹,有興趣的朋友可以看看線性代數(大學生的話多半應該學過的)。
OpenGL可以在最底層直接操作矩陣,不過作爲初學,這樣做的意義並不大。這裏就不做介紹了。


1、模型變換和視圖變換
從“相對移動”的觀點來看,改變觀察點的位置與方向和改變物體本身的位置與方向具有等效性。在OpenGL中,實現這兩種功能甚至使用的是同樣的函數。
由於模型和視圖的變換都通過矩陣運算來實現,在進行變換前,應先設置當前操作的矩陣爲“模型視圖矩陣”。設置的方法是以GL_MODELVIEW爲參數調用glMatrixMode函數,像這樣:
glMatrixMode(GL_MODELVIEW);
通常,我們需要在進行變換前把當前矩陣設置爲單位矩陣。這也只需要一行代碼:
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函數可以將當前的可視空間設置爲透視投影空間。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/20061007151547.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。
也可以使用更常用的gluPerspective函數。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/2006100715161.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。

正投影相當於在無限遠處觀察得到的結果,它只是一種理想狀態。但對於計算機來說,使用正投影有可能獲得更好的運行速度。
使用glOrtho函數可以將當前的可視空間設置爲正投影空間。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/20061007151619.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。

如果繪製的圖形空間本身就是二維的,可以使用gluOrtho2D。他的使用類似於glOrgho。


3、視口變換
當一切工作已經就緒,只需要把像素繪製到屏幕上了。這時候還剩最後一個問題:應該把像素繪製到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半。(即:把整個圖象填充到一半的窗口內)
http://blog.programfan.com/upfile/200610/20061007151639.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。

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

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(放大2500倍)。將地球到月亮的距離“修改”爲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。

好了,艱苦的一課終於完畢。我知道,本課的內容十分枯燥,就連最後的例子也是。但我也沒有更好的辦法了,希望大家能堅持過去。不必擔心,熟悉本課內容後,以後的一段時間內,都會是比較輕鬆愉快的了。
發佈了8 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章