今天要講的是三維變換的內容,課程比較枯燥。主要是因為很多函數在單獨使用時都不好描述其效果,我只好在最后舉一個比較綜合的例子。希望大家能一口氣看到底了。只看一次可能不夠,如果感覺到迷糊,不妨多看兩遍。有疑問可以在下面跟帖提出。
我也使用了若干圖形,希望可以幫助理解。
在前面繪制幾何圖形的時候,大家是否覺得我們繪圖的范圍太狹隘了呢?坐標只能從-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函數可以將當前的可視空間設置為透視投影空間。其參數的意義如下圖:
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。
也可以使用更常用的gluPerspective函數。其參數的意義如下圖:
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。
正投影相當于在無限遠處觀察得到的結果,它只是一種理想狀態。但對于計算機來說,使用正投影有可能獲得更好的運行速度。
使用glOrtho函數可以將當前的可視空間設置為正投影空間。其參數的意義如下圖:
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。
如果繪制的圖形空間本身就是二維的,可以使用gluOrtho2D。他的使用類似于glOrgho。
3、視口變換
當一切工作已經就緒,只需要把像素繪制到屏幕上了。這時候還剩最后一個問題:應該把像素繪制到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半。(即:把整個圖象填充到一半的窗口內)
聲明:該圖片來自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(放大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);
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。
好了,艱苦的一課終于完畢。我知道,本課的內容十分枯燥,就連最后的例子也是。但我也沒有更好的辦法了,希望大家能堅持過去。不必擔心,熟悉本課內容后,以后的一段時間內,都會是比較輕松愉快的了。
===================== 第五課 完 =====================
|