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函數可以將當前的可視空間設置為透視投影空間。具體解釋如下:

 

glFrustum是opengl類庫中的函數,它是將當前矩陣與一個透視矩陣相乘,把當前矩陣轉變成透視矩陣,在使用它之前,通常會先調用glMatrixMode(GL_PROJECTION).它的原型如下:
void glFrustum(
GLdouble
left,
 
GLdouble
right,
 
GLdouble
bottom,
 
GLdouble
top,
 
GLdouble
nearVal,
 
GLdouble
farVal);
參數解釋:
  left,right指明相對於垂直平面的左右坐標位置
  bottom,top指明相對於水平剪切面的下上位置
  nearVal,farVal指明相對於深度剪切面的遠近的距離,兩個必須為正數
如圖各個參數指示的位置。
 
進一步說明:
glFrustum()函數定義一個平截頭體,它計算一個用於實現透視投影的矩陣,並把它與當前的投影矩陣(一般是單位矩陣)相乘。也即是該函數構造了一個視景體用來將模型進行投影,來裁剪模型,決定模型哪些在視景體里面,哪些在視景體的外面,在視景體之外的就不可見。
 
也可以使用更常用的gluPerspective函數。具體解釋如下:
 
gluPerspective這個函數指定了觀察的視景體(frustum為錐台的意思,通常譯為視景體)在世界坐標系中的具體大小,一般而言,其中的參 數aspect應該與窗口的寬高比大小相同。比如說,aspect=2.0表示在觀察者的角度中物體的寬度是高度的兩倍,在視口中寬度也是高度的兩倍,這樣顯示出的物體才不會被扭曲。

gluPerspective -- set up a perspective projection matrix (設置透視投影矩陣)
void gluPerspective(
  GLdouble fovy, //角度
  GLdouble aspect,//視景體的寬高比
  GLdouble zNear,//沿z軸方向的兩裁面之間的距離的近處
  GLdouble zFar //沿z軸方向的兩裁面之間的距離的遠處
)
PARAMETERS(參數含義)
fovy
Specifies the field of view angle, in degrees, in the y direction.
指定視景體的視野的角度,以度數為單位,y軸的上下方向
 
aspect
Specifies the aspect ratio that determines the field of view in the x direction. The aspect ratio is the ratio of x (width) to y (height).
指定你的視景體的寬高比(x 平面上)
 
zNear
Specifies the distance from the viewer to the near clipping plane (always positive).
指定觀察者到視景體的最近的裁剪面的距離(必須為正數)
 
zFar
Specifies the distance from the viewer to the far clipping plane (always positive).
與上面的參數相反,這個指定觀察者到視景體的最遠的裁剪面的距離(必須為正數)
 
DESCRIPTION(說明)
由gluPerspective產生的矩陣是與當前矩陣與指定的矩陣相乘 得到的,就好像是調用glMatrix()產生的矩陣一樣。為了使透視矩陣替代當前矩陣,在調用gluPerspective之前要先調用 glLoadidentity()這個函數(就是把當前矩陣s設置為單位矩陣)。
補充,這段話的意思就是說(個人理解),這個 gluPerspective的實現是通過將當前矩陣與你通過這個函數指定的參數而建立的矩陣相乘來實現的,而在OpenGL中,矩陣的相乘都是連乘的, 也就是說,你調用這個函數會與其他的變化矩陣的函數效果相疊加從而影響原矩陣(當然有時候確實需要這樣做),所以,在調用這個函數之前,通常需要先調用 glLoadidentity來把當前矩陣單位化,從而使各種變換效果不會疊加,比如旋轉就只旋轉,透視就只透視,通過調用glLoadidentity 就不會既旋轉又透視了。
請參考《OpenGL編程指南》一書。
 
 
正投影相當於在無限遠處觀察得到的結果,它只是一種理想狀態。但對於計算機來說,使用正投影有可能獲得更好的運行速度。

使用glOrtho函數可以將當前的可視空間設置為正投影空間。具體解釋如下:
 
void glOrtho(
  GLdouble left,
    GLdouble right,
  GLdouble bottom,
  GLdouble top,
  GLdouble near,
  GLdouble far)
glOrtho就是一個正射投影函數。它創建一個平行視景體。實際上這個函數的操作是創建一個正射投影矩陣,並且用這個矩陣乘以當前矩陣。其中近裁剪平面 是一個矩形,矩形左下角點三維空間坐標是(left,bottom,-near),右上角點是(right,top,-near);遠裁剪平面也是一個矩 形,左下角點空間坐標是(left,bottom,-far),右上角點是(right,top,-far)。所有的near和far值同時為正或同時為 負。如果沒有其他變換,正射投影的方向平行於Z軸,且視點朝向Z負軸。這意味着物體在視點前面時far和near都為負值,物體在視點后面時far和 near都為正值。
 
 
 
如果繪制的圖形空間本身就是二維的,可以使用gluOrtho2D。他的使用類似於glOrgho。
 
3、視口變換
當一切工作已經就緒,只需要把像素繪制到屏幕上了。這時候還剩最后一個問題:應該把像素繪制到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半
 
使用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);
且后者的運行速度可能比前者快。


到此為止,我們終於可以得到整個“太陽,地球和月亮”系統的完整代碼。
 1 // 太陽、地球和月亮
 2 // 假設每個月都是30天
 3 // 一年12個月,共是360天
 4 static int day = 200; // day的變化:從0到359
 5 void myDisplay(void)
 6 {
 7     glEnable(GL_DEPTH_TEST);
 8     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 9 
10     glMatrixMode(GL_PROJECTION);
11     glLoadIdentity();
12     gluPerspective(75, 1, 1, 4000000);
13     glMatrixMode(GL_MODELVIEW);
14     glLoadIdentity();
15     gluLookAt(0, -2000000, 2000000, 0, 0, 0, 0, 0, 1);
16 
17     // 繪制紅色的“太陽”
18     glColor3f(1.0f, 0.0f, 0.0f);
19     glutSolidSphere(696000, 20, 20);
20     // 繪制藍色的“地球”
21     glColor3f(0.0f, 0.0f, 1.0f);
22     glRotatef(day / 360.0*360.0, 0.0f, 0.0f, -1.0f);
23     glTranslatef(1500000, 0.0f, 0.0f);
24     glutSolidSphere(159450, 20, 20);
25     // 繪制黃色的“月亮”
26     glColor3f(1.0f, 1.0f, 0.0f);
27     glRotatef(day / 30.0*360.0 - day / 360.0*360.0, 0.0f, 0.0f, -1.0f);
28     glTranslatef(380000, 0.0f, 0.0f);
29     glutSolidSphere(43450, 20, 20);
30 
31     glFlush();
32     
33 }

注:原本代碼顯示參數寫的太大,即參照物距離放大了,導致代碼運行看不到結果,上面代碼進行縮小100倍,即大數減去兩個0,,,

 

效果如下:

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


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

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

=====================    第五課 完    =====================
=====================TO BE CONTINUED=====================

 

 
 
 


免責聲明!

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



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