OpenGL基礎圖形編程
一、OpenGL與3D圖形世界
1.1、OpenGL使人們進入三維圖形世界
我們生活在一個充滿三維物體的三維世界中,為了使計算機能精確地再現這些物體,我們必須能在三維空間描繪這些物體。我們又生活在一個充滿信息的世界中,能否盡快地理解并運用這些信息將直接影響事業的成敗,所以我們需要用一種最直接的形式來表示這些信息。
最近幾年計算機圖形學的發展使得三維表現技術得以形成,這些三維表現技術使我們能夠再現三維世界中的物體,能夠用三維形體來表示復雜的信息,這種技術就是可視化(Visualization)技術。可視化技術使人能夠在三維圖形世界中直接對具有形體的信息進行操作,和計算機直接交流。這種技術已經把人和機器的力量以一種直覺而自然的方式加以統一,這種革命性的變化無疑將極大地提高人們的工作效率。可視化技術賦予人們一種仿真的、三維的并且具有實時交互的能力,這樣人們可以在三維圖形世界中用以前不可想象的手段來獲取信息或發揮自己創造性的思維。機械工程師可以從二維平面圖中得以解放直接進入三維世界,從而很快得到自己設計的三維機械零件模型。醫生可以從病人的三維掃描圖象分析病人的病灶。軍事指揮員可以面對用三維圖形技術生成的戰場地形,指揮具有真實感的三維飛機、軍艦、坦克向目標開進并分析戰斗方案的效果。
更令人驚奇的是目前正在發展的虛擬現實技術,它能使人們進入一個三維的、多媒體的虛擬世界,人們可以游歷遠古時代的城堡,也可以遨游浩翰的太空。所有這些都依賴于計算機圖形學、計算機可視化技術的發展。人們對計算機可視化技術的研究已經歷了一個很長的歷程,而且形成了許多可視化工具,其中SGI公司推出的GL三維圖形庫表現突出,易于使用而且功能強大。利用GL開發出來的三維應用軟件頗受許多專業技術人員的喜愛,這些三維應用軟件已涉及建筑、產品設計、醫學、地球科學、流體力學等領域。隨著計算機技術的繼續發展,GL已經進一步發展成為OpenGL,OpenGL已被認為是高性能圖形和交互式視景處理的標準,目前包括ATT公司UNIX軟件實驗室、IBM公司、DEC公司、SUN公司、HP公司、Microsoft公司和 SGI公司在內的幾家在計算機市場占領導地位的大公司都采用了OpenGL圖形標準。
值得一提的是,由于Microsoft公司在 Windows NT中提供OpenGL圖形標準,OpenGL將在微機中廣泛應用,尤其是OpenGL三維圖形加速卡和微機圖形工作站的推出,人們可以在微機上實現三維圖形應用,如CAD設計、仿真模擬、三維游戲等,從而更有機會、更方便地使用OpenGL及其應用軟件來建立自己的三維圖形世界。
1.2、OpenGL提供直觀的三維圖形開發環境
OpenGL實際上是一種圖形與硬件的接口。它包括了120個圖形函數,開發者可以用這些函數來建立三維模型和進行三維實時交互。與其他圖形程序設計接口不同,OpenGL提供了十分清晰明了的圖形函數,因此初學的程序設計員也能利用OpenGL的圖形處理能力和1670萬種色彩的調色板很快地設計出三維圖形以及三維交互軟件。
OpenGL強有力的圖形函數不要求開發者把三維物體模型的數據寫成固定的數據格式,這樣開發者不但可以直接使用自己的數據,而且可以利用其他不同格式的數據源。這種靈活性極大地節省了開發者的時間,提高了軟件開發效益。
長期以來,從事三維圖形開發的技術人員都不得不在自己的程序中編寫矩陣變換、外部設備訪問等函數,這樣為調制這些與自己的軟件開發目標關系并不十分密切的函數費腦筋,而OpenGL正是提供一種直觀的編程環境,它提供的一系列函數大大地簡化了三維圖形程序。例如:
- OpenGL提供一系列的三維圖形單元供開發者調用。
- OpenGL提供一系列的圖形變換函數。
- OpenGL提供一系列的外部設備訪問函數,使開發者可以方便地訪問鼠標、鍵盤、空間球、數據手套等這種直觀的三維圖形開發環境體現了OpenGL的技術優勢,這也是許多三維圖形開發者熱衷于OpenGL的緣由所在。
OpenGL成為目前三維圖形開發標準在計算機發展初期,人們就開始從事計算機圖形的開發。直到計算機硬軟件和計算機圖形學高度發達的九十年代,人們發現復雜的數據以視覺的形式表現時是最易理解的,因而三維圖形得以迅猛發展,于是各種三維圖形工具軟件包相繼推出,如PHIGS、PEX、 RenderMan等。這些三維圖形工具軟件包有些側重于使用方便,有些側重于渲染效果或與應用軟件的連接,但沒有一種三維工具軟件包在交互式三維圖形建模能力、外部設備管理以及編程方便程度上能夠OpenGL相比擬。
OpenGL經過對GL的進一步發展,實現二維和三維的高級圖形技術,在性能上表現得異常優越,它包括建模、變換、光線處理、色彩處理、動畫以及更先進的能力,如紋理影射、物體運動模糊等。OpenGL的這些能力為實現逼真的三維渲染效果、建立交互的三維景觀提供了優秀的軟件工具。OpenGL在硬件、窗口、操作系統方面是相互獨立的。
許多計算機公司已經把 OpenGL集成到各種窗口和操作系統中,其中操作系統包括UNIX、Windows NT、DOS等,窗口系統有X窗口、Windows等。為了實現一個完整功能的圖形處理系統,設計一個與OpenGL相關的系統結構為:其最底層是圖形硬件,第二層為操作系統,第三層為窗口系統,第四層為OpenGL,第五層為應用軟件。OpenGL是網絡透明的,在客戶 — 服務器(Client-Server)體系結構中,OpenGL允許本地和遠程繪圖。所以在網絡系統中,OpenGL在X窗口、Windows或其它窗口系統下都可以以一個獨立的圖形窗口出現。
OpenGL作為一個性能優越的圖形應用程序設計界面(API)而適合于廣泛的計算環境,從個人計算機到工作站和超級計算機,OpenGL都能實現高性能的三維圖形功能。由于許多在計算機界具有領導地位的計算機公司紛紛采用OpenGL作為三維圖形應用程序設計界面,OpenGL應用程序具有廣泛的移植性。因此,OpenGL已成為目前的三維圖形開發標準,是從事三維圖形開發工作的技術人員所必須掌握的開發工具。
二、OpenGL概念建立
OpenGL是一個與硬件圖形發生器的軟件接口,它包括了100多個圖形操作函數,開發者可以利用這些函數來構造景物模型、進行三維圖形交互軟件的開發。正如上一章所述,OpenGL是一個高性能的圖形開發軟件包。OpenGL支持網絡,在網絡系統中用戶可以在不同的圖形終端上運行程序顯示圖形。 OpenGL作為一個與硬件獨立的圖形接口,它不提供與硬件密切相關的設備操作函數,同時,它也不提供描述類似于飛機、汽車、分子形狀等復雜形體的圖形操作函數。用戶必須從點、線、面等最基本的圖形單元開始構造自己的三維模型。當然,象OpenInventor那樣更高一級的基于OpenGL的三維圖形建模開發軟件包將提供方便的工具。因此OpenGL的圖形操作函數十分基本、靈活。例如OpenGL中的模型繪制過程就多種多樣,內容十分豐富,OpenGL提供了以下的對三維物體的繪制方式:
- 網格線繪圖方式(wireframe)
這種方式僅繪制三維物體的網格輪廓線。
- 深度優先網格線繪圖方式(depth_cued)
用網格線方式繪圖,增加模擬人眼看物體一樣,遠處的物體比近處的物體要暗些。
- 反走樣網格線繪圖方式(antialiased)
用網格線方式繪圖,繪圖時采用反走樣技術以減少圖形線條的參差不齊。
- 平面消隱繪圖方式(flat_shade)
對模型的隱藏面進行消隱,對模型的平面單元按光照程度進行著色但不進行光滑處理。
- 光滑消隱繪圖方式(smooth_shade)
對模型進行消隱按光照渲染著色的過程中再進行光滑處理,這種方式更接近于現實。
- 加陰影和紋理的繪圖方式(shadows、textures)
在模型表面貼上紋理甚至于加上光照陰影,使得三維景觀象照片一樣。
- 運動模糊的繪圖方式(motion-blured)
模擬物體運動時人眼觀察所感覺的動感現象。
- 大氣環境效果(atmosphere-effects)
在三維景觀中加入如霧等大氣環境效果,使人身臨其境。
- 深度域效果(depth-of-effects)
類似于照相機鏡頭效果,模型在聚焦點處清晰,反之則模糊。
2.2、OpenGL工作流程
整個OpenGL的基本工作流程如下圖:
其中幾何頂點數據包括模型的頂點集、線集、多邊形集,這些數據經過流程圖的上部,包括運算器、逐個頂點操作等;圖像數據包括象素集、影像集、位圖集等,圖像象素數據的處理方式與幾何頂點數據的處理方式是不同的,但它們都經過光柵化、逐個片元(Fragment)處理直至把最后的光柵數據寫入幀緩沖器。在OpenGL中的所有數據包括幾何頂點數據和象素數據都可以被存儲在顯示列表中或者立即可以得到處理。OpenGL中,顯示列表技術是一項重要的技術。
OpenGL要求把所有的幾何圖形單元都用頂點來描述,這樣運算器和逐個頂點計算操作都可以針對每個頂點進行計算和操作,然后進行光柵化形成圖形碎片;對于象素數據,象素操作結果被存儲在紋理組裝用的內存中,再象幾何頂點操作一樣光柵化形成圖形片元。
整個流程操作的最后,圖形片元都要進行一系列的逐個片元操作,這樣最后的象素值BZ送入幀緩沖器實現圖形的顯示。
2.3、OpenGL圖形操作步驟
在上一節中說明了OpenGL的基本工作流程,根據這個流程可以歸納出在OpenGL中進行主要的圖形操作直至在計算機屏幕上渲染繪制出三維圖形景觀的基本步驟:
1)根據基本圖形單元建立景物模型,并且對所建立的模型進行數學描述(OpenGL中把:點、線、多邊形、圖像和位圖都作為基本圖形單元)。
2)把景物模型放在三維空間中的合適的位置,并且設置視點(viewpoint)以觀察所感興趣的景觀。
3)計算模型中所有物體的色彩,其中的色彩根據應用要求來確定,同時確定光照條件、紋理粘貼方式等。
4)把景物模型的數學描述及其色彩信息轉換至計算機屏幕上的象素,這個過程也就是光柵化(rasterization)。
在這些步驟的執行過程中,OpenGL可能執行其他的一些操作,例如自動消隱處理等。另外,景物光柵化之后被送入幀緩沖器之前還可以根據需要對象素數據進行操作。
三、WindowsNT下的OpenGL
3.1、Windows NT下的OpenGL函數
如前面的章節所述,Windows NT下的OpenGL同樣包含100多個庫函數,這些函數都按一定的格式來命名,即每個函數都以gl開頭。Windows NT下的OpenGL除了具有基本的OpenGL函數外,還支持其他四類函數:
相應函數 | 具體說明 |
OpenGL實用庫 | 43個函數,每個函數以glu開頭。 |
OpenGL輔助庫 | 31個函數,每個函數以aux開頭。 |
Windows專用庫函數(WGL) | 6個函數,每個函數以wgl開頭。 |
Win32 API函數 | 5個函數,函數前面沒有專用前綴。 |
在OpenGL中有115個核心函數,這些函數是最基本的,它們可以在任何OpenGL的工作平臺上應用。這些函數用于建立各種各樣的形體,產生光照效果,進行反走樣以及進行紋理映射,進行投影變換等等。由于這些核心函數有許多種形式并能夠接受不同類型的參數,實際上這些函數可以派生出300 多個函數。
OpenGL的實用函數是比OpenGL核心函數更高一層的函數,這些函數是通過調用核心函數來起作用的。這些函數提供了十分簡單的用法,從而減輕了開發者的編程負擔。OpenGL的實用函數包括紋理映射、坐標變換、多邊形分化、繪制一些如橢球、圓柱、茶壺等簡單多邊形實體(本指南將詳細講述這些函數的具體用法)等。這部分函數象核心函數一樣在任何OpenGL平臺都可以應用。
OpenGL的輔助庫是一些特殊的函數,這些函數本來是用于初學者做簡單的練習之用,因此這些函數不能在所有的OpenGL平臺上使用,在Windows NT環境下可以使用這些函數。這些函數使用簡單,它們可以用于窗口管理、輸入輸出處理以及繪制一些簡單的三維形體。為了使OpenGL的應用程序具有良好的移植性,在使用OpenGL輔助庫的時候應謹慎。
6個WGL函數是用于連接OpenGL與Windows NT的,這些函數用于在Windows NT環境下的OpenGL窗口能夠進行渲染著色,在窗口內繪制位圖字體以及把文本放在窗口的某一位置等。這些函數把Windows與OpenGL揉合在一起。最后的5個Win32函數用于處理象素存儲格式和雙緩沖區,顯然這些函數僅僅能夠用于Win32系統而不能用于其它OpenGL平臺。
3.2、OpenGL基本功能
OpenGL能夠對整個三維模型進行渲染著色,從而繪制出與客觀世界十分類似的三維景象。另外OpenGL還可以進行三維交互、動作模擬等。具體的功能主要有以下這些內容。
- 模型繪制
OpenGL能夠繪制點、線和多邊形。應用這些基本的形體,我們可以構造出幾乎所有的三維模型。OpenGL通常用模型的多邊形的頂點來描述三維模型。如何通過多邊形及其頂點來描述三維模型,在指南的在后續章節會有詳細的介紹。
- 模型觀察
在建立了三維景物模型后,就需要用OpenGL描述如何觀察所建立的三維模型。觀察三維模型是通過一系列的坐標變換進行的。模型的坐標變換在使觀察者能夠在視點位置觀察與視點相適應的三維模型景觀。在整個三維模型的觀察過程中,投影變換的類型決定觀察三維模型的觀察方式,不同的投影變換得到的三維模型的景象也是不同的。最后的視窗變換則對模型的景象進行裁剪縮放,即決定整個三維模型在屏幕上的圖象。
- 顏色模式的指定
OpenGL 應用了一些專門的函數來指定三維模型的顏色。程序員可以選擇二個顏色模式,即RGBA模式和顏色表模式。在RGBA模式中,顏色直接由RGB值來指定;在顏色表模式中,顏色值則由顏色表中的一個顏色索引值來指定。程序員還可以選擇平面著色和光滑著色二種著色方式對整個三維景觀進行著色。
- 光照應用
用OpenGL繪制的三維模型必須加上光照才能更加與客觀物體相似。OpenGL提供了管理四種光(輻射光、環境光、鏡面光和漫反射光)的方法,另外還可以指定模型表面的反射特性。
- 圖象效果增強
OpenGL提供了一系列的增強三維景觀的圖象效果的函數,這些函數通過反走樣、混合和霧化來增強圖象的效果。反走樣用于改善圖象中線段圖形的鋸齒而更平滑,混合用于處理模型的半透明效果,霧使得影像從視點到遠處逐漸褪色,更接近于真實。
- 位圖和圖象處理
OpenGL還提供了專門對位圖和圖象進行操作的函數。
- 紋理映射
三維景物因缺少景物的具體細節而顯得不夠真實,為了更加逼真地表現三維景物,OpenGL提供了紋理映射的功能。OpenGL提供的一系列紋理映射函數使得開發者可以十分方便地把真實圖象貼到景物的多邊形上,從而可以在視窗內繪制逼真的三維景觀。
- 實時動畫
為了獲得平滑的動畫效果,需要先在內存中生成下一幅圖象,然后把已經生成的圖象從內存拷貝到屏幕上,這就是OpenGL的雙緩存技術(double buffer)。OpenGL提供了雙緩存技術的一系列函數。
- 交互技術
目前有許多圖形應用需要人機交互,OpenGL提供了方便的三維圖形人機交互接口,用戶可以選擇修改三維景觀中的物體。
OpenGL的作用機制是客戶(client)/服務器(sever)機制,即客戶(用OpenGL繪制景物的應用程序)向服務器(即OpenGL內核)發布OpenGL命令,服務器則解釋這些命令。大多數情況下,客戶和服務器在同一機器上運行。正是OpenGL的這種客戶/服務器機制,OpenGL可以十分方便地在網絡環境下使用。因此Windows NT下的OpenGL是網絡透明的。正象Windows的圖形設備接口(GDI)把圖形函數庫封裝在一個動態鏈接庫(Windows NT下的GDI32.DLL)內一樣,OpenGL圖形庫也被封裝在一個動態鏈接庫內(OPENGL32.DLL)。受客戶應用程序調用的OpenGL函數都先在OPENGL32.DLL中處理,然后傳給服務器WINSRV.DLL。OpenGL的命令再次得到處理并且直接傳給Win32的設備驅動接口(Device Drive Interface,DDI),這樣就把經過處理的圖形命令送給視頻顯示驅動程序。下圖簡要說明這個過程:
圖3-1 OpenGL在Windows NT下運行機制
在三維圖形加速卡的GLINT圖形加速芯片的加速支持下,二個附加的驅動程序被加入這個過程中。一個OpenGL可安裝客戶驅動程序(Installable Client Driver,ICD)被加在客戶這一邊,一個硬件指定DDI(Hardware-specific DDI)被加在服務器這邊,這個驅動程序與Wind32 DDI是同一級別的。
圖3-2 在三維圖形加速下OpenGL運行機制
四、OpenGL基礎程序結構
用OpenGL編寫的程序結構類似于用其他語言編寫的程序。實際上,OpenGL是一個豐富的三維圖形函數庫,編寫OpenGL程序并非難事,只需在基本C語言中調用這些函數,用法同Turbo C、Microsoft C等類似,但也有許多不同之處。
本指南所有的程序都是在Windows NT的Microsoft Visual C++集成環境下編譯連接的,其中有部分頭文件和函數是為這個環境所用的,例如判別操作系統的頭文件“glos.h”。此外,為便于各類讀者同時快速入門,在短時間內掌握OpenGL編程的基本方法和技巧,指南中例子盡量采用標準ANSI C調用OpenGL函數來編寫,而且所有例程都只采用OpenGL附帶的輔助庫中的窗口系統。此外,這樣也便于程序在各平臺間移植,尤其往工作站UNIX 操作系統移植時,也只需改動頭文件等很少很少的部分。下面列出一個簡單的OpenGL程序:
例4-1 OpenGL簡單例程(Simple.c)
#include <GL/gl.h>
#include <GL/glaux.h>
#include "glos.h"
void main(void)
{
auxInitDisplayMode(AUX_SINGLE|AUX_RGBA);
auxInitPosition(0,0,500,500);
auxInitWindow("simple");
glClearColor(0.0,0.0,0.0,0.0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0,0.0,0.0);
glRectf(-0.5,-0.5,0.5,0.5);
glFlush();
_sleep(1000);
}
這個程序運行結果是在屏幕窗口內畫一個紅色的方塊。
下面具體分析整個程序結構:首先,在程序最開始處是OpenGL頭文件:<GL/gl.h>、<GL/glaux.h>。前一個是gl庫的頭文件,后一個是輔助庫的頭文件。此外,在以后的幾章中還將說明OpenGL的另外兩個頭文件,一個是<GL/glu.h>實用庫的頭文件,另一個是<GL/glx.h>X窗口擴充庫的頭文件(這個常用在工作站上)。接下來是主函數main()的定義:一般的程序結構是先定義一個窗口:
auxInitDisplayMode(AUX_SINGLE|AUX_RGBA);
auxInitPosition(0,0,500,500);
auxInitWindow("simple");
auxInitDisplayMode(AUX_SINGLE|AUX_RGBA)設置窗口顯示模式為RGBA方式,即彩色方式,并且圖形緩存為單緩存(SINGLE BUFFER)。 auxInitPosition(0, 0, 500, 500)定義窗口的初始位置,前兩個參數(0, 0)為窗口的左上角點的屏幕坐標,后兩個參數(500,500)為窗口的寬度和高度。auxInitWindow("simple")是窗口初始化,字符參數是窗口名稱。
然后是窗口內清屏:
glClearColor(0.0,0.0,0.0,0.0); glClear(GL_COLOR_BUFFER_BIT);
第一句將窗口清為黑色,第二句將顏色緩沖區清為glClearColor(0.0, 0.0, 0.0, 0.0)命令所設置的顏色,即同窗口背景顏色一致。
再接著是在窗口內畫一個物體:
glColor3f(1.0,0.0,0.0);
glRectf(-0.5,-0.5,0.5,0.5);
很明顯,第一句設置物體顏色,函數中前三個參數分別為R、G、B值,最后一個參數是Alpha值,范圍都從0至1;第二句繪制一個二維矩形。注意:OpenGL是針對三維圖形而言,因此用作OpenGL編程繪制物體必須意識到任何一個物體都是三維的,具有空間性,而顯示于屏幕上的物體都是三維物體在二維平面上的投影。
從表面上看,上述程序代碼很簡單,實際上已經用到了缺省的投影形式(正射投影)。再看glFlush()函數,表示強制繪圖完成。最后一句_sleep(1000),參數單位為毫秒,整句意思是保持現有狀況一秒鐘,然后結束程序運行。這個函數是VC++的庫函數。
總而言之,OpenGL程序基本結構為定義窗口、清理窗口、繪制物體、結束運行。
五、OpenGL的數據類型和函數名
OpenGL的數據類型定義可以與其它語言一致,但建議在ANSI C下最好使用以下定義的數據類型,例如GLint、GLfloat等。具體類型見表5-1。
前綴 數據類型 相應C語言類型 OpenGL類型
================================================================
b 8-bit integer signed char GLbyte
s 16-bit integer short GLshort
i 32-bit integer long GLint,GLsizei
f 32-bit floating-point float GLfloat,GLclampf
d 64-bit floating-point double GLdouble,GLclampd
ub 8-bit unsigned integer unsigned char GLubyte,GLboolean
us 16-bit unsigned integer unsigned short GLushort
ui 32-bit unsigned integer unsigned long GLuint,GLenum,GLbitfield
首先,每個庫函數有前綴gl、glu、glx或aux,表示此函數分屬于基本庫、實用庫、X窗口擴充庫或輔助庫,其后的函數名頭字母大寫,后綴是參數類型的簡寫,取i、f,參見表5-1。例:
glVertex2i(2,4);
glVertex3f(2.0,4.0,5.0);
注意:有的函數參數類型后綴前帶有數字2、3、4。2代表二維,3代表三維,4代表alpha值(以后介紹)。
有些OpenGL函數最后帶一個字母v,表示函數參數可用一個指針指向一個向量(或數組)來替代一系列單個參數值。下面兩種格式都表示設置當前顏色為紅色,二者等價。
glColor3f(1.0,0.0,0.0);
float color_array[]={1.0,0.0,0.0};
glColor3fv(color_array);
除了以上基本命名方式外,還有一種帶“*”星號的表示方法,例如glColor*(),它表示可以用函數的各種方式來設置當前顏色。同理,glVertex*v()表示用一個指針指向所有類型的向量來定義一系列頂點坐標值。
最后,OpenGL也定義GLvoid類型,如果用C語言編寫,可以用它替代void類型。
六、OpenGL輔組庫的基本使用
OpenGL是一個開放的系統,它是獨立于任何窗口系統或操作系統的。盡管它包含了許多圖形函數,但它卻沒有窗口函數,也沒有從鍵盤和鼠標讀取事件的函數,所以要初學者寫出一個完整的圖形程序是相當困難的。另外,OpenGL圖形函數中只提供基本的幾何原形:點、線、多邊形,因此要創建基本的三維幾何體如球、錐體等,也很不容易。而OpenGL輔助庫就是為解決這些基本問題專門設計的,它提供了一些基本的窗口管理函數和三維圖形繪制函數,能幫助初學者盡快進入OpenGL世界,掌握關鍵的三維圖形技術,體會其中奇妙的樂趣。但是,對于復雜的應用,這些函數遠遠不夠,只能作為參考。
6.1、輔助庫函數分類
這一節內容可以作為手冊查閱,初學者不必深究。
輔助庫函數大致分為六類:
6.1.1 窗口初始化和退出
相關函數有三個,它們在第一章已提到,這里將詳細介紹:
void auxInitWindow(GLbyte *titleString)
打開一個由auxInitDisplayMode()和auxInitPosition()指定的窗口。函數參數是窗口標題,窗口背景缺省顏色是RGBA下的黑色或顏色表(color_index)下的0號調色板的顏色。按下Escape鍵可以完成關掉窗口、結束程序、全部清屏三項功能。
void auxInitDisplayMode(GLbitfield mask)
設置窗口顯示模式。基本模式有RGBA或顏色表、單或雙緩存,也可指定其他附加模式:深度、模板或累積緩存(depth,stencil,and/or accumulation buffer)。參數mask是一組位標志的聯合(取或),AUX_RGBA或AUX_INDEX、AUX_SINGLE或AUX_DOUBLE,以及其它有效標志AUX_DEPTH、AUX_STENCIL或AUX_ACCUM。
void auxInitPosition(GLint x,GLint y,GLsizei width,GLsizei height)
設置窗口位置及大小。參數(x, y)為窗口的左上角點的屏幕坐標,參數(width, height)為窗口的寬度和高度,單位為象素,缺省值為(0, 0, 100, 100)。
6.1.2 窗口處理和事件輸入
當窗口創建后,且在進入主函數循環之前,應當登記以下列出的回調函數(callback function):
void auxReshapeFunc(void(*function)(GLsizei,GLsizei))
定義窗口改變時形狀重定函數。參數function是一個函數指針,這個函數帶有兩個參數,即窗口改變后的新寬度和新高度。通常,function是 glViewport(),顯示裁減后的新尺寸,重定義投影矩陣,以便使投影后圖像的比例與視點匹配,避免比例失調。若不調用 auxReshapeFunc(),缺省重定物體形狀的函數功能是調用一個二維的正射投影矩陣。運用輔助庫,窗口將在每個事件改變后自動重新繪制。
void auxKeyFunction(GLint key,void(*function)(void))
定義鍵盤響應函數。參數function就是當按下key鍵時所調用的函數指針,輔助庫為參數key定義了幾個常量:AUX_0至AUX_9、 AUX_A至AUX_Z、AUX_a至AUX_z、AUX_LEFT、AUX_RIGHT、AUX_UP、AUX_DOWN(方向鍵)、 AUX_ESCAPE、AUX_SPACE或AUX_RETURN。
void auxMouseFunc(GLint button,Glint mode,void(*function)(AUX_EVENTREC *))
定義鼠標響應函數。參數function就是當鼠標以mode方式作用于button時所調用的函數。參數button有 AUX_LEFTBUTTON、AUX_MIDDLEBUTTON或AUX_RIGHTBUTTON(以右手為標準)。參數mode代表鼠標觸擊狀態,擊中時為AUX_MOUSEDOWN,釋放時為AUX_MOUSEUP。參數function必須帶一個參數,它是指向結構AUX_EVENNTREC的指針。當函數auxMouseFunc()被調用時將為這個結構分配相應的內存。通常用法類似如下:
void function(AUX_EVENTREC *event)
{
GLint x,y;
x=event->data[AUX_MOUSEX];
y=event->data[AUX_MOUSEY];
...
}
6.1.3 顏色表裝入
因為OpenGL本身沒有窗口系統,所以依賴于窗口系統的顏色映射就沒法裝入顏色查找表。如果采用顏色表模式,就要用到輔助庫提供的用RGB值定義的單個顏色索引函數:
void auxSetOneColor(GLint index,GLfloat red,GLfloat green,GLfloat blue)
設置自定義顏色的索引。參數index即索引號,參數red、green、blue分別為紅、綠、藍值,范圍在(0~1)內。
6.1.4 三維物體繪制
每組三維物體包括兩種形式:網狀體(wire)和實心體(solid)。網狀體沒有平面法向,而實心體有,能進行光影計算,有光照時采用實心體模型。下面這些函數的 參數都是定義物體大小的,可以改變。
功能
|
函數 |
繪制球
|
void auxWireSphere(GLdouble radius) void auxSolidSphere(GLdouble radius) |
繪制立方體
|
void auxWireCube(GLdouble size) void auxSolidCube(GLdouble size) |
繪制長方體
|
void auxWireBox(GLdouble width,GLdouble height,GLdouble depth) void auxSolidBox(GLdouble width,GLdouble height,GLdouble depth) |
繪制環形圓紋面
|
void auxWireTorus(GLdouble innerRadius,GLdouble outerRadius) void auxSolidTorus(GLdouble innerRadius,GLdouble outerRadius) |
繪制圓柱
|
void auxWireCylinder(GLdouble radius,GLdouble height) void auxSolidCylinder(GLdouble radius,GLdouble height) |
繪制二十面體
|
void auxWireIcosahedron(GLdouble radius) void auxSolidIcosahedron(GLdouble radius) |
繪制八面體
|
void auxWireOctahedron(GLdouble radius) void auxSolidOctahedron(GLdouble radius) |
繪制四面體
|
void auxWireTetrahedron(GLdouble radius) void auxSolidTetrahedron(GLdouble radius) |
繪制十二面體
|
void auxWireDodecahedron(GLdouble radius) void auxSolidDodecahedron(GLdouble radius) |
繪制圓錐
|
void auxWireCone(GLdouble radius,GLdouble height) void auxSolidCone(GLdouble radius,GLdouble height) |
繪制茶壺
|
void auxWireTeapot(GLdouble size) void aucSolidTeapot(GLdouble size) |
表6-1
|
以上物體均以各自中心為原點繪制,所有坐標都已單位化,可以縮放。
6.1.5 背景過程管理
void auxIdleFunc(void *func)
定義空閑狀態執行函數。參數func是一個指針,指向所要執行的函數功能。當它為零時,func執行無效。
6.1.6 程序運行
void auxMainLoop(void(*displayFunc)(void))
定義場景繪制循環函數。displayFunc指針指向場景繪制函數。當窗口需要更新或場景發生改變時,程序便調用它所指的函數,重新繪制場景。
6.2、輔助庫應用示例
下面舉一個輔助庫的應用例子,testaux.c:
例6-1 輔助庫應用例程 testaux.c
#include "glos.h"
#include <GL/gl.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w,GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
glClearColor(0.0,0.0,0.0,0.0);
glClear(GL_COLOR_BUFFER_BIT);
}
void CALLBACK myReshape(GLsizei w,GLsizei h)
{
glViewport(0,0,w,h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h)
glOrtho(-1.5,1.5,-1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w,-10.0,10.0);
else
glOrtho(-1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w,-1.5,1.5,-10.0,10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void CALLBACK display(void)
{
glColor3f(1.0,1.0,0.0);
auxWireSphere(1.0);
glFlush();
}
void main(void)
{
auxInitDisplayMode(AUX_SINGLE|AUX_RGBA);
auxInitPosition(0,0,500,500);
auxInitWindow("AUX_SAMPLE");
myinit();
auxReshapeFunc(myReshape);
auxMainLoop(display);
}
圖6-1 網狀球體 |
以上程序運行結果是在屏幕窗口內繪制一個黃色的網狀球體,這個程序充分體現了輔助庫的基本應用方法。
首先,在主函數中用輔助庫函數定義一個窗口auxInitWindow(),然后初始化顏色myinit(),這些在第一章中已說明。接下來是兩個十分重要的函數 auxReshapeFunc()和auxMainLoop(),參數都是一個函數指針,指向的都是回調函數(回調函數定義用CALLBACK說明)。
前者是窗口形狀重定函數,參數指針指向函數myReshape(),它的兩個參數就是窗口的新寬度和新高度。然后用glViewport(0, 0, w, h)重定視口,并且在新視口內重新定義投影矩陣,
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h)
glOrtho(-1.5,1.5,-1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w,-10.0,10.0);
else
glOrtho(-1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w,-1.5,1.5,-10.0,10.0);
即先用glMatrixMode()說明當前矩陣操作與投影有關GL_PROJECTION,再用glLoadIdentity()將矩陣清為單位矩陣,避免受其它矩陣操作的干擾;然后調用glOrtho()對物體進行正射投影,并且用判斷語句給出了兩種情況,使投影后圖像的比例與視點匹配,避免比例失調。
再下來調用glMatrixMode()將矩陣操作改為對觀察物體有關的方式GL_MODELVIEW,同樣用 glLoadIdentity()清矩陣。后者是主函數循環函數,參數指針指向函數display(),即繪制物體。當窗口需要更新或物體發生改變時,程序便調用它重新繪制。以上例子是輔助庫的最基本應用,復雜的應用將在后續的章節中詳細介紹。
七、OpenGL建模
OpenGL基本庫提供了大量繪制各種類型圖元的方法,輔助庫也提供了不少描述復雜三維圖形的函數。這一章主要介紹基本圖元,如點、線、多邊形,有了這些圖元,就可以建立比較復雜的模型了。
7.1、描述圖元
OpenGL是三維圖形的函數庫,它所定義的點、線、多邊形等圖元與一般的定義不太一樣,存在一定的差別。對編程者來說,能否理解二者之間的差別十分重要。一種差別源于基于計算機計算的限制。OpenGL中所有浮點計算精度有限,故點、線、多邊形的坐標值存在一定的誤差。另一種差別源于位圖顯示的限制。以這種方式顯示圖形,最小的顯示圖元是一個象素,盡管每個象素寬度很小,但它們仍然比數學上所定義的點或線寬要大得多。當用OpenGL 進行計算時,雖然是用一系列浮點值定義點串,但每個點仍然是用單個象素顯示,只是近似擬合。
OpenGL圖元是抽象的幾何概念,不是真實世界中的物體,因此須用相關的數學模型來描述。
7.1.1 齊次坐標(Homogeneous Coordinate)
在空間直角坐標系中,任意一點可用一個三維坐標矩陣[x y z]表示。如果將該點用一個四維坐標的矩陣[Hx Hy Hz H]表示時,則稱為齊次坐標表示方法。在齊次坐標中,最后一維坐標H稱為比例因子。
在OpenGL中,二維坐標點全看作三維坐標點,所有的點都用齊次坐標來描述,統一作為三維齊次點來處理。每個齊次點用一個向量(x, y, z, w)表示,其中四個元素全不為零。齊次點具有下列幾個性質:
1)如果實數a非零,則(x, y, x, w)和(ax, ay, az, aw)表示同一個點,類似于x/y = (ax)/( ay)。
2)三維空間點(x, y, z)的齊次點坐標為(x, y, z, 1.0),二維平面點(x,y)的齊次坐標為(x, y, 0.0, 1.0)。
3)當w不為零時,齊次點坐標(x, y, z, w)即三維空間點坐標(x/w, y/w, z/w);當w為零時,齊次點(x, y, z, 0.0)表示此點位于某方向的無窮遠處。
注意:OpenGL中指定w大于或等于0.0。
7.1.2 點(Point)
用浮點值表示的點稱為頂點(Vertex)。所有頂點在OpenGL內部計算時都作為三維點處理,用二維坐標(x, y)定義的點在OpenGL中默認z值為0。所有頂點坐標用齊次坐標(x, y, z, w) 表示,如果w不為0.0,這些齊次坐標表示的頂點即為三維空間點(x/w, y/w, z/w)。編程者可以自己指定w值,但很少這樣做。一般來說,w缺省為1.0。
7.1.3 線(Line)
在OpenGL中,線代表線段(Line Segment),不是數學意義上的那種沿軸兩個方向無限延伸的線。這里的線由一系列頂點順次連結而成,有閉合和不閉合兩種。見圖7-1所示。
圖7-1 線段的兩種連結方式 |
7.1.4 多邊形(Polygon)
OpenGL中定義的多邊形是由一系列線段依次連結而成的封閉區域。這些線段不能交叉,區域內不能有空洞,多邊形必須在凸多邊形,否則不能被OpenGL函數接受。合法和非法多邊形圖示見圖7-2。
圖7-2 合法和非法多邊形 |
OpenGL多邊形可以是平面多邊形,即所有頂點在一個平面上,也可以是空間多邊形。更復雜的多邊形將在提高篇中介紹。
7.2、繪制圖元
7.2.1 定義頂點
在OpenGL中,所有幾何物體最終都由有一定順序的頂點集來描述。
函數glVertex{234}{sifd}[v](TYPE coords)可以用二維、三維或齊次坐標定義頂點。舉例如下:
glVertex2s(2,3);
glVertex3d(0.0,1.0,3.1414926535);
glVertex4f(2.4,1.0,-2.2,2.0);
GLfloat pp[3]={5.0,2.0,10.2};
glVertex3fv(pp);
第一例子表示一個空間頂點(2, 3, 0),第二個例子表示用雙精度浮點數定義一個頂點,第三個例子表示用齊次坐標定義一個頂點,其真實坐標為(1.2, 0.5, -1.1),最后一個例子表示用一個指針(或數組)定義頂點。
7.2.2 構造幾何圖元
在實際應用中,通常用一組相關的頂點序列以一定的方式組織起來定義某個幾何圖元,而不采用單獨定義多個頂點來構造幾何圖元。在OpenGL中,所有被定義的頂點必須放在glBegain()和glEnd()兩個函數之間才能正確表達一個幾何圖元或物體,否則,glVertex*()不完成任何操作。如:
glBegin(GL_POLYGON);
glVertex2f(0.0,0.0);
glVertex2f(0.0,3.0);
glVertex2f(3.0,3.0);
glVertex2f(4.0,1.5);
glVertex2f(3.0,0.0);
glEnd();
以上這段程序定義了一個多邊形,如果將glBegin()中的參數GL_POLYGON改為GL_POINTS,則圖形變為一組頂點(5個),見圖7-3所示。
圖7-3 繪制多邊形或一組頂點 |
點函數glBegin(GLenum mode)標志描述一個幾何圖元的頂點列表的開始,其參數mode表示幾何圖元的描述類型。所有類型及說明見表7-1所示,相應的圖示見圖7-4。
類型 | 說明 |
GL_POINTS | 單個頂點集 |
GL_LINES | 多組雙頂點線段 |
GL_POLYGON | 單個簡單填充凸多邊形 |
GL_TRAINGLES | 多組獨立填充三角形 |
GL_QUADS | 多組獨立填充四邊形 |
GL_LINE_STRIP | 不閉合折線 |
GL_LINE_LOOP | 閉合折線 |
GL_TRAINGLE_STRIP | 線型連續填充三角形串 |
GL_TRAINGLE_FAN | 扇形連續填充三角形串 |
GL_QUAD_STRIP | 連續填充四邊形串 |
表7-1 幾何圖元類型和說明
|
圖7-4 幾何圖元類型 |
函數glEnd()標志頂點列表的結束。
從圖7-4中可看出,可以采用許多方法構造幾何圖元,這些方法僅僅依賴于所給的頂點數據。
在glBegin()和glEnd()之間最重要的信息就是由函數glVertex*()定義的頂點,必要時也可為每個頂點指定顏色、法向、紋理坐標或其他,即調用相關的函數,見表7-2所示,具體用法以后會逐步介紹。
函數 | 函數意義 |
glVertex*() | 設置頂點坐標 |
glColor*() | 設置當前顏色 |
glIndex*() | 設置當前顏色表 |
glNormal*() | 設置法向坐標 |
glEvalCoord*() | 產生坐標 |
glCallList(),glCallLists() | 執行顯示列表 |
glTexCoord*() | 設置紋理坐標 |
glEdgeFlag*() | 控制邊界繪制 |
glMaterial*() | 設置材質 |
表7-2 在glBegin()和glEnd()之間可調用的函數
|
看如下幾句:
glBegin(GL_POINTS);
glColor3f(1.0,0.0,0.0); /* red color */
glVertex(...);
glColor3f(0.0,1.0,0.0); /* green color */
glColor3f(0.0,0.0,1.0); /* blue color */
glVertex(...);
glVertex(...);
glEnd();
顏色等的設置只對當前點或后續點有效。上一例中第一個點是紅色,第二個點和第三個點都是藍色。其中設置綠色時,之后沒有頂點操作,而是設置藍色,故只有當前藍色對緊跟其后的兩個頂點有效。
為了更好地理解構造幾何圖元函數的用法,下面舉一個簡單的例子:
例7-3 幾何圖元構造例程(drawgeom.c)
#include "glos.h"
#include<GL/gl.h>
#include<GL/glaux.h>
void myinit(void);
void DrawMyObjects(void);
void CALLBACK myReshape(GLsizei w,GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
glClearColor(0.0,0.0,0.0,0.0);
glClear(GL_COLOR_BUFFER_BIT);
glShadeModel(GL_FLAT);
}
void CALLBACK myReshape(GLsizei w,GLsizei h)
{
glViewport(0,0,w,h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h)
glOrtho(-20.0,20.0,-20.0*(GLfloat)h/(GLfloat)w, 20.0*(GLfloat)h/(GLfloat)w,-50.0,50.0);
else
glOrtho(-20.0*(GLfloat)h/(GLfloat)w, 20.0*(GLfloat)h/(GLfloat)w,-20.0,20.0,-50.0,50.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void CALLBACK display(void)
{
glColor3f(1.0,1.0,0.0);
DrawMyObjects();
glFlush();
}
void DrawMyObjects(void)
{
/* draw some points */
glBegin(GL_POINTS);
glColor3f(1.0,0.0,0.0);
glVertex2f(-10.0,11.0);
glColor3f(1.0,1.0,0.0);
glVertex2f(-9.0,10.0);
glColor3f(0.0,1.0,1.0);
glVertex2f(-8.0,12.0);
glEnd();
/* draw some line_segments */
glBegin(GL_LINES);
glColor3f(1.0,1.0,0.0);
glVertex2f(-11.0,8.0);
glVertex2f(-7.0,7.0);
glColor3f(1.0,0.0,1.0);
glVertex2f(-11.0,9.0);
glVertex2f(-8.0,6.0);
glEnd();
/* draw one opened_line */
glBegin(GL_LINE_STRIP);
glColor3f(0.0,1.0,0.0);
glVertex2f(-3.0,9.0);
glVertex2f(2.0,6.0);
glVertex2f(3.0,8.0);
glVertex2f(-2.5,6.5);
glEnd();
/* draw one closed_line */
glBegin(GL_LINE_LOOP);
glColor3f(0.0,1.0,1.0);
glVertex2f(7.0,7.0);
glVertex2f(8.0,8.0);
glVertex2f(9.0,6.5);
glVertex2f(10.3,7.5);
glVertex2f(11.5,6.0);
glVertex2f(7.5,6.0);
glEnd();
/* draw one filled_polygon */
glBegin(GL_POLYGON);
glColor3f(0.5,0.3,0.7);
glVertex2f(-7.0,2.0);
glVertex2f(-8.0,3.0);
glVertex2f(-10.3,0.5);
glVertex2f(-7.5,-2.0);
glVertex2f(-6.0,-1.0);
glEnd();
/* draw some filled_quandrangles */
glBegin(GL_QUADS);
glColor3f(0.7,0.5,0.2);
glVertex2f(0.0,2.0);
glVertex2f(-1.0,3.0);
glVertex2f(-3.3,0.5);
glVertex2f(-0.5,-1.0);
glColor3f(0.5,0.7,0.2);
glVertex2f(3.0,2.0);
glVertex2f(2.0,3.0);
glVertex2f(0.0,0.5);
glVertex2f(2.5,-1.0);
glEnd();
/* draw some filled_strip_quandrangles */
glBegin(GL_QUAD_STRIP);
glVertex2f(6.0,-2.0);
glVertex2f(5.5,1.0);
glVertex2f(8.0,-1.0);
glColor3f(0.8,0.0,0.0);
glVertex2f(9.0,2.0);
glVertex2f(11.0,-2.0);
glColor3f(0.0,0.0,0.8);
glVertex2f(11.0,2.0);
glVertex2f(13.0,-1.0);
glColor3f(0.0,0.8,0.0);
glVertex2f(14.0,1.0);
glEnd();
/* draw some filled_triangles */
glBegin(GL_TRIANGLES);
glColor3f(0.2,0.5,0.7);
glVertex2f(-10.0,-5.0);
glVertex2f(-12.3,-7.5);
glVertex2f(-8.5,-6.0);
glColor3f(0.2,0.7,0.5);
glVertex2f(-8.0,-7.0);
glVertex2f(-7.0,-4.5);
glVertex2f(-5.5,-9.0);
glEnd();
/* draw some filled_strip_triangles */
glBegin(GL_TRIANGLE_STRIP);
glVertex2f(-1.0,-8.0);
glVertex2f(-2.5,-5.0);
glColor3f(0.8,0.8,0.0);
glVertex2f(1.0,-7.0);
glColor3f(0.0,0.8,0.8);
glVertex2f(2.0,-4.0);
glColor3f(0.8,0.0,0.8);
glVertex2f(4.0,-6.0);
glEnd();
/* draw some filled_fan_triangles */
glBegin(GL_TRIANGLE_FAN);
glVertex2f(8.0,-6.0);
glVertex2f(10.0,-3.0);
glColor3f(0.8,0.2,0.5);
glVertex2f(12.5,-4.5);
glColor3f(0.2,0.5,0.8);
glVertex2f(13.0,-7.5);
glColor3f(0.8,0.5,0.2);
glVertex2f(10.5,-9.0);
glEnd();
}
void main(void)
{
auxInitDisplayMode(AUX_SINGLE|AUX_RGBA);
auxInitPosition(0,0,500,500);
auxInitWindow("Geometric Primitive Types");
myinit();
auxReshapeFunc(myReshape);
auxMainLoop(display);
}
以上程序運行結果就是圖7-4所示的內容,這個例子很好地說明了幾何圖元的類型及顏色等函數的用法。希望讀者自己仔細分析每個物體的繪制方法,體會其中的關鍵之處,達到舉一反三的效果。當然,還可利用上一章輔助庫中提供的基本三維圖元構造比較復雜的物體,你不妨也試一試。
八、OpenGL變換
OpenGL變換是本篇的重點內容,它包括計算機圖形學中最基本的三維變換,即幾何變換、投影變換、裁剪變換、視口變換,以及針對OpenGL的特殊變換概念理解和用法,如相機模擬、矩陣堆棧等。學好了這章,才開始真正走進三維世界。
8.1、從三維空間到二維平面
8.1.1 相機模擬
在真實世界里,所有的物體都是三維的。但是,這些三維物體在計算機世界中卻必須以二維平面物體的形式表現出來。那么,這些物體是怎樣從三維變換到二維的呢?下面我們采用相機(Camera)模擬的方式來講述這個概念,如圖8-1所示。
圖8-1 相機模擬 |
實際上,從三維空間到二維平面,就如同用相機拍照一樣,通常都要經歷以下幾個步驟 (括號內表示的是相應的圖形學概念):
第一步,將相機置于三角架上,讓它對準三維景物(視點變換,Viewing Transformation)。
第二步,將三維物體放在適當的位置(模型變換,Modeling Transformation)。
第三步,選擇相機鏡頭并調焦,使三維物體投影在二維膠片上(投影變換,Projection Transformation)。
第四步,決定二維像片的大小(視口變換,Viewport Transformation)。
這樣,一個三維空間里的物體就可以用相應的二維平面物體表示了,也就能在二維的電腦屏幕上正確顯示了。
8.1.2 三維圖形顯示流程
運用相機模擬的方式比較通俗地講解了三維圖形顯示的基本過程,但在具體應用OpenGL函數庫編程時,還必須了解三維圖形世界中的幾個特殊坐標系的概念,以及用這些概念表達的三維圖形顯示流程。
計算機本身只能處理數字,圖形在計算機內也是以數字的形式進行加工和處理的。大家都知道,坐標建立了圖形和數字之間的聯系。為了使被顯示的物體數字化,要在被顯示的物體所在的空間中定義一個坐標系。這個坐標系的長度單位和坐標軸的方向要適合對被顯示物體的描述,這個坐標系稱為世界坐標系。
計算機對數字化的顯示物體作了加工處理后,要在圖形顯示器上顯示,這就要在圖形顯示器屏幕上定義一個二維直角坐標系,這個坐標系稱為屏幕坐標系。這個坐標系坐標軸的方向通常取成平行于屏幕的邊緣,坐標原點取在左下角,長度單位常取成一個象素的長度,大小可以是整型數。
為了使顯示的物體能以合適的位置、大小和方向顯示出來,必須要通過投影。投影的方法有兩種,即正射投影和透視投影。
有時為了突出圖形的一部分,只把圖形的某一部分顯示出來,這時可以定義一個三維視景體(Viewing Volume)。正射投影時一般是一個長方體的視景體,透視投影時一般是一個棱臺似的視景體。只有視景體內的物體能被投影在顯示平面上,其他部分則不能。在屏幕窗口內可以定義一個矩形,稱為視口(Viewport),視景體投影后的圖形就在視口內顯示。
為了適應物理設備坐標和視口所在坐標的差別,還要作一適應物理坐標的變換。這個坐標系稱為物理設備坐標系。根據上面所述,三維圖形的顯示流程應如圖8-2所示。
圖8-2 三維圖形的顯示流程 |
8.1.3 基本變換簡單分析
下面舉一個簡單的變換例子,cube.c:
例8-4 簡單變換例程(cube.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
void CALLBACK display (void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f (1.0, 1.0, 1.0);
glLoadIdentity (); /* clear the matrix */
glTranslatef (0.0, 0.0, -5.0); /* viewing transformation */
glScalef (1.0, 2.0, 1.0); /* modeling transformation */
auxWireCube(1.0); /* draw the cube */
glFlush();
}
void myinit (void)
{
glShadeModel (GL_FLAT);
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glMatrixMode (GL_PROJECTION); /* prepare for and then */
glLoadIdentity (); /* define the projection */
glFrustum (-1.0, 1.0, -1.0, 1.0, 1.5, 20.0); /* transformation */
glMatrixMode (GL_MODELVIEW); /* back to modelview matrix */
glViewport (0, 0, w, h); /* define the viewport */
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Perspective 3-D Cube");
myinit ();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果就是繪制一個三維的正面透視立方體。其中已經用到了相機模擬中提到的四種基本變換,即視點變換、模型變換、投影變換和視口變換。
|
||
圖8-3 三維的正面透視立方體 |
下面簡單分析一下整個程序過程:
1)視點變換。視點變換是在視點坐標系中進行的。視點坐標系于一般的物體所在的世界坐標系不同,它遵循左手法則,即左手大拇指指向Z正軸,與之垂直的四個手指指向X正軸,四指彎曲90度的方向是Y正軸。而世界坐標系遵循右手法則的。如圖8-4所示。當矩陣初始化glLoadIdentity()后,調用glTranslatef()作視點變換。函數參數(x, y, z)表示視點或相機在視點坐標系中移動的位置,這里z=-5.0,意思是將相機沿Z負軸移動5個單位。
通常相機位置缺省值同場景中的物體一樣,都在原點處,而且相機初始方向都指向Z負軸。
這里相機移走后,仍然對準立方體。如果相機需要指向另一方向,則調用glRotatef()可以改變。
圖8-4 視點坐標系與世界坐標系 |
2)模型變換。模型變換是在世界坐標系中進行的。在這個坐標系中,可以對物體實施平移 glTranslatef()、旋轉glRotatef()和放大縮小glScalef()。例子里只對物體進行比例變換,glScalef(sx, sy, sz)的三個參數分別是X、Y、Z軸向的比例變換因子。缺省時都為1.0,即物體沒變化。程序中物體Y軸比例為2.0,其余都為1.0,就是說將立方體變成長方體。
3)投影變換。投影變換類似于選擇相機的鏡頭。本例中調用了一個透視投影函數 glFrustum(),在調用它之前先要用glMatrixMode()說明當前矩陣方式是投影GL_PROJECTION。這個投影函數一共有六個參數,由它們可以定義一個棱臺似的視景體。即視景體內的部分可見,視景體外的部分不可見,這也就包含了三維裁剪變換。
4)視口變換。視口變換就是將視景體內投影的物體顯示在二維的視口平面上。通常,都調用函數glViewport()來定義一個視口,這個過程類似于將照片放大或縮小。
總而言之,一旦所有必要的變換矩陣被指定后,場景中物體的每一個頂點都要按照被指定的變換矩陣序列逐一進行變換。注意:OpenGL 中的物體坐標一律采用齊次坐標,即(x, y, z, w),故所有變換矩陣都采用4X4矩陣。一般說來,每個頂點先要經過視點變換和模型變換,然后進行指定的投影,如果它位于視景體外,則被裁剪掉。最后,余下的已經變換過的頂點x、y、z坐標值都用比例因子w除,即x/w、y/w、z/w,再映射到視口區域內,這樣才能顯示在屏幕上。
8.2、幾何變換
實際上,上述所說的視點變換和模型變換本質上都是一回事,即圖形學中的幾何變換。
只是視點變換一般只有平移和旋轉,沒有比例變換。當視點進行平移或旋轉時,視點坐標系中的物體就相當于在世界坐標系中作反方向的平移或旋轉。因此,從某種意義上講,二者可以統一,只是各自出發點不一樣而已。讀者可以根據具體情況,選擇其中一個角度去考慮,這樣便于理解。
8.2.1 兩個矩陣函數解釋
這里先解釋兩個基本OpenGL矩陣操作函數,便于以后章節的講述。函數解釋如下:
void glLoadMatrix{fd}(const TYPE *m)
設置當前矩陣中的元素值。函數參數*m是一個指向16個元素(m0, m1, ..., m15)的指針,這16個元素就是當前矩陣M中的元素,其排列方式如下:
M = | | m0 m4 m8 m12 | | m1 m5 m9 m13 | | m2 m6 m10 m14 | | m3 m7 m11 M15 | |
void glMultMatrix{fd}(const TYPE *m)
用當前矩陣去乘*m所指定的矩陣,并將結果存放于*m中。當前矩陣可以是用glLoadMatrix() 指定的矩陣,也可以是其它矩陣變換函數的綜合結果。
當幾何變換時,調用OpenGL的三個變換函數glTranslate*()、glRotate*()和glScale*(),實質上相當于產生了一個近似的平移、旋轉和比例矩陣,然后調用glMultMatrix()與當前矩陣相乘。但是直接調用這三個函數程序運行得快一些,因OpenGL自動能計算矩陣。
8.2.2 平移
平移變換函數如下:
void glTranslate{fd}(TYPE x,TYPE y,TYPE z)
三個函數參數就是目標分別沿三個軸向平移的偏移量。這個函數表示用這三個偏移量生成的矩陣乘以當前矩陣。當參數是(0.0,0.0,0.0)時,表示對函數glTranslate*()的操作是單位矩陣,也就是對物體沒有影響。平移示意如圖8-5所示。
圖8-5 平移示意圖 |
8.2.3 旋轉
旋轉變換函數如下:
void glRotate{fd}(TYPE angle,TYPE x,TYPE y,TYPE z)
函數中第一個參數是表示目標沿從點(x, y, z)到原點的方向逆時針旋轉的角度,后三個參數是旋轉的方向點坐標。這個函數表示用這四個參數生成的矩陣乘以當前矩陣。當角度參數是0.0時,表示對物體沒有影響。旋轉示意如圖8-6所示。
圖8-6 旋轉示意圖 |
8.2.3 縮放和反射
縮放和反射變換函數如下:
void glScale{fd}(TYPE x,TYPE y,TYPE z)
三個函數參數值就是目標分別沿三個軸向縮放的比例因子。這個函數表示用這三個比例因子生成的矩陣乘以當前矩陣。這個函數能完成沿相應的軸對目標進行拉伸、壓縮和反射三項功能。當參數是(1.0, 1.0, 1.0)時,表示對函數glScale*()操作是單位矩陣,也就是對物體沒有影響。當其中某個參數為負值時,表示將對目標進行相應軸的反射變換,且這個參數不為1.0,則還要進行相應軸的縮放變換。最好不要令三個參數值都為零,這將導致目標沿三軸都縮為零。縮放和反射示意如圖8-7所示。
圖8-7 縮放和反射示意圖 |
8.2.5 幾何變換舉例
以上介紹了三個基本幾何變換函數,下面舉一個簡單的例子進一步說明它們的用法。程序如下:
例 8-5 幾何變換例程(geomtrsf.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void draw_triangle(void);
void CALLBACK display(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void draw_triangle(void)
{
glBegin(GL_LINE_LOOP);
glVertex2f(0.0, 25.0);
glVertex2f(25.0, -25.0);
glVertex2f(-25.0, -25.0);
glEnd();
}
void CALLBACK display(void)
{
glClearColor (0.0, 0.0, 0.0, 1.0);
glClear (GL_COLOR_BUFFER_BIT);
/* draw an original triangle */
glLoadIdentity ();
glColor3f (1.0, 1.0, 1.0); /* white */
draw_triangle ();
/* translating a triangle along X_axis */
glLoadIdentity ();
glTranslatef (-20.0, 0.0, 0.0);
glColor3f(1.0,0.0,0.0); /* red */
draw_triangle ();
/* scaling a triangle along X_axis by 1.5 and along Y_axis by 0.5 */
glLoadIdentity();
glScalef (1.5, 0.5, 1.0);
glColor3f(0.0,1.0,0.0); /* green */
draw_triangle ();
/* rotating a triangle in a counterclockwise direction about Z_axis */
glLoadIdentity ();
glRotatef (90.0, 0.0, 0.0, 1.0);
glColor3f(0.0,0.0,1.0); /* blue */
draw_triangle ();
/* scaling a triangle along Y_axis and reflecting it about Y_axis */
glLoadIdentity();
glScalef (1.0, -0.5, 1.0);
glColor3f(1.0,1.0,0.0); /* yellow */
draw_triangle ();
glFlush();
}
void myinit (void)
{
glShadeModel (GL_FLAT);
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho(-50.0, 50.0, -50.0*(GLfloat)h/(GLfloat)w, 50.0*(GLfloat)h/(GLfloat)w,-1.0,1.0);
else
glOrtho(-50.0*(GLfloat)w/(GLfloat)h, 50.0*(GLfloat)w/(GLfloat)h, -50.0, 50.0,-1.0,1.0);
glMatrixMode(GL_MODELVIEW);
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Geometric Transformations");
myinit ();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果:第一個白色三角形是原始三角形,第二個紅色三角形是白三角沿X 負軸平移后的三角形,第三個綠色三角形是白三角分別沿X軸和Y軸比例變換后的三角形,第四個藍色三角形是白三角繞Z正軸逆時針轉90度后的三角形,第五個黃色三角形是白三角沿Y軸方向縮小一倍且相對于X軸作反射后形成的三角形。
圖8-8 三角形的幾何變換 |
8.3、投影變換
投影變換是一種很關鍵的圖形變換,OpenGL中只提供了兩種投影方式,一種是正射投影,另一種是透視投影。不管是調用哪種投影函數,為了避免不必要的變換,其前面必須加上以下兩句:
glMAtrixMode(GL_PROJECTION);
glLoadIdentity();
事實上,投影變換的目的就是定義一個視景體,使得視景體外多余的部分裁剪掉,最終圖像只是視景體內的有關部分。本指南將詳細講述投影變換的概念以及用法。
8.3.1 正射投影(Orthographic Projection)
正射投影,又叫平行投影。這種投影的視景體是一個矩形的平行管道,也就是一個長方體,如圖8-9所示。正射投影的最大一個特點是無論物體距離相機多遠,投影后的物體大小尺寸不變。這種投影通常用在建筑藍圖繪制和計算機輔助設計等方面,這些行業要求投影后的物體尺寸及相互間的角度不變,以便施工或制造時物體比例大小正確。
圖8-9 正射投影視景體 |
OpenGL正射投影函數共有兩個,這在前面幾個例子中已用過。一個函數是:
void glOrtho(GLdouble left,GLdouble right,GLdouble bottom,GLdouble top,
GLdouble near,GLdouble far)
它創建一個平行視景體。實際上這個函數的操作是創建一個正射投影矩陣,并且用這個矩陣乘以當前矩陣。其中近裁剪平面是一個矩形,矩形左下角點三維空間坐標是(left,bottom,-near),右上角點是(right,top,-near);遠裁剪平面也是一個矩形,左下角點空間坐標是(left,bottom,-far),右上角點是(right,top,-far)。所有的near和far值同時為正或同時為負。如果沒有其他變換,正射投影的方向平行于Z軸,且視點朝向Z負軸。
這意味著物體在視點前面時far和near都為負值,物體在視點后面時far和near都為正值。另一個函數是:
void gluOrtho2D(GLdouble left,GLdouble right,GLdouble bottom,GLdouble top)
它是一個特殊的正射投影函數,主要用于二維圖像到二維屏幕上的投影。它的near和far缺省值分別為-1.0和1.0,所有二維物體的Z坐標都為0.0。因此它的裁剪面是一個左下角點為(left,bottom)、右上角點為(right,top)的矩形。
8.3.2 透視投影(Perspective Projection)
透視投影符合人們心理習慣,即離視點近的物體大,離視點遠的物體小,遠到極點即為消失,成為滅點。它的視景體類似于一個頂部和底部都被切除掉的棱椎,也就是棱臺。這個投影通常用于動畫、視覺仿真以及其它許多具有真實性反映的方面。
OpenGL透視投影函數也有兩個,其中函數glFrustum()在8.1.3節中提到過,它所形成的視景體如圖8-10所示。
圖8-10 函數glFrustum()透視投影視景體 |
這個函數原型為:
void glFrustum(GLdouble left,GLdouble Right,GLdouble bottom,GLdouble top,
GLdouble near,GLdouble far);
它創建一個透視視景體。其操作是創建一個透視投影矩陣,并且用這個矩陣乘以當前矩陣。這個函數的參數只定義近裁剪平面的左下角點和右上角點的三維空間坐標,即(left,bottom,-near)和(right,top,-near);最后一個參數far是遠裁剪平面的Z負值,其左下角點和右上角點空間坐標由函數根據透視投影原理自動生成。near和far表示離視點的遠近,它們總為正值。
另一個函數是:
void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);
它也創建一個對稱透視視景體,但它的參數定義于前面的不同,如圖8-11所示。其操作是創建一個對稱的透視投影矩陣,并且用這個矩陣乘以當前矩陣。參數 fovy定義視野在X-Z平面的角度,范圍是[0.0, 180.0];參數aspect是投影平面寬度與高度的比率;參數zNear和Far分別是遠近裁剪面沿Z負軸到視點的距離,它們總為正值。
圖8-11 函數gluPerspective()透視投影視景體 |
以上兩個函數缺省時,視點都在原點,視線沿Z軸指向負方向。二者的應用實例將在后續章節中介紹。
8.4、裁剪變換
在OpenGL中,空間物體的三維裁剪變換包括兩個部分:視景體裁剪和附加平面裁剪。視景體裁剪已經包含在投影變換里,前面已述,這里不再重復。下面簡單講一下平面裁剪函數的用法。
除了視景體定義的六個裁剪平面(上、下、左、右、前、后)外,用戶還可自己再定義一個或多個附加裁剪平面,以去掉場景中無關的目標,如圖8-12所示。
圖8-12 附加裁剪平面和視景體 |
附加平面裁剪函數為:
void glClipPlane(GLenum plane,Const GLdouble *equation);
函數定義一個附加的裁剪平面。其中參數equation指向一個擁有四個系數值的數組,這四個系數分別是裁剪平面Ax+By+Cz+D=0的A、B、 C、D值。因此,由這四個系數就能確定一個裁剪平面。參數plane是GL_CLIP_PLANEi(i=0,1,...),指定裁剪面號。
在調用附加裁剪函數之前,必須先啟動glEnable(GL_CLIP_PLANEi),使得當前所定義的裁剪平面有效;當不再調用某個附加裁剪平面時,可用glDisable(GL_CLIP_PLANEi)關閉相應的附加裁剪功能。
下面這個例子不僅說明了附加裁剪函數的用法,而且調用了gluPerspective()透視投影函數,讀者可以細細體會其中的用法。例程如下:
例8-6 裁剪變換例程(clipball.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
void CALLBACK display(void)
{
GLdouble eqn[4] = {1.0, 0.0, 0.0, 0.0};
glClear(GL_COLOR_BUFFER_BIT);
glColor3f (1.0, 0.0, 1.0);
glPushMatrix();
glTranslatef (0.0, 0.0, -5.0);
/* clip the left part of wire_sphere : x<0 */
glClipPlane (GL_CLIP_PLANE0, eqn);
glEnable (GL_CLIP_PLANE0);
glRotatef (-90.0, 1.0, 0.0, 0.0);
auxWireSphere(1.0);
glPopMatrix();
glFlush();
}
void myinit (void)
{
glShadeModel (GL_FLAT);
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGB);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Arbitrary Clipping Planes");
myinit ();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
圖8-13 剪取后的網狀半球體 |
8.5、視口變換
在前面幾節內容中已相繼提到過視口變換,這一節將針對OpenGL來講述視口變換的原理及其相關函數的用法。運用相機模擬方式,我們很容易理解視口變換就是類似于照片的放大與縮小。在計算機圖形學中,它的定義是將經過幾何變換、投影變換和裁剪變換后的物體顯示于屏幕窗口內指定的區域內,這個區域通常為矩形,稱為視口。OpenGL中相關函數是:
glViewport(GLint x,GLint y,GLsizei width, GLsizei height);
這個函數定義一個視口。函數參數(x, y)是視口在屏幕窗口坐標系中的左下角點坐標,參數width和height分別是視口的寬度和高度。缺省時,參數值即(0, 0, winWidth, winHeight) 指的是屏幕窗口的實際尺寸大小。所有這些值都是以象素為單位,全為整型數。
注意:在實際應用中,視口的長寬比率總是等于視景體裁剪面的長寬比率。如果兩個比率不相等,那么投影后的圖像顯示于視口內時會發生變形,如圖8-14所示。另外,屏幕窗口的改變一般不明顯影響視口的大小。因此,在調用這個函數時,最好實時檢測窗口尺寸,及時修正視口的大小,保證視口內的圖像能隨窗口的變化而變化,且不變形。
圖8-14 視景體到視口的映射 |
8.6 矩陣堆棧
學過計算機的人也許都知道這個使用頻率極高的名詞 — “堆棧”。顧名思義,堆棧指的是一個頂部打開底部封閉的柱狀物體,通常用來存放常用的東西。這些東西從頂部依次放入,但取出時也只能從頂部取出,即“先進后出,后進先出”。在計算機中,它常指在內存中開辟的一塊存放某些變量的連續區域。因此,OpenGL的矩陣堆棧指的就是內存中專門用來存放矩陣數據的某塊特殊區域。
實際上,在創建、裝入、相乘模型變換和投影變換矩陣時,都已用到堆棧操作。一般說來,矩陣堆棧常用于構造具有繼承性的模型,即由一些簡單目標構成的復雜模型。例如,一輛自行車就是由兩個輪子、一個三角架及其它一些零部件構成的。它的繼承性表現在當自行車往前走時,首先是前輪旋轉,然后整個車身向前平移,接著是后輪旋轉,然后整個車身向前平移,如此進行下去,這樣自行車就往前走了。
矩陣堆棧對復雜模型運動過程中的多個變換操作之間的聯系與獨立十分有利。因為所有矩陣操作函數如glLoadMatrix()、glMultMatrix()、 glLoadIdentity()等只處理當前矩陣或堆棧頂部矩陣,這樣堆棧中下面的其它矩陣就不受影響。堆棧操作函數有以下兩個:
void glPushMatrix(void);
void glPopMatrix(void);
第一個函數表示將所有矩陣依次壓入堆棧中,頂部矩陣是第二個矩陣的備份;壓入的矩陣數不能太多,否則出錯。第二個函數表示彈出堆棧頂部的矩陣,令原第二個矩陣成為頂部矩陣,接受當前操作,故原頂部矩陣被破壞;當堆棧中僅存一個矩陣時,不能進行彈出操作,否則出錯。由此看出,矩陣堆棧操作與壓入矩陣的順序剛好相反,編程時要特別注意矩陣操作的順序。
為了更好地理解這兩個函數,我們可以形象地認為glPushMatrix()就是“記住自己在哪”,glPopMatrix()就是“返回自己原來所在地”。請看下面一例:
例8-7 堆棧操作例程(arm.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void drawPlane(void);
void CALLBACK elbowAdd (void);
void CALLBACK elbowSubtract (void);
void CALLBACK shoulderAdd (void);
void CALLBACK shoulderSubtract (void);
void CALLBACK display(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
static int shoulder = 0, elbow = 0;
void CALLBACK elbowAdd (void)
{
elbow = (elbow + 5) % 360;
}
void CALLBACK elbowSubtract (void)
{
elbow = (elbow - 5) % 360;
}
void CALLBACK shoulderAdd (void)
{
shoulder = (shoulder + 5) % 360;
}
void CALLBACK shoulderSubtract (void)
{
shoulder = (shoulder - 5) % 360;
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0.0, 1.0, 1.0);
glPushMatrix();
glTranslatef (-0.5, 0.0, 0.0);
glRotatef ((GLfloat)
shoulder, 0.0, 0.0, 1.0);
glTranslatef (1.0, 0.0, 0.0);
auxWireBox(2.0, 0.2, 0.5);
glTranslatef (1.0, 0.0, 0.0);
glRotatef ((GLfloat) elbow, 0.0, 0.0, 1.0);
glTranslatef (0.8, 0.0, 0.0);
auxWireBox(1.6, 0.2, 0.5);
glPopMatrix();
glFlush();
}
void myinit (void)
{
glShadeModel (GL_FLAT);
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(65.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity(); glTranslatef (0.0, 0.0, -5.0); /* viewing transform */
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 400, 400);
auxInitWindow ("Composite Modeling Transformations");
myinit ();
auxKeyFunc (AUX_LEFT, shoulderSubtract);
auxKeyFunc (AUX_RIGHT, shoulderAdd);
auxKeyFunc (AUX_UP, elbowAdd);
auxKeyFunc (AUX_DOWN, elbowSubtract);
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
從以上例程可以看出,復雜的機械手臂是由兩個簡單的長方體依據一定的繼承關系構成的,而這個繼承關系是由矩陣堆棧的順序決定的。
圖8-15 簡單機械手臂的符合運動 |
九、OpenGL顏色
幾乎所有OpenGL應用目的都是在屏幕窗口內繪制彩色圖形,所以顏色在OpenGL編程中占有很重要的地位。這里的顏色與繪畫中的顏色概念不一樣,它屬于RGB顏色空間,只在監視器屏幕上顯示。另外,屏幕窗口坐標是以象素為單位,因此組成圖形的每個象素都有自己的顏色,而這種顏色值是通過對一系列OpenGL函數命令的處理最終計算出來的。本章將講述計算機顏色的概念以及OpenGL的顏色模式、顏色定義和兩種模式應用場合等內容,若掌握好顏色的應用,你就能走進繽紛絢麗的色彩世界,從中享受無窮的樂趣。
9.1、計算機顏色
9.1.1 顏色生成原理
計算機顏色不同于繪畫或印刷中的顏色,顯示于計算機屏幕上每一個點的顏色都是由監視器內部的電子槍激發的三束不同顏色的光(紅、綠、藍)混合而成,因此,計算機顏色通 常用R(Red)、G(Green)、B(Blue)三個值來表示,這三個值又稱為顏色分量。顏色生成原理 示意圖見圖9-1所示。
圖9-1 計算機顏色生成原理 |
9.1.2 RGB色立體(RGB Color Cube)
所有監視器屏幕的顏色都屬于RGB顏色空間,如果用一個立方體形象地表示RGB顏色組成關系,那么就稱這個立方體為RGB色立體,如圖9-2所示。
圖9-2 RGB色立體 |
在圖中,R、G、B三值的范圍都是從0.0到1.0。如果某顏色分量越大,則表示對應的顏色分量越亮,也就是它在此點所貢獻的顏色成分越多;反之,則越暗或越少。當R、G、B三個值都為0.0時,此點顏色為黑色(Black);當三者都為1.0時,此點顏色為白色(White);當三個顏色分量值相等時,表示三者貢獻一樣,因此呈現灰色(Grey),在圖中表現為從黑色頂點到白色頂點的那條對角線;當R=1.0、G=1.0、B=0.0時,此點顏色為黃色(Yellow);同理,R=1.0、G=0.0、B=1.0時為洋紅色,也叫品色(Magenta);R=0.0、G=1.0、B=1.0時為青色(Cyan)。
9.2、顏色模式
OpenGL顏色模式一共有兩個:RGB(RGBA)模式和顏色表模式。在RGB模式下,所有的顏色定義全用R、G、B三個值來表示,有時也加上 Alpha值(與透明度有關),即RGBA模式。在顏色表模式下,每一個象素的顏色是用顏色表中的某個顏色索引值表示,而這個索引值指向了相應的R、G、 B值。這樣的一個表成為顏色映射(Color Map)。
9.2.1 RGBA模式(RGBA Mode)
在RGBA模式下,可以用glColor*()來定義當前顏色。其函數形式為:
void glColor3{b s i f d ub us ui}(TYPE r,TYPE g,TYPE b);
void glColor4{b s i f d ub us ui}(TYPE r,TYPE g,TYPE b,TYPE a);
void glColor3{b s i f d ub us ui}v(TYPE *v);
void glColor4{b s i f d ub us ui}v(TYPE *v);
設置當前R、G、B和A值。這個函數有3和4兩種方式,在前一種方式下,a值缺省為1.0,后一種Alpha值由用戶自己設定,范圍從0.0到1.0。同樣,它也可用指針傳遞參數。另外,函數的第二個后綴的不同使用,其相應的參數值及范圍不同,見下表9-1所示。雖然這些參數值不同,但實際上 OpenGL已自動將它們映射在0.0到1.0或-1.0或范圍之內。因此,靈活使用這些后綴,會給你編程帶來很大的方便。
后綴 | 數據類型 | 最小值 | 最小值映射 | 最大值 | 最大值映射 |
b | 1字節整型數 | -128 | -1.0 | 127 | 1.0 |
s | 2字節整型數 | -32,768 | -1.0 | 32,767 | 1.0 |
i | 4字節整型數 | -2,147,483,648 | -1.0 | 2,147,483,647 | 1.0 |
ub | 1字節無符號整型數 | 0 | 0.0 | 255 | 1.0 |
us | 2字節無符號整型數 | 0 | 0.0 | 65,535 | 1.0 |
ui | 4字節無符號整型數 | 0 | 0.0 | 4,294,967,295 | 1.0 |
表9-1 整型顏色值到浮點數的轉換
|
9.2.2 顏色表模式(Color_Index Mode)
在顏色表方式下,可以調用glIndex*()函數從顏色表中選取當前顏色。其函數形式為:
void glIndex{sifd}(TYPE c);
void glIndex{sifd}v(TYPE *c);
設置當前顏色索引值,即調色板號。若值大于顏色位面數時則取模。
9.2.3 兩種模式應用場合
在大多情況下,采用RGBA模式比顏色表模式的要多,尤其許多效果處理,如陰影、光照、霧、反走樣、混合等,采用RGBA模式效果會更好些;另外,紋理映射只能在RGBA模式下進行。下面提供幾種運用顏色表模式的情況(僅供參考):
1)若原來應用程序采用的是顏色表模式則轉到OpenGL上來時最好仍保持這種模式,便于移植。
2)若所用顏色不在缺省提供的顏色許可范圍之內,則采用顏色表模式。
3)在其它許多特殊處理,如顏色動畫,采用這種模式會出現奇異的效果。
9.3、顏色應用舉例
顏色是一個極具吸引力的應用,在前面幾章中已經逐步介紹了RGBA模式的應用方式,這里就不再多述。下面著重說一下顏色表模式的應用方法,請看例程:
例9-1 顏色表應用例程(cindex.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glaux.h>
void myinit(void);
void InitPalette(void);
void DrawColorFans(void);
void CALLBACK myReshape(GLsizei w,GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
glClearColor(0.0,0.0,0.0,0.0);
glClear(GL_COLOR_BUFFER_BIT);
glShadeModel(GL_FLAT);
}
void InitPalette(void)
{
GLint j;
static GLfloat rgb[][3]={
{1.0,0.0,0.0},{1.0,0.0,0.5},{1.0,0.0,1.0},{0.0,0.0,1.0},
{0.0,1.0,1.0},{0.0,1.0,0.0},{1.0,1.0,0.0},{1.0,0.5,0.0}};
for(j=0;j<8;j++)
auxSetOneColor(j+1,rgb[j][0],rgb[j][1],rgb[j][2]);
}
void CALLBACK myReshape(GLsizei w,GLsizei h)
{
glViewport(0,0,w,h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h)
glOrtho(-12.0,12.0,-12.0*(GLfloat)h/(GLfloat)w, 12.0*(GLfloat)h/(GLfloat)w,-30.0,30.0);
else
glOrtho(-12.0*(GLfloat)h/(GLfloat)w, 12.0*(GLfloat)h/(GLfloat)w,-12.0,12.0,-30.0,30.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void CALLBACK display(void)
{
InitPalette();
DrawColorFans();
glFlush();
}
void DrawColorFans(void)
{
GLint n;
GLfloat pp[8][2]={
{7.0,-7.0},{0.0,-10.0},{-7.0,-7.0},{-10.0,0.0},
{-7.0,7.0}, {0.0,10.0},{7.0,7.0},{10.0,0.0}};
/* draw some filled_fan_triangles */
glBegin(GL_TRIANGLE_FAN);
glVertex2f(0.0,0.0);
glVertex2f(10.0,0.0);
for(n=0;n<8;n++)
{
glIndexi(n+1);
glVertex2fv(pp[n]);
}
glEnd();
}
void main(void)
{
auxInitDisplayMode(AUX_SINGLE|AUX_INDEX);
auxInitPosition(0,0,500,500);
auxInitWindow("Color Index");
myinit();
auxReshapeFunc(myReshape);
auxMainLoop(display);
}
這個程序運行結果是在屏幕上顯示八個連成扇形的不同顏色的三角形,每個三角形的顏色定義采用顏色表模式。其中,調用了輔助庫函數auxSetOneColor()來裝載顏色映射表,即調色板。因為將某個顏色裝載到顏色查找表(color lookup table)中的過程必須依賴窗口系統,而OpenGL函數與窗口系統無關,所以這里就調用輔助庫的函數來完成這個過程,然后才調用OpenGL自己的函數glIndex()設置當前的顏色號。
圖9-3 自定義調色板 |
十、OpenGL光照
10.1、真實感圖形基本概念
真實感圖形繪制是計算機圖形學的一個重要組成部分,它綜合利用數學、物理學、計算機科學和其它科學知識在計算機圖形設備上生成象彩色照片那樣的具有真實感的圖形。一般說來,用計算機在圖形設備上生成真實感圖形必須完成以下四個步驟:一是用建模,即用一定的數學方法建立所需三維場景的幾何描述,場景的幾何描述直接影響圖形的復雜性和圖形繪制的計算耗費;二是將三維幾何模型經過一定變換轉為二維平面透視投影圖;三是確定場景中所有可見面,運用隱藏面消隱算法將視域外或被遮擋住的不可見面消去;四是計算場景中可見面的顏色,即根據基于光學物理的光照模型計算可見面投射到觀察者眼中的光亮度大小和顏色分量,并將它轉換成適合圖形設備的顏色值,從而確定投影畫面上每一象素的顏色,最終生成圖形。
由于真實感圖形是通過景物表面的顏色和明暗色調來表現景物的幾何形狀、空間位置以及表面材料的,而一個物體表面所呈現的顏色是由表面向視線方向輻射的光能決定的。在計算機圖形學中,常采用一個既能表示光能大小又能表示其顏色組成的物理量即光亮度(luminance)或光強(intensity of light)來描述物體表面朝某方向輻射光能的顏色。采用這個物理量可以正確描述光在物體表面的反射、透射和吸收現象,因而可以正確計算處物體表面在空間給定方向上的光能顏色。
物體表面向空間給定方向輻射的光強可應用光照模型進行計算。簡單的光照模型通常假定物體表面是光滑的且由理想材料構成,因此只考慮光源照射在物體表面產生的反射光,所生成的圖形可以模擬處不透明物體表面的明暗過渡,具有一定的真實感效果。復雜的光照模型除了考慮上述因素外,還要考慮周圍環境的光對物體表面的影響。如光亮平滑的物體表面會將環境中其它物體映像在表面上,而通過透明物體也可看到其后的環境景象。這類光照模型稱為整體光照模型,它能模擬出鏡面映像、透明等較精致的光照效果。為了更真實的繪制圖形,還要考慮物體表面的細節紋理,這通常使用一種稱為“紋理映射”(texture mapping)的技術把已有的平面花紋圖案映射到物體表面上,并在應用光照模型時將這些花紋的顏色考慮進去,物體表面細節的模擬使繪制的圖形更接近自然景物。
以上內容中,真實感圖形繪制的四大步驟前兩步在前面的章節已經詳細介紹過,這里不再重復,第三步OpenGL將自動完成所有消隱過程,第四步下面幾節詳述。另外,部分復雜光照模型應用將在后續章節里介紹。
10.2、光照模型
10.2.1 簡單光照模型
當光照射到一個物體表面上時,會出現三種情形。首先,光可以通過物體表面向空間反射,產生反射光。其次,對于透明體,光可以穿透該物體并從另一端射出,產生透射光。最后,部分光將被物體表面吸收而轉換成熱。在上述三部分光中,僅僅是透射光和反射光能夠進入人眼產生視覺效果。這里介紹的簡單光照模型只考慮被照明物體表面的反射光影響,假定物體表面光滑不透明且由理想材料構成,環境假設為由白光照明。
一般來說,反射光可以分成三個分量,即環境反射、漫反射和鏡面反射。環境反射分量假定入射光均勻地從周圍環境入射至景物表面并等量地向各個方向反射出去,通常物體表面還會受到從周圍環境來的反射光(如來自地面、天空、墻壁等的反射光)的照射,這些光常統稱為環境光(Ambient Light);漫反射分量表示特定光源在景物表面的反射光中那些向空間各方向均勻反射出去的光,這些光常稱為漫射光(Diffuse Light);鏡面反射光為朝一定方向的反射光,如一個點光源照射一個金屬球時會在球面上形成一塊特別亮的區域,呈現所謂“高光(Highlight)”,它是光源在金屬球面上產生的鏡面反射光(Specular Light)。對于較光滑物體,其鏡面反射光的高光區域小而亮;相反,粗糙表面的鏡面反射光呈發散狀態,其高光區域大而不亮。下面先看一個簡單的光照例程。
例10-1 簡單光照例程(light0.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auxSolidSphere(1.0);
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho (-1.5, 1.5, -1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w, -10.0, 10.0);
else
glOrtho (-1.5*(GLfloat)w/(GLfloat)h, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Simple Lighting");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果是顯示一個具有灰色光影的球。其中函數myinit()中包含了關鍵的設定光源位置、啟動光照等幾句,而其它程序語言幾乎與以前的沒有多大區別,但效果卻完全不一樣。下面幾個小節將詳細介紹有關函數的用法。
圖10-1 帶光影的灰色球體 |
10.2.2 OpenGL光組成
在OpenGL簡單光照模型中的幾種光分為:輻射光(Emitted Light)、環境光(Ambient Light)、漫射光(Diffuse Light)、鏡面光(Specular Light)。
輻射光是最簡單的一種光,它直接從物體發出并且不受任何光源影響。
環境光是由光源發出經環境多次散射而無法確定其方向的光,即似乎來自所有方向。一般說來,房間里的環境光成分要多些,戶外的相反要少得多,因為大部分光按相同方向照射,而且在戶外很少有其他物體反射的光。當環境光照到曲面上時,它在各個方向上均等地發散(類似于無影燈光)。
漫射光來自一個方向,它垂直于物體時比傾斜時更明亮。一旦它照射到物體上,則在各個方向上均勻地發散出去。于是,無論視點在哪里它都一樣亮。來自特定位置和特定方向的任何光,都可能有散射成分。
鏡面光來自特定方向并沿另一方向反射出去,一個平行激光束在高質量的鏡面上產生100%的鏡面反射。光亮的金屬和塑料具有很高非反射成分,而象粉筆和地毯等幾乎沒有反射成分。因此,三某種意義上講,物體的反射程度等同于其上的光強(或光亮度)。
10.2.3 創建光源(Light Source)
光源有許多特性,如顏色、位置、方向等。選擇不同的特性值,則對應的光源作用在物體上的效果也不一樣,這在以后的章節中會逐步介紹的。下面詳細講述定義光源特性的函數glLight*():
void glLight{if}[v](GLenum light , GLenum pname, TYPE param)
創建具有某種特性的光源。其中第一個參數light指定所創建的光源號,如GL_LIGHT0、GL_LIGHT1、...、GL_LIGHT7。第二個參數pname指定光源特性,這個參數的輔助信息見表10-1所示。最后一個參數設置相應的光源特性值。
pname 參數名 | 缺省值 | 說明 |
GL_AMBIENT | (0.0, 0.0, 0.0, 1.0) | RGBA模式下環境光 |
GL_DIFFUSE | (1.0, 1.0, 1.0, 1.0) | RGBA模式下漫反射光 |
GL_SPECULAR | (1.0,1.0,1.0,1.0) | RGBA模式下鏡面光 |
GL_POSITION | (0.0,0.0,1.0,0.0) | 光源位置齊次坐標(x,y,z,w) |
GL_SPOT_DIRECTION | (0.0,0.0,-1.0) | 點光源聚光方向矢量(x,y,z) |
GL_SPOT_EXPONENT | 0.0 | 點光源聚光指數 |
GL_SPOT_CUTOFF | 180.0 | 點光源聚光截止角 |
GL_CONSTANT_ATTENUATION | 1.0 | 常數衰減因子 |
GL_LINER_ATTENUATION | 0.0 | 線性衰減因子 |
GL_QUADRATIC_ATTENUATION | 0.0 | 平方衰減因子 |
表10-1 函數glLight*()參數pname說明
|
注意:以上列出的GL_DIFFUSE和GL_SPECULAR的缺省值只能用于GL_LIGHT0,其他幾個光源的GL_DIFFUSE和GL_SPECULAR缺省值為(0.0,0.0,0.0,1.0)。另外,表中后六個參數的應用放在下一篇中介紹。在上面例程中,光源的創建為:
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
其中light_position是一個指針,指向定義的光源位置齊次坐標數組。其它幾個光源特性都為缺省值。同樣,我們也可用類似的方式定義光源的其他幾個特性值,例如:
GLfloat light_ambient [] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat light_diffuse [] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat light_specular[] = { 1.0, 1.0, 1.0, 1.0 };
glLightfv(GL_LIGHT0, GL_AMBIENT , light_ambient );
glLightfv(GL_LIGHT0, GL_DIFFUSE , light_diffuse );
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);
10.2.4 啟動光照
在OpenGL中,必須明確指出光照是否有效或無效。如果光照無效,則只是簡單地將當前顏色映射到當前頂點上去,不進行法向、光源、材質等復雜計算,那么顯示的圖形就沒有真實感,如前幾章例程運行結果顯示。要使光照有效,首先得啟動光照,即:
glEnable(GL_LIGHTING);
若使光照無效,則調用gDisable(GL_LIGHTING)可關閉當前光照。然后,必須使所定義的每個光源有效,例light0.c中只用了一個光源,即:
glEnable(GL_LIGHT0);
其它光源類似,只是光源號不同而已。
10.3、明暗處理
在計算機圖形學中,光滑的曲面表面常用多邊形予以逼近和表示,而每個小多邊形輪廓(或內部)就用單一的顏色或許多不同的顏色來勾畫(或填充),這種處理方式就稱為明暗處理。在OpenGL中,用單一顏色處理的稱為平面明暗處理(Flat Shading),用許多不同顏色處理的稱為光滑明暗處理(Smooth Shading),也稱為Gourand明暗處理(Gourand Shading)。設置明暗處理模式的函數為:
void glShadeModel(GLenum mode);
函數參數為GL_FLAT或GL_SMOOTH,分別表示平面明暗處理和光滑明暗處理。
應用平面明暗處理模式時,多邊形內每個點的法向一致,且顏色也一致;應用光滑明暗處理模式時,多邊形所有點的法向是由內插生成的,具有一定的連續性,因此每個點的顏色也相應內插,故呈現不同色。這種模式下,插值方法采用的是雙線性插值法,如圖10-2所示。
圖10-2 Gouraud明暗處理 |
Gouraud明暗處理通常算法為:先用多邊形頂點的光強線性插值出當前掃描線與多邊形邊交點處的光強,然后再用交點的光強線插值處掃描線位于多邊形內區段上每一象素處的光強值。圖中顯示出一條掃描線與多邊形相交,交線的端點是A點和B點,P點是掃描線上位于多邊形內的任一點,多邊形三個頂點的光強分別為I1、I2和I3.取A點的光強Ia為I1和I2的線性插值,B點的光強Ib為I1和I3的線性插值,P點的光強Ip則為Ia和Ib的線性插值。采用Gouraud明暗處理不但可以使用多邊形表示的曲面光強連續,而且計算量很小。這種算法還可以以增量的形式改進,且能用硬件直接實現算法,從而廣泛用于計算機實時圖形生成。請看下面光滑明暗處理的例程:
例10-2 明暗處理例程(Shading.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void object(void);
void CALLBACK display(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
/* GL_SMOOTH is actually the default shading model. */
void myinit (void)
{
glShadeModel (GL_SMOOTH);
}
void object(void)
{
glBegin (GL_POLYGON);
glColor3f (1.0, 0.0, 0.0);
glVertex2f (4.0, 4.0);
glColor3f(1.0,1.0,1.0);
glVertex2f (12.0, 4.0);
glColor3f(0.0,0.0,1.0);
glVertex2f (12.0, 12.0);
glColor3f(0.0,1.0,0.0);
glVertex2f (4.0, 12.0);
glEnd ();
}
void CALLBACK display(void)
{
glClear (GL_COLOR_BUFFER_BIT);
object ();
glFlush ();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
gluOrtho2D (0.0, 16.0, 0.0, 16.0 * (GLfloat) h/(GLfloat) w);
else
gluOrtho2D (0.0, 16.0 * (GLfloat) w/(GLfloat) h, 0.0, 16.0);
glMatrixMode(GL_MODELVIEW);
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Smooth Shading");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果是在屏幕上顯示一個色彩連續變化的三角形。這個程序是用的RGBA顯示模式,若改用顏色表模式,則顏色內插實際上是顏色表的內插,因此呈現的顏色可能不連續。網友不妨自己試試。
另外,若在light0.c程序中加上一句定義GL_FLAT明暗處理模式,則又會出現怎樣的情形呢?讀者可以仔細比較一下。
圖10-3 高氏明暗處理的正方形 |
10.4、材質
10.4.1 材質顏色
OpenGL用材料對光的紅、綠、藍三原色的反射率來近似定義材料的顏色。象光源一樣,材料顏色也分成環境、漫反射和鏡面反射成分,它們決定了材料對環境光、漫反射光和鏡面反射光的反射程度。在進行光照計算時,材料對環境光的反射率與每個進入光源的環境光結合,對漫反射光的反射率與每個進入光源的漫反射光結合,對鏡面光的反射率與每個進入光源的鏡面反射光結合。對環境光與漫反射光的反射程度決定了材料的顏色,并且它們很相似。對鏡面反射光的反射率通常是白色或灰色(即對鏡面反射光中紅、綠、藍的反射率相同)。鏡面反射高光最亮的地方將變成具有光源鏡面光強度的顏色。例如一個光亮的紅色塑料球,球的大部分表現為紅色,光亮的高光將是白色的。
10.4.2 材質定義
材質的定義與光源的定義類似。其函數為:
void glMaterial{if}[v](GLenum face,GLenum pname,TYPE param);
定義光照計算中用到的當前材質。face可以是GL_FRONT、GL_BACK、GL_FRONT_AND_BACK,它表明當前材質應該應用到物體的哪一個面上;pname說明一個特定的材質;param是材質的具體數值,若函數為向量形式,則param是一組值的指針,反之為參數值本身。非向量形式僅用于設置GL_SHINESS。pname參數值具體內容見表10-1。另外,參數GL_AMBIENT_AND_DIFFUSE表示可以用相同的 RGB值設置環境光顏色和漫反射光顏色。
參數名 | 缺省值 | 說明 |
GL_AMBIENT | (0.2, 0.2, 0.2, 1.0) | 材料的環境光顏色 |
GL_DIFFUSE | (0.8, 0.8, 0.8, 1.0) | 材料的漫反射光顏色 |
GL_AMBIENT_AND_DIFFUSE | 材料的環境光和漫反射光顏色 | |
GL_SPECULAR | (0.0, 0.0, 0.0, 1.0) | 材料的鏡面反射光顏色 |
GL_SHINESS | 0.0 | 鏡面指數(光亮度) |
GL_EMISSION | (0.0, 0.0, 0.0, 1.0) | 材料的輻射光顏色 |
GL_COLOR_INDEXES | (0, 1, 1) | 材料的環境光、漫反射光和鏡面光顏色 |
表10-2 函數glMaterial*()參數pname的缺省值
|
例10-3 材質定義例程(light1.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void); void myinit(void)
{
/* 設置材質的各種光的顏色成分反射比率 */
GLfloat mat_ambient[]={0.8,0.8,0.8,1.0};
GLfloat mat_diffuse[]={0.8,0.0,0.8,1.0}; /* 紫色 */
GLfloat mat_specular[] = { 1.0, 0.0, 1.0, 1.0 }; /* 亮紫色 */
GLfloat mat_shininess[] = { 50.0 };
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auxSolidSphere(1.0);
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho (-1.5, 1.5, -1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w, -10.0, 10.0);
else
glOrtho (-1.5*(GLfloat)w/(GLfloat)h, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA | AUX_DEPTH16);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Lighting_1 ");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果是一個紫色的球。在函數myinit()中定義了球的材質顏色,光源的定義仍延用light0.c中的,而light.c物體的光源定義為缺省形式。從例子中明顯地看出,物體的材質顏色定義與光源顏色定義幾乎一樣,物體反射到眼中的顏色與二者都有關系,具體關系請看下一小節。
10.4.3 材質RGB值和光源RGB值的關系
材質的顏色與光源的顏色有些不同。對于光源,R、G、B值等于R、G、B對其最大強度的百分比。若光源顏色的R、G、B值都是1.0,則是最強的白光;若值變為0.5,顏色仍為白色,但強度為原來的一半,于是表現為灰色;若R=G=1.0,B=0.0,則光源為黃色。對于材質,R、G、B值為材質對光的 R、G、B成分的反射率。比如,一種材質的R=1.0、G=0.5、B=0.0,則材質反射全部的紅色成分,一半的綠色成分,不反射藍色成分。也就是說,若OpenGL的光源顏色為(LR、LG、LB),材質顏色為(MR、MG、MB),那么,在忽略所有其他反射效果的情況下,最終到達眼睛的光的顏色為(LR*MR、LG*MG、LB*MB)。
同樣,如果有兩束光,相應的值分別為(R1、G1、B1)和(R2、G2、B2),則OpenGL 將各個顏色成分相加,得到(R1+R2、G1+G2、B1+B2),若任一成分的和值大于1(超出了設備所能顯示的亮度)則約簡到1.0。下面一例程就說明了二者之間的關系。
例10-4 材質與光源的RGB關系例程(light2.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
GLfloat mat_ambient[]= { 0.8, 0.8, 0.8, 1.0 };
GLfloat mat_diffuse[]= { 0.8, 0.0, 0.8, 1.0 }; /* 紫色 */
GLfloat mat_specular[] = { 1.0, 0.0, 1.0, 1.0 };
GLfloat mat_shininess[] = { 50.0 };
GLfloat light_diffuse[]= { 0.0, 0.0, 1.0, 1.0}; /* 藍色 */
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auxSolidSphere(1.0);
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho (-1.5, 1.5, -1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w, -10.0, 10.0);
else
glOrtho (-1.5*(GLfloat)w/(GLfloat)h, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA | AUX_DEPTH16);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Lighting_2 ");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序運行結果是一個藍色的球,其中高光部分仍為上一例的亮紫色。從上可看出,球漫反射光的結果是mat_diffuse[]與 light_diffuse[]中的三個顏色分量值相乘,即 (0.0*1.0,0.0*1.0,0.8*1.0,1.0*1.0)=(0.0,0.0,0.8,1.0),所以球大部分呈現藍色。
圖10-4 光照藍色球(高光為紅色) |
10.4.4 材質改變
在實際應用的許多情況下,不同的物體或同一物體的不同部分都有可能設置不同的材質,OpenGL函數庫提供了兩種方式實現這種要求。下面一例程采用的是設置矩陣堆棧來保存不同物體的材質信息:
例10-5 矩陣堆棧改變材質例程(chgmat1.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK display(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
/* 初始化z-buffer、光源和光照模型,在此不具體定義材質。*/
void myinit(void)
{
GLfloat ambient[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat position[] = { 0.0, 3.0, 2.0, 0.0 };
GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
glLightfv(GL_LIGHT0, GL_POSITION, position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glClearColor(0.0, 0.1, 0.1, 0.0);
}
void CALLBACK display(void)
{
GLfloat no_mat[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat mat_ambient[] = { 0.7, 0.7, 0.7, 1.0 };
GLfloat mat_ambient_color[] = { 0.8, 0.8, 0.2, 1.0 };
GLfloat mat_diffuse[] = { 0.1, 0.5, 0.8, 1.0 };
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat no_shininess[] = { 0.0 };
GLfloat low_shininess[] = { 5.0 };
GLfloat high_shininess[] = { 100.0 };
GLfloat mat_emission[] = {0.3, 0.2, 0.2, 0.0};
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* 第一行第一列繪制的球僅有漫反射光而無環境光和鏡面光。*/
glPushMatrix();
glTranslatef (-3.75, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第一行第二列繪制的球有漫反射光和鏡面光,并有低高光,而無環境光 。*/
glPushMatrix();
glTranslatef (-1.25, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, low_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第一行第三列繪制的球有漫反射光和鏡面光,并有很亮的高光,而無環境光 。*/
glPushMatrix();
glTranslatef (1.25, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第一行第四列繪制的球有漫反射光和輻射光,而無環境和鏡面反射光。*/
glPushMatrix();
glTranslatef (3.75, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
auxSolidSphere(1.0);
glPopMatrix();
/* 第二行第一列繪制的球有漫反射光和環境光,而鏡面反射光。*/
glPushMatrix();
glTranslatef (-3.75, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第二行第二列繪制的球有漫反射光、環境光和鏡面光,且有低高光。*/
glPushMatrix();
glTranslatef (-1.25, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, low_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第二行第三列繪制的球有漫反射光、環境光和鏡面光,且有很亮的高光。*/
glPushMatrix();
glTranslatef (1.25, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第二行第四列繪制的球有漫反射光、環境光和輻射光,而無鏡面光。*/
glPushMatrix();
glTranslatef (3.75, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
auxSolidSphere(1.0); glPopMatrix();
/* 第三行第一列繪制的球有漫反射光和有顏色的環境光,而無鏡面光。*/
glPushMatrix();
glTranslatef (-3.75, -3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient_color);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第三行第二列繪制的球有漫反射光和有顏色的環境光以及鏡面光,且有低高光。*/
glPushMatrix();
glTranslatef (-1.25, -3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient_color);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, low_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第三行第三列繪制的球有漫反射光和有顏色的環境光以及鏡面光,且有很亮的高光。*/
glPushMatrix();
glTranslatef (1.25, -3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient_color);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere(1.0);
glPopMatrix();
/* 第三行第四列繪制的球有漫反射光和有顏色的環境光以及輻射光,而無鏡面光。*/
glPushMatrix();
glTranslatef (3.75, -3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient_color);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
auxSolidSphere(1.0);
glPopMatrix();
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= (h * 2))
glOrtho (-6.0, 6.0, -3.0*((GLfloat)h*2)/(GLfloat)w,
3.0*((GLfloat)h*2)/(GLfloat)w, -10.0, 10.0);
else
glOrtho (-6.0*(GLfloat)w/((GLfloat)h*2),
6.0*(GLfloat)w/((GLfloat)h*2), -3.0, 3.0, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGBA);
auxInitPosition (0, 0, 600, 450);
auxInitWindow ("Material");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
圖10-5 多種光和材質的變化效果 |
以上程序運行結果是繪制12個球(3行4列)。第一行的球材質都沒有環境反射光,第二行的都有一定的環境反射光,第三行的都有某種顏色的環境光。而第一列的球材質僅有藍色的漫反射光;第二列的不僅有藍漫反射光,而且還有鏡面反射光,較低的高光;第三列的不僅有藍漫反射光,而且還有鏡面反射光,很亮的高光;第四列的還包括輻射光,但無鏡面光。
這個程序運用矩陣堆棧多次調用glMaterialfv()來設置每個球的材質,也就是改變同一場景中的不同物體的顏色。但由于這個函數的應用有個性能開銷,因此建議最好盡可能少的改變材質,以減少改變材質時所帶來的性能開銷,可采用另一種方式即改變材質顏色,相應函數為glColorMaterial(),說明如下:
void glColorMaterial(GLenum face,GLenum mode);
函數參數face指定面,值有GL_FRONT、GL_BACK或GL_FRONT_AND_BACK(缺省值)。mode指定材質成分,值有 GL_AMBIENT、GL_DIFFUSE、GL_AMBIENT_AND_DIFFUSE(缺省值)、GL_SPECULAR或 GLEMISSION。
注意:這個函數說明了兩個獨立的值,第一個參數說明哪一個面和哪些面被修改,而第二個參數說明這些面的哪一個或哪些材質成分要被修改。OpenGL并不為每一種face保持獨立的mode變量。在調用glColorMterial() 以后,首先需要用GL_COLOR_MATERIAL作為參數調用glEnable()來啟動顏色材質,然后在繪圖時調用glColor*()來改變當前顏色,或用glMaterial()來改變材質成分。當不用這種方式來改變材質時,可調用glDisable(GL_COLOR_MATERIAL)來關閉取消。如下面一段代碼:
glColorMaterial(GL_FRONT,GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
glColor3f(0.3,0.5,0.7);
/* draw some objects here. */
glcolor3f(0.0,1.0,0.0);
/* draw other objects here.*/
glDisable(GL_COLOR_MATERIAL);
當需要改變場景中大部分方面的單個材質時,最好調用glColorMaterial();當需要修改不止一個材質參數時,最好調用glMaterial*()。注意,當不需要顏色材質時一定要關閉它,以避免相應的開銷。下面來看一個顏色材質的具體應用例子:
例10-6 顏色定義改變材質例程(chgmat2.c)
#include "glos.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
glColorMaterial(GL_FRONT, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* draw one yellow ball */
glLoadIdentity();
glTranslatef(-0.7,0.0,0.0);
glColor3f(1.0,1.0,0.0);
auxSolidSphere(0.5);
/* draw one red cone */
glLoadIdentity();
glRotatef(-65.0,1.0,0.0,0.0);
glTranslatef(0.7,0.0,0.0);
glColor3f(1.0,0.0,0.0);
auxSolidCone(0.4,0.6);
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho (-1.5, 1.5, -1.5*(GLfloat)h/(GLfloat)w, 1.5*(GLfloat)h/(GLfloat)w, -10.0, 10.0);
else
glOrtho (-1.5*(GLfloat)w/(GLfloat)h, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE | AUX_RGB | AUX_DEPTH16);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("ColorMaterial Mode");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
以上程序改變的是漫反射顏色。場景中顯示了一個黃色的球和一個紅色的錐體。
圖10-6 漫反射材質改變 |