OpenGL(五)之初入三維變換


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

咱們生活在一個三維的世界——若是要觀察一個物體,咱們能夠:
一、從不一樣的位置去觀察它。(視圖變換)
二、移動或者旋轉它,固然了,若是它只是計算機裏面的物體,咱們還能夠放大或縮小它。(模型變換)
三、若是把物體畫下來,咱們能夠選擇:是否須要一種「近大遠小」的透視效果。另外,咱們可能只但願看到物體的一部分,而不是所有(剪裁)。(投影變換)
四、咱們可能但願把整個看到的圖形畫下來,但它只佔據紙張的一部分,而不是所有。(視口變換)
這些,均可以在OpenGL中實現。

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


一、模型變換和視圖變換
從「相對移動」的觀點來看,改變觀察點的位置與方向和改變物體自己的位置與方向具備等效性。在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)的直線,它表示了觀察者認爲的「上」方向。


二、投影變換

投影變換就是定義一個可視空間,可視空間之外的物體不會被繪製到屏幕上。(注意,從如今起,座標能夠再也不是-1.0到1.0了!)
OpenGL支持兩種類型的投影變換,即透視投影和正投影。投影也是使用矩陣來實現的。若是須要操做投影矩陣,須要以GL_PROJECTION爲參數調用glMatrixMode函數。
glMatrixMode(GL_PROJECTION);
一般,咱們須要在進行變換前把當前矩陣設置爲單位矩陣。
glLoadIdentity();

透視投影所產生的結果相似於照片,有近大遠小的效果,好比在火車頭內向前照一個鐵軌的照片,兩條鐵軌彷佛在遠處相交了。
使用glFrustum函數能夠將當前的可視空間設置爲透視投影空間。具體解釋以下:html

 

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。
 
三、視口變換
當一切工做已經就緒,只須要把像素繪製到屏幕上了。這時候還剩最後一個問題:應該把像素繪製到窗口的哪一個區域呢?一般狀況下,默認是完整的填充整個窗口,但咱們徹底能夠只填充一半
 
使用glViewport來定義視口。其中前兩個參數定義了視口的左下腳(0,0表示最左下方),後兩個參數分別是寬度和高度。
 
四、操做矩陣堆棧
介因而入門教程,先簡單介紹一下堆棧。你能夠把堆棧想象成一疊盤 子。開始的時候一個盤子也沒有,你能夠一個一個往上放,也能夠一個一個取下來。每次取下的,都是最後一次被放上去的盤子。一般,在計算機實現堆棧時,堆棧 的容量是有限的,若是盤子過多,就會出錯。固然,若是沒有盤子了,再要求取一個盤子,也會出錯。
咱們在進行矩陣操做時,有可能須要先保存某個矩 陣,過一段時間再恢復它。當咱們須要保存時,調用glPushMatrix函數,它至關於把矩陣(至關於盤子)放到堆棧上。當須要恢復最近一次的保存時, 調用glPopMatrix函數,它至關於把矩陣從堆棧上取下。OpenGL規定堆棧的容量至少能夠容納32個矩陣,某些OpenGL實現中,堆棧的容量 實際上超過了32個。所以沒必要過於擔憂矩陣的容量問題。
一般,用這種先保存後恢復的措施,比先變換再逆變換要更方便,更快速。
注意:模型視圖矩陣和投影矩陣都有相應的堆棧。使用glMatrixMode來指定當前操做的到底是模型視圖矩陣仍是投影矩陣。


五、綜合舉例
好了,視圖變換的入門知識差很少就講完了。但咱們不能就這樣結束。由於本次課程的內容實在過於枯燥,若是分別舉例,可能效果不佳。我只好綜合的講一個例子,算是給你們一個參考。至於實際的掌握,還要靠你們本身花功夫。閒話少說,如今進入正題。

我 們要製做的是一個三維場景,包括了太陽、地球和月亮。假定一年有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把三維座標中的物體繪製到二維屏幕,繪製的順序是按照代碼的順序來進行的。所以後繪製的物體會遮住先繪製的物體,即便後繪製的物體在先繪製的 物體的「後面」也是如此。使用深度測試能夠解決這一問題。使用的方法是:
一、以GL_DEPTH_TEST爲參數調用glEnable函數,啓動深度測試。
二、在必要時(一般是每次繪製畫面開始時),清空深度緩衝,即: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=====================
ide

相關文章
相關標籤/搜索