OpenGL入門學習

jopen 10年前發布 | 78K 次閱讀 OpenGL OpenCV 圖形/圖像處理

說起編程作圖,大概還有很多人想起TC的#include <graphics.h>吧?

但是各位是否想過,那些畫面絢麗的PC游戲是如何編寫出來的?就靠TC那可憐的640*480分辨率、16色來做嗎?顯然是不行的。

本帖的目的是讓大家放棄TC的老舊圖形接口,讓大家接觸一些新事物。

OpenGL作為當前主流的圖形API之一,它在一些場合具有比DirectX更優越的特性。

1、與C語言緊密結合。

OpenGL命令最初就是用C語言函數來進行描述的,對于學習過C語言的人來講,OpenGL是容易理解和學習的。如果你曾經接觸過TC的graphics.h,你會發現,使用OpenGL作圖甚至比TC更加簡單。

2、強大的可移植性。

微軟的Direct3D雖然也是十分優秀的圖形API,但它只用于Windows系統(現在還要加上一個XBOX游戲機)。而OpenGL不僅用于 Windows,還可以用于Unix/Linux等其它系統,它甚至在大型計算機、各種專業計算機(如:醫療用顯示設備)上都有應用。并且,OpenGL 的基本命令都做到了硬件無關,甚至是平臺無關。

3、高性能的圖形渲染。

OpenGL是一個工業標準,它的技術緊跟時代,現今各個顯卡廠家無一不對OpenGL提供強力支持,激烈的競爭中使得OpenGL性能一直領先。

總之,OpenGL是一個很NB的圖形軟件接口。至于究竟有多NB,去看看DOOM3和QUAKE4等專業游戲就知道了。

OpenGL官方網站(英文)

http://www.opengl.org

下面將對Windows下的OpenGL編程進行簡單介紹。

學習OpenGL前的準備工作

第一步,選擇一個編譯環境

現在Windows系統的主流編譯環境有Visual Studio,Broland C++ Builder,Dev-C++等,它們都是支持OpenGL的。但這里我們選擇Visual Studio 2005作為學習OpenGL的環境。

第二步,安裝GLUT工具包

GLUT不是OpenGL所必須的,但它會給我們的學習帶來一定的方便,推薦安裝。

Windows環境下的GLUT下載地址:(大小約為150k)

http://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip

無法從以上地址下載的話請使用下面的連接:

http://upload.programfan.com/upfile/200607311626279.zip

Windows環境下安裝GLUT的步驟:

1、將下載的壓縮包解開,將得到5個文件

2、在“我的電腦”中搜索“gl.h”,并找到其所在文件夾(如果是VisualStudio2005,則應該是其安裝目錄下面的“VC\PlatformSDK\include\gl文件夾”)。把解壓得到的glut.h放到這個文件夾。

3、把解壓得到的glut.lib和glut32.lib放到靜態函數庫所在文件夾(如果是VisualStudio2005,則應該是其安裝目錄下面的“VC\lib”文件夾)。

4、把解壓得到的glut.dll和glut32.dll放到操作系統目錄下面的system32文件夾內。(典型的位置為:C:\Windows\System32)

第三步,建立一個OpenGL工程

這里以VisualStudio2005為例。

選擇File->New->Project,然后選擇Win32 Console Application,選擇一個名字,然后按OK。

在談出的對話框左邊點Application Settings,找到Empty project并勾上,選擇Finish。

然后向該工程添加一個代碼文件,取名為“OpenGL.c”,注意用.c來作為文件結尾。

搞定了,就跟平時的工程沒什么兩樣的。

 

第一個OpenGL程序

一個簡單的OpenGL程序如下:(注意,如果需要編譯并運行,需要正確安裝GLUT,安裝方法如上所述)

#include <GL/glut.h>

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);

     glFlush();

}

int main(int argc, char *argv[])

{

     glutInit(&argc, argv);

     glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);

     glutInitWindowPosition(100, 100);

     glutInitWindowSize(400, 400);

     glutCreateWindow("第一個OpenGL程序");

     glutDisplayFunc(&myDisplay);

     glutMainLoop();

     return 0;

}

該程序的作用是在一個黑色的窗口中央畫一個白色的矩形。下面對各行語句進行說明。

首先,需要包含頭文件#include <GL/glut.h>,這是GLUT的頭文件。

本來OpenGL程序一般還要包含<GL/gl.h>和<GL/glu.h>,但GLUT的頭文件中已經自動將這兩個文件包含了,不必再次包含。

然后看main函數。

int main(int argc, char *argv[]),這個是帶命令行參數的main函數,各位應該見過吧?沒見過的同志們請多翻翻書,等弄明白了再往下看。

注意main函數中的各語句,除了最后的return之外,其余全部以glut開頭。這種以glut開頭的函數都是GLUT工具包所提供的函數,下面對用到的幾個函數進行介紹。

1、glutInit,對GLUT進行初始化,這個函數必須在其它的GLUT使用之前調用一次。其格式比較死板,一般照抄這句glutInit(&argc, argv)就可以了。

2、 glutInitDisplayMode,設置顯示方式,其中GLUT_RGB表示使用RGB顏色,與之對應的還有GLUT_INDEX(表示使用索引顏色)。GLUT_SINGLE表示使用單緩沖,與之對應的還有GLUT_DOUBLE(使用雙緩沖)。更多信息,請自己Google。當然以后的教程也會有一些講解。

3、glutInitWindowPosition,這個簡單,設置窗口在屏幕中的位置。

4、glutInitWindowSize,這個也簡單,設置窗口的大小。

5、glutCreateWindow,根據前面設置的信息創建窗口。參數將被作為窗口的標題。注意:窗口被創建后,并不立即顯示到屏幕上。需要調用glutMainLoop才能看到窗口。

6、glutDisplayFunc,設置一個函數,當需要進行畫圖時,這個函數就會被調用。(這個說法不夠準確,但準確的說法可能初學者不太好理解,暫時這樣說吧)。

7、glutMainLoop,進行一個消息循環。(這個可能初學者也不太明白,現在只需要知道這個函數可以顯示窗口,并且等待窗口關閉后才會返回,這就足夠了。)

在glutDisplayFunc函數中,我們設置了“當需要畫圖時,請調用myDisplay 函數”。于是myDisplay函數就用來畫圖。觀察myDisplay中的三個函數調用,發現它們都以gl開頭。這種以gl開頭的函數都是OpenGL 的標準函數,下面對用到的函數進行介紹。

1、glClear,清除。GL_COLOR_BUFFER_BIT表示清除顏色,glClear函數還可以清除其它的東西,但這里不作介紹。

2、glRectf,畫一個矩形。四個參數分別表示了位于對角線上的兩個點的橫、縱坐標。

3、glFlush,保證前面的OpenGL命令立即執行(而不是讓它們在緩沖區中等待)。其作用跟fflush(stdout)類似。

OpenGL入門學習[二]

本次課程所要講的是繪制簡單的幾何圖形,在實際繪制之前,讓我們先熟悉一些概念。

一、點、直線和多邊形
我們知道數學(具體的說,是幾何學)中有點、直線和多邊形的概念,但這些概念在計算機中會有所不同。
數學上的點,只有位置,沒有大小。但在計算機中,無論計算精度如何提高,始終不能表示一個無窮小的點。另一方面,無論圖形輸出設備(例如,顯示器)如何精確,始終不能輸出一個無窮小的點。一般情況下,OpenGL中的點將被畫成單個的像素(像素的概念,請自己搜索之~),雖然它可能足夠小,但并不會是無窮小。同一像素上,OpenGL可以繪制許多坐標只有稍微不同的點,但該像素的具體顏色將取決于OpenGL的實現。當然,過度的注意細節就是鉆牛角尖,我們大可不必花費過多的精力去研究“多個點如何畫到同一像素上”。
同樣的,數學上的直線沒有寬度,但OpenGL的直線則是有寬度的。同時,OpenGL的直線必須是有限長度,而不是像數學概念那樣是無限的。可以認為,OpenGL的“直線”概念與數學上的“線段”接近,它可以由兩個端點來確定。
多邊形是由多條線段首尾相連而形成的閉合區域。OpenGL規定,一個多邊形必須是一個“凸多邊形”(其定義為:多邊形內任意兩點所確定的線段都在多邊形內,由此也可以推導出,凸多邊形不能是空心的)。多邊形可以由其邊的端點(這里可稱為頂點)來確定。(注意:如果使用的多邊形不是凸多邊形,則最后輸出的效果是未定義的——OpenGL為了效率,放寬了檢查,這可能導致顯示錯誤。要避免這個錯誤,盡量使用三角形,因為三角形都是凸多邊形)

可以想象,通過點、直線和多邊形,就可以組合成各種幾何圖形。甚至于,你可以把一段弧看成是很多短的直線段相連,這些直線段足夠短,以至于其長度小于一個像素的寬度。這樣一來弧和圓也可以表示出來了。通過位于不同平面的相連的小多邊形,我們還可以組成一個“曲面”。

二、在OpenGL中指定頂點
由以上的討論可以知道,“點”是一切的基礎。
如何指定一個點呢?OpenGL提供了一系列函數。它們都以glVertex開頭,后面跟一個數字和1~2個字母。例如:
glVertex2d
glVertex2f
glVertex3f
glVertex3fv
等等。
數字表示參數的個數,2表示有兩個參數,3表示三個,4表示四個(我知道有點羅嗦~)。
字母表示參數的類型,s表示16位整數(OpenGL中將這個類型定義為GLshort),
                   i表示32位整數(OpenGL中將這個類型定義為GLint和GLsizei),
                   f表示32位浮點數(OpenGL中將這個類型定義為GLfloat和GLclampf),
                   d表示64位浮點數(OpenGL中將這個類型定義為GLdouble和GLclampd)。
                   v表示傳遞的幾個參數將使用指針的方式,見下面的例子。
這些函數除了參數的類型和個數不同以外,功能是相同的。例如,以下五個代碼段的功能是等效的:
(一)glVertex2i(1, 3);
(二)glVertex2f(1.0f, 3.0f);
(三)glVertex3f(1.0f, 3.0f, 0.0f);
(四)glVertex4f(1.0f, 3.0f, 0.0f, 1.0f);
(五)GLfloat VertexArr3[] = {1.0f, 3.0f, 0.0f};
      glVertex3fv(VertexArr3);
以后我們將用glVertex*來表示這一系列函數。
注意:OpenGL的很多函數都是采用這樣的形式,一個相同的前綴再加上參數說明標記,這一點會隨著學習的深入而有更多的體會。


三、開始繪制
假設現在我已經指定了若干頂點,那么OpenGL是如何知道我想拿這些頂點來干什么呢?是一個一個的畫出來,還是連成線?或者構成一個多邊形?或者做其它什么事情?
為了解決這一問題,OpenGL要求:指定頂點的命令必須包含在glBegin函數之后,glEnd函數之前(否則指定的頂點將被忽略)。并由glBegin來指明如何使用這些點。
例如我寫:
glBegin(GL_POINTS);
     glVertex2f(0.0f, 0.0f);
     glVertex2f(0.5f, 0.0f);
glEnd();
則這兩個點將分別被畫出來。如果將GL_POINTS替換成GL_LINES,則兩個點將被認為是直線的兩個端點,OpenGL將會畫出一條直線。
我們還可以指定更多的頂點,然后畫出更復雜的圖形。
另一方面,glBegin支持的方式除了GL_POINTS和GL_LINES,還有GL_LINE_STRIP,GL_LINE_LOOP,GL_TRIANGLES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN等,每種方式的大致效果見下圖:

聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。

我并不準備在glBegin的各種方式上大作文章。大家可以自己嘗試改變glBegin的方式和頂點的位置,生成一些有趣的圖案。

程序代碼:
void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin( /* 在這里填上你所希望的模式 */ );
        /* 在這里使用glVertex*系列函數 */
        /* 指定你所希望的頂點位置 */
     glEnd();
     glFlush();
}
把這段代碼改成你喜歡的樣子,然后用它替換第一課中的myDisplay函數,編譯后即可運行。



兩個例子
例一、畫一個圓
/
正四邊形,正五邊形,正六邊形,……,直到正n邊形,當n越大時,這個圖形就越接近圓
當n大到一定程度后,人眼將無法把它跟真正的圓相區別
這時我們已經成功的畫出了一個“圓”
(注:畫圓的方法很多,這里使用的是比較簡單,但效率較低的一種)
試修改下面的const int n的值,觀察當n=3,4,5,8,10,15,20,30,50等不同數值時輸出的變化情況
將GL_POLYGON改為GL_LINE_LOOP、GL_POINTS等其它方式,觀察輸出的變化情況
/

include <math.h>

const int n = 20;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     int i;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_POLYGON);
     for(i=0; i<n; ++i)
         glVertex2f(Rcos(2Pi/ni), Rsin(2Pi/ni));
     glEnd();
     glFlush();
}


例二、畫一個五角星
/
設五角星的五個頂點分布位置關系如下:
      A
E        B

    D    C
首先,根據余弦定理列方程,計算五角星的中心到頂點的距離a
(假設五角星對應正五邊形的邊長為.0)
a = 1 / (2-2
cos(72Pi/180));
然后,根據正弦和余弦的定義,計算B的x坐標bx和y坐標by,以及C的y坐標
(假設五角星的中心在坐標原點)
bx = a
cos(18 Pi/180);
by = a
sin(18 Pi/180);
cy = -a
cos(18 Pi/180);
五個點的坐標就可以通過以上四個量和一些常數簡單的表示出來
/

include <math.h>

const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     GLfloat a = 1 / (2-2cos(72Pi/180));
     GLfloat bx = a cos(18 Pi/180);
     GLfloat by = a sin(18 Pi/180);
     GLfloat cy = -a cos(18 Pi/180);
     GLfloat
         PointA[2] = { 0, a },
         PointB[2] = { bx, by },
         PointC[2] = { 0.5, cy },
         PointD[2] = { -0.5, cy },
         PointE[2] = { -bx, by };

     glClear(GL_COLOR_BUFFER_BIT);
     // 按照A->C->E->B->D->A的順序,可以一筆將五角星畫出
     glBegin(GL_LINE_LOOP);
         glVertex2fv(PointA);
         glVertex2fv(PointC);
         glVertex2fv(PointE);
         glVertex2fv(PointB);
         glVertex2fv(PointD);
     glEnd();
     glFlush();
}


例三、畫出正弦函數的圖形
/
由于OpenGL默認坐標值只能從-1到1,(可以修改,但方法留到以后講)
所以我們設置一個因子factor,把所有的坐標值等比例縮小,
這樣就可以畫出更多個正弦周期
試修改factor的值,觀察變化情況
/

include <math.h>

const GLfloat factor = 0.1f;
void myDisplay(void)
{
     GLfloat x;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_LINES);
         glVertex2f(-1.0f, 0.0f);
         glVertex2f(1.0f, 0.0f);         // 以上兩個點可以畫x軸
         glVertex2f(0.0f, -1.0f);
         glVertex2f(0.0f, 1.0f);         // 以上兩個點可以畫y軸
     glEnd();
     glBegin(GL_LINE_STRIP);
     for(x=-1.0f/factor; x<1.0f/factor; x+=0.01f)
     {
         glVertex2f(xfactor, sin(x)factor);
     }
     glEnd();
     glFlush();
}


小結
本課講述了點、直線和多邊形的概念,以及如何使用OpenGL來描述點,并使用點來描述幾何圖形。
大家可以發揮自己的想象,畫出各種幾何圖形,當然,也可以用GL_LINE_STRIP把很多位置相近的點連接起來,構成函數圖象。如果有興趣,也可以去找一些圖象比較美觀的函數,自己動手,用OpenGL把它畫出來。

=====================    第二課 完    =====================
=====================TO BE CONTINUED=====================

OpenGL入門學習[三]</p>

在第二課中,我們學習了如何繪制幾何圖形,但大家如果多寫幾個程序,就會發現其實還是有些郁悶之處。例如:點太小,難以看清楚;直線也太細,不舒服;或者想畫虛線,但不知道方法只能用許多短直線,甚至用點組合而成。

這些問題將在本課中被解決。

下面就點、直線、多邊形分別討論。

1、關于點

點的大小默認為1個像素,但也可以改變之。改變的命令為glPointSize,其函數原型如下:

void glPointSize(GLfloat size);

size必須大于0.0f,默認值為1.0f,單位為“像素”。

注意:對于具體的OpenGL實現,點的大小都有個限度的,如果設置的size超過最大值,則設置可能會有問題。

例子:

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glPointSize(5.0f);

     glBegin(GL_POINTS);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.5f, 0.5f);

     glEnd();

     glFlush();

}

2、關于直線

(1)直線可以指定寬度:

void glLineWidth(GLfloat width);

其用法跟glPointSize類似。

(2)畫虛線。

首先,使用glEnable(GL_LINE_STIPPLE);來啟動虛線模式(使用glDisable(GL_LINE_STIPPLE)可以關閉之)。

然后,使用glLineStipple來設置虛線的樣式。

void glLineStipple(GLint factor, GLushort pattern);

pattern是由1和0組成的長度為16的序列,從最低位開始看,如果為1,則直線上接下來應該畫的factor個點將被畫為實的;如果為0,則直線上接下來應該畫的factor個點將被畫為虛的。

以下是一些例子:

聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。

示例代碼:

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_LINE_STIPPLE);

     glLineStipple(2, 0x0F0F);

     glLineWidth(10.0f);

     glBegin(GL_LINES);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.5f, 0.5f);

     glEnd();

     glFlush();

}

3、關于多邊形

多邊形的內容較多,我們將講述以下四個方面。

(1)多邊形的兩面以及繪制方式。

雖然我們目前還沒有真正的使用三維坐標來畫圖,但是建立一些三維的概念還是必要的。

從三維的角度來看,一個多邊形具有兩個面。每一個面都可以設置不同的繪制方式:填充、只繪制邊緣輪廓線、只繪制頂點,其中“填充”是默認的方式。可以為兩個面分別設置不同的方式。

glPolygonMode(GL_FRONT, GL_FILL);            // 設置正面為填充方式

glPolygonMode(GL_BACK, GL_LINE);             // 設置反面為邊緣繪制方式

glPolygonMode(GL_FRONT_AND_BACK, GL_POINT); // 設置兩面均為頂點繪制方式

(2)反轉

一般約定為“頂點以逆時針順序出現在屏幕上的面”為“正面”,另一個面即成為“反面”。生活中常見的物體表面,通常都可以用這樣的“正面”和“反面”,“合理的”被表現出來(請找一個比較透明的礦泉水瓶子,在正對你的一面沿逆時針畫一個圓,并標明畫的方向,然后將背面轉為正面,畫一個類似的圓,體會一下“正面”和“反面”。你會發現正對你的方向,瓶的外側是正面,而背對你的方向,瓶的內側才是正面。正對你的內側和背對你的外側則是反面。這樣一來,同樣屬于“瓶的外側”這個表面,但某些地方算是正面,某些地方卻算是反面了)。

但也有一些表面比較特殊。例如“麥比烏斯帶”(請自己Google一下),可以全部使用“正面”或全部使用“背面”來表示。

可以通過glFrontFace函數來交換“正面”和“反面”的概念。

glFrontFace(GL_CCW);   // 設置CCW方向為“正面”,CCW即CounterClockWise,逆時針

glFrontFace(GL_CW);    // 設置CW方向為“正面”,CW即ClockWise,順時針

下面是一個示例程序,請用它替換第一課中的myDisplay函數,并將glFrontFace(GL_CCW)修改為glFrontFace(GL_CW),并觀察結果的變化。

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glPolygonMode(GL_FRONT, GL_FILL); // 設置正面為填充模式

     glPolygonMode(GL_BACK, GL_LINE);   // 設置反面為線形模式

     glFrontFace(GL_CCW);               // 設置逆時針方向為正面

     glBegin(GL_POLYGON);               // 按逆時針繪制一個正方形,在左下方

         glVertex2f(-0.5f, -0.5f);

         glVertex2f(0.0f, -0.5f);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(-0.5f, 0.0f);

     glEnd();

     glBegin(GL_POLYGON);               // 按順時針繪制一個正方形,在右上方

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.0f, 0.5f);

         glVertex2f(0.5f, 0.5f);

         glVertex2f(0.5f, 0.0f);

     glEnd();

     glFlush();

}

(3)剔除多邊形表面

在三維空間中,一個多邊形雖然有兩個面,但我們無法看見背面的那些多邊形,而一些多邊形雖然是正面的,但被其他多邊形所遮擋。如果將無法看見的多邊形和可見的多邊形同等對待,無疑會降低我們處理圖形的效率。在這種時候,可以將不必要的面剔除。

首先,使用glEnable(GL_CULL_FACE);來啟動剔除功能(使用glDisable(GL_CULL_FACE)可以關閉之)

然后,使用glCullFace來進行剔除。

glCullFace的參數可以是GL_FRONT,GL_BACK或者GL_FRONT_AND_BACK,分別表示剔除正面、剔除反面、剔除正反兩面的多邊形。

注意:剔除功能只影響多邊形,而對點和直線無影響。例如,使用glCullFace(GL_FRONT_AND_BACK)后,所有的多邊形都將被剔除,所以看見的就只有點和直線。

(4)鏤空多邊形

直線可以被畫成虛線,而多邊形則可以進行鏤空。

首先,使用glEnable(GL_POLYGON_STIPPLE);來啟動鏤空模式(使用glDisable(GL_POLYGON_STIPPLE)可以關閉之)。

然后,使用glPolygonStipple來設置鏤空的樣式。

void glPolygonStipple(const GLubyte *mask);

其中的參數mask指向一個長度為128字節的空間,它表示了一個32*32的矩形應該如何鏤空。其中:第一個字節表示了最左下方的從左到右(也可以是從右到左,這個可以修改)8個像素是否鏤空(1表示不鏤空,顯示該像素;0表示鏤空,顯示其后面的顏色),最后一個字節表示了最右上方的8個像素是否鏤空。

但是,如果我們直接定義這個mask數組,像這樣:

static GLubyte Mask[128] =

{

     0x00, 0x00, 0x00, 0x00,    //   這是最下面的一行

     0x00, 0x00, 0x00, 0x00,

     0x03, 0x80, 0x01, 0xC0,    //   麻

     0x06, 0xC0, 0x03, 0x60,    //   煩

     0x04, 0x60, 0x06, 0x20,    //   的

     0x04, 0x30, 0x0C, 0x20,    //   初

     0x04, 0x18, 0x18, 0x20,    //   始

     0x04, 0x0C, 0x30, 0x20,    //   化

     0x04, 0x06, 0x60, 0x20,    //   ,

     0x44, 0x03, 0xC0, 0x22,    //   不

     0x44, 0x01, 0x80, 0x22,    //   建

     0x44, 0x01, 0x80, 0x22,    //   議

     0x44, 0x01, 0x80, 0x22,    //   使

     0x44, 0x01, 0x80, 0x22,    //   用

     0x44, 0x01, 0x80, 0x22,

     0x44, 0x01, 0x80, 0x22,

     0x66, 0x01, 0x80, 0x66,

     0x33, 0x01, 0x80, 0xCC,

     0x19, 0x81, 0x81, 0x98,

     0x0C, 0xC1, 0x83, 0x30,

     0x07, 0xE1, 0x87, 0xE0,

     0x03, 0x3F, 0xFC, 0xC0,

     0x03, 0x31, 0x8C, 0xC0,

     0x03, 0x3F, 0xFC, 0xC0,

     0x06, 0x64, 0x26, 0x60,

     0x0C, 0xCC, 0x33, 0x30,

     0x18, 0xCC, 0x33, 0x18,

     0x10, 0xC4, 0x23, 0x08,

     0x10, 0x63, 0xC6, 0x08,

     0x10, 0x30, 0x0C, 0x08,

     0x10, 0x18, 0x18, 0x08,

     0x10, 0x00, 0x00, 0x08    // 這是最上面的一行

};

這樣一堆數據非常缺乏直觀性,我們需要很費勁的去分析,才會發現它表示的竟然是一只蒼蠅。

如果將這樣的數據保存成圖片,并用專門的工具進行編輯,顯然會方便很多。下面介紹如何做到這一點。

首先,用Windows自帶的畫筆程序新建一副圖片,取名為mask.bmp,注意保存時,應該選擇“單色位圖”。在“圖象”->“屬性”對話框中,設置圖片的高度和寬度均為32。

用放大鏡觀察圖片,并編輯之。黑色對應二進制零(鏤空),白色對應二進制一(不鏤空),編輯完畢后保存。

然后,就可以使用以下代碼來獲得這個Mask數組了。

static GLubyte Mask[128];

FILE *fp;

fp = fopen("mask.bmp", "rb");

if( !fp )

     exit(0);

// 移動文件指針到這個位置,使得再讀sizeof(Mask)個字節就會遇到文件結束

// 注意-(int)sizeof(Mask)雖然不是什么好的寫法,但這里它確實是正確有效的

// 如果直接寫-sizeof(Mask)的話,因為sizeof取得的是一個無符號數,取負號會有問題

if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )

     exit(0);

// 讀取sizeof(Mask)個字節到Mask

if( !fread(Mask, sizeof(Mask), 1, fp) )

     exit(0);

fclose(fp);

好的,現在請自己編輯一個圖片作為mask,并用上述方法取得Mask數組,運行后觀察效果。

說明:繪制虛線時可以設置factor因子,但多邊形的鏤空無法設置factor因子。請用鼠標改變窗口的大小,觀察鏤空效果的變化情況。

#include <stdio.h>

#include <stdlib.h>

void myDisplay(void)

{

     static GLubyte Mask[128];

     FILE *fp;

     fp = fopen("mask.bmp", "rb");

     if( !fp )

         exit(0);

     if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )

         exit(0);

     if( !fread(Mask, sizeof(Mask), 1, fp) )

         exit(0);

     fclose(fp);

     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_POLYGON_STIPPLE);

     glPolygonStipple(Mask);

     glRectf(-0.5f, -0.5f, 0.0f, 0.0f);   // 在左下方繪制一個有鏤空效果的正方形

     glDisable(GL_POLYGON_STIPPLE);

     glRectf(0.0f, 0.0f, 0.5f, 0.5f);     // 在右上方繪制一個無鏤空效果的正方形

     glFlush();

}

小結

本課學習了繪制幾何圖形的一些細節。

點可以設置大小。

直線可以設置寬度;可以將直線畫成虛線。

多邊形的兩個面的繪制方法可以分別設置;在三維空間中,不可見的多邊形可以被剔除;可以將填充多邊形繪制成鏤空的樣式。

了解這些細節會使我們在一些圖象繪制中更加得心應手。

另外,把一些數據寫到程序之外的文件中,并用專門的工具編輯之,有時可以顯得更方便。

=====================    第三課 完    =====================

=====================TO BE CONTINUED=====================



OpenGL入門學習[四]

2008-10-06 21:26

本次學習的是顏色的選擇。終于要走出黑白的世界了~~


OpenGL支持兩種顏色模式:一種是RGBA,一種是顏色索引模式。
無論哪種顏色模式,計算機都必須為每一個像素保存一些數據。不同的是,RGBA模式中,數據直接就代表了顏色;而顏色索引模式中,數據代表的是一個索引,要得到真正的顏色,還必須去查索引表。

1. RGBA顏色
RGBA模式中,每一個像素會保存以下數據:R值(紅色分量)、G值(綠色分量)、B值(藍色分量)和A值(alpha分量)。其中紅、綠、藍三種顏色相組合,就可以得到我們所需要的各種顏色,而alpha不直接影響顏色,它將留待以后介紹。
在RGBA模式下選擇顏色是十分簡單的事情,只需要一個函數就可以搞定。
glColor系列函數可以用于設置顏色,其中三個參數的版本可以指定R、G、B的值,而A值采用默認;四個參數的版本可以分別指定R、G、B、A的值。例如:
void glColor3f(GLfloat red, GLfloat green, GLfloat blue);
void glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
(還記得嗎?3f表示有三個浮點參數~請看第二課中關于glVertex
函數的敘述。)
將浮點數作為參數,其中0.0表示不使用該種顏色,而1.0表示將該種顏色用到最多。例如:
glColor3f(1.0f, 0.0f, 0.0f);    表示不使用綠、藍色,而將紅色使用最多,于是得到最純凈的紅色。
glColor3f(0.0f, 1.0f, 1.0f);    表示使用綠、藍色到最多,而不使用紅色。混合的效果就是淺藍色。
glColor3f(0.5f, 0.5f, 0.5f);    表示各種顏色使用一半,效果為灰色。
注意:浮點數可以精確到小數點后若干位,這并不表示計算機就可以顯示如此多種顏色。實際上,計算機可以顯示的顏色種數將由硬件決定。如果OpenGL找不到精確的顏色,會進行類似“四舍五入”的處理。

大家可以通過改變下面代碼中glColor3f的參數值,繪制不同顏色的矩形。
void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glColor3f(0.0f, 1.0f, 1.0f);
     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
     glFlush();
}

注意:glColor系列函數,在參數類型不同時,表示“最大”顏色的值也不同。
采用f和d做后綴的函數,以1.0表示最大的使用。
采用b做后綴的函數,以127表示最大的使用。
采用ub做后綴的函數,以255表示最大的使用。
采用s做后綴的函數,以32767表示最大的使用。
采用us做后綴的函數,以65535表示最大的使用。
這些規則看似麻煩,但熟悉后實際使用中不會有什么障礙。

2、索引顏色
在索引顏色模式中,OpenGL需要一個顏色表。這個表就相當于畫家的調色板:雖然可以調出很多種顏色,但同時存在于調色板上的顏色種數將不會超過調色板的格數。試將顏色表的每一項想象成調色板上的一個格子:它保存了一種顏色。
在使用索引顏色模式畫圖時,我說“我把第i種顏色設置為某某”,其實就相當于將調色板的第i格調為某某顏色。“我需要第k種顏色來畫圖”,那么就用畫筆去蘸一下第k格調色板。
顏色表的大小是很有限的,一般在256~4096之間,且總是2的整數次冪。在使用索引顏色方式進行繪圖時,總是先設置顏色表,然后選擇顏色。

2.1、選擇顏色
使用glIndex*系列函數可以在顏色表中選擇顏色。其中最常用的可能是glIndexi,它的參數是一個整形。
void glIndexi(GLint c);
是的,這的確很簡單。

2.2、設置顏色表
OpenGL 并直接沒有提供設置顏色表的方法,因此設置顏色表需要使用操作系統的支持。我們所用的Windows和其他大多數圖形操作系統都具有這個功能,但所使用的函數卻不相同。正如我沒有講述如何自己寫代碼在Windows下建立一個窗口,這里我也不會講述如何在Windows下設置顏色表。
GLUT工具包提供了設置顏色表的函數glutSetColor,但我測試始終有問題。現在為了讓大家體驗一下索引顏色,我向大家介紹另一個OpenGL 工具包: aux。這個工具包是VisualStudio自帶的,不必另外安裝,但它已經過時,這里僅僅是體驗一下,大家不必深入。

include <windows.h>

include <GL/gl.h>

include <GL/glaux.h>


pragma comment (lib, "opengl32.lib")

pragma comment (lib, "glaux.lib")


include <math.h>

const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
     int i;
     for(i=0; i<8; ++i)
         auxSetOneColor(i, (float)(i&0x04), (float)(i&0x02), (float)(i&0x01));
     glShadeModel(GL_FLAT);
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLE_FAN);
     glVertex2f(0.0f, 0.0f);
     for(i=0; i<=8; ++i)
     {
         glIndexi(i);
         glVertex2f(cos(iPi/4), sin(iPi/4));
     }
     glEnd();
     glFlush();
}

int main(void)
{
     auxInitDisplayMode(AUX_SINGLE|AUX_INDEX);
     auxInitPosition(0, 0, 400, 400);
     auxInitWindow(L"");
     myDisplay();
     Sleep(10 * 1000);
     return 0;
}

其它部分大家都可以不管,只看myDisplay函數就可以了。首先,使用auxSetOneColor設置顏色表中的一格。循環八次就可以設置八格。
glShadeModel等下再講,這里不提。
然后在循環中用glVertex設置頂點,同時用glIndexi改變頂點代表的顏色。
最終得到的效果是八個相同形狀、不同顏色的三角形。

索引顏色雖然講得多了點。索引顏色的主要優勢是占用空間小(每個像素不必單獨保存自己的顏色,只用很少的二進制位就可以代表其顏色在顏色表中的位置),花費系統資源少,圖形運算速度快,但它編程稍稍顯得不是那么方便,并且畫面效果也會比RGB顏色差一些。“星際爭霸”可能代表了256色的顏色表的畫面效果,雖然它在一臺很爛的PC上也可以運行很流暢,但以目前的眼光來看,其畫面效果就顯得不足了。
目前的PC機性能已經足夠在各種場合下使用RGB顏色,因此PC程序開發中,使用索引顏色已經不是主流。當然,一些小型設備例如GBA、手機等,索引顏色還是有它的用武之地。


3、指定清除屏幕用的顏色
我們寫:glClear(GL_COLOR_BUFFER_BIT);意思是把屏幕上的顏色清空。
但實際上什么才叫“空”呢?在宇宙中,黑色代表了“空”;在一張白紙上,白色代表了“空”;在信封上,信封的顏色才是“空”。
OpenGL用下面的函數來定義清楚屏幕后屏幕所擁有的顏色。
在RGB模式下,使用glClearColor來指定“空”的顏色,它需要四個參數,其參數的意義跟glColor4f相似。
在索引顏色模式下,使用glClearIndex來指定“空”的顏色所在的索引,它需要一個參數,其意義跟glIndexi相似。
void myDisplay(void)
{
     glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
     glClear(GL_COLOR_BUFFER_BIT);
     glFlush();
}
呵,這個還真簡單~


4、指定著色模型
OpenGL允許為同一多邊形的不同頂點指定不同的顏色。例如:

include <math.h>

const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
     int i;
     // glShadeModel(GL_FLAT);
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLE_FAN);
     glColor3f(1.0f, 1.0f, 1.0f);
     glVertex2f(0.0f, 0.0f);
     for(i=0; i<=8; ++i)
     {
         glColor3f(i&0x04, i&0x02, i&0x01);
         glVertex2f(cos(iPi/4), sin(iPi/4));
     }
     glEnd();
     glFlush();
}
在默認情況下,OpenGL會計算兩點頂點之間的其它點,并為它們填上“合適”的顏色,使相鄰的點的顏色值都比較接近。如果使用的是RGB模式,看起來就具有漸變的效果。如果是使用顏色索引模式,則其相鄰點的索引值是接近的,如果將顏色表中接近的項設置成接近的顏色,則看起來也是漸變的效果。但如果顏色表中接近的項顏色卻差距很大,則看起來可能是很奇怪的效果。
使用glShadeModel函數可以關閉這種計算,如果頂點的顏色不同,則將頂點之間的其它點全部設置為與某一個點相同。(直線以后指定的點的顏色為準,而多邊形將以任意頂點的顏色為準,由實現決定。)為了避免這個不確定性,盡量在多邊形中使用同一種顏色。
glShadeModel的使用方法:
glShadeModel(GL_SMOOTH);    // 平滑方式,這也是默認方式
glShadeModel(GL_FLAT);      // 單色方式

小結:
本課學習了如何設置顏色。其中RGB顏色方式是目前PC機上的常用方式。
可以設置glClear清除后屏幕所剩的顏色。
可以設置顏色填充方式:平滑方式或單色方式。

=====================    第四課 完    =====================
=====================TO BE CONTINUED=====================</div> </div> </td> </tr> </tbody> </table>



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函數可以將當前的可視空間設置為透視投影空間。其參數的意義如下圖:

聲明:該圖片來自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。

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

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



OpenGL入門學習[六]


今天要講的是動畫制作——可能是各位都很喜歡的。除了講授知識外,我們還會讓昨天那個“太陽、地球和月亮”天體圖畫動起來。緩和一下枯燥的氣氛。


本次課程,我們將進入激動人心的計算機動畫世界。

想必大家都知道電影和動畫的工作原理吧?是的,快速的把看似連續的畫面一幅幅的呈現在人們面前。一旦每秒鐘呈現的畫面超過24幅,人們就會錯以為它是連續的。
我們通常觀看的電視,每秒播放25或30幅畫面。但對于計算機來說,它可以播放更多的畫面,以達到更平滑的效果。如果速度過慢,畫面不夠平滑。如果速度過快,則人眼未必就能反應得過來。對于一個正常人來說,每秒60~120幅圖畫是比較合適的。具體的數值因人而異。

假設某動畫一共有n幅畫面,則它的工作步驟就是:
顯示第1幅畫面,然后等待一小段時間,直到下一個1/24秒
顯示第2幅畫面,然后等待一小段時間,直到下一個1/24秒
……
顯示第n幅畫面,然后等待一小段時間,直到下一個1/24秒
結束
如果用C語言偽代碼來描述這一過程,就是:
for(i=0; i<n; ++i)
{
     DrawScene(i);
     Wait();
}


1、雙緩沖技術
在計算機上的動畫與實際的動畫有些不同:實際的動畫都是先畫好了,播放的時候直接拿出來顯示就行。計算機動畫則是畫一張,就拿出來一張,再畫下一張,再拿出來。如果所需要繪制的圖形很簡單,那么這樣也沒什么問題。但一旦圖形比較復雜,繪制需要的時間較長,問題就會變得突出。
讓我們把計算機想象成一個畫圖比較快的人,假如他直接在屏幕上畫圖,而圖形比較復雜,則有可能在他只畫了某幅圖的一半的時候就被觀眾看到。而后面雖然他把畫補全了,但觀眾的眼睛卻又沒有反應過來,還停留在原來那個殘缺的畫面上。也就是說,有時候觀眾看到完整的圖象,有時卻又只看到殘缺的圖象,這樣就造成了屏幕的閃爍。
如何解決這一問題呢?我們設想有兩塊畫板,畫圖的人在旁邊畫,畫好以后把他手里的畫板與掛在屏幕上的畫板相交換。這樣以來,觀眾就不會看到殘缺的畫了。這一技術被應用到計算機圖形中,稱為雙緩沖技術。即:在存儲器(很有可能是顯存)中開辟兩塊區域,一塊作為發送到顯示器的數據,一塊作為繪畫的區域,在適當的時候交換它們。由于交換兩塊內存區域實際上只需要交換兩個指針,這一方法效率非常高,所以被廣泛的采用。
注意:雖然絕大多數平臺都支持雙緩沖技術,但這一技術并不是OpenGL標準中的內容。OpenGL為了保證更好的可移植性,允許在實現時不使用雙緩沖技術。當然,我們常用的PC都是支持雙緩沖技術的。
要啟動雙緩沖功能,最簡單的辦法就是使用GLUT工具包。我們以前在main函數里面寫:
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
其中GLUT_SINGLE表示單緩沖,如果改成GLUT_DOUBLE就是雙緩沖了。
當然還有需要更改的地方——每次繪制完成時,我們需要交換兩個緩沖區,把繪制好的信息用于屏幕顯示(否則無論怎么繪制,還是什么都看不到)。如果使用GLUT工具包,也可以很輕松的完成這一工作,只要在繪制完成時簡單的調用glutSwapBuffers函數就可以了。


2、實現連續動畫
似乎沒有任何疑問,我們應該把繪制動畫的代碼寫成下面這個樣子:
for(i=0; i<n; ++i)
{
     DrawScene(i);
     glutSwapBuffers();
     Wait();
}
但事實上,這樣做不太符合窗口系統的程序設計思路。還記得我們的第一個OpenGL程序嗎?我們在main函數里寫:glutDisplayFunc(&myDisplay);
意思是對系統說:如果你需要繪制窗口了,請調用myDisplay這個函數。為什么我們不直接調用myDisplay,而要采用這種看似“舍近求遠”的做法呢?原因在于——我們自己的程序無法掌握究竟什么時候該繪制窗口。因為一般的窗口系統——拿我們熟悉一點的來說——Windows和X窗口系統,都是支持同時顯示多個窗口的。假如你的程序窗口碰巧被別的窗口遮住了,后來用戶又把原來遮住的窗口移開,這時你的窗口需要重新繪制。很不幸的,你無法知道這一事件發生的具體時間。因此這一切只好委托操作系統來辦了。
現在我們再看上面那個循環。既然DrawScene都可以交給操作系統來代辦了,那讓整個循環運行起來的工作是否也可以交給操作系統呢?答案是肯定的。我們先前的思路是:繪制,然后等待一段時間;再繪制,再等待一段時間。但如果去掉等待的時間,就變成了繪制,繪制,……,不停的繪制。——當然了,資源是公用的嘛,殺毒軟件總要工作吧?我的下載不能停下來吧?我的mp3播放還不能給耽擱了。總不能因為我們的動畫,讓其他的工作都停下來。因此,我們需要在 CPU空閑的時間繪制。
這里的“在CPU空閑的時間繪制”和我們在第一課講的“在需要繪制的時候繪制”有些共通,都是“在XX時間做XX事”,GLUT工具包也提供了一個比較類似的函數:glutIdleFunc,表示在CPU空閑的時間調用某一函數。其實GLUT還提供了一些別的函數,例如“在鍵盤按下時做某事”等。

到現在,我們已經可以初步開始制作動畫了。好的,就拿上次那個“太陽、地球和月亮”的程序開刀,讓地球和月亮自己動起來。

Code:


#include <GL/glut.h>

// 太陽、地球和月亮
// 假設每個月都是30天
// 一年12個月,共是360天
static int day = 200; // day的變化:從0到359
void myDisplay(void)
{
     /****************************************************
      這里的內容照搬上一課的,只因為使用了雙緩沖,補上最后這句
     *****************************************************/
     glutSwapBuffers();
}

void myIdle(void)
{
     /* 新的函數,在空閑時調用,作用是把日期往后移動一天并重新繪制,達到動畫效果 */
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了參數為GLUT_DOUBLE
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太陽,地球和月亮");    // 改了窗口標題
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);                // 新加入了這句
     glutMainLoop();
     return 0;
}


3、關于垂直同步
代碼是寫好了,但相信大家還有疑問。某些朋友可能在運行時發現,雖然CPU幾乎都用上了,但運動速度很快,根本看不清楚,另一些朋友在運行時發現CPU使用率很低,根本就沒有把空閑時間完全利用起來。但對于上面那段代碼來說,這些現象都是合理的。這里就牽涉到關于垂直同步的問題。

大家知道顯示器的刷新率是比較有限的,一般為60~120Hz,也就是一秒鐘刷新60~120次。但如果叫計算機繪制一個簡單的畫面,例如只有一個三角形,則一秒鐘可以繪制成千上萬次。因此,如果最大限度的利用計算機的處理能力,繪制很多幅畫面,但顯示器的刷新速度卻跟不上,這不僅造成性能的浪費,還可能帶來一些負面影響(例如,顯示器只刷新到一半時,需要繪制的內容卻變化了,由于顯示器是逐行刷新的,于是顯示器上半部分和下半部分實際上是來自兩幅畫面)。采用垂直同步技術可以解決這一問題。即,只有在顯示器刷新時,才把繪制好的圖象傳輸出去供顯示。這樣一來,計算機就不必去繪制大量的根本就用不到的圖象了。如果顯示器的刷新率為85Hz,則計算機一秒鐘只需要繪制85幅圖象就足夠,如果場景足夠簡單,就會造成比較多的CPU空閑。
幾乎所有的顯卡都支持“垂直同步”這一功能。
垂直同步也有它的問題。如果刷新頻率為60Hz,則在繪制比較簡單的場景時,繪制一幅圖畫需要的時間很段,幀速可以恒定在60FPS(即60幀/秒)。如果場景變得復雜,繪制一幅圖畫的時間超過了1/60秒,則幀速將急劇下降。
如果繪制一幅圖畫的時間為1/50,則在第一個1/60秒時,顯示器需要刷新了,但由于新的圖畫沒有畫好,所以只能顯示原來的圖畫,等到下一個1/60秒時才顯示新的圖畫。于是顯示一幅圖畫實際上用了1/30秒,幀速為30FPS。(如果不采用垂直同步,則幀速應該是50FPS)
如果繪制一幅圖畫的時間更長,則下降的趨勢就是階梯狀的:60FPS,30FPS,20FPS,……(60/1,60/2,60/3,……)
如果每一幅圖畫的復雜程度是不一致的,且繪制它們需要的時間都在1/60上下。則在1/60時間內畫完時,幀速為60FPS,在1/60時間未完成時,幀速為30FPS,這就造成了幀速的跳動。這是很麻煩的事情,需要避免它——要么想辦法簡化每一畫面的繪制時間,要么都延遲一小段時間,以作到統一。

回過頭來看前面的問題。如果使用了大量的CPU而且速度很快無法看清,則打開垂直同步可以解決該問題。當然如果你認為垂直同步有這樣那樣的缺點,也可以關閉它。——至于如何打開和關閉,因操作系統而異了。具體步驟請自己搜索之。

當然,也有其它辦法可以控制動畫的幀速,或者盡量讓動畫的速度盡量和幀速無關。不過這里面很多內容都是與操作系統比較緊密的,況且它們跟OpenGL關系也不太大。這里就不做介紹了。


4、計算幀速
不知道大家玩過3D Mark這個軟件沒有,它可以運行各種場景,測出幀速,并且為你的系統給出評分。這里我也介紹一個計算幀速的方法。
根據定義,幀速就是一秒鐘內播放的畫面數目(FPS)。我們可以先測量繪制兩幅畫面之間時間t,然后求它的倒數即可。假如t=0.05s,則FPS的值就是1/0.05=20。
理論上是如此了,可是如何得到這個時間呢?通常C語言的time函數精確度一般只到一秒,肯定是不行了。clock函數也就到十毫秒左右,還是有點不夠。因為FPS為60和FPS為100的時候,t的值都是十幾毫秒。
你知道如何測量一張紙的厚度嗎?一個粗略的辦法就是:用很多張紙疊在一起測厚度,計算平均值就可以了。我們這里也可以這樣辦。測量繪制50幅畫面(包括垂直同步等因素的等待時間)需要的時間t',由t'=t*50很容易的得到FPS=1/t=50/t'
下面這段代碼可以統計該函數自身的調用頻率,(原理就像上面說的那樣),程序并不復雜,并且這并不屬于OpenGL的內容,所以我不打算詳細講述它。

Code:


#include <time.h>
double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}



最后,要把計算的幀速顯示出來,但我們并沒有學習如何使用OpenGL把文字顯示到屏幕上。——但不要忘了,在我們的圖形窗口背后,還有一個命令行窗口~使用printf函數就可以輕易的輸出文字了。

include <stdio.h>


double FPS = CalFrequency();
printf("FPS = %f\n", FPS);
最后的一步,也被我們解決了——雖然做法不太雅觀,沒關系,以后我們還會改善它的。


時間過得太久,每次給的程序都只是一小段,一些朋友難免會出問題。
現在,我給出一個比較完整的程序,供大家參考。</p>

Code:


#include <GL/glut.h>

include <stdio.h>

include <time.h>


// 太陽、地球和月亮
// 假設每個月都是12天
// 一年12個月,共是360天
static int day = 200; // day的變化:從0到359

double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}

void myDisplay(void)
{
     double FPS = CalFrequency();
     printf("FPS = %f\n", FPS);

     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.0360.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.0360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char
argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太陽,地球和月亮");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
} </p>



小結:
OpenGL動畫和傳統意義上的動畫相似,都是把畫面一幅一幅的呈現在觀眾面前。一旦畫面變換的速度快了,觀眾就會認為畫面是連續的。
雙緩沖技術是一種在計算機圖形中普遍采用的技術,絕大多數OpenGL實現都支持雙緩沖技術。
通常都是利用CPU空閑的時候繪制動畫,但也可以有其它的選擇。
介紹了垂直同步的相關知識。
介紹了一種簡單的計算幀速(FPS)的方法。
最后,我們列出了一份完整的天體動畫程序清單。

=====================    第六課 完    =====================
=====================TO BE CONTINUED=====================




OpenGL入門學習[七]


今天要講的是OpenGL光照的基本知識。雖然內容顯得有點多,但條理還算比較清晰,理解起來應該沒有困難。即使對于一些內容沒有記住,問題也不大——光照部分是一個比較獨立的內容,它的學習與其它方面的學習可以分開,不像視圖變換那樣,影響到許多方面。課程的最后給出了一個有關光照效果的動畫演示程序,我想大家會喜歡的。
從生理學的角度上講,眼睛之所以看見各種物體,是因為光線直接或間接的從它們那里到達了眼睛。人類對于光線強弱的變化的反應,比對于顏色變化的反應來得靈敏。因此對于人類而言,光線很大程度上表現了物體的立體感。
請看圖1,圖中繪制了兩個大小相同的白色球體。其中右邊的一個是沒有使用任何光照效果的,它看起來就像是一個二維的圓盤,沒有立體的感覺。左邊的一個是使用了簡單的光照效果的,我們通過光照的層次,很容易的認為它是一個三維的物體。

圖1

OpenGL對于光照效果提供了直接的支持,只需要調用某些函數,便可以實現簡單的光照效果。但是在這之前,我們有必要了解一些基礎知識。
一、建立光照模型
在現實生活中,某些物體本身就會發光,例如太陽、電燈等,而其它物體雖然不會發光,但可以反射來自其它物體的光。這些光通過各種方式傳播,最后進入我們的眼睛——于是一幅畫面就在我們的眼中形成了。
就目前的計算機而言,要準確模擬各種光線的傳播,這是無法做到的事情。比如一個四面都是粗糙墻壁的房間,一盞電燈所發出的光線在很短的時間內就會經過非常多次的反射,最終幾乎布滿了房間的每一個角落,這一過程即使使用目前運算速度最快的計算機,也無法精確模擬。不過,我們并不需要精確的模擬各種光線,只需要找到一種近似的計算方式,使它的最終結果讓我們的眼睛認為它是真實的,這就可以了。
OpenGL在處理光照時采用這樣一種近似:把光照系統分為三部分,分別是光源、材質和光照環境。光源就是光的來源,可以是前面所說的太陽或者電燈等。材質是指接受光照的各種物體的表面,由于物體如何反射光線只由物體表面決定(OpenGL中沒有考慮光的折射),材質特點就決定了物體反射光線的特點。光照環境是指一些額外的參數,它們將影響最終的光照畫面,比如一些光線經過多次反射后,已經無法分清它究竟是由哪個光源發出,這時,指定一個“環境亮度”參數,可以使最后形成的畫面更接近于真實情況。
在物理學中,光線如果射入理想的光滑平面,則反射后的光線是很規則的(這樣的反射稱為鏡面反射)。光線如果射入粗糙的、不光滑的平面,則反射后的光線是雜亂的(這樣的反射稱為漫反射)。現實生活中的物體在反射光線時,并不是絕對的鏡面反射或漫反射,但可以看成是這兩種反射的疊加。對于光源發出的光線,可以分別設置其經過鏡面反射和漫反射后的光線強度。對于被光線照射的材質,也可以分別設置光線經過鏡面反射和漫反射后的光線強度。這些因素綜合起來,就形成了最終的光照效果。

二、法線向量
根據光的反射定律,由光的入射方向和入射點的法線就可以得到光的出射方向。因此,對于指定的物體,在指定了光源后,即可計算出光的反射方向,進而計算出光照效果的畫面。在OpenGL中,法線的方向是用一個向量來表示。
不幸的是,OpenGL并不會根據你所指定的多邊形各個頂點來計算出這些多邊形所構成的物體的表面的每個點的法線(這話聽著有些迷糊),通常,為了實現光照效果,需要在代碼中為每一個頂點指定其法線向量。
指定法線向量的方式與指定顏色的方式有雷同之處。在指定顏色時,只需要指定每一個頂點的顏色,OpenGL就可以自行計算頂點之間的其它點的顏色。并且,顏色一旦被指定,除非再指定新的顏色,否則以后指定的所有頂點都將以這一向量作為自己的顏色。在指定法線向量時,只需要指定每一個頂點的法線向量,OpenGL會自行計算頂點之間的其它點的法線向量。并且,法線向量一旦被指定,除非再指定新的法線向量,否則以后指定的所有頂點都將以這一向量作為自己的法線向量。使用glColor函數可以指定顏色,而使用glNormal函數則可以指定法線向量。
注意:使用glTranslate函數或者glRotate函數可以改變物體的外觀,但法線向量并不會隨之改變。然而,使用glScale函數,對每一坐標軸進行不同程度的縮放,很有可能導致法線向量的不正確,雖然OpenGL提供了一些措施來修正這一問題,但由此也帶來了各種開銷。因此,在使用了法線向量的場合,應盡量避免使用glScale函數。即使使用,也最好保證各坐標軸進行等比例縮放。
三、控制光源
在OpenGL中,僅僅支持有限數量的光源。使用GL_LIGHT0表示第0號光源,GL_LIGHT1表示第1號光源,依次類推,OpenGL至少會支持8個光源,即GL_LIGHT0到GL_LIGHT7。使用glEnable函數可以開啟它們。例如,glEnable(GL_LIGHT0);可以開啟第0號光源。使用glDisable函數則可以關閉光源。一些OpenGL實現可能支持更多數量的光源,但總的來說,開啟過多的光源將會導致程序運行速度的嚴重下降,玩過3D Mark的朋友可能多少也有些體會。一些場景中可能有成百上千的電燈,這時可能需要采取一些近似的手段來進行編程,否則以目前的計算機而言,是無法運行這樣的程序的。
每一個光源都可以設置其屬性,這一動作是通過glLight函數完成的。glLight函數具有三個參數,第一個參數指明是設置哪一個光源的屬性,第二個參數指明是設置該光源的哪一個屬性,第三個參數則是指明把該屬性值設置成多少。光源的屬性眾多,下面將分別介紹。
(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR屬性。這三個屬性表示了光源所發出的光的反射特性(以及顏色)。每個屬性由四個值表示,分別代表了顏色的R, G, B, A值。GL_AMBIENT表示該光源所發出的光,經過非常多次的反射后,最終遺留在整個光照環境中的強度(顏色)。GL_DIFFUSE表示該光源所發出的光,照射到粗糙表面時經過漫反射,所得到的光的強度(顏色)。GL_SPECULAR表示該光源所發出的光,照射到光滑表面時經過鏡面反射,所得到的光的強度(顏色)。
(2)GL_POSITION屬性。表示光源所在的位置。由四個值(X, Y, Z, W)表示。如果第四個值W為零,則表示該光源位于無限遠處,前三個值表示了它所在的方向。這種光源稱為方向性光源,通常,太陽可以近似的被認為是方向性光源。如果第四個值W不為零,則X/W, Y/W, Z/W表示了光源的位置。這種光源稱為位置性光源。對于位置性光源,設置其位置與設置多邊形頂點的方式相似,各種矩陣變換函數例如:glTranslate、glRotate等在這里也同樣有效。方向性光源在計算時比位置性光源快了不少,因此,在視覺效果允許的情況下,應該盡可能的使用方向性光源。
(3)GL_SPOT_DIRECTION、GL_SPOT_EXPONENT、GL_SPOT_CUTOFF屬性。表示將光源作為聚光燈使用(這些屬性只對位置性光源有效)。很多光源都是向四面八方發射光線,但有時候一些光源則是只向某個方向發射,比如手電筒,只向一個較小的角度發射光線。 GL_SPOT_DIRECTION屬性有三個值,表示一個向量,即光源發射的方向。GL_SPOT_EXPONENT屬性只有一個值,表示聚光的程度,為零時表示光照范圍內向各方向發射的光線強度相同,為正數時表示光照向中央集中,正對發射方向的位置受到更多光照,其它位置受到較少光照。數值越大,聚光效果就越明顯。GL_SPOT_CUTOFF屬性也只有一個值,表示一個角度,它是光源發射光線所覆蓋角度的一半(見圖2),其取值范圍在0到90之間,也可以取180這個特殊值。取值為180時表示光源發射光線覆蓋360度,即不使用聚光燈,向全周圍發射。

圖2

(4)GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、 GL_QUADRATIC_ATTENUATION屬性。這三個屬性表示了光源所發出的光線的直線傳播特性(這些屬性只對位置性光源有效)。現實生活中,光線的強度隨著距離的增加而減弱,OpenGL把這個減弱的趨勢抽象成函數:
衰減因子 = 1 / (k1 + k2 d + k3 k3 d)
其中d表示距離,光線的初始強度乘以衰減因子,就得到對應距離的光線強度。k1, k2, k3分別就是GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION。通過設置這三個常數,就可以控制光線在傳播過程中的減弱趨勢。

屬性還真是不少。當然了,如果是使用方向性光源,(3)(4)這兩類屬性就不會用到了,問題就變得簡單明了。
四、控制材質
材質與光源相似,也需要設置眾多的屬性。不同的是,光源是通過glLight
函數來設置的,而材質則是通過glMaterial函數來設置的。
glMaterial
函數有三個參數。第一個參數表示指定哪一面的屬性。可以是GL_FRONT、GL_BACK或者 GL_FRONT_AND_BACK。分別表示設置“正面”“背面”的材質,或者兩面同時設置。(關于“正面”“背面”的內容需要參看前些課程的內容)第二、第三個參數與glLight函數的第二、三個參數作用類似。下面分別說明glMaterial函數可以指定的材質屬性。
(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR屬性。這三個屬性與光源的三個對應屬性類似,每一屬性都由四個值組成。 GL_AMBIENT表示各種光線照射到該材質上,經過很多次反射后最終遺留在環境中的光線強度(顏色)。GL_DIFFUSE表示光線照射到該材質上,經過漫反射后形成的光線強度(顏色)。GL_SPECULAR表示光線照射到該材質上,經過鏡面反射后形成的光線強度(顏色)。通常,GL_AMBIENT和GL_DIFFUSE都取相同的值,可以達到比較真實的效果。使用GL_AMBIENT_AND_DIFFUSE可以同時設置 GL_AMBIENT和GL_DIFFUSE屬性。
(2)GL_SHININESS屬性。該屬性只有一個值,稱為“鏡面指數”,取值范圍是0到128。該值越小,表示材質越粗糙,點光源發射的光線照射到上面,也可以產生較大的亮點。該值越大,表示材質越類似于鏡面,光源照射到上面后,產生較小的亮點。
(3)GL_EMISSION屬性。該屬性由四個值組成,表示一種顏色。OpenGL認為該材質本身就微微的向外發射光線,以至于眼睛感覺到它有這樣的顏色,但這光線又比較微弱,以至于不會影響到其它物體的顏色。
(4)GL_COLOR_INDEXES屬性。該屬性僅在顏色索引模式下使用,由于顏色索引模式下的光照比RGBA模式要復雜,并且使用范圍較小,這里不做討論。
五、選擇光照模型
這里所說的“光照模型”是OpenGL的術語,它相當于我們在前面提到的“光照環境”。在OpenGL中,光照模型包括四個部分的內容:全局環境光線(即那些充分散射,無法分清究竟來自哪個光源的光線)的強度、觀察點位置是在較近位置還是在無限遠處、物體正面與背面是否分別計算光照、鏡面顏色(即GL_SPECULAR屬性所指定的顏色)的計算是否從其它光照計算中分離出來,并在紋理操作以后在進行應用。
以上四方面的內容都通過同一個函數glLightModel*來進行設置。該函數有兩個參數,第一個表示要設置的項目,第二個參數表示要設置成的值。
GL_LIGHT_MODEL_AMBIENT表示全局環境光線強度,由四個值組成。
GL_LIGHT_MODEL_LOCAL_VIEWER表示是否在近處觀看,若是則設置為GL_TRUE,否則(即在無限遠處觀看)設置為GL_FALSE。
GL_LIGHT_MODEL_TWO_SIDE表示是否執行雙面光照計算。如果設置為GL_TRUE,則OpenGL不僅將根據法線向量計算正面的光照,也會將法線向量反轉并計算背面的光照。
GL_LIGHT_MODEL_COLOR_CONTROL表示顏色計算方式。如果設置為GL_SINGLE_COLOR,表示按通常順序操作,先計算光照,再計算紋理。如果設置為GL_SEPARATE_SPECULAR_COLOR,表示將GL_SPECULAR屬性分離出來,先計算光照的其它部分,待紋理操作完成后再計算GL_SPECULAR。后者通常可以使畫面效果更為逼真(當然,如果本身就沒有執行任何紋理操作,這樣的分離就沒有任何意義)。

六、最后的準備
到現在可以說是完事俱備了。不過,OpenGL默認是關閉光照處理的。要打開光照處理功能,使用下面的語句:
glEnable(GL_LIGHTING);
要關閉光照處理功能,使用glDisable(GL_LIGHTING);即可。
七、示例程序
到現在,我們已經可以編寫簡單的使用光照的OpenGL程序了。
我們仍然以太陽、地球作為例子(這次就不考慮月亮了^-^),把太陽作為光源,模擬地球圍繞太陽轉動時光照的變化。于是,需要設置一個光源——太陽,設置兩種材質——太陽的材質和地球的材質。把太陽光線設置為白色,位置在畫面正中。把太陽的材質設置為微微散發出紅色的光芒,把地球的材質設置為微微散發出暗淡的藍色光芒,并且反射藍色的光芒,鏡面指數設置成一個比較小的值。簡單起見,不再考慮太陽和地球的大小關系,用同樣大小的球體來代替之。
關于法線向量。球體表面任何一點的法線向量,就是球心到該點的向量。如果使用glutSolidSphere函數來繪制球體,則該函數會自動的指定這些法線向量,不必再手工指出。如果是自己指定若干的頂點來繪制一個球體,則需要自己指定法線響亮。
由于我們使用的太陽是一個位置性光源,在設置它的位置時,需要利用到矩陣變換。因此,在設置光源的位置以前,需要先設置好各種矩陣。利用gluPerspective函數來創建具有透視效果的視圖。我們也將利用前面課程所學習的動畫知識,讓整個畫面動起來。

下面給出具體的代碼:

include <gl/glut.h>


define WIDTH 400

define HEIGHT 400


static GLfloat angle = 0.0f;

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 創建透視效果視圖
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(90.0f, 1.0f, 1.0f, 20.0f);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0.0, 5.0, -10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

     // 定義太陽光源,它是一種白色的光源
     {
     GLfloat sun_light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     GLfloat sun_light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   sun_light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   sun_light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, sun_light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
     }

     // 定義太陽的材質并繪制太陽
     {
         GLfloat sun_mat_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_diffuse[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_emission[] = {0.5f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_shininess   = 0.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    sun_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    sun_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   sun_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   sun_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, sun_mat_shininess);

         glutSolidSphere(2.0, 40, 32);
     }

     // 定義地球的材質并繪制地球
     {
         GLfloat earth_mat_ambient[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_diffuse[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
         GLfloat earth_mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat earth_mat_shininess   = 30.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    earth_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    earth_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   earth_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   earth_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, earth_mat_shininess);

         glRotatef(angle, 0.0f, -1.0f, 0.0f);
         glTranslatef(5.0f, 0.0f, 0.0f);
         glutSolidSphere(2.0, 40, 32);
     }

     glutSwapBuffers();
}
void myIdle(void)
{
     angle += 1.0f;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}

int main(int argc, char argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(200, 200);
     glutInitWindowSize(WIDTH, HEIGHT);
     glutCreateWindow("OpenGL光照演示");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}
小結:
本課介紹了OpenGL光照的基本知識。OpenGL把光照分解為光源、材質、光照模式三個部分,根據這三個部分的各種信息,以及物體表面的法線向量,可以計算得到最終的光照效果。
光源、材質和光照模式都有各自的屬性,盡管屬性種類繁多,但這些屬性都只用很少的幾個函數來設置。使用glLight
函數可設置光源的屬性,使用glMaterial函數可設置材質的屬性,使用glLightModel函數可設置光照模式。
GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR這三種屬性是光源和材質所共有的,如果某光源發出的光線照射到某材質的表面,則最終的漫反射強度由兩個GL_DIFFUSE屬性共同決定,最終的鏡面反射強度由兩個GL_SPECULAR屬性共同決定。
可以使用多個光源來實現各種逼真的效果,然而,光源數量的增加將造成程序運行速度的明顯下降。
在使用OpenGL光照過程中,屬性的種類和數量都非常繁多,通常,需要很多的經驗才可以熟練的設置各種屬性,從而形成逼真的光照效果。(各位也看到了,其實這個課程的示例程序中,屬性設置也不怎么好)。然而,設置這些屬性的藝術性遠遠超過了技術性,往往是一些美術制作人員設置好各種屬性(并保存為文件),然后由程序員編寫的程序去執行繪制工作。因此,即使目前無法熟練運用各種屬性,也不必過于擔心。如果條件允許,可以玩玩類似3DS MAX之類的軟件,對理解光照、熟悉各種屬性設置會有一些幫助。
在課程的最后,我們給出了一個樣例程序,演示了太陽和地球模型中的光照效果。




OpenGL入門學習[八]</p>

今天介紹關于OpenGL顯示列表的知識。本課內容并不多,但需要一些理解能力。在學習時,可以將顯示列表與C語言的“函數”進行類比,加深體會。

我們已經知道,使用OpenGL其實只要調用一系列的OpenGL函數就可以了。然而,這種方式在一些時候可能導致問題。比如某個畫面中,使用了數千個多邊形來表現一個比較真實的人物,OpenGL為了產生這數千個多邊形,就需要不停的調用glVertex*函數,每一個多邊形將至少調用三次(因為多邊形至少有三個頂點),于是繪制一個比較真實的人物就需要調用上萬次的glVertex*函數。更糟糕的是,如果我們需要每秒鐘繪制60幅畫面,則每秒調用的glVertex*函數次數就會超過數十萬次,乃至接近百萬次。這樣的情況是我們所不愿意看到的。
同時,考慮這樣一段代碼:

const int segments = 100;
const GLfloat pi = 3.14f;
int i;
glLineWidth(10.0);
glBegin(GL_LINE_LOOP);
for(i=0; i<segments; ++i)
{
     GLfloat tmp = 2 * pi * i / segments;
     glVertex2f(cos(tmp), sin(tmp));
}
glEnd();


這段代碼將繪制一個圓環。如果我們在每次繪制圖象時調用這段代碼,則雖然可以達到繪制圓環的目的,但是cos、sin等開銷較大的函數被多次調用,浪費了 CPU資源。如果每一個頂點不是通過cos、sin等函數得到,而是使用更復雜的運算方式來得到,則浪費的現象就更加明顯。

經過分析,我們可以發現上述兩個問題的共同點:程序多次執行了重復的工作,導致CPU資源浪費和運行速度的下降。使用顯示列表可以較好的解決上述兩個問題。
在編寫程序時,遇到重復的工作,我們往往是將重復的工作編寫為函數,在需要的地方調用它。類似的,在編寫OpenGL程序時,遇到重復的工作,可以創建一個顯示列表,把重復的工作裝入其中,并在需要的地方調用這個顯示列表。
使用顯示列表一般有四個步驟:分配顯示列表編號、創建顯示列表、調用顯示列表、銷毀顯示列表。

一、分配顯示列表編號
OpenGL允許多個顯示列表同時存在,就好象C語言允許程序中有多個函數同時存在。C語言中,不同的函數用不同的名字來區分,而在OpenGL中,不同的顯示列表用不同的正整數來區分。
你可以自己指定一些各不相同的正整數來表示不同的顯示列表。但是如果你不夠小心,可能出現一個顯示列表將另一個顯示列表覆蓋的情況。為了避免這一問題,使用glGenLists函數來自動分配一個沒有使用的顯示列表編號。
glGenLists函數有一個參數i,表示要分配i個連續的未使用的顯示列表編號。返回的是分配的若干連續編號中最小的一個。例如,glGenLists(3);如果返回20,則表示分配了20、21、22這三個連續的編號。如果函數返回零,表示分配失敗。
可以使用glIsList函數判斷一個編號是否已經被用作顯示列表。

二、創建顯示列表
創建顯示列表實際上就是把各種OpenGL函數的調用裝入到顯示列表中。使用glNewList開始裝入,使用glEndList結束裝入。 glNewList有兩個參數,第一個參數是一個正整數表示裝入到哪個顯示列表。第二個參數有兩種取值,如果為GL_COMPILE,則表示以下的內容只是裝入到顯示列表,但現在不執行它們;如果為GL_COMPILE_AND_EXECUTE,表示在裝入的同時,把裝入的內容執行一遍。
例如,需要把“設置顏色為紅色,并且指定一個坐標為(0, 0)的頂點”這兩條命令裝入到編號為list的顯示列表中,并且在裝入的時候不執行,則可以用下面的代碼:
glNewList(list, GL_COMPILE);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();

注意:顯示列表只能裝入OpenGL函數,而不能裝入其它內容。例如:
int i = 3;
glNewList(list, GL_COMPILE);
if( i > 20 )
     glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();
其中if這個判斷就沒有被裝入到顯示列表。以后即使修改i的值,使i>20的條件成立,則glColor3f這個函數也不會被執行。因為它根本就不存在于顯示列表中。

另外,并非所有的OpenGL函數都可以裝入到顯示列表中。例如,各種用于查詢的函數,它們無法被裝入到顯示列表,因為它們都具有返回值,而 glCallList和glCallLists函數都不知道如何處理這些返回值。在網絡方式下,設置客戶端狀態的函數也無法被裝入到顯示列表,這是因為顯示列表被保存到服務器端,各種設置客戶端狀態的函數在發送到服務器端以前就被執行了,而服務器端無法執行這些函數。分配、創建、刪除顯示列表的動作也無法被裝入到另一個顯示列表,但調用顯示列表的動作則可以被裝入到另一個顯示列表。

三、調用顯示列表
使用glCallList函數可以調用一個顯示列表。該函數有一個參數,表示要調用的顯示列表的編號。例如,要調用編號為10的顯示列表,直接使用glCallList(10);就可以了。
使用glCallLists函數可以調用一系列的顯示列表。該函數有三個參數,第一個參數表示了要調用多少個顯示列表。第二個參數表示了這些顯示列表的編號的儲存格式,可以是GL_BYTE(每個編號用一個GLbyte表示),GL_UNSIGNED_BYTE(每個編號用一個GLubyte表示),GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT。第三個參數表示了這些顯示列表的編號所在的位置。在使用該函數前,需要用glListBase函數來設置一個偏移量。假設偏移量為k,且glCallLists中要求調用的顯示列表編號依次為l1, l2, l3, ...,則實際調用的顯示列表為l1+k, l2+k, l3+k, ...。
例如:
GLuint lists[] = {1, 3, 4, 8};
glListBase(10);
glCallLists(4, GL_UNSIGNED_INT, lists);
則實際上調用的是編號為11, 13, 14, 18的四個顯示列表。
注:“調用顯示列表”這個動作本身也可以被裝在另一個顯示列表中。

四、銷毀顯示列表
銷毀顯示列表可以回收資源。使用glDeleteLists來銷毀一串編號連續的顯示列表。
例如,使用glDeleteLists(20, 4);將銷毀20,21,22,23這四個顯示列表。
使用顯示列表將會帶來一些開銷,例如,把各種動作保存到顯示列表中會占用一定數量的內存資源。但如果使用得當,顯示列表可以提升程序的性能。這主要表現在以下方面:
1、明顯的減少OpenGL函數的調用次數。如果函數調用是通過網絡進行的(Linux等操作系統支持這樣的方式,即由應用程序在客戶端發出OpenGL請求,由網絡上的另一臺服務器進行實際的繪圖操作),將顯示列表保存在服務器端,可以大大減少網絡負擔。
2、保存中間結果,避免一些不必要的計算。例如前面的樣例程序中,cos、sin函數的計算結果被直接保存到顯示列表中,以后使用時就不必重復計算。
3、便于優化。我們已經知道,使用glTranslate*、glRotate*、glScale*等函數時,實際上是執行矩陣乘法操作,由于這些函數經常被組合在一起使用,通常會出現矩陣的連乘。這時,如果把這些操作保存到顯示列表中,則一些復雜的OpenGL版本會嘗試先計算出連乘的一部分結果,從而提高程序的運行速度。在其它方面也可能存在類似的例子。
同時,顯示列表也為程序的設計帶來方便。我們在設置一些屬性時,經常把一些相關的函數放在一起調用,(比如,把設置光源的各種屬性的函數放到一起)這時,如果把這些設置屬性的操作裝入到顯示列表中,則可以實現屬性的成組的切換。
當然了,即使使用顯示列表在某些情況下可以提高性能,但這種提高很可能并不明顯。畢竟,在硬件配置和大致的軟件算法都不變的前提下,性能可提升的空間并不大。
顯示列表的內容就是這么多了,下面我們看一個例子。
假設我們需要繪制一個旋轉的彩色正四面體,則可以這樣考慮:設置一個全局變量angle,然后讓它的值不斷的增加(到達360后又恢復為0,周而復始)。每次需要繪制圖形時,根據angle的值進行旋轉,然后繪制正四面體。這里正四面體采用顯示列表來實現,即把繪制正四面體的若干OpenGL函數裝到一個顯示列表中,然后每次需要繪制時,調用這個顯示列表即可。
將正四面體的四個頂點顏色分別設置為紅、黃、綠、藍,通過數學計算,將坐標設置為:
(-0.5, -5*sqrt(5)/48,   sqrt(3)/6),
( 0.5, -5*sqrt(5)/48,   sqrt(3)/6),
(    0, -5*sqrt(5)/48, -sqrt(3)/3),
(    0, 11*sqrt(6)/48,           0)
2007年4月24日修正:以上結果有誤,通過計算AB, AC, AD, BC, BD, CD的長度,發現AD, BD, CD的長度與1.0有較大偏差。正確的坐標應該是:
    A點:(   0.5,    -sqrt(6)/12, -sqrt(3)/6)
    B點:( -0.5,    -sqrt(6)/12, -sqrt(3)/6)
    C點:(     0,    -sqrt(6)/12,   sqrt(3)/3)
    D點:(     0,     sqrt(6)/4,            0)
    程序代碼中也做了相應的修改


下面給出程序代碼,大家可以從中體會一下顯示列表的用法。

#include <gl/glut.h>

define WIDTH 400

define HEIGHT 400


include <math.h>

define ColoredVertex(c, v) do{ glColor3fv(c); glVertex3fv(v); }while(0)


GLfloat angle = 0.0f;

void myDisplay(void)
{
     static int list = 0;
     if( list == 0 )
     {
         // 如果顯示列表不存在,則創建
        /* GLfloat
             PointA[] = {-0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointB[] = { 0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointC[] = {    0, -5*sqrt(5)/48, -sqrt(3)/3},
             PointD[] = {    0, 11*sqrt(6)/48,           0}; */

        // 2007年4月27日修改
         GLfloat
             PointA[] = { 0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointB[] = {-0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointC[] = { 0.0f, -sqrt(6.0f)/12,   sqrt(3.0f)/3},
             PointD[] = { 0.0f,    sqrt(6.0f)/4,              0};

         GLfloat
             ColorR[] = {1, 0, 0},
             ColorG[] = {0, 1, 0},
             ColorB[] = {0, 0, 1},
             ColorY[] = {1, 1, 0};

         list = glGenLists(1);
         glNewList(list, GL_COMPILE);
         glBegin(GL_TRIANGLES);
         // 平面ABC
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorB, PointC);
         // 平面ACD
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorY, PointD);
         // 平面CBD
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorY, PointD);
         // 平面BAD
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorY, PointD);
         glEnd();
         glEndList();

         glEnable(GL_DEPTH_TEST);
     }
     // 已經創建了顯示列表,在每次繪制正四面體時將調用它
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glPushMatrix();
     glRotatef(angle, 1, 0.5, 0);
     glCallList(list);
     glPopMatrix();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++angle;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}

int main(int argc, char* argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(200, 200);
     glutInitWindowSize(WIDTH, HEIGHT);
     glutCreateWindow("OpenGL 窗口");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}</div>

在程序中,我們將繪制正四面體的OpenGL函數裝到了一個顯示列表中,但是,關于旋轉的操作卻在顯示列表之外進行。這是因為如果把旋轉的操作也裝入到顯示列表,則每次旋轉的角度都是一樣的,不會隨著angle的值的變化而變化,于是就不能表現出動態的旋轉效果了。
程序運行時,可能感覺到畫面的立體感不足,這主要是因為沒有使用光照的緣故。如果將glColor3fv函數去掉,改為設置各種材質,然后開啟光照效果,則可以產生更好的立體感。大家可以自己試著使用光照效果,唯一需要注意的地方就是法線向量的計算。由于這里的正四面體四個頂點坐標選取得比較特殊,使得正四面體的中心坐標正好是(0, 0, 0),因此,每三個頂點坐標的平均值正好就是這三個頂點所組成的平面的法線向量的值。

void setNormal(GLfloat* Point1, GLfloat* Point2, GLfloat* Point3)
{
     GLfloat normal[3];
     int i;
     for(i=0; i<3; ++i)
         normal[i] = (Point1[i]+Point2[i]+Point3[i]) / 3;
     glNormal3fv(normal);
}


限于篇幅,這里就不給出完整的程序了。不過,大家可以自行嘗試,看看使用光照后效果有何種改觀。尤其是注意四面體各個表面交界的位置,在未使用光照前,幾乎看不清輪廓,在使用光照后,可比較容易的區分各個平面,因此立體感得到加強。(見圖1,圖2)當然了,這樣的效果還不夠。如果在各表面的交界處設置很多細小的平面,進行平滑處理,則光照后的效果將更真實。但這已經遠離本課的內容了。
圖一
圖二
小結
本課介紹了顯示列表的知識和簡單的應用。
可以把各種OpenGL函數調用的動作裝到顯示列表中,以后調用顯示列表,就相當于調用了其中的OpenGL函數。顯示列表中除了存放對OpenGL函數的調用外,不會存放其它內容。
使用顯示列表的過程是:分配一個未使用的顯示列表編號,把OpenGL函數調用裝入顯示列表,調用顯示列表,銷毀顯示列表。
使用顯示列表有可能帶來程序運行速度的提升,但是這種提升并不一定會很明顯。顯示列表本身也存在一定的開銷。
把繪制固定的物體的OpenGL函數放到一個顯示列表中,是一種不錯的編程思路。本課最后的例子中使用了這種思路。



OpenGL入門學習[九]


今天介紹關于OpenGL混合的基本知識。混合是一種常用的技巧,通常可以用來實現半透明。但其實它也是十分靈活的,你可以通過不同的設置得到不同的混合結果,產生一些有趣或者奇怪的圖象。
混合是什么呢?混合就是把兩種顏色混在一起。具體一點,就是把某一像素位置原來的顏色和將要畫上去的顏色,通過某種方式混在一起,從而實現特殊的效果。
假設我們需要繪制這樣一個場景:透過紅色的玻璃去看綠色的物體,那么可以先繪制綠色的物體,再繪制紅色玻璃。在繪制紅色玻璃的時候,利用“混合”功能,把將要繪制上去的紅色和原來的綠色進行混合,于是得到一種新的顏色,看上去就好像玻璃是半透明的。
要使用OpenGL的混合功能,只需要調用:glEnable(GL_BLEND);即可。
要關閉OpenGL的混合功能,只需要調用:glDisable(GL_BLEND);即可。
注意:只有在RGBA模式下,才可以使用混合功能,顏色索引模式下是無法使用混合功能的。
一、源因子和目標因子
前面我們已經提到,混合需要把原來的顏色和將要畫上去的顏色找出來,經過某種方式處理后得到一種新的顏色。這里把將要畫上去的顏色稱為“源顏色”,把原來的顏色稱為“目標顏色”。
OpenGL會把源顏色和目標顏色各自取出,并乘以一個系數(源顏色乘以的系數稱為“源因子”,目標顏色乘以的系數稱為“目標因子”),然后相加,這樣就得到了新的顏色。(也可以不是相加,新版本的OpenGL可以設置運算方式,包括加、減、取兩者中較大的、取兩者中較小的、邏輯運算等,但我們這里為了簡單起見,不討論這個了)
下面用數學公式來表達一下這個運算方式。假設源顏色的四個分量(指紅色,綠色,藍色,alpha值)是(Rs, Gs, Bs, As),目標顏色的四個分量是(Rd, Gd, Bd, Ad),又設源因子為(Sr, Sg, Sb, Sa),目標因子為(Dr, Dg, Db, Da)。則混合產生的新顏色可以表示為:
(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)
當然了,如果顏色的某一分量超過了1.0,則它會被自動截取為1.0,不需要考慮越界的問題。

源因子和目標因子是可以通過glBlendFunc函數來進行設置的。glBlendFunc有兩個參數,前者表示源因子,后者表示目標因子。這兩個參數可以是多種值,下面介紹比較常用的幾種。
GL_ZERO:      表示使用0.0作為因子,實際上相當于不使用這種顏色參與混合運算。
GL_ONE:       表示使用1.0作為因子,實際上相當于完全的使用了這種顏色參與混合運算。
GL_SRC_ALPHA:表示使用源顏色的alpha值來作為因子。
GL_DST_ALPHA:表示使用目標顏色的alpha值來作為因子。
GL_ONE_MINUS_SRC_ALPHA:表示用1.0減去源顏色的alpha值來作為因子。
GL_ONE_MINUS_DST_ALPHA:表示用1.0減去目標顏色的alpha值來作為因子。
除此以外,還有GL_SRC_COLOR(把源顏色的四個分量分別作為因子的四個分量)、GL_ONE_MINUS_SRC_COLOR、 GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前兩個在OpenGL舊版本中只能用于設置目標因子,后兩個在OpenGL 舊版本中只能用于設置源因子。新版本的OpenGL則沒有這個限制,并且支持新的GL_CONST_COLOR(設定一種常數顏色,將其四個分量分別作為因子的四個分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、 GL_ONE_MINUS_CONST_ALPHA。另外還有GL_SRC_ALPHA_SATURATE。新版本的OpenGL還允許顏色的alpha 值和RGB值采用不同的混合因子。但這些都不是我們現在所需要了解的。畢竟這還是入門教材,不需要整得太復雜~

舉例來說:
如果設置了glBlendFunc(GL_ONE, GL_ZERO);,則表示完全使用源顏色,完全不使用目標顏色,因此畫面效果和不使用混合的時候一致(當然效率可能會低一點點)。如果沒有設置源因子和目標因子,則默認情況就是這樣的設置。
如果設置了glBlendFunc(GL_ZERO, GL_ONE);,則表示完全不使用源顏色,因此無論你想畫什么,最后都不會被畫上去了。(但這并不是說這樣設置就沒有用,有些時候可能有特殊用途)
如果設置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,則表示源顏色乘以自身的alpha值,目標顏色乘以1.0減去源顏色的alpha值,這樣一來,源顏色的alpha值越大,則產生的新顏色中源顏色所占比例就越大,而目標顏色所占比例則減小。這種情況下,我們可以簡單的將源顏色的alpha值理解為“不透明度”。這也是混合時最常用的方式。
如果設置了glBlendFunc(GL_ONE, GL_ONE);,則表示完全使用源顏色和目標顏色,最終的顏色實際上就是兩種顏色的簡單相加。例如紅色(1, 0, 0)和綠色(0, 1, 0)相加得到(1, 1, 0),結果為黃色。
注意:
所謂源顏色和目標顏色,是跟繪制的順序有關的。假如先繪制了一個紅色的物體,再在其上繪制綠色的物體。則綠色是源顏色,紅色是目標顏色。如果順序反過來,則紅色就是源顏色,綠色才是目標顏色。在繪制時,應該注意順序,使得繪制的源顏色與設置的源因子對應,目標顏色與設置的目標因子對應。不要被混亂的順序搞暈了。
二、二維圖形混合舉例
下面看一個簡單的例子,實現將兩種不同的顏色混合在一起。為了便于觀察,我們繪制兩個矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,這兩個矩形有一個重疊的區域,便于我們觀察混合的效果。
先來看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的結果與不使用混合時相同。

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_BLEND);
     glBlendFunc(GL_ONE, GL_ZERO);

     glColor4f(1, 0, 0, 0.5);
     glRectf(-1, -1, 0.5, 0.5);
     glColor4f(0, 1, 0, 0.5);
     glRectf(-0.5, -0.5, 1, 1);

     glutSwapBuffers();
}


嘗試把glBlendFunc的參數修改為glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);以及glBlendFunc(GL_ONE, GL_ONE);,觀察效果。第一種情況下,效果與沒有使用混合時相同,后繪制的圖形會覆蓋先繪制的圖形。第二種情況下,alpha被當作“不透明度”,由于被設置為0.5,所以兩個矩形看上去都是半透明的,乃至于看到黑色背景。第三種是將顏色相加,紅色和綠色相加得到黃色。

三、實現三維混合
也許你迫不及待的想要繪制一個三維的帶有半透明物體的場景了。但是現在恐怕還不行,還有一點是在進行三維場景的混合時必須注意的,那就是深度緩沖。
深度緩沖是這樣一段數據,它記錄了每一個像素距離觀察者有多近。在啟用深度緩沖測試的情況下,如果將要繪制的像素比原來的像素更近,則像素將被繪制。否則,像素就會被忽略掉,不進行繪制。這在繪制不透明的物體時非常有用——不管是先繪制近的物體再繪制遠的物體,還是先繪制遠的物體再繪制近的物體,或者干脆以混亂的順序進行繪制,最后的顯示結果總是近的物體遮住遠的物體。
然而在你需要實現半透明效果時,發現一切都不是那么美好了。如果你繪制了一個近距離的半透明物體,則它在深度緩沖區內保留了一些信息,使得遠處的物體將無法再被繪制出來。雖然半透明的物體仍然半透明,但透過它看到的卻不是正確的內容了。
要解決以上問題,需要在繪制半透明物體時將深度緩沖區設置為只讀,這樣一來,雖然半透明物體被繪制上去了,深度緩沖區還保持在原來的狀態。如果再有一個物體出現在半透明物體之后,在不透明物體之前,則它也可以被繪制(因為此時深度緩沖區中記錄的是那個不透明物體的深度)。以后再要繪制不透明物體時,只需要再將深度緩沖區設置為可讀可寫的形式即可。嗯?你問我怎么繪制一個一部分半透明一部分不透明的物體?這個好辦,只需要把物體分為兩個部分,一部分全是半透明的,一部分全是不透明的,分別繪制就可以了。
即使使用了以上技巧,我們仍然不能隨心所欲的按照混亂順序來進行繪制。必須是先繪制不透明的物體,然后繪制透明的物體。否則,假設背景為藍色,近處一塊紅色玻璃,中間一個綠色物體。如果先繪制紅色半透明玻璃的話,它先和藍色背景進行混合,則以后繪制中間的綠色物體時,想單獨與紅色玻璃混合已經不能實現了。
總結起來,繪制順序就是:首先繪制所有不透明的物體。如果兩個物體都是不透明的,則誰先誰后都沒有關系。然后,將深度緩沖區設置為只讀。接下來,繪制所有半透明的物體。如果兩個物體都是半透明的,則誰先誰后只需要根據自己的意愿(注意了,先繪制的將成為“目標顏色”,后繪制的將成為“源顏色”,所以繪制的順序將會對結果造成一些影響)。最后,將深度緩沖區設置為可讀可寫形式。
調用glDepthMask(GL_FALSE);可將深度緩沖區設置為只讀形式。調用glDepthMask(GL_TRUE);可將深度緩沖區設置為可讀可寫形式。
一些網上的教程,包括大名鼎鼎的NeHe教程,都在使用三維混合時直接將深度緩沖區禁用,即調用glDisable(GL_DEPTH_TEST);。這樣做并不正確。如果先繪制一個不透明的物體,再在其背后繪制半透明物體,本來后面的半透明物體將不會被顯示(被不透明的物體遮住了),但如果禁用深度緩沖,則它仍然將會顯示,并進行混合。NeHe提到某些顯卡在使用glDepthMask函數時可能存在一些問題,但可能是由于我的閱歷有限,并沒有發現這樣的情況。

那么,實際的演示一下吧。我們來繪制一些半透明和不透明的球體。假設有三個球體,一個紅色不透明的,一個綠色半透明的,一個藍色半透明的。紅色最遠,綠色在中間,藍色最近。根據前面所講述的內容,紅色不透明球體必須首先繪制,而綠色和藍色則可以隨意修改順序。這里為了演示不注意設置深度緩沖的危害,我們故意先繪制最近的藍色球體,再繪制綠色球體。
為了讓這些球體有一點立體感,我們使用光照。在(1, 1, -1)處設置一個白色的光源。代碼如下:
void setLight(void)
{
     static const GLfloat light_position[] = {1.0f, 1.0f, -1.0f, 1.0f};
     static const GLfloat light_ambient[]   = {0.2f, 0.2f, 0.2f, 1.0f};
     static const GLfloat light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     static const GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
}
每一個球體顏色不同。所以它們的材質也都不同。這里用一個函數來設置材質。
void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)
{
     static const GLfloat mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
     static const GLfloat mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};

     glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
     glMaterialfv(GL_FRONT, GL_SPECULAR,   mat_specular);
     glMaterialfv(GL_FRONT, GL_EMISSION,   mat_emission);
     glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}
有了這兩個函數,我們就可以根據前面的知識寫出整個程序代碼了。這里只給出了繪制的部分,其它部分大家可以自行完成。
void myDisplay(void)
{
     // 定義一些材質顏色
     const static GLfloat red_color[] = {1.0f, 0.0f, 0.0f, 1.0f};
     const static GLfloat green_color[] = {0.0f, 1.0f, 0.0f, 0.3333f};
     const static GLfloat blue_color[] = {0.0f, 0.0f, 1.0f, 0.5f};

     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 啟動混合并設置混合因子
     glEnable(GL_BLEND);
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

     // 設置光源
     setLight();

     // 以(0, 0, 0.5)為中心,繪制一個半徑為.3的不透明紅色球體(離觀察者最遠)
     setMatirial(red_color, 30.0);
     glPushMatrix();
     glTranslatef(0.0f, 0.0f, 0.5f);
     glutSolidSphere(0.3, 30, 30);
     glPopMatrix();

     // 下面將繪制半透明物體了,因此將深度緩沖設置為只讀
     glDepthMask(GL_FALSE);

     // 以(0.2, 0, -0.5)為中心,繪制一個半徑為.2的半透明藍色球體(離觀察者最近)
     setMatirial(blue_color, 30.0);
     glPushMatrix();
     glTranslatef(0.2f, 0.0f, -0.5f);
     glutSolidSphere(0.2, 30, 30);
     glPopMatrix();

     // 以(0.1, 0, 0)為中心,繪制一個半徑為.15的半透明綠色球體(在前兩個球體之間)
     setMatirial(green_color, 30.0);
     glPushMatrix();
     glTranslatef(0.1, 0, 0);
     glutSolidSphere(0.15, 30, 30);
     glPopMatrix();

     // 完成半透明物體的繪制,將深度緩沖區恢復為可讀可寫的形式
     glDepthMask(GL_TRUE);

     glutSwapBuffers();
}

大家也可以將上面兩處glDepthMask刪去,結果會看到最近的藍色球雖然是半透明的,但它的背后直接就是紅色球了,中間的綠色球沒有被正確繪制。


小結:
本課介紹了OpenGL混合功能的相關知識。
混合就是在繪制時,不是直接把新的顏色覆蓋在原來舊的顏色上,而是將新的顏色與舊的顏色經過一定的運算,從而產生新的顏色。新的顏色稱為源顏色,原來舊的顏色稱為目標顏色。傳統意義上的混合,是將源顏色乘以源因子,目標顏色乘以目標因子,然后相加。
源因子和目標因子是可以設置的。源因子和目標因子設置的不同直接導致混合結果的不同。將源顏色的alpha值作為源因子,用1.0減去源顏色alpha值作為目標因子,是一種常用的方式。這時候,源顏色的alpha值相當于“不透明度”的作用。利用這一特點可以繪制出一些半透明的物體。
在進行混合時,繪制的順序十分重要。因為在繪制時,正要繪制上去的是源顏色,原來存在的是目標顏色,因此先繪制的物體就成為目標顏色,后來繪制的則成為源顏色。繪制的順序要考慮清楚,將目標顏色和設置的目標因子相對應,源顏色和設置的源因子相對應。
在進行三維混合時,不僅要考慮源因子和目標因子,還應該考慮深度緩沖區。必須先繪制所有不透明的物體,再繪制半透明的物體。在繪制半透明物體時前,還需要將深度緩沖區設置為只讀形式,否則可能出現畫面錯誤。





OpenGL入門學習[十]


今天我們先簡單介紹Windows中常用的BMP文件格式,然后講OpenGL的像素操作。雖然看起來內容可能有點多,但實際只有少量幾個知識點,如果讀者對諸如“顯示BMP圖象”等內容比較感興趣的話,可能不知不覺就看完了。
像素操作可以很復雜,這里僅涉及了簡單的部分,讓大家對OpenGL像素操作有初步的印象。
學過多媒體技術的朋友可能知道,計算機保存圖象的方法通常有兩種:一是“矢量圖”,一是“像素圖”。矢量圖保存了圖象中每一幾何物體的位置、形狀、大小等信息,在顯示圖象時,根據這些信息計算得到完整的圖象。“像素圖”是將完整的圖象縱橫分為若干的行、列,這些行列使得圖象被分割為很細小的分塊,每一分塊稱為像素,保存每一像素的顏色也就保存了整個圖象。
這兩種方法各有優缺點。“矢量圖”在圖象進行放大、縮小時很方便,不會失真,但如果圖象很復雜,那么就需要用非常多的幾何體,數據量和運算量都很龐大。“像素圖”無論圖象多么復雜,數據量和運算量都不會增加,但在進行放大、縮小等操作時,會產生失真的情況。
前面我們曾介紹了如何使用OpenGL來繪制幾何體,我們通過重復的繪制許多幾何體,可以繪制出一幅矢量圖。那么,應該如何繪制像素圖呢?這就是我們今天要學習的內容了。
1、BMP文件格式簡單介紹
BMP文件是一種像素文件,它保存了一幅圖象中所有的像素。這種文件格式可以保存單色位圖、16色或256色索引模式像素圖、24位真彩色圖象,每種模式種單一像素的大小分別為1/8字節,1/2字節,1字節和3字節。目前最常見的是256色BMP和24位色BMP。這種文件格式還定義了像素保存的幾種方法,包括不壓縮、RLE壓縮等。常見的BMP文件大多是不壓縮的。
這里為了簡單起見,我們僅討論24位色、不使用壓縮的BMP。(如果你使用Windows自帶的畫圖程序,很容易繪制出一個符合以上要求的BMP)
Windows所使用的BMP文件,在開始處有一個文件頭,大小為54字節。保存了包括文件格式標識、顏色數、圖象大小、壓縮方式等信息,因為我們僅討論 24位色不壓縮的BMP,所以文件頭中的信息基本不需要注意,只有“大小”這一項對我們比較有用。圖象的寬度和高度都是一個32位整數,在文件中的地址分別為0x0012和0x0016,于是我們可以使用以下代碼來讀取圖象的大小信息:

GLint width, height; // 使用OpenGL的GLint類型,它是32位的。
                      // 而C語言本身的int則不一定是32位的。
FILE* pFile;
// 在這里進行“打開文件”的操作
fseek(pFile, 0x0012, SEEK_SET);          // 移動到0x0012位置
fread(&width, sizeof(width), 1, pFile); // 讀取寬度
fseek(pFile, 0x0016, SEEK_SET);          // 移動到0x0016位置
                                         // 由于上一句執行后本就應該在0x0016位置
                                         // 所以這一句可省略
fread(&height, sizeof(height), 1, pFile); // 讀取高度

54個字節以后,如果是16色或256色BMP,則還有一個顏色表,但24位色BMP沒有這個,我們這里不考慮。接下來就是實際的像素數據了。24位色的BMP文件中,每三個字節表示一個像素的顏色。
注意,OpenGL通常使用RGB來表示顏色,但BMP文件則采用BGR,就是說,順序被反過來了。
另外需要注意的地方是:像素的數據量并不一定完全等于圖象的高度乘以寬度乘以每一像素的字節數,而是可能略大于這個值。原因是BMP文件采用了一種“對齊”的機制,每一行像素數據的長度若不是4的倍數,則填充一些數據使它是4的倍數。這樣一來,一個17*15的24位BMP大小就應該是834字節(每行 17個像素,有51字節,補充為52字節,乘以15得到像素數據總長度780,再加上文件開始的54字節,得到834字節)。分配內存時,一定要小心,不能直接使用“圖象的高度乘以寬度乘以每一像素的字節數”來計算分配空間的長度,否則有可能導致分配的內存空間長度不足,造成越界訪問,帶來各種嚴重后果。
一個很簡單的計算數據長度的方法如下:

int LineLength, TotalLength;
LineLength = ImageWidth * BytesPerPixel; // 每行數據長度大致為圖象寬度乘以
                                          // 每像素的字節數
while( LineLength % 4 != 0 )              // 修正LineLength使其為4的倍數
     ++LineLenth;
TotalLength = LineLength * ImageHeight;   // 數據總長 = 每行長度 * 圖象高度

這并不是效率最高的方法,但由于這個修正本身運算量并不大,使用頻率也不高,我們就不需要再考慮更快的方法了。
2、簡單的OpenGL像素操作
OpenGL提供了簡潔的函數來操作像素:
glReadPixels:讀取一些像素。當前可以簡單理解為“把已經繪制好的像素(它可能已經被保存到顯卡的顯存中)讀取到內存”。
glDrawPixels:繪制一些像素。當前可以簡單理解為“把內存中一些數據作為像素數據,進行繪制”。
glCopyPixels:復制一些像素。當前可以簡單理解為“把已經繪制好的像素從一個位置復制到另一個位置”。雖然從功能上看,好象等價于先讀取像素再繪制像素,但實際上它不需要把已經繪制的像素(它可能已經被保存到顯卡的顯存中)轉換為內存數據,然后再由內存數據進行重新的繪制,所以要比先讀取后繪制快很多。
這三個函數可以完成簡單的像素讀取、繪制和復制任務,但實際上也可以完成更復雜的任務。當前,我們僅討論一些簡單的應用。由于這幾個函數的參數數目比較多,下面我們分別介紹。
3、glReadPixels的用法和舉例
3.1 函數的參數說明
該函數總共有七個參數。前四個參數可以得到一個矩形,該矩形所包括的像素都會被讀取出來。(第一、二個參數表示了矩形的左下角橫、縱坐標,坐標以窗口最左下角為零,最右上角為最大值;第三、四個參數表示了矩形的寬度和高度)
第五個參數表示讀取的內容,例如:GL_RGB就會依次讀取像素的紅、綠、藍三種數據,GL_RGBA則會依次讀取像素的紅、綠、藍、alpha四種數據,GL_RED則只讀取像素的紅色數據(類似的還有GL_GREEN,GL_BLUE,以及GL_ALPHA)。如果采用的不是RGBA顏色模式,而是采用顏色索引模式,則也可以使用GL_COLOR_INDEX來讀取像素的顏色索引。目前僅需要知道這些,但實際上還可以讀取其它內容,例如深度緩沖區的深度數據等。
第六個參數表示讀取的內容保存到內存時所使用的格式,例如:GL_UNSIGNED_BYTE會把各種數據保存為GLubyte,GL_FLOAT會把各種數據保存為GLfloat等。
第七個參數表示一個指針,像素數據被讀取后,將被保存到這個指針所表示的地址。注意,需要保證該地址有足夠的可以使用的空間,以容納讀取的像素數據。例如一幅大小為256*256的圖象,如果讀取其RGB數據,且每一數據被保存為GLubyte,總大小就是:256*256*3 = 196608字節,即192千字節。如果是讀取RGBA數據,則總大小就是256*256*4 = 262144字節,即256千字節。

注意:glReadPixels實際上是從緩沖區中讀取數據,如果使用了雙緩沖區,則默認是從正在顯示的緩沖(即前緩沖)中讀取,而繪制工作是默認繪制到后緩沖區的。因此,如果需要讀取已經繪制好的像素,往往需要先交換前后緩沖。

再看前面提到的BMP文件中兩個需要注意的地方:
3.2 解決OpenGL常用的RGB像素數據與BMP文件的BGR像素數據順序不一致問題
可以使用一些代碼交換每個像素的第一字節和第三字節,使得RGB的數據變成BGR的數據。當然也可以使用另外的方式解決問題:新版本的OpenGL除了可以使用GL_RGB讀取像素的紅、綠、藍數據外,也可以使用GL_BGR按照相反的順序依次讀取像素的藍、綠、紅數據,這樣就與BMP文件格式相吻合了。即使你的gl/gl.h頭文件中沒有定義這個GL_BGR,也沒有關系,可以嘗試使用GL_BGR_EXT。雖然有的OpenGL實現(尤其是舊版本的實現)并不能使用GL_BGR_EXT,但我所知道的Windows環境下各種OpenGL實現都對GL_BGR提供了支持,畢竟Windows中各種表示顏色的數據幾乎都是使用BGR的順序,而非RGB的順序。這可能與IBM-PC的硬件設計有關。

3.3 消除BMP文件中“對齊”帶來的影響
實際上OpenGL也支持使用了這種“對齊”方式的像素數據。只要通過glPixelStore修改“像素保存時對齊的方式”就可以了。像這樣:
int alignment = 4;
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
第一個參數表示“設置像素的對齊值”,第二個參數表示實際設置為多少。這里像素可以單字節對齊(實際上就是不使用對齊)、雙字節對齊(如果長度為奇數,則再補一個字節)、四字節對齊(如果長度不是四的倍數,則補為四的倍數)、八字節對齊。分別對應alignment的值為1, 2, 4, 8。實際上,默認的值是4,正好與BMP文件的對齊方式相吻合。
glPixelStorei也可以用于設置其它各種參數。但我們這里并不需要深入討論了。


現在,我們已經可以把屏幕上的像素讀取到內存了,如果需要的話,我們還可以將內存中的數據保存到文件。正確的對照BMP文件格式,我們的程序就可以把屏幕中的圖象保存為BMP文件,達到屏幕截圖的效果。
我們并沒有詳細介紹BMP文件開頭的54個字節的所有內容,不過這無傷大雅。從一個正確的BMP文件中讀取前54個字節,修改其中的寬度和高度信息,就可以得到新的文件頭了。假設我們先建立一個1*1大小的24位色BMP,文件名為dummy.bmp,又假設新的BMP文件名稱為grab.bmp。則可以編寫如下代碼:

FILE* pOriginFile = fopen("dummy.bmp", "rb);
FILE* pGrabFile = fopen("grab.bmp", "wb");
char   BMP_Header[54];
GLint width, height;

/* 先在這里設置好圖象的寬度和高度,即width和height的值,并計算像素的總長度 */

// 讀取dummy.bmp中的頭54個字節到數組
fread(BMP_Header, sizeof(BMP_Header), 1, pOriginFile);
// 把數組內容寫入到新的BMP文件
fwrite(BMP_Header, sizeof(BMP_Header), 1, pGrabFile);

// 修改其中的大小信息
fseek(pGrabFile, 0x0012, SEEK_SET);
fwrite(&width, sizeof(width), 1, pGrabFile);
fwrite(&height, sizeof(height), 1, pGrabFile);

// 移動到文件末尾,開始寫入像素數據
fseek(pGrabFile, 0, SEEK_END);

/* 在這里寫入像素數據到文件 */

fclose(pOriginFile);
fclose(pGrabFile);
我們給出完整的代碼,演示如何把整個窗口的圖象抓取出來并保存為BMP文件。

#define WindowWidth   400

define WindowHeight 400


include <stdio.h>

include <stdlib.h>


/* 函數grab

  • 抓取窗口中的像素
  • 假設窗口寬度為WindowWidth,高度為WindowHeight
    /
    #define BMP_Header_Length 54
    void grab(void)
    {
         FILE
         pDummyFile;
         FILE     pWritingFile;
         GLubyte
    pPixelData;
         GLubyte   BMP_Header[BMP_Header_Length];
         GLint     i, j;
         GLint     PixelDataLength;

         // 計算像素數據的實際長度
         i = WindowWidth 3;    // 得到每一行的像素數據長度
         while( i%4 != 0 )       // 補充數據,直到i是的倍數
             ++i;                // 本來還有更快的算法,
                                // 但這里僅追求直觀,對速度沒有太高要求
         PixelDataLength = i
    WindowHeight;

         // 分配內存和打開文件
         pPixelData = (GLubyte)malloc(PixelDataLength);
         if( pPixelData == 0 )
             exit(0);

         pDummyFile = fopen("dummy.bmp", "rb");
         if( pDummyFile == 0 )
             exit(0);

         pWritingFile = fopen("grab.bmp", "wb");
         if( pWritingFile == 0 )
             exit(0);

         // 讀取像素
         glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
         glReadPixels(0, 0, WindowWidth, WindowHeight,
             GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

         // 把dummy.bmp的文件頭復制為新文件的文件頭
         fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
         fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
         fseek(pWritingFile, 0x0012, SEEK_SET);
         i = WindowWidth;
         j = WindowHeight;
         fwrite(&i, sizeof(i), 1, pWritingFile);
         fwrite(&j, sizeof(j), 1, pWritingFile);

         // 寫入像素數據
         fseek(pWritingFile, 0, SEEK_END);
         fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

         // 釋放內存和關閉文件
         fclose(pDummyFile);
         fclose(pWritingFile);
         free(pPixelData);
    }</div>



    把這段代碼復制到以前任何課程的樣例程序中,在繪制函數的最后調用grab函數,即可把圖象內容保存為BMP文件了。(在我寫這個教程的時候,不少地方都用這樣的代碼進行截圖工作,這段代碼一旦寫好,運行起來是很方便的。)
    4、glDrawPixels的用法和舉例
    glDrawPixels函數與glReadPixels函數相比,參數內容大致相同。它的第一、二、三、四個參數分別對應于 glReadPixels函數的第三、四、五、六個參數,依次表示圖象寬度、圖象高度、像素數據內容、像素數據在內存中的格式。兩個函數的最后一個參數也是對應的,glReadPixels中表示像素讀取后存放在內存中的位置,glDrawPixels則表示用于繪制的像素數據在內存中的位置。
    注意到glDrawPixels函數比glReadPixels函數少了兩個參數,這兩個參數在glReadPixels中分別是表示圖象的起始位置。在 glDrawPixels中,不必顯式的指定繪制的位置,這是因為繪制的位置是由另一個函數glRasterPos
    來指定的。glRasterPos 函數的參數與glVertex類似,通過指定一個二維/三維/四維坐標,OpenGL將自動計算出該坐標對應的屏幕位置,并把該位置作為繪制像素的起始位置。
    很自然的,我們可以從BMP文件中讀取像素數據,并使用glDrawPixels繪制到屏幕上。我們選擇Windows XP默認的桌面背景Bliss.bmp作為繪制的內容(如果你使用的是Windows XP系統,很可能可以在硬盤中搜索到這個文件。當然你也可以使用其它BMP文件來代替,只要它是24位的BMP文件。注意需要修改代碼開始部分的 FileName的定義),先把該文件復制一份放到正確的位置,我們在程序開始時,就讀取該文件,從而獲得圖象的大小后,根據該大小來創建合適的 OpenGL窗口,并繪制像素。
    繪制像素本來是很簡單的過程,但是這個程序在骨架上與前面的各種示例程序稍有不同,所以我還是打算給出一份完整的代碼。</p>

    #include <gl/glut.h>

    #define FileName "Bliss.bmp"

    static GLint     ImageWidth;
    static GLint     ImageHeight;
    static GLint     PixelLength;
    static GLubyte* PixelData;

    #include <stdio.h>
    #include <stdlib.h>

    void display(void)
    {
         // 清除屏幕并不必要
         // 每次繪制時,畫面都覆蓋整個屏幕
         // 因此無論是否清除屏幕,結果都一樣
         // glClear(GL_COLOR_BUFFER_BIT);

         // 繪制像素
         glDrawPixels(ImageWidth, ImageHeight,
             GL_BGR_EXT, GL_UNSIGNED_BYTE, PixelData);

         // 完成繪制
         glutSwapBuffers();
    }

    int main(int argc, char* argv[])
    {
         // 打開文件
         FILE* pFile = fopen("Bliss.bmp", "rb");
         if( pFile == 0 )
             exit(0);

         // 讀取圖象的大小信息
         fseek(pFile, 0x0012, SEEK_SET);
         fread(&ImageWidth, sizeof(ImageWidth), 1, pFile);
         fread(&ImageHeight, sizeof(ImageHeight), 1, pFile);

         // 計算像素數據長度
         PixelLength = ImageWidth * 3;
         while( PixelLength % 4 != 0 )
             ++PixelLength;
         PixelLength *= ImageHeight;

         // 讀取像素數據
         PixelData = (GLubyte*)malloc(PixelLength);
         if( PixelData == 0 )
             exit(0);

         fseek(pFile, 54, SEEK_SET);
         fread(PixelData, PixelLength, 1, pFile);

         // 關閉文件
         fclose(pFile);

         // 初始化GLUT并運行
         glutInit(&argc, argv);
         glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
         glutInitWindowPosition(100, 100);
         glutInitWindowSize(ImageWidth, ImageHeight);
         glutCreateWindow(FileName);
         glutDisplayFunc(&display);
         glutMainLoop();

         // 釋放內存
         // 實際上,glutMainLoop函數永遠不會返回,這里也永遠不會到達
         // 這里寫釋放內存只是出于一種個人習慣
         // 不用擔心內存無法釋放。在程序結束時操作系統會自動回收所有內存
         free(PixelData);

         return 0;
    }



    這里僅僅是一個簡單的顯示24位BMP圖象的程序,如果讀者對BMP文件格式比較熟悉,也可以寫出適用于各種BMP圖象的顯示程序,在像素處理時,它們所使用的方法是類似的。
    OpenGL在繪制像素之前,可以對像素進行若干處理。最常用的可能就是對整個像素圖象進行放大/縮小。使用glPixelZoom來設置放大/縮小的系數,該函數有兩個參數,分別是水平方向系數和垂直方向系數。例如設置glPixelZoom(0.5f, 0.8f);則表示水平方向變為原來的50%大小,而垂直方向變為原來的80%大小。我們甚至可以使用負的系數,使得整個圖象進行水平方向或垂直方向的翻轉(默認像素從左繪制到右,但翻轉后將從右繪制到左。默認像素從下繪制到上,但翻轉后將從上繪制到下。因此,glRasterPos*函數設置的“開始位置”不一定就是矩形的左下角)。
    5、glCopyPixels的用法和舉例
    從效果上看,glCopyPixels進行像素復制的操作,等價于把像素讀取到內存,再從內存繪制到另一個區域,因此可以通過 glReadPixels和glDrawPixels組合來實現復制像素的功能。然而我們知道,像素數據通常數據量很大,例如一幅1024*768的圖象,如果使用24位BGR方式表示,則需要至少1024*768*3字節,即2.25兆字節。這么多的數據要進行一次讀操作和一次寫操作,并且因為在 glReadPixels和glDrawPixels中設置的數據格式不同,很可能涉及到數據格式的轉換。這對CPU無疑是一個不小的負擔。使用 glCopyPixels直接從像素數據復制出新的像素數據,避免了多余的數據的格式轉換,并且也可能減少一些數據復制操作(因為數據可能直接由顯卡負責復制,不需要經過主內存),因此效率比較高。
    glCopyPixels函數也通過glRasterPos*系列函數來設置繪制的位置,因為不需要涉及到主內存,所以不需要指定數據在內存中的格式,也不需要使用任何指針。
    glCopyPixels函數有五個參數,第一、二個參數表示復制像素來源的矩形的左下角坐標,第三、四個參數表示復制像素來源的舉行的寬度和高度,第五個參數通常使用GL_COLOR,表示復制像素的顏色,但也可以是GL_DEPTH或GL_STENCIL,分別表示復制深度緩沖數據或模板緩沖數據。
    值得一提的是,glDrawPixels和glReadPixels中設置的各種操作,例如glPixelZoom等,在glCopyPixels函數中同樣有效。
    下面看一個簡單的例子,繪制一個三角形后,復制像素,并同時進行水平和垂直方向的翻轉,然后縮小為原來的一半,并繪制。繪制完畢后,調用前面的grab函數,將屏幕中所有內容保存為grab.bmp。其中WindowWidth和WindowHeight是表示窗口寬度和高度的常量。

    void display(void)
    {
         // 清除屏幕
         glClear(GL_COLOR_BUFFER_BIT);

         // 繪制
         glBegin(GL_TRIANGLES);
             glColor3f(1.0f, 0.0f, 0.0f);     glVertex2f(0.0f, 0.0f);
             glColor3f(0.0f, 1.0f, 0.0f);     glVertex2f(1.0f, 0.0f);
             glColor3f(0.0f, 0.0f, 1.0f);     glVertex2f(0.5f, 1.0f);
         glEnd();
         glPixelZoom(-0.5f, -0.5f);
         glRasterPos2i(1, 1);
         glCopyPixels(WindowWidth/2, WindowHeight/2,
             WindowWidth/2, WindowHeight/2, GL_COLOR);

         // 完成繪制,并抓取圖象保存為BMP文件
         glutSwapBuffers();
         grab();
    }




    小結:
    本課結合Windows系統常見的BMP圖象格式,簡單介紹了OpenGL的像素處理功能。包括使用glReadPixels讀取像素、glDrawPixels繪制像素、glCopyPixels復制像素。
    本課僅介紹了像素處理的一些簡單應用,但相信大家已經可以體會到,圍繞這三個像素處理函數,還存在一些“外圍”函數,比如glPixelStore*,glRasterPos*,以及glPixelZoom等。我們僅使用了這些函數的一少部分功能。
    本課內容并不多,例子足夠豐富,三個像素處理函數都有例子,大家可以結合例子來體會。



    OpenGL入門學習[十一]


    我們在前一課中,學習了簡單的像素操作,這意味著我們可以使用各種各樣的BMP文件來豐富程序的顯示效果,于是我們的OpenGL圖形程序也不再像以前總是只顯示幾個多邊形那樣單調了。——但是這還不夠。雖然我們可以將像素數據按照矩形進行縮小和放大,但是還不足以滿足我們的要求。例如要將一幅世界地圖繪制到一個球體表面,只使用glPixelZoom這樣的函數來進行縮放顯然是不夠的。OpenGL紋理映射功能支持將一些像素數據經過變換(即使是比較不規則的變換)將其附著到各種形狀的多邊形表面。紋理映射功能十分強大,利用它可以實現目前計算機動畫中的大多數效果,但是它也很復雜,我們不可能一次性的完全講解。這里的課程只是關于二維紋理的簡單使用。但即使是這樣,也會使我們的程序在顯示效果上邁出一大步。
    下面幾張圖片說明了紋理的效果。前兩張是我們需要的紋理,后一張是我們使用紋理后,利用OpenGL所產生出的效果。





    紋理的使用是非常復雜的。因此即使是入門教程,在編寫時我也多次進行刪改,很多東西都被精簡掉了,但本課的內容仍然較多,大家要有一點心理準備~
    1、啟用紋理和載入紋理
    就像我們曾經學習過的OpenGL光照、混合等功能一樣。在使用紋理前,必須啟用它。OpenGL支持一維紋理、二維紋理和三維紋理,這里我們僅介紹二維紋理。可以使用以下語句來啟用和禁用二維紋理:

         glEnable(GL_TEXTURE_2D);   // 啟用二維紋理
         glDisable(GL_TEXTURE_2D); // 禁用二維紋理



    使用紋理前,還必須載入紋理。利用glTexImage2D函數可以載入一個二維的紋理,該函數有多達九個參數(雖然某些參數我們可以暫時不去了解),現在分別說明如下:
    第一個參數為指定的目標,在我們的入門教材中,這個參數將始終使用GL_TEXTURE_2D。
    第二個參數為“多重細節層次”,現在我們并不考慮多重紋理細節,因此這個參數設置為零。
    第三個參數有兩種用法。在OpenGL 1.0,即最初的版本中,使用整數來表示顏色分量數目,例如:像素數據用RGB顏色表示,總共有紅、綠、藍三個值,因此參數設置為3,而如果像素數據是用 RGBA顏色表示,總共有紅、綠、藍、alpha四個值,因此參數設置為4。而在后來的版本中,可以直接使用GL_RGB或GL_RGBA來表示以上情況,顯得更直觀(并帶來其它一些好處,這里暫時不提)。注意:雖然我們使用Windows的BMP文件作為紋理時,一般是藍色的像素在最前,其真實的格式為GL_BGR而不是GL_RGB,在數據的順序上有所不同,但因為同樣是紅、綠、藍三種顏色,因此這里仍然使用GL_RGB。(如果使用 GL_BGR,OpenGL將無法識別這個參數,造成錯誤)
    第四、五個參數是二維紋理像素的寬度和高度。這里有一個很需要注意的地方:OpenGL在以前的很多版本中,限制紋理的大小必須是2的整數次方,即紋理的寬度和高度只能是16, 32, 64, 128, 256等值,直到最近的新版本才取消了這個限制。而且,一些OpenGL實現(例如,某些PC機上板載顯卡的驅動程序附帶的OpenGL)并沒有支持到如此高的OpenGL版本。因此在使用紋理時要特別注意其大小。盡量使用大小為2的整數次方的紋理,當這個要求無法滿足時,使用gluScaleImage 函數把圖象縮放至所指定的大小(在后面的例子中有用到)。另外,無論舊版本還是新版本,都限制了紋理大小的最大值,例如,某OpenGL實現可能要求紋理最大不能超過1024*1024。可以使用如下的代碼來獲得OpenGL所支持的最大紋理:

    GLint max;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);


    這樣max的值就是當前OpenGL實現中所支持的最大紋理。
    在很長一段時間內,很多圖形程序都喜歡使用256*256大小的紋理,不僅因為256是2的整數次方,也因為某些硬件可以使用8位的整數來表示紋理坐標,2的8次方正好是256,這一巧妙的組合為處理紋理坐標時的硬件優化創造了一些不錯的條件。

    第六個參數是紋理邊框的大小,我們沒有使用紋理邊框,因此這里設置為零。
    最后三個參數與glDrawPixels函數的最后三個參數的使用方法相同,其含義可以參考glReadPixels的最后三個參數。大家可以復習一下第10課的相關內容,這里不再重復。
    舉個例子,如果有一幅大小為width*height,格式為Windows系統中使用最普遍的24位BGR,保存在pixels中的像素圖象。則把這樣一幅圖象載入為紋理可使用以下代碼:

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);



    注意,載入紋理的過程可能比較慢,原因是紋理數據通常比較大,例如一幅512*512的BGR格式的圖象,大小為0.75M。把這些像素數據從主內存傳送到專門的圖形硬件,這個過程中還可能需要把程序中所指定的像素格式轉化為圖形硬件所能識別的格式(或最能發揮圖形硬件性能的格式),這些操作都需要較多時間。
    2、紋理坐標
    我們先來回憶一下之前學過的一點內容:
    當我們繪制一個三角形時,只需要指定三個頂點的顏色。三角形中其它各點的顏色不需要我們指定,這些點的顏色是OpenGL自己通過計算得到的。
    在我們學習OpneGL光照時,法線向量、材質的指定,都是只需要在頂點處指定一下就可以了,其它地方的法線向量和材質都是OpenGL自己通過計算去獲得。

    紋理的使用方法也與此類似。只要指定每一個頂點在紋理圖象中所對應的像素位置,OpenGL就會自動計算頂點以外的其它點在紋理圖象中所對應的像素位置。
    這聽起來比較令人迷惑。我們可以這樣類比一下:
    在繪制一條線段時,我們設置其中一個端點為紅色,另一個端點為綠色,則OpenGL會自動計算線段中其它各像素的顏色,如果是使用glShadeMode(GL_SMOOTH);,則最終會形成一種漸變的效果(例如線段中點,就是紅色和綠色的中間色)。
    類似的,在繪制一條線段時,我們設置其中一個端點使用“紋理圖象中最左下角的顏色”作為它的顏色,另一個端點使用“紋理圖象中最右上角的顏色”作為它的顏色,則OpenGL會自動在紋理圖象中選擇合適位置的顏色,填充到線段的各個像素(例如線段中點,可能就是選擇紋理圖象中央的那個像素的顏色)。

    我們在類比時,使用了“紋理圖象中最左下角的顏色”這種說法。但這種說法在很多時候不夠精確,我們需要一種精確的方式來表示我們究竟使用紋理中的哪個像素。紋理坐標也就是因為這樣的要求而產生的。以二維紋理為例,規定紋理最左下角的坐標為(0, 0),最右上角的坐標為(1, 1),于是紋理中的每一個像素的位置都可以用兩個浮點數來表示(三維紋理會用三個浮點數表示,一維紋理則只用一個即可)。
    使用glTexCoord*系列函數來指定紋理坐標。這些函數的用法與使用glVertex*系列函數來指定頂點坐標十分相似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)紋理坐標。
    通常,每個頂點使用不同的紋理,于是下面這樣形式的代碼是比較常見的。

    glBegin( /* ... */ );
         glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
         glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
         /* ... */
    glEnd();



    當我們用一個坐標表示頂點在三維空間的位置時,可以使用glRotate*等函數來對坐標進行轉換。紋理坐標也可以進行這種轉換。只要使用 glMatrixMode(GL_TEXTURE);,就可以切換到紋理矩陣(另外還有透視矩陣GL_PROJECTION和模型視圖矩陣 GL_MODELVIEW,詳細情況在第五課有講述),然后glRotate*,glScale*,glTranslate*等操作矩陣的函數就可以用來處理“對紋理坐標進行轉換”的工作了。在簡單應用中,可能不會對矩陣進行任何變換,這樣考慮問題會比較簡單。
    3、紋理參數
    到這里,入門所需要掌握的所有難點都被我們掌握了。但是,我們的知識仍然是不夠的,如果僅利用現有的知識去使用紋理的話,你可能會發現紋理完全不起作用。這是因為在使用紋理前還有某些參數是必須設置的。
    使用glTexParameter*系列函數來設置紋理參數。通常需要設置下面四個參數:
    GL_TEXTURE_MAG_FILTER:指當紋理圖象被使用到一個大于它的形狀上時(即:有可能紋理圖象中的一個像素會被應用到實際繪制時的多個像素。例如將一幅256*256的紋理圖象應用到一個512*512的正方形),應該如何處理。可選擇的設置有GL_NEAREST和GL_LINEAR,前者表示“使用紋理中坐標最接近的一個像素的顏色作為需要繪制的像素顏色”,后者表示“使用紋理中坐標最接近的若干個顏色,通過加權平均算法得到需要繪制的像素顏色”。前者只經過簡單比較,需要運算較少,可能速度較快,后者需要經過加權平均計算,其中涉及除法運算,可能速度較慢(但如果有專門的處理硬件,也可能兩者速度相同)。從視覺效果上看,前者效果較差,在一些情況下鋸齒現象明顯,后者效果會較好(但如果紋理圖象本身比較大,則兩者在視覺效果上就會比較接近)。
    GL_TEXTURE_MIN_FILTER:指當紋理圖象被使用到一個小于(或等于)它的形狀上時(即有可能紋理圖象中的多個像素被應用到實際繪制時的一個像素。例如將一幅256*256的紋理圖象應用到一個128*128的正方形),應該如何處理。可選擇的設置有 GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST 和GL_LINEAR_MIPMAP_LINEAR。其中后四個涉及到mipmap,現在暫時不需要了解。前兩個選項則和 GL_TEXTURE_MAG_FILTER中的類似。此參數似乎是必須設置的(在我的計算機上,不設置此參數將得到錯誤的顯示結果,但我目前并沒有找到根據)。
    GL_TEXTURE_WRAP_S:指當紋理坐標的第一維坐標值大于1.0或小于0.0時,應該如何處理。基本的選項有GL_CLAMP和 GL_REPEAT,前者表示“截斷”,即超過1.0的按1.0處理,不足0.0的按0.0處理。后者表示“重復”,即對坐標值加上一個合適的整數(可以是正數或負數),得到一個在[0.0, 1.0]范圍內的值,然后用這個值作為新的紋理坐標。例如:某二維紋理,在繪制某形狀時,一像素需要得到紋理中坐標為(3.5, 0.5)的像素的顏色,其中第一維的坐標值3.5超過了1.0,則在GL_CLAMP方式中將被轉化為(1.0, 0.5),在GL_REPEAT方式中將被轉化為(0.5, 0.5)。在后來的OpenGL版本中,又增加了新的處理方式,這里不做介紹。如果不指定這個參數,則默認為GL_REPEAT。
    GL_TEXTURE_WRAP_T:指當紋理坐標的第二維坐標值大于1.0或小于0.0時,應該如何處理。選項與GL_TEXTURE_WRAP_S類似,不再重復。如果不指定這個參數,則默認為GL_REPEAT。

    設置參數的代碼如下所示:

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

    4、紋理對象
    前面已經提到過,載入一幅紋理所需要的時間是比較多的。因此應該盡量減少載入紋理的次數。如果只有一幅紋理,則應該在第一次繪制前就載入它,以后就不需要再次載入了。這點與glDrawPixels函數很不相同。每次使用glDrawPixels函數,都需要把像素數據重新載入一次,因此用glDrawPixels函數來反復繪制圖象的效率是較低的(如果只繪制一次,則不會有此問題),使用紋理來反復繪制圖象是可取的做法。
    但是,在每次繪制時要使用兩幅或更多幅的紋理時,這個辦法就行不通了。你可能會編寫下面的代碼:

    glTexImage2D( /* ... */ ); // 載入第一幅紋理
    // 使用第一幅紋理
    glTexImage2D( /* ... */ ); // 載入第二幅紋理
    // 使用第二幅紋理
    // 當紋理的數量增加時,這段代碼會變得更加復雜。



    在繪制動畫時,由于每秒鐘需要將畫面繪制數十次,因此如果使用上面的代碼,就會反復載入紋理,這對計算機是非常大的負擔,以目前的個人計算機配置來說,根本就無法讓動畫能夠流暢的運行。因此,需要有一種機制,能夠在不同的紋理之間進行快速的切換。

    紋理對象正是這樣一種機制。我們可以把每一幅紋理(包括紋理的像素數據、紋理大小等信息,也包括了前面所講的紋理參數)放到一個紋理對象中,通過創建多個紋理對象來達到同時保存多幅紋理的目的。這樣一來,在第一次使用紋理前,把所有的紋理都載入,然后在繪制時只需要指明究竟使用哪一個紋理對象就可以了。

    使用紋理對象和使用顯示列表有相似之處:使用一個正整數來作為紋理對象的編號。在使用前,可以調用glGenTextures來分配紋理對象。該函數有兩種比較常見的用法:

    GLuint texture_ID;
    glGenTextures(1, &texture_ID); // 分配一個紋理對象的編號


    或者:

    GLuint texture_ID_list[5];
    glGenTextures(5, texture_ID_list); // 分配5個紋理對象的編號



    零是一個特殊的紋理對象編號,表示“默認的紋理對象”,在分配正確的情況下,glGenTextures不會分配這個編號。與glGenTextures對應的是glDeleteTextures,用于銷毀一個紋理對象。

    在分配了紋理對象編號后,使用glBindTexture函數來指定“當前所使用的紋理對象”。然后就可以使用glTexImage*系列函數來指定紋理像素、使用glTexParameter*系列函數來指定紋理參數、使用glTexCoord*系列函數來指定紋理坐標了。如果不使用 glBindTexture函數,那么glTexImage*、glTexParameter*、glTexCoord*系列函數默認在一個編號為0的紋理對象上進行操作。glBindTexture函數有兩個參數,第一個參數是需要使用紋理的目標,因為我們現在只學習二維紋理,所以指定為 GL_TEXTURE_2D,第二個參數是所使用的紋理的編號。
    使用多個紋理對象,就可以使OpenGL同時保存多個紋理。在使用時只需要調用glBindTexture函數,在不同紋理之間進行切換,而不需要反復載入紋理,因此動畫的繪制速度會有非常明顯的提升。典型的代碼如下所示:

    // 在程序開始時:分配好紋理編號,并載入紋理
    glGenTextures( /* ... */ );
    glBindTexture(GL_TEXTURE_2D, texture_ID_1);
    // 載入第一幅紋理
    glBindTexture(GL_TEXTURE_2D, texture_ID_2);
    // 載入第二幅紋理

    // 在繪制時,切換并使用紋理,不需要再進行載入
    glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅紋理
    // 使用第一幅紋理
    glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅紋理
    // 使用第二幅紋理



    提示:紋理對象是從OpenGL 1.1版開始才有的,最舊版本的OpenGL 1.0并沒有處理紋理對象的功能。不過,我想各位的機器不會是比OpenGL 1.1更低的版本(Windows 95就自帶了OpenGL 1.1版本,遺憾的是,Microsoft對OpenGL的支持并不積極,Windows XP也還采用1.1版本。據說Vista使用的是OpenGL 1.4版。當然了,如果安裝顯卡驅動的話,現在的主流顯卡一般都附帶了適用于該顯卡的OpenGL 1.4版或更高版本),所以這個問題也就不算是問題了。
    5、示例程序
    紋理入門所需要掌握的知識點就介紹到這里了。但是如果不實際動手操作的話,也是不可能真正掌握的。下面我們來看看本課開頭的那個紋理效果是如何實現的吧。
    因為代碼比較長,我把它拆分成了三段,大家如果要編譯的話,應該把三段代碼按順序連在一起編譯。如果要運行的話,除了要保證有一個名稱為 dummy.bmp,圖象大小為1*1的24位BMP文件,還要把本課開始的兩幅紋理圖片保存到正確位置(一幅名叫ground.bmp,另一幅名叫 wall.bmp。注意:我為了節省網絡空間,把兩幅圖片都轉成jpg格式了,讀者把圖片保存到本地后,需要把它們再轉化為BMP格式。可以使用 Windows XP帶的畫圖程序中的“另存為”功能完成這一轉換)。
    第一段代碼如下。其中的主體——grab函數,是我們在第十課介紹過的,這里僅僅是抄過來用一下,目的是為了將最終效果圖保存到一個名字叫grab.bmp的文件中。(當然了,為了保證程序的正確運行,那個大小為1*1的dummy.bmp文件仍然是必要的,參見第十課)

    #define WindowWidth   400
    #define WindowHeight 400
    #define WindowTitle  "OpenGL紋理測試"

    #include <gl/glut.h>
    #include <stdio.h>
    #include <stdlib.h>

    /* 函數grab
  • 抓取窗口中的像素
  • 假設窗口寬度為WindowWidth,高度為WindowHeight
    /
    #define BMP_Header_Length 54
    void grab(void)
    {
         FILE
         pDummyFile;
         FILE     pWritingFile;
         GLubyte
    pPixelData;
         GLubyte   BMP_Header[BMP_Header_Length];
         GLint     i, j;
         GLint     PixelDataLength;

         // 計算像素數據的實際長度
         i = WindowWidth 3;    // 得到每一行的像素數據長度
        while( i%4 != 0 )       // 補充數據,直到i是的倍數
             ++i;                // 本來還有更快的算法,
                                // 但這里僅追求直觀,對速度沒有太高要求
         PixelDataLength = i
    WindowHeight;

         // 分配內存和打開文件
         pPixelData = (GLubyte)malloc(PixelDataLength);
        if( pPixelData == 0 )
            exit(0);

         pDummyFile = fopen("dummy.bmp","rb");
        if( pDummyFile == 0 )
            exit(0);

         pWritingFile = fopen("grab.bmp","wb");
        if( pWritingFile == 0 )
            exit(0);

         // 讀取像素
         glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
         glReadPixels(0, 0, WindowWidth, WindowHeight,
             GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

         // 把dummy.bmp的文件頭復制為新文件的文件頭
        fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
        fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
        fseek(pWritingFile, 0x0012, SEEK_SET);
         i = WindowWidth;
         j = WindowHeight;
        fwrite(&i, sizeof(i), 1, pWritingFile);
        fwrite(&j, sizeof(j), 1, pWritingFile);

         // 寫入像素數據
        fseek(pWritingFile, 0, SEEK_END);
        fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

         // 釋放內存和關閉文件
        fclose(pDummyFile);
        fclose(pWritingFile);
        free(pPixelData);
    }</div>

    第二段代碼是我們的重點。它包括兩個函數。其中power_of_two比較簡單,雖然實現手段有點奇特,但也并非無法理解(即使真的無法理解,讀者也可以給出自己的解決方案,用一些循環以及多使用一些位操作也沒關系。反正,這里不是重點啦)。另一個load_texture函數卻是重頭戲:打開BMP文件、讀取其中的高度和寬度信息、計算像素數據所占的字節數、為像素數據分配空間、讀取像素數據、對像素圖象進行縮放(如果必要的話)、分配新的紋理編號、填寫紋理參數、載入紋理,所有的功能都在同一個函數里面完成了。為了敘述方便,我把所有的解釋都放在了注釋里。

    / 函數power_of_two
  • 檢查一個整數是否為2的整數次方,如果是,返回1,否則返回0
  • 實際上只要查看其二進制位中有多少個,如果正好有1個,返回1,否則返回0
  • 在“查看其二進制位中有多少個”時使用了一個小技巧
  • 使用n &= (n-1)可以使得n中的減少一個(具體原理大家可以自己思考)
    /
    int power_of_two(int n)
    {
        if( n <= 0 )
            return 0;
        return (n & (n-1)) == 0;
    }

    /
    函數load_texture
  • 讀取一個BMP文件作為紋理
  • 如果失敗,返回0,如果成功,返回紋理編號
    /
    GLuint load_texture(const char
    file_name)
    {
         GLint width, height, total_bytes;
         GLubyte pixels = 0;
         GLuint last_texture_ID, texture_ID = 0;

         // 打開文件,如果失敗,返回
         FILE
    pFile = fopen(file_name, "rb");
        if( pFile == 0 )
            return 0;

         // 讀取文件中圖象的寬度和高度
        fseek(pFile, 0x0012, SEEK_SET);
        fread(&width, 4, 1, pFile);
        fread(&height, 4, 1, pFile);
        fseek(pFile, BMP_Header_Length, SEEK_SET);

         // 計算每行像素所占字節數,并根據此數據計算總像素字節數
         {
             GLint line_bytes = width 3;
            while( line_bytes % 4 != 0 )
                 ++line_bytes;
             total_bytes = line_bytes
    height;
         }

         // 根據總像素字節數分配內存
         pixels = (GLubyte)malloc(total_bytes);
        if( pixels == 0 )
         {
            fclose(pFile);
            return 0;
         }

         // 讀取像素數據
        if( fread(pixels, total_bytes, 1, pFile) <= 0 )
         {
            free(pixels);
            fclose(pFile);
            return 0;
         }

         // 在舊版本的OpenGL中
         // 如果圖象的寬度和高度不是的整數次方,則需要進行縮放
         // 這里并沒有檢查OpenGL版本,出于對版本兼容性的考慮,按舊版本處理
         // 另外,無論是舊版本還是新版本,
         // 當圖象的寬度和高度超過當前OpenGL實現所支持的最大值時,也要進行縮放
         {
             GLint max;
             glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
            if( !power_of_two(width)
              || !power_of_two(height)
              || width > max
              || height > max )
             {
                const GLint new_width = 256;
                const GLint new_height = 256; // 規定縮放后新的大小為邊長的正方形
                 GLint new_line_bytes, new_total_bytes;
                 GLubyte
    new_pixels = 0;

                 // 計算每行需要的字節數和總字節數
                 new_line_bytes = new_width 3;
                while( new_line_bytes % 4 != 0 )
                     ++new_line_bytes;
                 new_total_bytes = new_line_bytes
    new_height;

                 // 分配內存
                 new_pixels = (GLubyte)malloc(new_total_bytes);
                if( new_pixels == 0 )
                 {
                    free(pixels);
                    fclose(pFile);
                    return 0;
                 }

                 // 進行像素縮放
                 gluScaleImage(GL_RGB,
                     width, height, GL_UNSIGNED_BYTE, pixels,
                     new_width, new_height, GL_UNSIGNED_BYTE, new_pixels);

                 // 釋放原來的像素數據,把pixels指向新的像素數據,并重新設置width和height
                free(pixels);
                 pixels = new_pixels;
                 width = new_width;
                 height = new_height;
             }
         }

         // 分配一個新的紋理編號
         glGenTextures(1, &texture_ID);
        if( texture_ID == 0 )
         {
            free(pixels);
            fclose(pFile);
            return 0;
         }

         // 綁定新的紋理,載入紋理并設置紋理參數
         // 在綁定前,先獲得原來綁定的紋理編號,以便在最后進行恢復
         glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture_ID);
         glBindTexture(GL_TEXTURE_2D, texture_ID);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
         glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
             GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
         glBindTexture(GL_TEXTURE_2D, last_texture_ID);

         // 之前為pixels分配的內存可在使用glTexImage2D以后釋放
         // 因為此時像素數據已經被OpenGL另行保存了一份(可能被保存到專門的圖形硬件中)
        free(pixels);
        return texture_ID;
    }</div>

    第三段代碼是關于顯示的部分,以及main函數。注意,我們只在main函數中讀取了兩幅紋理,并把它們保存在各自的紋理對象中,以后就再也不載入紋理。每次繪制時使用glBindTexture在不同的紋理對象中切換。另外,我們使用了超過1.0 的紋理坐標,由于GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T參數都被設置為GL_REPEAT,所以得到的效果就是紋理像素的重復,有點向地板磚的花紋那樣。讀者可以試著修改“墻”的紋理坐標,將5.0修改為10.0,看看效果有什么變化。

    / 兩個紋理對象的編號
    /
    GLuint texGround;
    GLuint texWall;

    void display(void)
    {
         // 清除屏幕
         glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

         // 設置視角
         glMatrixMode(GL_PROJECTION);
         glLoadIdentity();
         gluPerspective(75, 1, 1, 21);
         glMatrixMode(GL_MODELVIEW);
         glLoadIdentity();
         gluLookAt(1, 5, 5, 0, 0, 0, 0, 0, 1);

         // 使用“地”紋理繪制土地
         glBindTexture(GL_TEXTURE_2D, texGround);
         glBegin(GL_QUADS);
             glTexCoord2f(0.0f, 0.0f); glVertex3f(-8.0f, -8.0f, 0.0f);
             glTexCoord2f(0.0f, 5.0f); glVertex3f(-8.0f, 8.0f, 0.0f);
             glTexCoord2f(5.0f, 5.0f); glVertex3f(8.0f, 8.0f, 0.0f);
             glTexCoord2f(5.0f, 0.0f); glVertex3f(8.0f, -8.0f, 0.0f);
         glEnd();
         // 使用“墻”紋理繪制柵欄
         glBindTexture(GL_TEXTURE_2D, texWall);
         glBegin(GL_QUADS);
             glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
             glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
             glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
             glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
         glEnd();

         // 旋轉后再繪制一個
         glRotatef(-90, 0, 0, 1);
         glBegin(GL_QUADS);
             glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
             glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
             glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
             glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
         glEnd();

         // 交換緩沖區,并保存像素數據到文件
         glutSwapBuffers();
         grab();
    }

    int main(int argc,char
    argv[])
    {
         // GLUT初始化
         glutInit(&argc, argv);
         glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
         glutInitWindowPosition(100, 100);
         glutInitWindowSize(WindowWidth, WindowHeight);
         glutCreateWindow(WindowTitle);
         glutDisplayFunc(&display);

         // 在這里做一些初始化
         glEnable(GL_DEPTH_TEST);
         glEnable(GL_TEXTURE_2D);
         texGround = load_texture("ground.bmp");
         texWall = load_texture("wall.bmp");

         // 開始顯示
         glutMainLoop();

        return 0;
    }</div>

    小結:
    本課介紹了OpenGL紋理的入門知識。
    利用紋理可以進行比glReadPixels和glDrawPixels更復雜的像素繪制,因此可以實現很多精彩的效果。
    本課只涉及了二維紋理。OpenGL還支持一維和三維紋理,其原理是類似的。
    在使用紋理前,要啟用紋理。并且,還需要將像素數據載入到紋理中。注意紋理的寬度和高度,目前很多OpenGL的實現都還要求其值為2的整數次方,如果紋理圖象本身并不滿足這個條件,可以使用gluScaleImage函數來進行縮放。為了正確的使用紋理,需要設置紋理參數。
    載入紋理所需要的系統開銷是比較大的,應該盡可能減少載入紋理的次數。如果程序中只使用一幅紋理,則只在第一次使用前載入,以后不必重新載入。如果程序中要使用多幅紋理,不應該反復載入它們,而應該將每個紋理都用一個紋理對象來保存,并使用glBindTextures在各個紋理之間進行切換。
    本課還給出了一個程序(到目前為止,它是這個OpenGL教程系列中所給出的程序中最長的)。該程序演示了紋理的基本使用方法,本課程涉及到的幾乎所有內容都被包括其中,這是對本課中文字說明的一個補充。如果讀者有什么不明白的地方,也可以以這個程序作為參考。



    OpenGL入門學習[十二]

    片斷測試其實就是測試每一個像素,只有通過測試的像素才會被繪制,沒有通過測試的像素則不進行繪制。OpenGL提供了多種測試操作,利用這些操作可以實現一些特殊的效果。
    我們在前面的課程中,曾經提到了“深度測試”的概念,它在繪制三維場景的時候特別有用。在不使用深度測試的時候,如果我們先繪制一個距離較近的物體,再繪制距離較遠的物體,則距離遠的物體因為后繪制,會把距離近的物體覆蓋掉,這樣的效果并不是我們所希望的。
    如果使用了深度測試,則情況就會有所不同:每當一個像素被繪制,OpenGL就記錄這個像素的“深度”(深度可以理解為:該像素距離觀察者的距離。深度值越大,表示距離越遠),如果有新的像素即將覆蓋原來的像素時,深度測試會檢查新的深度是否會比原來的深度值小。如果是,則覆蓋像素,繪制成功;如果不是,則不會覆蓋原來的像素,繪制被取消。這樣一來,即使我們先繪制比較近的物體,再繪制比較遠的物體,則遠的物體也不會覆蓋近的物體了。
    實際上,只要存在深度緩沖區,無論是否啟用深度測試,OpenGL在像素被繪制時都會嘗試將深度數據寫入到緩沖區內,除非調用了glDepthMask(GL_FALSE)來禁止寫入。這些深度數據除了用于常規的測試外,還可以有一些有趣的用途,比如繪制陰影等等。

    除了深度測試,OpenGL還提供了剪裁測試、Alpha測試和模板測試。

    1、剪裁測試
    剪裁測試用于限制繪制區域。我們可以指定一個矩形的剪裁窗口,當啟用剪裁測試后,只有在這個窗口之內的像素才能被繪制,其它像素則會被丟棄。換句話說,無論怎么繪制,剪裁窗口以外的像素將不會被修改。
    有的朋友可能玩過《魔獸爭霸3》這款游戲。游戲時如果選中一個士兵,則畫面下方的一個方框內就會出現該士兵的頭像。為了保證該頭像無論如何繪制都不會越界而覆蓋到外面的像素,就可以使用剪裁測試。

    可以通過下面的代碼來啟用或禁用剪裁測試:

    glEnable(GL_SCISSOR_TEST);   // 啟用剪裁測試
    glDisable(GL_SCISSOR_TEST); // 禁用剪裁測試



    可以通過下面的代碼來指定一個位置在(x, y),寬度為width,高度為height的剪裁窗口。

    glScissor(x, y, width, height);


    注意,OpenGL窗口坐標是以左下角為(0, 0),右上角為(width, height)的,這與Windows系統窗口有所不同。

    還有一種方法可以保證像素只繪制到某一個特定的矩形區域內,這就是視口變換(在第五課第3節中有介紹)。但視口變換和剪裁測試是不同的。視口變換是將所有內容縮放到合適的大小后,放到一個矩形的區域內;而剪裁測試不會進行縮放,超出矩形范圍的像素直接忽略掉。

    =====================未完,請勿跟帖=====================

    2、Alpha測試
    在前面的課程中,我們知道像素的Alpha值可以用于混合操作。其實Alpha值還有一個用途,這就是Alpha測試。當每個像素即將繪制時,如果啟動了Alpha測試,OpenGL會檢查像素的Alpha值,只有Alpha值滿足條件的像素才會進行繪制(嚴格的說,滿足條件的像素會通過本項測試,進行下一種測試,只有所有測試都通過,才能進行繪制),不滿足條件的則不進行繪制。這個“條件”可以是:始終通過(默認情況)、始終不通過、大于設定值則通過、小于設定值則通過、等于設定值則通過、大于等于設定值則通過、小于等于設定值則通過、不等于設定值則通過。
    如果我們需要繪制一幅圖片,而這幅圖片的某些部分又是透明的(想象一下,你先繪制一幅相片,然后繪制一個相框,則相框這幅圖片有很多地方都是透明的,這樣就可以透過相框看到下面的照片),這時可以使用Alpha測試。將圖片中所有需要透明的地方的Alpha值設置為0.0,不需要透明的地方Alpha值設置為1.0,然后設置Alpha測試的通過條件為:“大于0.5則通過”,這樣便能達到目的。當然也可以設置需要透明的地方Alpha值為1.0,不需要透明的地方Alpha值設置為0.0,然后設置條件為“小于0.5則通過”。Alpha測試的設置方式往往不只一種,可以根據個人喜好和實際情況需要進行選擇。

    可以通過下面的代碼來啟用或禁用Alpha測試:

    glEnable(GL_ALPHA_TEST);   // 啟用Alpha測試
    glDisable(GL_ALPHA_TEST); // 禁用Alpha測試



    可以通過下面的代碼來設置Alpha測試條件為“大于0.5則通過”:

    glAlphaFunc(GL_GREATER, 0.5f);



    該函數的第二個參數表示設定值,用于進行比較。第一個參數是比較方式,除了GL_LESS(小于則通過)外,還可以選擇:
    GL_ALWAYS(始終通過),
    GL_NEVER(始終不通過),
    GL_LESS(小于則通過),
    GL_LEQUAL(小于等于則通過),
    GL_EQUAL(等于則通過),
    GL_GEQUAL(大于等于則通過),
    GL_NOTEQUAL(不等于則通過)。

    =====================未完,請勿跟帖=====================

    現在我們來看一個實際例子。一幅照片圖片,一幅相框圖片,如何將它們組合在一起呢?為了簡單起見,我們使用前面兩課一直使用的24位BMP文件來作為圖片格式。(因為發布到網絡上,為了節約容量,我所發布的是JPG格式。大家下載后可以用Windows XP自帶的畫圖工具打開,并另存為24位BMP格式)

    注:第一幅圖片是著名網絡游戲《魔獸世界》的一幅桌面背景,用在這里希望沒有涉及版權問題。如果有什么不妥,請及時指出,我會立即更換。

    在24位的BMP文件格式中,BGR三種顏色各占8位,沒有保存Alpha值,因此無法直接使用Alpha測試。注意到相框那幅圖片中,所有需要透明的位置都是白色,所以我們在程序中設置所有白色(或很接近白色)的像素Alpha值為0.0,設置其它像素Alpha值為1.0,然后設置Alpha測試的條件為“大于0.5則通過”即可。這種使用某種特殊顏色來代表透明顏色的技術,有時又被成為Color Key技術。
    利用前面第11課的一段代碼,將圖片讀取為紋理,然后利用下面這個函數來設置“當前紋理”中每一個像素的Alpha值。

    /* 將當前紋理BGR格式轉換為BGRA格式
  • 紋理中像素的RGB值如果與指定rgb相差不超過absolute,則將Alpha設置為0.0,否則設置為1.0
    /
    void texture_colorkey(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute)
    {
         GLint width, height;
         GLubyte
    pixels = 0;

         // 獲得紋理的大小信息
         glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
         glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);

         // 分配空間并獲得紋理像素
         pixels = (GLubyte)malloc(widthheight4);
        if( pixels == 0 )
            return;
         glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);

         // 修改像素中的Alpha值
         // 其中pixels[i
    4], pixels[i4+1], pixels[i4+2], pixels[i4+3]
         //    分別表示第i個像素的藍、綠、紅、Alpha四種分量,0表示最小,255表示最大
         {
             GLint i;
             GLint count = width
    height;
            for(i=0; i<count; ++i)
             {
                if( abs(pixels[i4] - b) <= absolute
                  && abs(pixels[i
    4+1] - g) <= absolute
                  && abs(pixels[i4+2] - r) <= absolute )
                     pixels[i
    4+3] = 0;
                else
                     pixels[i4+3] = 255;
             }
         }

         // 將修改后的像素重新設置到紋理中,釋放內存
         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
             GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
        free(pixels);
    }</div>



    =====================未完,請勿跟帖=====================

    有了紋理后,我們開啟紋理,指定合適的紋理坐標并繪制一個矩形,這樣就可以在屏幕上將圖片繪制出來。我們先繪制相片的紋理,再繪制相框的紋理。程序代碼如下:

    void display(void)
    {
        static int initialized    = 0;
        static GLuint texWindow   = 0;
        static GLuint texPicture = 0;

         // 執行初始化操作,包括:讀取相片,讀取相框,將相框由BGR顏色轉換為BGRA,啟用二維紋理
        if( !initialized )
         {
             texPicture = load_texture("pic.bmp");
             texWindow   = load_texture("window.bmp");
             glBindTexture(GL_TEXTURE_2D, texWindow);
             texture_colorkey(255, 255, 255, 10);

             glEnable(GL_TEXTURE_2D);

             initialized = 1;
         }

         // 清除屏幕
         glClear(GL_COLOR_BUFFER_BIT);

         // 繪制相片,此時不需要進行Alpha測試,所有的像素都進行繪制
         glBindTexture(GL_TEXTURE_2D, texPicture);
         glDisable(GL_ALPHA_TEST);
         glBegin(GL_QUADS);
             glTexCoord2f(0, 0);      glVertex2f(-1.0f, -1.0f);
             glTexCoord2f(0, 1);      glVertex2f(-1.0f,   1.0f);
             glTexCoord2f(1, 1);      glVertex2f( 1.0f,   1.0f);
             glTexCoord2f(1, 0);      glVertex2f( 1.0f, -1.0f);
         glEnd();

         // 繪制相框,此時進行Alpha測試,只繪制不透明部分的像素
         glBindTexture(GL_TEXTURE_2D, texWindow);
         glEnable(GL_ALPHA_TEST);
         glAlphaFunc(GL_GREATER, 0.5f);
         glBegin(GL_QUADS);
             glTexCoord2f(0, 0);      glVertex2f(-1.0f, -1.0f);
             glTexCoord2f(0, 1);      glVertex2f(-1.0f,   1.0f);
             glTexCoord2f(1, 1);      glVertex2f( 1.0f,   1.0f);
             glTexCoord2f(1, 0);      glVertex2f( 1.0f, -1.0f);
         glEnd();

         // 交換緩沖
         glutSwapBuffers();
    }


    其中:load_texture函數是從第11課中照搬過來的(該函數還使用了一個power_of_two函數,一個BMP_Header_Length常數,同樣照搬),無需進行修改。main函數跟其它課程的基本相同,不再重復。
    程序運行后,會發現相框與相片的銜接有些不自然,這是因為相框某些邊緣部分雖然肉眼看上去是白色,但其實RGB值與純白色相差并不少,因此程序計算其 Alpha值時認為其不需要透明。解決辦法是仔細處理相框中的每個像素,在需要透明的地方涂上純白色,這也許是一件很需要耐心的工作。

    =====================未完,請勿跟帖=====================

    大家可能會想:前面我們學習過混合操作,混合可以實現半透明,自然也可以通過設定實現全透明。也就是說,Alpha測試可以實現的效果幾乎都可以通過OpenGL混合功能來實現。那么為什么還需要一個Alpha測試呢?答案就是,這與性能相關。Alpha測試只要簡單的比較大小就可以得到最終結果,而混合操作一般需要進行乘法運算,性能有所下降。另外,OpenGL測試的順序是:剪裁測試、Alpha測試、模板測試、深度測試。如果某項測試不通過,則不會進行下一步,而只有所有測試都通過的情況下才會執行混合操作。因此,在使用Alpha測試的情況下,透明的像素就不需要經過模板測試和深度測試了;而如果使用混合操作,即使透明的像素也需要進行模板測試和深度測試,性能會有所下降。還有一點:對于那些“透明”的像素來說,如果使用Alpha測試,則 “透明”的像素不會通過測試,因此像素的深度值不會被修改;而使用混合操作時,雖然像素的顏色沒有被修改,但它的深度值則有可能被修改掉了。
    因此,如果所有的像素都是“透明”或“不透明”,沒有“半透明”時,應該盡量采用Alpha測試而不是采用混合操作。當需要繪制半透明像素時,才采用混合操作。

    =====================未完,請勿跟帖=====================

    3、模板測試
    模板測試是所有OpenGL測試中比較復雜的一種。

    首先,模板測試需要一個模板緩沖區,這個緩沖區是在初始化OpenGL時指定的。如果使用GLUT工具包,可以在調用glutInitDisplayMode函數時在參數中加上GLUT_STENCIL,例如:

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);


    在Windows操作系統中,即使沒有明確要求使用模板緩沖區,有時候也會分配模板緩沖區。但為了保證程序的通用性,最好還是明確指定使用模板緩沖區。如果確實沒有分配模板緩沖區,則所有進行模板測試的像素全部都會通過測試。

    通過glEnable/glDisable可以啟用或禁用模板測試。

    glEnable(GL_STENCIL_TEST);   // 啟用模板測試
    glDisable(GL_STENCIL_TEST); // 禁用模板測試



    OpenGL在模板緩沖區中為每個像素保存了一個“模板值”,當像素需要進行模板測試時,將設定的模板參考值與該像素的“模板值”進行比較,符合條件的通過測試,不符合條件的則被丟棄,不進行繪制。
    條件的設置與Alpha測試中的條件設置相似。但注意Alpha測試中是用浮點數來進行比較,而模板測試則是用整數來進行比較。比較也有八種情況:始終通過、始終不通過、大于則通過、小于則通過、大于等于則通過、小于等于則通過、等于則通過、不等于則通過。

    glStencilFunc(GL_LESS, 3, mask);


    這段代碼設置模板測試的條件為:“小于3則通過”。glStencilFunc的前兩個參數意義與glAlphaFunc的兩個參數類似,第三個參數的意義為:如果進行比較,則只比較mask中二進制為1的位。例如,某個像素模板值為5(二進制101),而mask的二進制值為00000011,因為只比較最后兩位,5的最后兩位為01,其實是小于3的,因此會通過測試。

    如何設置像素的“模板值”呢?glClear函數可以將所有像素的模板值復位。代碼如下:

    glClear(GL_STENCIL_BUFFER_BIT);


    可以同時復位顏色值和模板值:

    glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);


    正如可以使用glClearColor函數來指定清空屏幕后的顏色那樣,也可以使用glClearStencil函數來指定復位后的“模板值”。

    每個像素的“模板值”會根據模板測試的結果和深度測試的結果而進行改變。

    glStencilOp(fail, zfail, zpass);


    該函數指定了三種情況下“模板值”該如何變化。第一個參數表示模板測試未通過時該如何變化;第二個參數表示模板測試通過,但深度測試未通過時該如何變化;第三個參數表示模板測試和深度測試均通過時該如何變化。如果沒有起用模板測試,則認為模板測試總是通過;如果沒有啟用深度測試,則認為深度測試總是通過)
    變化可以是:
    GL_KEEP(不改變,這也是默認值),
    GL_ZERO(回零),
    GL_REPLACE(使用測試條件中的設定值來代替當前模板值),
    GL_INCR(增加1,但如果已經是最大值,則保持不變),
    GL_INCR_WRAP(增加1,但如果已經是最大值,則從零重新開始),
    GL_DECR(減少1,但如果已經是零,則保持不變),
    GL_DECR_WRAP(減少1,但如果已經是零,則重新設置為最大值),
    GL_INVERT(按位取反)。

    在新版本的OpenGL中,允許為多邊形的正面和背面使用不同的模板測試條件和模板值改變方式,于是就有了glStencilFuncSeparate函數和glStencilOpSeparate函數。這兩個函數分別與glStencilFunc和glStencilOp類似,只在最前面多了一個參數 face,用于指定當前設置的是哪個面。可以選擇GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。

    注意:模板緩沖區與深度緩沖區有一點不同。無論是否啟用深度測試,當有像素被繪制時,總會重新設置該像素的深度值(除非設置 glDepthMask(GL_FALSE);)。而模板測試如果不啟用,則像素的模板值會保持不變,只有啟用模板測試時才有可能修改像素的模板值。(這一結論是我自己的實驗得出的,暫時沒發現什么資料上是這樣寫。如果有不正確的地方,歡迎指正)
    另外,模板測試雖然是從OpenGL 1.0就開始提供的功能,但是對于個人計算機而言,硬件實現模板測試的似乎并不多,很多計算機系統直接使用CPU運算來完成模板測試。因此在一些老的顯卡,或者是多數集成顯卡上,大量而頻繁的使用模板測試可能造成程序運行效率低下。即使是當前配置比較高端的個人計算機,也盡量不要使用 glStencilFuncSeparate和glStencilOpSeparate函數。

    從前面所講可以知道,使用剪裁測試可以把繪制區域限制在一個矩形的區域內。但如果需要把繪制區域限制在一個不規則的區域內,則需要使用模板測試。
    例如:繪制一個湖泊,以及周圍的樹木,然后繪制樹木在湖泊中的倒影。為了保證倒影被正確的限制在湖泊表面,可以使用模板測試。具體的步驟如下:
    (1) 關閉模板測試,繪制地面和樹木。
    (2) 開啟模板測試,使用glClear設置所有像素的模板值為0。
    (3) 設置glStencilFunc(GL_ALWAYS, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);繪制湖泊水面。這樣一來,湖泊水面的像素的“模板值”為1,而其它地方像素的“模板值”為0。
    (4) 設置glStencilFunc(GL_EQUAL, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);繪制倒影。這樣一來,只有“模板值”為1的像素才會被繪制,因此只有“水面”的像素才有可能被倒影的像素替換,而其它像素則保持不變。

    =====================未完,請勿跟帖=====================

    我們仍然來看一個實際的例子。這是一個比較簡單的場景:空間中有一個球體,一個平面鏡。我們站在某個特殊的觀察點,可以看到球體在平面鏡中的鏡像,并且鏡像處于平面鏡的邊緣,有一部分因為平面鏡大小的限制,而無法顯示出來。整個場景的效果如下圖:


    繪制這個場景的思路跟前面提到的湖面倒影是接近的。
    假設平面鏡所在的平面正好是X軸和Y軸所確定的平面,則球體和它在平面鏡中的鏡像是關于這個平面對稱的。我們用一個draw_sphere函數來繪制球體,先調用該函數以繪制球體本身,然后調用glScalef(1.0f, 1.0f, -1.0f); 再調用draw_sphere函數,就可以繪制球體的鏡像。
    另外需要注意的地方就是:因為是繪制三維的場景,我們開啟了深度測試。但是站在觀察者的位置,球體的鏡像其實是在平面鏡的“背后”,也就是說,如果按照常規的方式繪制,平面鏡會把鏡像覆蓋掉,這不是我們想要的效果。解決辦法就是:設置深度緩沖區為只讀,繪制平面鏡,然后設置深度緩沖區為可寫的狀態,繪制平面鏡“背后”的鏡像。
    有的朋友可能會問:如果在繪制鏡像的時候關閉深度測試,那鏡像不就不會被平面鏡遮擋了嗎?為什么還要開啟深度測試,又需要把深度緩沖區設置為只讀呢?實際情況是:雖然關閉深度測試確實可以讓鏡像不被平面鏡遮擋,但是鏡像本身會出現若干問題。我們看到的鏡像是一個球體,但實際上這個球體是由很多的多邊形所組成的,這些多邊形有的代表了我們所能看到的“正面”,有的則代表了我們不能看到的“背面”。如果關閉深度測試,而有的“背面”多邊形又比“正面”多邊形先繪制,就會造成球體的背面反而把正面擋住了,這不是我們想要的效果。為了確保正面可以擋住背面,應該開啟深度測試。
    繪制部分的代碼如下:

    void draw_sphere()
    {
         // 設置光源
         glEnable(GL_LIGHTING);
         glEnable(GL_LIGHT0);
         {
             GLfloat
                 pos[]      = {5.0f, 5.0f, 0.0f, 1.0f},
                 ambient[] = {0.0f, 0.0f, 1.0f, 1.0f};
             glLightfv(GL_LIGHT0, GL_POSITION, pos);
             glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
         }

         // 繪制一個球體
         glColor3f(1, 0, 0);
         glPushMatrix();
         glTranslatef(0, 0, 2);
         glutSolidSphere(0.5, 20, 20);
         glPopMatrix();
    }

    void display(void)
    {
         // 清除屏幕
         glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

         // 設置觀察點
         glMatrixMode(GL_PROJECTION);
         glLoadIdentity();
         gluPerspective(60, 1, 5, 25);
         glMatrixMode(GL_MODELVIEW);
         glLoadIdentity();
         gluLookAt(5, 0, 6.5, 0, 0, 0, 0, 1, 0);

         glEnable(GL_DEPTH_TEST);

         // 繪制球體
         glDisable(GL_STENCIL_TEST);
         draw_sphere();

         // 繪制一個平面鏡。在繪制的同時注意設置模板緩沖。
         // 另外,為了保證平面鏡之后的鏡像能夠正確繪制,在繪制平面鏡時需要將深度緩沖區設置為只讀的。
         // 在繪制時暫時關閉光照效果
         glClearStencil(0);
         glClear(GL_STENCIL_BUFFER_BIT);
         glStencilFunc(GL_ALWAYS, 1, 0xFF);
         glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
         glEnable(GL_STENCIL_TEST);

         glDisable(GL_LIGHTING);
         glColor3f(0.5f, 0.5f, 0.5f);
         glDepthMask(GL_FALSE);
         glRectf(-1.5f, -1.5f, 1.5f, 1.5f);
         glDepthMask(GL_TRUE);

         // 繪制一個與先前球體關于平面鏡對稱的球體,注意光源的位置也要發生對稱改變
         // 因為平面鏡是在X軸和Y軸所確定的平面,所以只要Z坐標取反即可實現對稱
         // 為了保證球體的繪制范圍被限制在平面鏡內部,使用模板測試
         glStencilFunc(GL_EQUAL, 1, 0xFF);
         glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
         glScalef(1.0f, 1.0f, -1.0f);
         draw_sphere();

         // 交換緩沖
         glutSwapBuffers();

         // 截圖
         grab();
    }



    其中display函數的末尾調用了一個grab函數,它保存當前的圖象到一個BMP文件。這個函數本來是在第十課和第十一課中都有所使用的。但是我發現它有一個bug,現在進行了修改:在函數最開頭的部分加上一句:glReadBuffer(GL_FRONT);即可。注意這個函數最好是在繪制完畢后(如果是使用雙緩沖,則應該在交換緩沖后)立即調用。

    =====================未完,請勿跟帖=====================

    大家可能會有這樣的感覺:模板測試的設置是如此復雜,它可以實現的功能應該很多,肯定不止這樣一個“限制像素的繪制范圍”。事實上也是如此,不過現在我們暫時只講這些。

    其實,如果不需要繪制半透明效果,有時候可以用混合功能來代替模板測試。就繪制鏡像這個例子來說,可以采用下面的步驟:
    (1) 清除屏幕,在glClearColor中設置合適的值確保清除屏幕后像素的Alpha值為0.0
    (2) 關閉混合功能,繪制球體本身,設置合適的顏色(或者光照與材質)以確保所有被繪制的像素的Alpha值為0.0
    (3) 繪制平面鏡,設置合適的顏色(或者光照與材質)以確保所有被繪制的像素的Alpha值為1.0
    (4) 啟用混合功能,用GL_DST_ALPHA作為源因子,GL_ONE_MINUS_DST_ALPHA作為目標因子,這樣就實現了只有原來Alpha為 1.0的像素才能被修改,而原來Alpha為0.0的像素則保持不變。這時再繪制鏡像物體,注意確保所有被繪制的像素的Alpha值為1.0。
    在有的OpenGL實現中,模板測試是軟件實現的,而混合功能是硬件實現的,這時候可以考慮這樣的代替方法以提高運行效率。但是并非所有的模板測試都可以用混合功能來代替,并且這樣的代替顯得不自然,復雜而且容易出錯。
    另外始終注意:使用混合來模擬時,即使某個像素原來的Alpha值為0.0,以致于在繪制后其顏色不會有任何變化,但是這個像素的深度值有可能會被修改,而如果是使用模板測試,沒有通過測試的像素其深度值不會發生任何變化。而且,模板測試和混合功能中,像素模板值的修改方式是不一樣的。

    =====================未完,請勿跟帖=====================

    4、深度測試
    在本課的開頭,已經簡單的敘述了深度測試。這里是完整的內容。

    深度測試需要深度緩沖區,跟模板測試需要模板緩沖區是類似的。如果使用GLUT工具包,可以在調用glutInitDisplayMode函數時在參數中加上GLUT_DEPTH,這樣來明確指定要求使用深度緩沖區。
    深度測試和模板測試的實現原理很類似,都是在一個緩沖區保存像素的某個值,當需要進行測試時,將保存的值與另一個值進行比較,以確定是否通過測試。兩者的區別在于:模板測試是設定一個值,在測試時用這個設定值與像素的“模板值”進行比較,而深度測試是根據頂點的空間坐標計算出深度,用這個深度與像素的“深度值”進行比較。也就是說,模板測試需要指定一個值作為比較參考,而深度測試中,這個比較用的參考值是OpenGL根據空間坐標自動計算的。

    通過glEnable/glDisable函數可以啟用或禁用深度測試。
    glEnable(GL_DEPTH_TEST);   // 啟用深度測試
    glDisable(GL_DEPTH_TEST); // 禁用深度測試

    至于通過測試的條件,同樣有八種,與Alpha測試中的條件設置相同。條件設置是通過glDepthFunc函數完成的,默認值是GL_LESS。
    glDepthFunc(GL_LESS);

    與模板測試相比,深度測試的應用要頻繁得多。幾乎所有的三維場景繪制都使用了深度測試。正因為這樣,幾乎所有的OpenGL實現都對深度測試提供了硬件支持,所以雖然兩者的實現原理類似,但深度測試很可能會比模板測試快得多。當然了,兩種測試在應用上很少有交集,一般不會出現使用一種測試去代替另一種測試的情況。

    =====================未完,請勿跟帖=====================

    小結:
    本次課程介紹了OpenGL所提供的四種測試,分別是剪裁測試、Alpha測試、模板測試、深度測試。OpenGL會對每個即將繪制的像素進行以上四種測試,每個像素只有通過一項測試后才會進入下一項測試,而只有通過所有測試的像素才會被繪制,沒有通過測試的像素會被丟棄掉,不進行繪制。每種測試都可以單獨的開啟或者關閉,如果某項測試被關閉,則認為所有像素都可以順利通過該項測試。
    剪裁測試是指:只有位于指定矩形內部的像素才能通過測試。
    Alpha測試是指:只有Alpha值與設定值相比較,滿足特定關系條件的像素才能通過測試。
    模板測試是指:只有像素模板值與設定值相比較,滿足特定關系條件的像素才能通過測試。
    深度測試是指:只有像素深度值與新的深度值比較,滿足特定關系條件的像素才能通過測試。
    上面所說的特定關系條件可以是大于、小于、等于、大于等于、小于等于、不等于、始終通過、始終不通過這八種。
    模板測試需要模板緩沖區,深度測試需要深度緩沖區。這些緩沖區都是在初始化OpenGL時指定的。如果使用GLUT工具包,則可以在 glutInitDisplayMode函數中指定。無論是否開啟深度測試,OpenGL在像素被繪制時都會嘗試修改像素的深度值;而只有開啟模板測試時,OpenGL才會嘗試修改像素的模板值,模板測試被關閉時,OpenGL在像素被繪制時也不會修改像素的模板值。
    利用這些測試操作可以控制像素被繪制或不被繪制,從而實現一些特殊效果。利用混合功能可以實現半透明,通過設置也可以實現完全透明,因而可以模擬像素顏色的繪制或不繪制。但注意,這里僅僅是顏色的模擬。OpenGL可以為像素保存顏色、深度值和模板值,利用混合實現透明時,像素顏色不發生變化,但深度值則會可能變化,模板值受glStencilFunc函數中第三個參數影響;利用測試操作實現透明時,像素顏色不發生變化,深度值也不發生變化,模板值受 glStencilFunc函數中前兩個參數影響。
    此外,修正了第十課、第十一課中的一個函數中的bug。在grab函數中,應該在最開頭加上一句glReadBuffer(GL_FRONT);以保證讀取到的內容正好就是顯示的內容。

    因為論壇支持附件了,我會把程序源代碼和所使用的圖片上傳到附件里,方便大家下載。

    =====================   第十二課 完   =====================
    =====================TO BE CONTINUED=====================


    OpenGL入門學習[十三]

    前一段時間里,論壇有位朋友問什么是狀態機。按我的理解,狀態機就是一種存在于理論中的機器,它具有以下的特點:

    1. 它有記憶的能力,能夠記住自己當前的狀態。

    2. 它可以接收輸入,根據輸入的內容和自己的狀態,修改自己的狀態,并且可以得到輸出。

    3. 當它進入某個特殊的狀態(停機狀態)的時候,它不再接收輸入,停止工作。

    理論說起來很抽象,但實際上是很好理解的。

    首先,從本質上講,我們現在的電腦就是典型的狀態機。可以對照理解:

    1. 電腦的存儲器(內存、硬盤等等),可以記住電腦自己當前的狀態(當前安裝在電腦中的軟件、保存在電腦中的數據,其實都是二進制的值,都屬于當前的狀態)。

    2. 電腦的輸入設備接收輸入(鍵盤輸入、鼠標輸入、文件輸入),根據輸入的內容和自己的狀態(主要指可以運行的程序代碼),修改自己的狀態(修改內存中的值),并且可以得到輸出(將結果顯示到屏幕)。

    3. 當它進入某個特殊的狀態(關機狀態)的時候,它不再接收輸入,停止工作。

    OpenGL也可以看成這樣的一種機器。讓我們先對照理解一下:

    1. OpenGL可以記錄自己的狀態(比如:當前所使用的顏色、是否開啟了混合功能,等等,這些都是要記錄的)

    2. OpenGL可以接收輸入(當我們調用OpenGL函數的時候,實際上可以看成OpenGL在接收我們的輸入),根據輸入的內容和自己的狀態,修改自己的狀態,并且可以得到輸出(比如我們調用glColor3f,則OpenGL接收到這個輸入后會修改自己的“當前顏色”這個狀態;我們調用glRectf,則OpenGL會輸出一個矩形)

    3. OpenGL可以進入停止狀態,不再接收輸入。這個可能在我們的程序中表現得不太明顯,不過在程序退出前,OpenGL總會先停止工作的。

    還是沒理解?呵呵,看來這真不是個好的開始呀,難得等了這么久,好不容易教程有更新了,怎么如此的難懂啊??沒關系,實在沒理解,咱就不理解它了。接著往下看。

    為什么我要提到“狀態機”這個枯燥的、晦澀的概念呢?其實它可以幫助我們理解一些東西。

    比如我在前面的教程里面,經常說:

    可以使用glColor函數來選擇一種顏色,以后繪制的所有物體都是這種顏色,除非再次使用glColor函數重新設定。</p>

    可以使用glTexCoord函數來設置一個紋理坐標,以后繪制的所有物體都是采用這種紋理坐標,除非再次使用glTexCoord函數重新設置。</p>

    可以使用glBlendFunc函數來指定混合功能的源因子和目標因子,以后繪制的所有物體都是采用這個源因子和目標因子,除非再次使用glBlendFunc函數重新指定。

    可以使用glLight函數來指定光源的位置、顏色,以后繪制的所有物體都是采用這個光源的位置、顏色,除非再次使用glBlendFunc函數重新指定。</p>

    ……

    呵呵,很繁,是吧?“狀態機”可以簡化這個描述。

    OpenGL是一個狀態機,它保持自身的狀態,除非用戶輸入一條命令讓它改變狀態。

    顏色、紋理坐標、源因子和目標因子、光源的各種參數,等等,這些都是狀態,所以這一句話就包含了上面敘述的所有內容。

    此外,“是否啟用了光照”、“是否啟用了紋理”、“是否啟用了混合”、“是否啟用了深度測試”等等,這些也都是狀態,也符合上面的描述:OpenGL會保持狀態,除非我們調用OpenGL函數來改變它。

    取得OpenGL的當前狀態

    OpenGL保存了自己的狀態,我們可以通過一些函數來取得這些狀態。

    首先來說一些啟用/禁用的狀態。

    我們通過glEnable來啟用狀態,通過glDisable來禁用它們。例如:

    glEnable(GL_DEPTH_TEST);

    glEnable(GL_BLEND);

    glEnable(GL_CULL_FACE);

    glEnable(GL_LIGHTING);

    glEnable(GL_TEXTURE_2D);

    可以用glIsEnabled函數來檢測這些狀態是否被開啟。例如:

    glIsEnabled(GL_DEPTH_TEST);

    glIsEnabled(GL_BLEND);

    glIsEnabled(GL_CULL_FACE);

    glIsEnabled(GL_LIGHTING);

    glIsEnabled(GL_TEXTURE_2D);

    如果狀態是開啟的,則glIsEnabled函數返回GL_TRUE(這是一個不為零的常量,一般被定義為1);否則返回GL_FALSE(這是一個常量,其值為零)

    我們可以在程序里面寫:

    if( glIsEnabled(GL_BLEND) ) {

         // 當前開啟了混合功能

    } else {

         // 當前沒有開啟混合功能

    }

    再看其它類型的狀態。

    比如當前顏色,其值是四個浮點數,當前設置的直線寬度,其值是一個浮點數,當前的視口(Viewport,參見第五課),其值是四個整數。

    為了取得整數類型、浮點數類型的狀態,OpenGL提供了glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev這四個函數。調用函數時,指定需要得到的狀態的名稱,以及需要將狀態值存放到的位置(一個指針),則這四個函數可以把狀態值存放到指針所值位置。例如:

    // 取得當前的直線寬度

    GLfloat lw;

    glGetFloatv(GL_LINE_WIDTH, &lw);

    // 取得當前的顏色

    GLfloat cc[4];

    glGetFloatv(GL_CURRENT_COLOR, cc);

    // 取得當前的視口

    GLint viewport[4];

    glGetIntegerv(GL_VIEWPORT, viewport);

    說明:

    1. 注意元素的個數。比如GL_LINE_WIDTH狀態只有一個值,而GL_CURRENT_COLOR有四個值。應該小心的定義變量或者數組,避免下標越界。

    2. 使用四個不同的函數,同一種狀態也可以返回為不同類型的值。比如要得到當前的顏色,一般可以返回GLfloat類型或者GLdouble類型。代碼如下:

    GLfloat cc[4];

    GLdouble dcc[4];

    glGetFloatv(GL_CURRENT_COLOR, cc);

    glGetDoublev(GL_CURRENT_COLOR, dcc);

    glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev這四個函數可以得到OpenGL中多數的狀態,但是還有一些狀態不便用這四個函數來取得。比如光源的狀態,因為可能有多個光源,所以不可能使用類似glGetFloatv(GL_LIGHT_POSITION, pos);這樣的方法來得到光源位置。為了解決這個問題,OpenGL專門提供了glGetLight*系列函數,來取得光源的狀態。

    類似的,還有glGetMaterial*, glGetTexParameter*等,每個函數都有自己的適用范圍。

    設置OpenGL狀態

    呵呵,讀者可能會有疑問。既然有getXXX這樣的函數來取得OpenGL的狀態,那么為什么沒有setXXX這樣的函數來設置OpenGL狀態呢?

    答案很簡單,因為OpenGL已經提供了大量的函數來設置狀態了:glColor*, glMaterial*, glEnable, glDisable, 等等,大多數OpenGL函數都是用來設置OpenGL狀態的,因此不需要再設計一個setXXX函數來設置OpenGL狀態。

    從“狀態機”的角度來看。狀態機根據輸入來修改自己的狀態,而不是由外界直接修改自己的狀態。所以不設置setXXX這樣的函數,也是很合理的。

    OpenGL工作流程

    教程都放到第十三課了,但是我一直沒有對“工作流程”這種東西做過說明。OpenGL是按照什么樣的流程來進行工作的呢?下面的圖片可以簡要的說明一下:

    聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由于該書的舊版(第一版,1994年)已經流傳于網絡,我希望沒有觸及到版權問題。

    因為圖片中的文字是英語,這里還翻譯一下。說明文字也夾雜在翻譯之中了。

    1. Vertex data: 頂點數據。比如我們指定的顏色、紋理坐標、法線向量、頂點坐標等,都屬于頂點數據。

    2. Pixel data: 像素數據。我們在繪制像素、指定紋理時都會用到像素數據。

    3. Display list: 顯示列表。可以把調用的OpenGL函數保存起來。(參見第八課)

    4. Evaluators: 求值器。這個我們在前面的課程中沒有提到,以后估計也不太會提到。利用求值器可以指定貝賽爾曲線或者貝賽爾曲面,但是實際上還是可以理解為指定頂點、指定紋理坐標、指定法線向量等。

    5. Per-vertex operations and primitive assembly: 單一的頂點操作以及圖元裝配。首先對單一的頂點進行操作,比如變換(參見第五課)。然后把頂點裝配為圖元(圖元就是OpenGL所能繪制的最簡單的圖形,比如點、線段、三角形、四邊形、多邊形等,參見第二課)

    6. Pixel operations: 像素操作。例如把內存中的像素數據格式轉化為圖形硬件所支持的數據格式。對于紋理,可以替換其中的一部分像素,這也屬于像素操作。

    7. Rasterization: 光柵化。頂點數據和像素數據在這里交匯(可以想像成:頂點和紋理,一起組合成了具有紋理的三角形),形成完整的、可以顯示的一整塊(可能是點、線段、三角形、四邊形,或者其它不規則圖形),里面包含若干個像素。這一整塊被稱為fragment(片段)。

    8. Per-fragment operations: 片段操作。包括各種片段測試(參見第十二課)。

    9. Framebuffer: 幀緩沖。這是一塊存儲空間,顯示設備從這里讀取數據,然后顯示到屏幕。

    10. Texture assembly: 紋理裝配,這里我也沒怎么弄清楚:(,大概是說紋理的操作和像素操作是相關的吧。

    說明:圖片中實線表示正常的處理流程,虛線表示數據可以反方向讀取,比如可以用glReadPixels從幀緩沖中讀取像素數據(實際上是從幀緩沖讀取數據,經過像素操作,把顯示設備中的像素數據格式轉化為內存中的像素數據格式,最終成為內存中的像素數據)。

    小結

    本課是枯燥的理論知識。

    OpenGL是一個狀態機,它維持自己的狀態,并根據用戶調用的函數來改變自己的狀態。根據狀態的不同,調用同樣的函數也可能產生不同的效果。

    可以通過一些函數來獲取OpenGL當前的狀態。常用的函數有:glIsEnabled, glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev。

    OpenGL的工作流程,輸入像素數據和頂點數據,兩種數據分別操作后,通過光柵化,得到片段,再經過片段處理,最后繪制到幀緩沖區。繪制的結果也可以逆方向傳送,最終轉化為像素數據。



    OpenGL入門學習[十四]


    OpenGL從推出到現在,已經有相當長的一段時間了。其間,OpenGL不斷的得到更新。到今天為止,正式的OpenGL已經有九個版本。(1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1)
    每個OpenGL版本的推出,都增加了一些當時流行的或者迫切需要的新功能。同時,到現在為止,OpenGL是向下兼容的,就是說如果某個功能在一個低版本中存在,則在更高版本中也一定存在。這一特性也為我們編程提供了一點方便。
    當前OpenGL的最新版本是OpenGL 2.1,但是并不是所有的計算機系統都有這樣最新版本的OpenGL實現。舉例來說,Windows系統如果沒有安裝顯卡驅動,或者顯卡驅動中沒有附帶 OpenGL,則Windows系統默認提供一個軟件實現的OpenGL,它沒有使用硬件加速,因此速度可能較慢,版本也很低,僅支持1.1版本(聽說 Windows Vista默認提供的OpenGL支持到1.4版本,我也不太清楚)。nVidia和ATI這樣的顯卡巨頭,其主流顯卡基本上都提供了對OpenGL 2.1的支持。但一些舊型號的顯卡因為性能不足等原因,只能支持到OpenGL 2.0或者OpenGL 1.5。Intel的集成顯卡,很多都只提供了OpenGL 1.4(據說目前也有更高版本的了,但是我沒有見到)。
    OpenGL 2.0是一次比較大的改動,也因此升級了主版本號。可以認為OpenGL 2.0版本是一個分水嶺,是否支持OpenGL 2.0版本,直接關系到運行OpenGL程序時的效果。如果要類比一下的話,我覺得OpenGL 1.5和OpenGL 2.0的差距,就像是DirectX 8.1和DirectX 9.0c的差距了。
    檢查自己的OpenGL版本
    可以很容易的知道自己系統中的OpenGL版本,方法就是調用glGetString函數。

    const char* version = (const char*)glGetString(GL_VERSION);
    printf("OpenGL 版本:%s\n", version);



    glGetString(GL_VERSION);會返回一個表示版本的字符串,字符串的格式為X.X.X,就是三個整數,用小數點隔開,第一個數表示OpenGL主版本號,第二個數表示OpenGL次版本號,第三個數表示廠商發行代號。比如我在運行時得到的是"2.0.1",這表示我的OpenGL版本為2.0(主版本號為2,次版本號為0),是廠商的第一個發行版本。
    通過sscanf函數,也可以把字符串分成三個整數,以便詳細的進行判斷。

    int main_version, sub_version, release_version;
    const char* version = (const char*)glGetString(GL_VERSION);
    sscanf(version, "%d.%d.%d", &main_version, &sub_version, &release_version);
    printf("OpenGL 版本:%s\n", version);
    printf("主版本號:%d\n", main_version);
    printf("次版本號:%d\n", sub_version);
    printf("發行版本號:%d\n", release_version);



    glGetString還可以取得其它的字符串。
    glGetString(GL_VENDOR); 返回OpenGL的提供廠商。
    glGetString(GL_RENDERER); 返回執行OpenGL渲染的設備,通常就是顯卡的名字。
    glGetString(GL_EXTENSIONS); 返回所支持的所有擴展,每兩個擴展之間用空格隔開。詳細情況參見下面的關于“OpenGL擴展”的敘述。
    版本簡要歷史
    版本不同,提供功能的多少就不同。這里列出每個OpenGL版本推出時,所增加的主要功能。當然每個版本的修改并不只是下面的內容,讀者如果需要知道更詳細的情形,可以查閱OpenGL標準。
    OpenGL 1.1
    頂點數組。把所有的頂點數據(顏色、紋理坐標、頂點坐標等)都放到數組中,可以大大的減少諸如glColor*, glVertex*等函數的調用次數。雖然顯示列表也可以減少這些函數的調用次數,但是顯示列表中的數據是不可以修改的,頂點數組中的數據則可以修改。
    紋理對象。把紋理作為對象來管理,同一時間OpenGL可以保存多個紋理(但只使用其中一個)。以前沒有紋理對象時,OpenGL只能保存一個“當前紋理”。要使用其它紋理時,只能拋棄當前的紋理,重新載入。原來的方式非常影響效率。
    OpenGL 1.2
    三維紋理。以前的OpenGL只支持一維、二維紋理。
    像素格式。新增加了GL_BGRA等原來沒有的像素格式。允許壓縮的像素格式,例如GL_UNSIGNED_SHORT_5_5_5_1格式,表示兩個字節,存放RGBA數據,其中R, G, B各占5個二進制位,A占一個二進制位。
    圖像處理。新增了一個“圖像處理子集”,提供一些圖像處理的專用功能,例如卷積、計算柱狀圖等。這個子集雖然是標準規定,但是OpenGL實現時也可以選擇不支持它。
    OpenGL 1.2.1
    沒有加入任何新的功能。但是引入了“ARB擴展”的概念。詳細情況參見下面的關于“OpenGL擴展”的敘述。
    OpenGL 1.3
    壓縮紋理。在處理紋理時,使用壓縮后的紋理而不是紋理本身,這樣可以節省空間(節省顯存)和傳輸帶寬(節省從內存到顯存的數據流量)
    多重紋理。同時使用多個紋理。
    多重采樣。一種全屏抗鋸齒技術,使用后可以讓畫面顯示更加平滑,減輕鋸齒現象。對于nvidia顯卡,在設置時有一項“3D平滑處理設置”,實際上就是多重采樣。通常可以選擇2x, 4x,高性能的顯卡也可以選擇8x, 16x。其它顯卡也幾乎都有類似的設置選項,但是也有的顯卡不支持多重采樣,所以是0x。
    OpenGL 1.4
    深度紋理。可以把深度值像像素值一樣放到紋理中,在繪制陰影時特別有用。
    輔助顏色。頂點除了有顏色外還有輔助顏色。在使用光照時可以表現出更真實的效果。
    OpenGL 1.5
    緩沖對象。允許把數據(主要指頂點數據)交由OpenGL保存到較高性能的存儲器中,提高繪制速度。比頂點數組有更多優勢。頂點數組只是減少函數調用次數,緩沖對象不僅減少函數調用次數,還加快數據訪問速度。
    遮擋查詢。可以計算一個物體有幾個像素會被繪制到屏幕上。如果物體沒有任何像素會被繪制,則不需要加載相關的數據(例如紋理數據)。
    OpenGL 2.0
    可編程著色。允許編寫一小段代碼來代替OpenGL原來的頂點操作/片段操作。這樣提供了巨大的靈活性,可以實現各種各樣的豐富的效果。
    紋理大小不再必須是2的整數次方。
    點塊紋理。把紋理應用到一個點(大小可能不只一個像素)上,這樣比繪制一個矩形可能效率更高。
    OpenGL 2.1
    可編程著色,編程語言由原來的1.0版本升級為1.2版本。
    緩沖對象,原來僅允許存放頂點數據,現在也允許存放像素數據。
    獲得新版本的OpenGL
    要獲得新版本OpenGL,首先應該登陸你的顯卡廠商網站,并查詢相關的最新信息。根據情況,下載最新的驅動或者OpenGL軟件包。
    如果自己的顯卡不支持高版本的OpenGL,或者自己的操作系統根本就沒有提供OpenGL,怎么辦呢?有一個被稱為MESA的開源項目,用C語言編寫了一個OpenGL實現,最新的mesa 7.0已經實現了OpenGL 2.1標準中所規定的各種功能。下載MESA的代碼,然后編譯,就可以得到一個最新版本的OpenGL了。呵呵,不要高興的太早。MESA是軟件實現的,就是說沒有用到硬件加速,因此運行起來會較慢,尤其是使用新版本的OpenGL所規定的一些高級特性時,慢得幾乎無法忍受。MESA不能讓你用舊的顯卡玩新的游戲(很可能慢得沒法玩),但是如果你只是想學習或嘗試一下新版本OpenGL的各種功能,MESA可以滿足你的一部分要求。
    OpenGL擴展
    OpenGL版本的更新并不快。如果某種技術變得流行起來,但是OpenGL標準中又沒有相關的規定對這種技術提供支持,那就只能通過擴展來實現了。
    廠商在發行OpenGL時,除了遵照OpenGL標準,提供標準所規定的各種功能外,往往還提供其它一些額外的功能,這就是擴展。
    擴展的存在,使得各種新的技術可以迅速的被應用到OpenGL中。比如“多重紋理”,它是在OpenGL 1.3中才被加入到標準中的,在OpenGL 1.3出現以前,很多OpenGL實現都通過擴展來支持“多重紋理”。這樣,即使OpenGL版本不更新,只要增加新的擴展,也可以提供新的功能了。這也說明,即使OpenGL版本較低,也不一定不支持一些高版本OpenGL才提供的功能。實際上某些OpenGL 1.5的實現,也可能提供了最新的OpenGL 2.1版本所規定的大部分功能。
    當然擴展也有缺點,那就是程序在運行的時候必須檢查每個擴展功能是否被支持,導致編寫程序代碼復雜。

    擴展的名字
    每個OpenGL擴展,都必須向OpenGL的網站注冊,確認后才能成為擴展。注冊后的擴展有編號和名字。編號僅僅是一個序號,名字則與擴展所提供的功能相關。
    名字用下劃線分為三部分。舉例來說,一個擴展的名字可能為:GL_NV_half_float,其意義如下:
    第一部分為擴展的目標。比如GL表示這是一個OpenGL擴展。如果是WGL則表示這是一個針對Windows的OpenGL擴展,如果是GLX則表示這是一個針對linux的X Window系統的OpenGL擴展。
    第二部分為提供擴展的廠商。比如NV表示這是nVidia公司所提供的擴展。相應的還有ATI, IBM, SGI, APPLE, MESA等。
    剩下的部分就表示擴展所提供的內容了。比如half_float,表示半精度的浮點數,每個浮點數的精度只有單精度浮點數的一半,因此只需要兩個字節就可以保存。這種擴展功能可以節省內存空間,也節省從內存到顯卡的數據傳輸量,代價就是精確度有所降低。
    EXT擴展和ARB擴展
    最初的時候,每個廠商都提供自己的擴展。這樣導致的結果就是,即使是提供相同的功能,不同的廠商卻提供不同的擴展,這樣在編寫程序的時候,使用一種功能就需要依次檢查每個可能支持這種功能的擴展,非常繁瑣。
    于是出現了EXT擴展和ARB擴展。
    EXT擴展是由多個廠商共同協商后形成的擴展,在擴展名字中,“提供擴展的廠商”一欄將不再是具體的廠商名,而是EXT三個字母。比如GL_EXT_bgra,就是一個EXT擴展。
    ARB擴展不僅是由多個廠商共同協商形成,還需要經過OpenGL體系結構審核委員會(即ARB)的確認。在擴展名字中,“提供擴展的廠商”一欄不再是具體的廠商名字,而是ARB三個字母。比如GL_ARB_imaging,就是一個ARB擴展。
    通常,一種功能如果有多個廠商提出,則它成為EXT擴展。在以后的時間里,如果經過了ARB確認,則它成為ARB擴展。再往后,如果OpenGL的維護者認為這種功能需要加入到標準規定中,則它不再是擴展,而成為標準的一部分。
    例如point_parameters,就是先有GL_EXT_point_parameters,再有GL_ARB_point_parameters,最后到OpenGL 1.4版本時,這個功能為標準規定必須提供的功能,不再是一個擴展。
    在使用OpenGL所提供的功能時,應該按照標準功能、ARB擴展、EXT擴展、其它擴展這樣的優先順序。例如有ARB擴展支持這個功能時,就不使用EXT擴展。
    在程序中,判斷OpenGL是否支持某個擴展
    前面已經說過,glGetString(GL_EXTENSIONS)會返回當前OpenGL所支持的所有擴展的名字,中間用空格分開,這就是我們判斷是否支持某個擴展的依據。

    #include <string.h>
    // 判斷OpenGL是否支持某個指定的擴展
    // 若支持,返回1。否則返回0。
    int hasExtension(const char* name) {
        const char* extensions = (const char*)glGetString(GL_EXTENSIONS);
        const char* end = extensions +strlen(extensions);
        size_t name_length = strlen(name);
        while( extensions < end ) {
            size_t position = strchr(extensions, ' ') - extensions;
            if( position == name_length &&
                    strncmp(extensions, name, position) == 0 )
                return 1;
             extensions += (position + 1);
         }
        return 0;
    }



    上面這段代碼,判斷了OpenGL是否支持指定的擴展,可以看到,判斷時完全是靠字符串處理來實現的。循環檢測,找到第一個空格,然后比較空格之前的字符串是否與指定的名字一致。若一致,說明擴展是被支持的;否則,繼續比較。若所有內容都比較完,則說明擴展不被支持。
    編寫程序調用擴展的功能
    擴展的函數、常量,在命名時與通常的OpenGL函數、常量有少許區別。那就是擴展的函數、常量將以廠商的名字作為后綴。
    比如ARB擴展,所有ARB擴展的函數,函數名都以ARB結尾,常量名都以_ARB結尾。例如:
    glGenBufferARB(函數)
    GL_ARRAY_BUFFER_ARB(常量)
    如果已經知道OpenGL支持某個擴展,則如何調用擴展中的函數?大致的思路就是利用函數指針。但是不幸的是,在不同的操作系統中,取得這些函數指針的方法各不相同。為了能夠在各個操作系統中都能順利的使用擴展,我向大家介紹一個小巧的工具:GLEE。
    GLEE是一個開放源代碼的項目,可以從網絡上搜索并下載。其代碼由兩個文件組成,一個是GLee.c,一個是GLee.h。把兩個文件都放到自己的源代碼一起編譯,運行的時候,GLee可以自動的判斷所有擴展是否被支持,如果支持,GLEE會自動讀取對應的函數,供我們調用。
    我們自己編寫代碼時,需要首先包含GLee.h,然后才包含GL/glut.h(注意順序不能調換),然后就可以方便的使用各種擴展功能了。

    #include "GLee.h"
    #include <GL/glut.h> // 注意順序,GLee.h要在glut.h之前使用



    GLEE也可以幫助我們判斷OpenGL是否支持某個擴展,因此有了GLEE,前面那個判斷是否支持擴展的函數就不太必要了。
    示例代碼
    讓我們用一段示例代碼結束本課。
    我們選擇一個目前絕大多數顯卡都支持的擴展GL_ARB_window_pos,來說明如何使用GLEE來調用OpenGL擴展功能。通常我們在繪制像素時,需要用glRasterPos*函數來指定繪制的位置。但是,glRasterPos*函數使用的不是屏幕坐標,例如指定(0, 0)不一定是左下角,這個坐標需要經過各種變換(參見第五課,變換),最后才得到屏幕上的窗口位置。
    通過GL_ARB_window_pos擴展,我們可以直接用屏幕上的坐標來指定繪制的位置,不再需要經過變換,這樣在很多場合會顯得簡單。

    #include "GLee.h"
    #include <GL/glut.h>

    void display(void) {
         glClear(GL_COLOR_BUFFER_BIT);

        if( GLEE_ARB_window_pos ) { // 如果支持GL_ARB_window_pos
                                     // 則使用glWindowPos2iARB函數,指定繪制位置
            printf("支持GL_ARB_window_pos\n");
            printf("使用glWindowPos函數\n");
             glWindowPos2iARB(100, 100);
         } else {                     // 如果不支持GL_ARB_window_pos
                                     // 則只能使用glRasterPos*系列函數
                                     // 先計算出一個經過變換后能夠得到
                                     //    (100, 100)的坐標(x, y, z)
                                     // 然后調用glRasterPos3d(x, y, z);
             GLint viewport[4];
             GLdouble modelview[16], projection[16];
             GLdouble x, y, z;

            printf("不支持GL_ARB_window_pos\n");
            printf("使用glRasterPos函數\n");

             glGetIntegerv(GL_VIEWPORT, viewport);
             glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
             glGetDoublev(GL_PROJECTION_MATRIX, projection);
             gluUnProject(100, 100, 0.5, modelview, projection, viewport,
                 &x, &y, &z);
             glRasterPos3d(x, y, z);
         }

         { // 繪制一個5*5的像素塊
             GLubyte pixels[5][5][4];
             // 把像素中的所有像素都設置為紅色
            int i, j;
            for(i=0; i<5; ++i)
                for(j=0; j<5; ++j) {
                     pixels[i][j][0] = 255; // red
                     pixels[i][j][1] = 0;    // green
                     pixels[i][j][2] = 0;    // blue
                     pixels[i][j][3] = 255; // alpha
                 }
             glDrawPixels(5, 5, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
         }

         glutSwapBuffers();
    }

    int main(int argc,char* argv[]) {
         glutInit(&argc, argv);
         glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
         glutInitWindowPosition(100, 100);
         glutInitWindowSize(512, 512);
         glutCreateWindow("OpenGL");
         glutDisplayFunc(&display);
         glutMainLoop();
    }



    可以看到,使用了擴展以后,代碼會簡單得多了。不支持GL_ARB_window_pos擴展時必須使用較多的代碼才能實現的功能,使用GL_ARB_window_pos擴展后即可簡單的解決。
    如果把代碼修改一下,不使用擴展而直接使用else里面的代碼,可以發現運行效果是一樣的。
    工具軟件
    在課程的最后我還向大家介紹一個免費的工具軟件,這就是OpenGL Extension Viewer(各大軟件網站均有下載,請自己搜索之),目前較新的版本是3.0。
    這個軟件可以查看自己計算機系統的OpenGL信息。包括OpenGL版本、提供廠商、設備名稱、所支持的擴展等。
    軟件可以查看的信息很詳細,比如查看允許的最大紋理大小、最大光源數目等。
    在查看擴展時,可以在最下面一欄輸入擴展的名字,按下回車后即可連接到OpenGL官方網站,查找關于這個擴展的詳細文檔,非常不錯。
    可以根據電腦的配置情況,自動連接到對應的官方網站,方便下載最新驅動。(比如我是nVidia的顯卡,則連接到nVidia的驅動下載頁面)
    可以進行OpenGL測試,看看運行起來性能如何。
    可以給出總體報告,如果一些比較重要的功能不被支持,則會用粗體字標明。
    軟件還帶有一個數據庫,可以查詢各廠商、各型號的顯卡對OpenGL各種擴展的支持情況。
    小結

    本課介紹了OpenGL版本和OpenGL擴展。
    OpenGL從誕生到現在,經歷了1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1這些版本。
    每個系統中的OpenGL版本可能不同。使用glGetString(GL_VERSION);可以查看當前的OpenGL版本。
    新版本的OpenGL將兼容舊版本的OpenGL,同時提供更多的新特性和新功能。
    OpenGL在實現時可以通過擴展,來提供額外的功能。
    OpenGL擴展有廠家擴展、EXT擴展、ARB擴展。通常應該盡量使用標準功能,其次才是ARB擴展、EXT擴展、廠家擴展。
    GLEE是一個可以免費使用的工具,使用它可以方便的判斷當前的OpenGL是否支持某擴展,也可以方便的調用擴展。
    OpenGL Extension Viewer是一個軟件,可以檢查系統所支持OpenGL的版本、支持的擴展、以及很多的詳細信息。


    OpenGL入門學習[十五]


    這次講的所有內容都裝在一個立方體中,呵呵。
    呵呵,繪制一個立方體,簡單呀,我們學了第一課第二課,早就會了。
    先別著急,立方體是很簡單,但是這里只是拿立方體做一個例子,來說明OpenGL在繪制方法上的改進。
    從原始一點的辦法開始
    一個立方體有六個面,每個面是一個正方形,好,繪制六個正方形就可以了。

    glBegin(GL_QUADS);
         glVertex3f(...);
         glVertex3f(...);
         glVertex3f(...);
         glVertex3f(...);

         // ...
    glEnd();



    為了繪制六個正方形,我們為每個正方形指定四個頂點,最終我們需要指定6*4=24個頂點。但是我們知道,一個立方體其實總共只有八個頂點,要指定24 次,就意味著每個頂點其實重復使用了三次,這樣可不是好的現象。最起碼,像上面這樣重復煩瑣的代碼,是很容易出錯的。稍有不慎,即使相同的頂點也可能被指定成不同的頂點了。
    如果我們定義一個數組,把八個頂點都放到數組里,然后每次指定頂點都使用指針,而不是使用直接的數據,這樣就避免了在指定頂點時考慮大量的數據,于是減少了代碼出錯的可能性。

    // 將立方體的八個頂點保存到一個數組里面
    static const GLfloat vertex_list[][3] = {
         -0.5f, -0.5f, -0.5f,
          0.5f, -0.5f, -0.5f,
         // ...
    };
    // 指定頂點時,用指針,而不用直接用具體的數據
    glBegin(GL_QUADS);
         glVertex3fv(vertex_list[0]);
         glVertex3fv(vertex_list[2]);
         glVertex3fv(vertex_list[3]);
         glVertex3fv(vertex_list[1]);

         // ...
    glEnd();



    修改之后,雖然代碼變長了,但是確實易讀得多。很容易就看出第0, 2, 3, 1這四個頂點構成一個正方形。
    稍稍觀察就可以發現,我們使用了大量的glVertex3fv函數,其實每一句都只有其中的頂點序號不一樣,因此我們可以再定義一個序號數組,把所有的序號也放進去。這樣一來代碼就更加簡單了。

    // 將立方體的八個頂點保存到一個數組里面
    static const GLfloat vertex_list[][3] = {
         -0.5f, -0.5f, -0.5f,
          0.5f, -0.5f, -0.5f,
         -0.5f,   0.5f, -0.5f,
          0.5f,   0.5f, -0.5f,
         -0.5f, -0.5f,   0.5f,
          0.5f, -0.5f,   0.5f,
         -0.5f,   0.5f,   0.5f,
          0.5f,   0.5f,   0.5f,
    };

    // 將要使用的頂點的序號保存到一個數組里面
    static const GLint index_list[][4] = {
         0, 2, 3, 1,
         0, 4, 6, 2,
         0, 1, 5, 4,
         4, 5, 7, 6,
         1, 3, 7, 5,
         2, 6, 7, 3,
    };

    int i, j;

    // 繪制的時候代碼很簡單
    glBegin(GL_QUADS);
    for(i=0; i<6; ++i)          // 有六個面,循環六次
        for(j=0; j<4; ++j)      // 每個面有四個頂點,循環四次
             glVertex3fv(vertex_list[index_list[i][j]]);
    glEnd();



    這樣,我們就得到一個比較成熟的繪制立方體的版本了。它的數據和程序代碼基本上是分開的,所有的頂點放到一個數組中,使用頂點的序號放到另一個數組中,而利用這兩個數組來繪制立方體的代碼則很簡單。
    關于頂點的序號,下面這個圖片可以幫助理解。


    正對我們的面,按逆時針順序,背對我們的面,則按順時針順序,這樣就得到了上面那個index_list數組。
    為什么要按照順時針逆時針的規則呢?因為這樣做可以保證無論從哪個角度觀察,看到的都是“正面”,而不是背面。在計算光照時,正面和背面的處理可能是不同的,另外,剔除背面只繪制正面,可以提高程序的運行效率。(關于正面、背面,以及剔除,參見第三課,繪制幾何圖形的一些細節問題)
    例如在繪制之前調用如下的代碼:

    glFrontFace(GL_CCW);
    glCullFace(GL_BACK);
    glEnable(GL_CULL_FACE);
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);


    則繪制出來的圖形就只有正面,并且只顯示邊線,不進行填充。
    效果如圖:

    頂點數組
    (提示:頂點數組是OpenGL 1.1所提供的功能)
    前面的方法中,我們將數據和代碼分離開,看起來只要八個頂點就可以繪制一個立方體了。但是實際上,循環還是執行了6*4=24次,也就是說雖然代碼的結構清晰了不少,但是程序運行的效率,還是和最原始的那個方法一樣。
    減少函數的調用次數,是提高運行效率的方法之一。于是我們想到了顯示列表。把繪制立方體的代碼裝到一個顯示列表中,以后只要調用這個顯示列表即可。
    這樣看起來很不錯,但是顯示列表有一個缺點,那就是一旦建立后不可再改。如果我們要繪制的不是立方體,而是一個能夠走動的人物,因為人物走動時,四肢的位置不斷變化,幾乎沒有辦法把所有的內容裝到一個顯示列表中。必須每種動作都使用單獨的顯示列表,這樣會導致大量的顯示列表管理困難。
    頂點數組是解決這個問題的一個方法。使用頂點數組的時候,也是像前面的方法一樣,用一個數組保存所有的頂點,用一個數組保存頂點的序號。但最后繪制的時候,不是編寫循環語句逐個的指定頂點了,而是通知OpenGL,“保存頂點的數組”和“保存頂點序號的數組”所在的位置,由OpenGL自動的找到頂點,并進行繪制。
    下面的代碼說明了頂點數組是如何使用的:

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3, GL_FLOAT, 0, vertex_list);
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);



    其中:
    glEnableClientState(GL_VERTEX_ARRAY); 表示啟用頂點數組。
    glVertexPointer(3, GL_FLOAT, 0, vertex_list); 指定頂點數組的位置,3表示每個頂點由三個量構成(x, y, z),GL_FLOAT表示每個量都是一個GLfloat類型的值。第三個參數0,參見后面介紹“stride參數”。最后的vertex_list指明了數組實際的位置。
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list); 根據序號數組中的序號,查找到相應的頂點,并完成繪制。GL_QUADS表示繪制的是四邊形,24表示總共有24個頂點,GL_UNSIGNED_INT 表示序號數組內每個序號都是一個GLuint類型的值,index_list指明了序號數組實際的位置。
    上面三行代碼代替了原來的循環。可以看到,原來的glBegin/glEnd不再需要了,也不需要調用glVertex*系列函數來指定頂點,因此可以明顯的減少函數調用次數。另外,數組中的內容可以隨時修改,比顯示列表更加靈活。

    詳細一點的說明。
    頂點數組實際上是多個數組,頂點坐標、紋理坐標、法線向量、頂點顏色等等,頂點的每一個屬性都可以指定一個數組,然后用統一的序號來進行訪問。比如序號 3,就表示取得顏色數組的第3個元素作為顏色、取得紋理坐標數組的第3個元素作為紋理坐標、取得法線向量數組的第3個元素作為法線向量、取得頂點坐標數組的第3個元素作為頂點坐標。把所有的數據綜合起來,最終得到一個頂點。
    可以用glEnableClientState/glDisableClientState單獨的開啟和關閉每一種數組。
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    用以下的函數來指定數組的位置:
    glVertexPointer
    glColorPointer
    glNormalPointer
    glTexCoordPointer

    為什么不使用原來的glEnable/glDisable函數,而要專門的規定一個glEnableClientState /glDisableClientState函數呢?這跟OpenGL的工作機制有關。OpenGL在設計時,認為可以將整個OpenGL系統分為兩部分,一部分是客戶端,它負責發送OpenGL命令。一部分是服務端,它負責接收OpenGL命令并執行相應的操作。對于個人計算機來說,可以將CPU、內存等硬件,以及用戶編寫的OpenGL程序看做客戶端,而將OpenGL驅動程序、顯示設備等看做服務端。
    通常,所有的狀態都是保存在服務端的,便于OpenGL使用。例如,是否啟用了紋理,服務端在繪制時經常需要知道這個狀態,而我們編寫的客戶端OpenGL程序只在很少的時候需要知道這個狀態。所以將這個狀態放在服務端是比較有利的。
    但頂點數組的狀態則不同。我們指定頂點,實際上就是把頂點數據從客戶端發送到服務端。是否啟用頂點數組,只是控制發送頂點數據的方式而已。服務端只管接收頂點數據,而不必管頂點數據到底是用哪種方式指定的(可以直接使用glBegin/glEnd/glVertex*,也可以使用頂點數組)。所以,服務端不需要知道頂點數組是否開啟。因此,頂點數組的狀態放在客戶端是比較合理的。
    為了表示服務端狀態和客戶端狀態的區別,服務端的狀態用glEnable/glDisable,客戶端的狀態則用glEnableClientState/glDisableClientState。
    stride參數。
    頂點數組并不要求所有的數據都連續存放。如果數據沒有連續存放,則指定數據之間的間隔即可。
    例如:我們使用一個struct來存放頂點中的數據。注意每個頂點除了坐標外,還有額外的數據(這里是一個int類型的值)。

    typedef struct __point__ {
         GLfloat position[3];
        int      id;
    } Point;
    Point vertex_list[] = {
         -0.5f, -0.5f, -0.5f, 1,
          0.5f, -0.5f, -0.5f, 2,
         -0.5f,   0.5f, -0.5f, 3,
          0.5f,   0.5f, -0.5f, 4,
         -0.5f, -0.5f,   0.5f, 5,
          0.5f, -0.5f,   0.5f, 6,
         -0.5f,   0.5f,   0.5f, 7,
          0.5f,   0.5f,   0.5f, 8,
    };
    static GLint index_list[][4] = {
         0, 2, 3, 1,
         0, 4, 6, 2,
         0, 1, 5, 4,
         4, 5, 7, 6,
         1, 3, 7, 5,
         2, 6, 7, 3,
    };
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3, GL_FLOAT, sizeof(Point), vertex_list);
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);



    注意最后三行代碼,可以看到,幾乎所有的地方都和原來一樣,只在glVertexPointer函數的第三個參數有所不同。這個參數就是stride,它表示“從一個數據的開始到下一個數據的開始,所相隔的字節數”。這里設置為sizeof(Point)就剛剛好。如果設置為0,則表示數據是緊密排列的,對于3個GLfloat的情況,數據緊密排列時stride實際上為3*4=12。
    混合數組。如果需要同時使用顏色數組、頂點坐標數組、紋理坐標數組、等等,有一種方式是把所有的數據都混合起來,指定到同一個數組中。這就是混合數組。

    GLfloat arr_c3f_v3f[] = {
         1, 0, 0, 0, 1, 0,
         0, 1, 0, 1, 0, 0,
         0, 0, 1, -1, 0, 0,
    };
    GLuint index_list[] = {0, 1, 2};
    glInterleavedArrays(GL_C3F_V3F, 0, arr_c3f_v3f);
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, index_list);



    glInterleavedArrays,可以設置混合數組。這個函數會自動調用glVertexPointer, glColorPointer等函數,并且自動的開啟或禁用相關的數組。
    函數的第一個參數表示了混合數組的類型。例如GL_C3F_V3F表示:三個浮點數作為顏色、三個浮點數作為頂點坐標。也可以有其它的格式,比如 GL_V2F, GL_V3F, GL_C4UB_V2F, GL_C4UB_V3F, GL_C3F_V3F, GL_N3F_V3F, GL_C4F_N3F_V3F, GL_T2F_V3F, GL_T4F_V4F, GL_T2F_C4UB_V3F, GL_T2F_C3F_V3F, GL_T2F_N3F_V3F, GL_T2F_C4F_N3F_V3F, GL_T4F_C4F_N3F_V4F等等。其中T表示紋理坐標,C表示顏色,N表示法線向量,V表示頂點坐標。
    再來說說頂點數組與顯示列表的區別。兩者都可以明顯的減少函數的調用次數,但是還是各有優點的。
    對于頂點數組,頂點數據是存放在內存中的,也就是存放在客戶端。每次繪制的時候,需要把所有的頂點數據從客戶端(內存)發送到服務端(顯示設備),然后進行處理。對于顯示列表,頂點數據是放在顯示列表中的,顯示列表本身又是存放在服務器端的,所以不會重復的發送數據。
    對于頂點數組,因為頂點數據放在內存中,所以可以隨時修改,每次繪制的時候都會把當前數組中的內容作為頂點數據發送并進行繪制。對于顯示列表,數據已經存放到服務器段,并且無法取出,所以無法修改。
    也就是說,顯示列表可以避免數據的重復發送,效率會較高;頂點數組雖然會重復的發送數據,但由于數據可以隨時修改,靈活性較好
    頂點緩沖區對象
    (提示:頂點緩沖區對象是OpenGL 1.5所提供的功能,但它在成為標準前是一個ARB擴展,可以通過GL_ARB_vertex_buffer_object擴展來使用這項功能。前面已經講過,ARB擴展的函數名稱以字母ARB結尾,常量名稱以字母_ARB結尾,而標準函數、常量則去掉了ARB字樣。很多的OpenGL實現同時支持 vertex buffer object的標準版本和ARB擴展版本。我們這里以ARB擴展來講述,因為目前絕大多數個人計算機都支持ARB擴展版本,但少數顯卡僅支持OpenGL 1.4,無法使用標準版本。)
    前面說到頂點數組和顯示列表在繪制立方體時各有優劣,那么有沒有辦法將它們的優點集中到一起,并且盡可能的減少缺點呢?頂點緩沖區對象就是為了解決這個問題而誕生的。它數據存放在服務端,同時也允許客戶端靈活的修改,兼顧了運行效率和靈活性。
    頂點緩沖區對象跟紋理對象有很多相似之處。首先,分配一個緩沖區對象編號,然后,為對應編號的緩沖區對象指定數據,以后可以隨時修改其中的數據。下面的表格可以幫助類比理解。

                                       紋理對象          頂點緩沖區對象
    分配編號                           glGenTextures     glGenBuffersARB
    綁定(指定為當前所使用的對象)     glBindTexture     glBindBufferARB
    指定數據                           glTexImage*       glBufferDataARB
    修改數據                           glTexSubImage*    glBufferSubDataARB



    頂點數據和序號各自使用不同的緩沖區。具體的說,就是頂點數據放在GL_ARRAY_BUFFER_ARB類型的緩沖區中,序號數據放在GL_ELEMENT_ARRAY_BUFFER_ARB類型的緩沖區中。
    具體的情況可以用下面的代碼來說明:

    static GLuint vertex_buffer;
    static GLuint index_buffer;

    // 分配一個緩沖區,并將頂點數據指定到其中
    glGenBuffersARB(1, &vertex_buffer);
    glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
    glBufferDataARB(GL_ARRAY_BUFFER_ARB,
        sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

    // 分配一個緩沖區,并將序號數據指定到其中
    glGenBuffersARB(1, &index_buffer);
    glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);
    glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,
        sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);



    在指定緩沖區數據時,最后一個參數是關于性能的提示。一共有STREAM_DRAW, STREAM_READ, STREAM_COPY, STATIC_DRAW, STATIC_READ, STATIC_COPY, DYNAMIC_DRAW, DYNAMIC_READ, DYNAMIC_COPY這九種。每一種都表示了使用頻率和用途,OpenGL會根據這些提示進行一定程度的性能優化。
    (提示僅僅是提示,不是硬性規定。也就是說,即使使用了STREAM_DRAW,告訴OpenGL這段緩沖區數據一旦指定,以后不會修改,但實際上以后仍可修改,不過修改時可能有較大的性能代價)

    當使用glBindBufferARB后,各種使用指針為參數的OpenGL函數,行為會發生變化。
    以glColor3fv為例,通常,這個函數接受一個指針作為參數,從指針所指的位置取出連續的三個浮點數,作為當前的顏色。
    但使用glBindBufferARB后,這個函數不再從指針所指的位置取數據。函數會先把指針轉化為整數,假設轉化后結果為k,則會從當前緩沖區的第k 個字節開始取數據。特別一點,如果我們寫glColor3fv(NULL);因為NULL轉化為整數后通常是零,所以從緩沖區的第0個字節開始取數據,也就是從緩沖區最開始的位置取數據。
    這樣一來,原來寫的

    glVertexPointer(3, GL_FLOAT, 0, vertex_list);
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);


    在使用緩沖區對象后,就變成了

    glVertexPointer(3, GL_FLOAT, 0, NULL);
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);

    以下是完整的使用了頂點緩沖區對象的代碼:

    static GLfloat vertex_list[][3] = {
         -0.5f, -0.5f, -0.5f,
          0.5f, -0.5f, -0.5f,
         -0.5f,   0.5f, -0.5f,
          0.5f,   0.5f, -0.5f,
         -0.5f, -0.5f,   0.5f,
          0.5f, -0.5f,   0.5f,
         -0.5f,   0.5f,   0.5f,
          0.5f,   0.5f,   0.5f,
    };

    static GLint index_list[][4] = {
         0, 2, 3, 1,
         0, 4, 6, 2,
         0, 1, 5, 4,
         4, 5, 7, 6,
         1, 3, 7, 5,
         2, 6, 7, 3,
    };

    if( GLEE_ARB_vertex_buffer_object ) {
         // 如果支持頂點緩沖區對象
        static int isFirstCall = 1;
        static GLuint vertex_buffer;
        static GLuint index_buffer;
        if( isFirstCall ) {
             // 第一次調用時,初始化緩沖區
             isFirstCall = 0;

             // 分配一個緩沖區,并將頂點數據指定到其中
             glGenBuffersARB(1, &vertex_buffer);
             glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
             glBufferDataARB(GL_ARRAY_BUFFER_ARB,
                sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

             // 分配一個緩沖區,并將序號數據指定到其中
             glGenBuffersARB(1, &index_buffer);
             glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);
             glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,
                sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);
         }
         glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
         glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);

         // 實際使用時與頂點數組非常相似,只是在指定數組時不再指定實際的數組,改為指定NULL即可
         glEnableClientState(GL_VERTEX_ARRAY);
         glVertexPointer(3, GL_FLOAT, 0, NULL);
         glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);
    } else {
         // 不支持頂點緩沖區對象
         // 使用頂點數組
         glEnableClientState(GL_VERTEX_ARRAY);
         glVertexPointer(3, GL_FLOAT, 0, vertex_list);
         glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
    }

    可以分配多個緩沖區對象,頂點坐標、顏色、紋理坐標等數據,可以各自單獨使用一個緩沖區。
    每個緩沖區可以有不同的性能提示,比如在繪制一個運動的人物時,頂點坐標數據經常變化,但法線向量、紋理坐標等則不會變化,可以給予不同的性能提示,以提高性能。
    小結

    本課從繪制一個立方體出發,描述了OpenGL在各個版本中對于繪制的處理。
    繪制物體的時候,應該將數據單獨存放,盡量不要到處寫類似glVertex3f(1.0f, 0.0f, 1.0f)這樣的代碼。將頂點坐標、頂點序號都存放到單獨的數組中,可以讓繪制的代碼變得簡單。
    可以把繪制物體的所有命令裝到一個顯示列表中,這樣可以避免重復的數據傳送。但是因為顯示列表一旦建立,就無法修改,所以靈活性很差。
    OpenGL 1.1版本,提供了頂點數組。它可以指定數據的位置、頂點序號的位置,從而有效的減少函數調用次數,達到提高效率的目的。但是它沒有避免重復的數據傳送,所以效率還有待進一步提高。
    OpenGL 1.5版本,提供了頂點緩沖區對象。它綜合了顯示列表和頂點數組的優點,同時兼顧運行效率和靈活性,是繪制物體的一個好選擇。如果系統不支持OpenGL 1.5,也可以檢查是否支持擴展GL_ARB_vertex_buffer_object。



    第十六課,在Windows系統中顯示文字 

    增加了兩個文件,showline.c, showtext.c。分別為第二個和第三個示例程序的main函數相關部分。
    在ctbuf.h和textarea.h最開頭部分增加了一句#include <stdlib.h>
    附件中一共有三個示例程序:
    第一個,飄動的“曹”字旗。代碼為:flag.c, GLee.c, GLee.h
    第二個,帶緩沖的顯示文字。代碼為:showline.c, ctbuf.c, ctbuf.h, GLee.c, GLee.h
    第三個,顯示歌詞。代碼為:showtext.c, ctbuf.c, ctbuf.h, textarea.c, textarea.h, GLee.c, GLee.h
    其中,GLee.h和GLee.c可以從網上下載,因此這里并沒有放到附件中。在編譯的時候應該將這兩個文件和其它代碼文件一起編譯。

    本課我們來談談如何顯示文字。
    OpenGL并沒有直接提供顯示文字的功能,并且,OpenGL也沒有自帶專門的字庫。因此,要顯示文字,就必須依賴操作系統所提供的功能了。
    各種流行的圖形操作系統,例如Windows系統和Linux系統,都提供了一些功能,以便能夠在OpenGL程序中方便的顯示文字。
    最常見的方法就是,我們給出一個字符,給出一個顯示列表編號,然后操作系統由把繪制這個字符的OpenGL命令裝到指定的顯示列表中。當需要繪制字符的時候,我們只需要調用這個顯示列表即可。
    不過,Windows系統和Linux系統,產生這個顯示列表的方法是不同的(雖然大同小異)。作為我個人,只在Windows系統中編程,沒有使用Linux系統的相關經驗,所以本課我們僅針對Windows系統。


    OpenGL版的“Hello, World!”
    寫完了本課,我的感受是:顯示文字很簡單,顯示文字很復雜。看似簡單的功能,背后卻隱藏了深不可測的玄機。
    呵呵,別一開始就被嚇住了,讓我們先從“Hello, World!”開始。
    前面已經說過了,要顯示字符,就需要通過操作系統,把繪制字符的動作裝到顯示列表中,然后我們調用顯示列表即可繪制字符。
    假如我們要顯示的文字全部是ASCII字符,則總共只有0到127這128種可能,因此可以預先把所有的字符分別裝到對應的顯示列表中,然后在需要時調用這些顯示列表。
    Windows系統中,可以使用wglUseFontBitmaps函數來批量的產生顯示字符用的顯示列表。函數有四個參數:
    第一個參數是HDC,學過Windows GDI的朋友應該會熟悉這個。如果沒有學過,那也沒關系,只要知道調用wglGetCurrentDC函數,就可以得到一個HDC了。具體的情況可以看下面的代碼。
    第二個參數表示第一個要產生的字符,因為我們要產生0到127的字符的顯示列表,所以這里填0。
    第三個參數表示要產生字符的總個數,因為我們要產生0到127的字符的顯示列表,總共有128個字符,所以這里填128。
    第四個參數表示第一個字符所對應顯示列表的編號。假如這里填1000,則第一個字符的繪制命令將被裝到第1000號顯示列表,第二個字符的繪制命令將被裝到第1001號顯示列表,依次類推。我們可以先用glGenLists申請128個連續的顯示列表編號,然后把第一個顯示列表編號填在這里。
    還要說明一下,因為wglUseFontBitmaps是Windows系統特有的函數,所以在使用前需要加入頭文件:#include <windows.h>。
    現在讓我們來看具體的代碼:

    #include <windows.h>

    // ASCII字符總共只有0到127,一共128種字符
    #define MAX_CHAR       128

    void drawString(const char* str) {
        static int isFirstCall = 1;
        static GLuint lists;

        if( isFirstCall ) { // 如果是第一次調用,執行初始化
                            // 為每一個ASCII字符產生一個顯示列表
            isFirstCall = 0;

            // 申請MAX_CHAR個連續的顯示列表編號
            lists = glGenLists(MAX_CHAR);

            // 把每個字符的繪制命令都裝到對應的顯示列表中
            wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
        }
        // 調用每個字符對應的顯示列表,繪制每個字符
        for(; *str!='\0'; ++str)
            glCallList(lists + *str);
    }



    顯示列表一旦產生就一直存在(除非調用glDeleteLists銷毀),所以我們只需要在第一次調用的時候初始化,以后就可以很方便的調用這些顯示列表來繪制字符了。
    繪制字符的時候,可以先用glColor*等指定顏色,然后用glRasterPos*指定位置,最后調用顯示列表來繪制。

    void display(void) {
        glClear(GL_COLOR_BUFFER_BIT);

        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(0.0f, 0.0f);
        drawString("Hello, World!");

        glutSwapBuffers();
    }



    效果如圖:


    指定字體
    在產生顯示列表前,Windows允許選擇字體。
    我做了一個selectFont函數來實現它,大家可以看看代碼。

    void selectFont(int size, int charset, const char* face) {
        HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
            charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
            DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face);
        HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
        DeleteObject(hOldFont);
    }

    void display(void) {
        selectFont(48, ANSI_CHARSET, "Comic Sans MS");

        glClear(GL_COLOR_BUFFER_BIT);

        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(0.0f, 0.0f);
        drawString("Hello, World!");

        glutSwapBuffers();
    }


    最主要的部分就在于那個參數超多的CreateFont函數,學過Windows GDI的朋友應該不會陌生。沒有學過GDI的朋友,有興趣的話可以自己翻翻MSDN文檔。這里我并不準備仔細講這些參數了,下面的內容還多著呢:(
    如果需要在自己的程序中選擇字體的話,把selectFont函數抄下來,在調用glutCreateWindow之后、在調用 wglUseFontBitmaps之前使用selectFont函數即可指定字體。函數的三個參數分別表示了字體大小、字符集(英文字體可以用 ANSI_CHARSET,簡體中文字體可以用GB2312_CHARSET,繁體中文字體可以用CHINESEBIG5_CHARSET,對于中文的 Windows系統,也可以直接用DEFAULT_CHARSET表示默認字符集)、字體名稱。
    效果如圖:



    顯示中文
    原則上,顯示中文和顯示英文并無不同,同樣是把要顯示的字符做成顯示列表,然后進行調用。
    但是有一個問題,英文字母很少,最多只有幾百個,為每個字母創建一個顯示列表,沒有問題。但是漢字有非常多個,如果每個漢字都產生一個顯示列表,這是不切實際的。
    我們不能在初始化時就為每個字符建立一個顯示列表,那就只有在每次繪制字符時創建它了。當我們需要繪制一個字符時,創建對應的顯示列表,等繪制完畢后,再將它銷毀。
    這里還經常涉及到中文亂碼的問題,我對這個問題也不甚了解,但是網上流傳的版本中,使用了MultiByteToWideChar這個函數的,基本上都沒有出現亂碼,所以我也準備用這個函數:)
    通常我們在C語言里面使用的字符串,如果中英文混合的話,例如“this is 中文字符.”,則英文字符只占用一個字節,而中文字符則占用兩個字節。用 MultiByteToWideChar函數,可以轉化為所有的字符都占兩個字節(同時解決了前面所說的亂碼問題:))。
    轉化的代碼如下:

    // 計算字符的個數
    // 如果是雙字節字符的(比如中文字符),兩個字節才算一個字符
    // 否則一個字節算一個字符
    len = 0;
    for(i=0; str[i]!='\0'; ++i)
    {
        if( IsDBCSLeadByte(str[i]) )
            ++i;
        ++len;
    }

    // 將混合字符轉化為寬字符
    wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
    wstring[len] = L'\0';

    // 用完后記得釋放內存
    free(wstring);



    加上前面所講到的wglUseFontBitmaps函數,即可顯示中文字符了。

    void drawCNString(const char* str) {
        int len, i;
        wchar_twstring;
        HDC hDC = wglGetCurrentDC();
        GLuint list = glGenLists(1);

        // 計算字符的個數
        // 如果是雙字節字符的(比如中文字符),兩個字節才算一個字符
        // 否則一個字節算一個字符
        len = 0;
        for(i=0; str[i]!='\0'; ++i)
        {
            if( IsDBCSLeadByte(str[i]) )
                ++i;
            ++len;
        }

        // 將混合字符轉化為寬字符
        wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
        wstring[len] = L'\0';

        // 逐個輸出字符
        for(i=0; i<len; ++i)
        {
            wglUseFontBitmapsW(hDC, wstring[i], 1, list);
            glCallList(list);
        }

        // 回收所有臨時資源
        free(wstring);
        glDeleteLists(list, 1);
    }



    注意我用了wglUseFontBitmapsW函數,而不是wglUseFontBitmaps。wglUseFontBitmapsW是 wglUseFontBitmaps函數的寬字符版本,它認為字符都占兩個字節。因為這里使用了MultiByteToWideChar,每個字符其實是占兩個字節的,所以應該用wglUseFontBitmapsW。

    void display(void) {
        glClear(GL_COLOR_BUFFER_BIT);

        selectFont(48, ANSI_CHARSET, "Comic Sans MS");
        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(-0.7f, 0.4f);
        drawString("Hello, World!");

        selectFont(48, GB2312_CHARSET, "楷體_GB2312");
        glColor3f(1.0f, 1.0f, 0.0f);
        glRasterPos2f(-0.7f, -0.1f);
        drawCNString("當代的中國漢字");

        selectFont(48, DEFAULT_CHARSET, "華文仿宋");
        glColor3f(0.0f, 1.0f, 0.0f);
        glRasterPos2f(-0.7f, -0.6f);
        drawCNString("傳統的中國漢字");

        glutSwapBuffers();
    }


    效果如圖:

    紋理字體
    把文字放到紋理中有很多好處,例如,可以任意修改字符的大小(而不必重新指定字體)。
    對一面飄動的旗幟使用帶有文字的紋理,則文字也會隨著飄動。這個技術在“三國志”系列游戲中經常用到,比如關羽的部隊,旗幟上就飄著個“關”字,張飛的部隊,旗幟上就飄著個“張”字,曹操的大營,旗幟上就飄著個“曹”字。三國人物何其多,不可能為每種姓氏都單獨制作一面旗幟紋理,如果能夠把文字放到紋理上,則可以解決這個問題。(參見后面的例子:繪制一面“曹”字旗)
    如何把文字放到紋理中呢?自然的想法就是:“如果前面所用的顯示列表,可以直接往紋理里面繪制,那就好了”。不過,“繪制到紋理”這種技術要涉及的內容可不少,足夠我們專門拿一課的篇幅來講解了。這里我們不是直接繪制到紋理,而是用簡單一點的辦法:先把漢字繪制出來,成為像素,然后用 glCopyTexImage2D把像素復制為紋理。
    glCopyTexImage2D與glTexImage2D的用法是類似的(參見第11課),不過前者是直接把繪制好的像素復制到紋理中,后者是從內存傳送數據到紋理中。要使用到的代碼大致如下:

    // 先把文字繪制好
    glRasterPos2f(XXX, XXX);
    drawCNString("關");

    // 分配紋理編號
    glGenTextures(1, &texID);

    // 指定為當前紋理
    glBindTexture(GL_TEXTURE_2D, texID);

    // 把像素作為紋理數據
    // 將屏幕(0, 0) 到 (64, 64)的矩形區域的像素復制到紋理中
    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 64, 64, 0);

    // 設置紋理參數
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MAG_FILTER, GL_LINEAR);


    然后,我們就可以像使用普通的紋理一樣來做了。繪制各種物體時,指定合適的紋理坐標即可。


    有一個細節問題需要特別注意。大家看上面的代碼,指定文字顯示的位置,寫的是glRasterPos2f(XXX, XXX);這里來講講如何計算這個顯示坐標。
    讓我們首先從計算文字的大小談起。大家知道即使是同一字號的同一個文字,大小也可能是不同的,英文字母尤其如此,有的字體中大寫字母O和小寫字母l是一樣寬的(比如Courier New),有的字體中大寫字母O比較寬,而小寫字母l比較窄(比如Times New Roman),漢字通常比英文字母要寬。
    為了計算文字的寬度,Windows專門提供了一個函數GetCharABCWidths,它計算一系列連續字符的ABC寬度。所謂ABC寬度,包括了 a, b, c三個量,a表示字符左邊的空白寬度,b表示字符實際的寬度,c表示字符右邊的空白寬度,三個寬度值相加得到整個字符所占寬度。如果只需要得到總的寬度,可以使用GetCharWidth32函數。如果要支持漢字,應該使用寬字符版本,即GetCharABCWidthsW和 GetCharWidth32W。在使用前需要用MultiByteToWideChar函數,將通常的字符串轉化為寬字符串,就像前面的 wglUseFontBitmapsW那樣。
    解決了寬度,我們再來看看高度。本來,在指定字體的時候指定大小為s的話,所有的字符高度都為s,只有寬度不同。但是,如果我們使用 glRasterPos2i(-1, -1)從最左下角開始顯示字符的話,其實是不能得到完整的字符的:(。我們知道英文字母在寫的時候可以分上中下三欄,這時繪制出來只有上、中兩欄是可見的,下面一欄則不見了,字母g尤其明顯。見下圖:


    所以,需要把繪制的位置往上移一點,具體來說就是移動下面一欄的高度。這個高度是多少像素呢?這個我也不知道有什么好辦法來計算,根據我的經驗,移動整個字符高度的八分之一是比較合適的。例如字符大小為24,則移動3個像素。
    還要注意,OpenGL 2.0以前的版本,通常要求紋理的大小必須是2的整數次方,因此我們應該設置字體的高度為2的整數次方,例如16, 32, 64,這樣用起來就會比較方便。
    現在讓我們整理一下思路。首先要做的是將字符串轉化為寬字符的形式,以便使用wglUseFontBitmapsW和GetCharWidth32W函數。然后設置字體大小,接下來計算字體寬度,計算實際繪制的位置。然后產生顯示列表,利用顯示列表繪制字符,銷毀顯示列表。最后分配一個紋理編號,把字符像素復制到紋理中。
    呵呵,內容已經不少了,讓我們來看看代碼。 

    #define FONT_SIZE       64
    #define TEXTURE_SIZE    FONT_SIZE

    GLuint drawChar_To_Texture(const char* s) {
        wchar_t w;
        HDC hDC = wglGetCurrentDC();

        // 選擇字體字號、顏色
        // 不指定字體名字,操作系統提供默認字體
        // 設置顏色為白色
        selectFont(FONT_SIZE, DEFAULT_CHARSET, "");
        glColor3f(1.0f, 1.0f, 1.0f);

        // 轉化為寬字符
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, s, 2, &w, 1);

        // 計算繪制的位置
        {
            int width, x, y;
            GetCharWidth32W(hDC, w, w, &width);    // 取得字符的寬度
            x = (TEXTURE_SIZE - width) / 2;
            y = FONT_SIZE / 8;
            glWindowPos2iARB(x, y); // 一個擴展函數
        }

        // 繪制字符
        // 繪制前應該將各種可能影響字符顏色的效果關閉
        // 以保證能夠繪制出白色的字符
        {
            GLuint list = glGenLists(1);

            glDisable(GL_DEPTH_TEST);
            glDisable(GL_LIGHTING);
            glDisable(GL_FOG);
            glDisable(GL_TEXTURE_2D);

            wglUseFontBitmaps(hDC, w, 1, list);
            glCallList(list);
            glDeleteLists(list, 1);
        }

        // 復制字符像素到紋理
        // 注意紋理的格式
        // 不使用通常的GL_RGBA,而使用GL_LUMINANCE4
        // 因為字符本來只有一種顏色,使用GL_RGBA浪費了存儲空間
        // GL_RGBA可能占16位或者32位,而GL_LUMINANCE4只占4位
        {
            GLuint texID;
            glGenTextures(1, &texID);
            glBindTexture(GL_TEXTURE_2D, texID);
            glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4,
                0, 0, TEXTURE_SIZE, TEXTURE_SIZE, 0);
            glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            return texID;
        }
    }
    為了方便,我使用了glWindowPos2iARB這個擴展函數來指定繪制的位置。如果某個系統中OpenGL沒有支持這個擴展,則需要使用較多的代碼來實現類似的功能。為了方便的調用這個擴展,我使用了GLEE。詳細的情形可以看本教程第十四課,最后的那一個例子。GL_ARB_window_pos 擴展在OpenGL 1.3版本中已經成為標準的一部分,而幾乎所有現在還能用的顯卡在正確安裝驅動后都至少支持OpenGL 1.4,所以不必擔心不支持的問題。
    另外,占用的空間也是需要考慮的問題。通常,我們的紋理都是用GL_RGBA格式,OpenGL會保存紋理中每個像素的紅、綠、藍、alpha四個值,通常,一個像素就需要16或32個二進制位才能保存,也就是2個字節或者4個字節才保存一個像素。我們的字符只有“繪制”和“不繪制”兩種狀態,因此一個二進制位就足夠了,前面用16個或32個,浪費了大量的空間。緩解的辦法就是使用GL_LUMINANCE4這種格式,它不單獨保存紅、綠、藍顏色,而是把這三種顏色合起來稱為“亮度”,紋理中只保存這種亮度,一個像素只用四個二進制位保存亮度,比原來的16個、32個要節省不少。注意這種格式不會保存 alpha值,如果要從紋理中取alpha值的話,總是返回1.0。


    應用紋理字體的實例:飄動的旗幟
    (提示:這一段需要一些數學知識)
    有了紋理,只要我們繪制一個正方形,適當的設置紋理坐標,就可以輕松的顯示紋理圖象了(參見第十一課),因為這里紋理圖象實際上就是字符,所以我們也就顯示出了字符。并且,隨著正方形大小的變化,字符的大小也會隨著變化。
    直接貼上紋理,太簡單了。現在我們來點挑戰性的:畫一個飄動的曹操軍旗幟。效果如下圖,很酷吧?呵呵。


    效果是不錯,不過它也不是那么容易完成的,接下來我們一點一點的講解。 

    為了完成上面的效果,我們需要具備以下的知識:
    1. 用多個四邊形(實際上是矩形)連接起來,制作飄動的效果
    2. 使用光照,計算法線向量
    3. 把紋理融合進去

    因為要使用光照,法線向量是不可少的。這里我們通過不共線的三個點來得到三個點所在平面的法線向量。
    從數學的角度看,原理很簡單。三個點v1, v2, v3,可以用v2減v1,v3減v1,得到從v1到v2和從v1到v3的向量s1和s2。然后向量s1和s2進行叉乘,得到垂直于s1和s2所在平面的向量,即法線向量。
    為了方便使用,應該把法線向量縮放至單位長度,這個也很簡單,計算向量的模,然后向量的每個分量都除以這個模即可。

    #include <math.h>

    // 設置法線向量
    // 三個不在同一直線上的點可以確定一個平面
    // 先計算這個平面的法線向量,然后指定到OpenGL
    void setNormal(const GLfloat v1[3],
                   const GLfloat v2[3],
                   const GLfloat v3[3]) {
        // 首先根據三個點坐標,相減計算出兩個向量
        const GLfloat s1[] = {
            v2[0]-v1[0], v2[1]-v1[1], v2[2]-v1[2]};
        const GLfloat s2[] = {
            v3[0]-v1[0], v3[1]-v1[1], v3[2]-v1[2]};

        // 兩個向量叉乘得到法線向量的方向
        GLfloat n[] = {
            s1[1]*s2[2] - s1[2]*s2[1],
            s1[2]*s2[0] - s1[0]*s2[2],
            s1[0]*s2[1] - s1[1]*s2[0]
        };

        // 把法線向量縮放至單位長度
        GLfloat abs = sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]);
        n[0] /= abs;
        n[1] /= abs;
        n[2] /= abs;

        // 指定到OpenGL
        glNormal3fv(n);
    }



    好的,飄動的旗幟已經做好,現在來看最后的步驟,將紋理貼到旗幟上。
    細心的朋友可能會想到這樣一個問題:明明繪制文字的時候使用的是白色,放到紋理中也是白色,那個“曹”字是如何顯示為黃色的呢?
    這就要說到紋理的使用方法了。大家在看了第十一課“紋理的使用入門”以后,難免認為紋理就是用一幅圖片上的像素顏色來替換原來的顏色。其實這只是紋理最簡單的一種用法,它還可以有其它更復雜但是實用的用法。
    這里我們必須提到一個函數:glTexEnv*。從OpenGL 1.0到OpenGL 1.5,每個OpenGL版本都對這個函數進行了修改,如今它的功能已經變的非常強大(但同時也非常復雜,如果要全部講解,只怕又要花費一整課的篇幅了)。
    最簡單的用法就是:

    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);



    它指定紋理的使用方式為“代替”,即用紋理中的顏色代替原來的顏色。
    我們這里使用另一種用法:

    GLfloat color[] = {1.0f, 1.0f, 0.0f, 1.0f};
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
    glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);



    其中第二行指定紋理的使用方式為“混合”,它與OpenGL的混合功能類似,但源因子和目標因子是固定的,無法手工指定。最終產生的顏色為:紋理的顏色*常量顏色 + (1.0-紋理顏色)*原來的顏色。常量顏色是由第三行代碼指定為黃色。
    因為我們的紋理里面裝的是文字,只有黑、白兩種顏色。如果紋理中某個位置是黑色,套用上面的公式,發現結果就是原來的顏色,沒有變化;如果紋理中某個位置是白色,套用上面的公式,發現結果就是常量顏色。所以,文字的顏色就由常量顏色決定。我們指定常量顏色,也就指定了文字的顏色。

    主要的知識就是這些了,結合前面課程講過的視圖變換(設置觀察點)、光照(設置光源、材質),以及動畫,飄動的旗幟就算制作完成。
    呵呵,代碼已經比較龐大了,限于篇幅,完整的版本這里就不發上來了,不過附件里面有一份源代碼flag.c

    緩沖機制
    走出做完旗幟的喜悅后,讓我們回到二維文字的問題上來。
    前面說到因為漢字的數目眾多,無法在初始化時就為每個漢字都產生一個顯示列表。不過,如果每次顯示漢字時都重新產生顯示列表,效率上也說不過去。一個好的辦法就是,把經常使用的漢字的顯示列表保存起來,當需要顯示漢字時,如果這個漢字的顯示列表已經保存,則不再需要重新產生。如果有很多的漢字都需要產生顯示列表,占用容量過多,則刪除一部分最近沒有使用的顯示列表,以便釋放出一些空間來容納新的顯示列表。
    學過操作系統原理的朋友應該想起來了,沒錯,這與內存置換的算法是一樣的。內存速度快但是容量小,硬盤(虛擬內存)速度慢但是容量大,需要找到一種機制,使性能盡可能的達到最高。這就是內存置換算法。
    常見的內存置換算法有好幾種,這里我們選擇一種簡單的。那就是隨機選擇一個顯示列表并且刪除,空出一個位置用來裝新的顯示列表。
    還要說一下,我們不再直接用顯示列表來顯示漢字了,改用紋理。因為紋理更加靈活,而且根據實驗,紋理比顯示列表更快。一個顯示列表只能保存一個字符,但是紋理只要足夠大,則可以保存很多的字符。假設字符的高度是32,則寬度不超過32,如果紋理是256*256的話,就可以保存8行8列,總共64個漢字。
    我們要做的功能:
    1. 緩沖機制的初始化
    2. 緩沖機制的退出
    3. 根據一個文字字符,返回對應的紋理坐標。如果字符本身不在紋理中,則應該先把字符加入到紋理中(如果紋理已經裝不下了,則先刪除一個),然后返回紋理坐標。
    要改進緩沖機制的性能,則應該使用更高效的置換算法,不過這個已經遠超出OpenGL的范圍了。大家如果有空也可以看看linux源碼什么的,應該會找到好的置換算法。
    即使我們使用最簡單的置換算法,完整的代碼仍然有將近200行,其實這些都是算法基本功了,跟OpenGL關系并不太大。仍然是由于篇幅限制,僅在附件中給出,就不貼在這里了。文件名為ctbuf.h和ctbuf.c,在使用的時候把這兩個文件都加入到工程中,并調用ctbuf.h中聲明的函數即可。
    這里我們僅僅給出調用部分的代碼。

    #include "ctbuf.h"

    void display(void) {
        static int isFirstCall = 1;

        if( isFirstCall ) {
            isFirstCall = 0;
            ctbuf_init(32, 256, "黑體");
        }

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glEnable(GL_TEXTURE_2D);
        glPushMatrix();
        glTranslatef(-1.0f, 0.0f, 0.0f);
        ctbuf_drawString("美好明天就要到來", 0.1f, 0.15f);
        glTranslatef(0.0f, -0.15f, 0.0f);
        ctbuf_drawString("Best is yet to come", 0.1f, 0.15f);
        glPopMatrix();

        glutSwapBuffers();
    }





    注意這里我們是用紋理來實現字符顯示的,因此文字的大小會隨著窗口大小而變化。最初的Hello, World程序就不會有這樣的效果,因為它的字體硬性的規定了大小,不如紋理來得靈活。 

    顯示大段的文字


    有了緩沖機制,顯示文字的速度會比沒有緩沖時快很多,這樣我們也可以考慮顯示大段的文字了。
    基本上,前面的ctbuf_drawString函數已經可以快速的顯示一個較長的字符串,但是它有兩個缺點。
    第一個缺點是不會換行,一直橫向顯示到底。
    第二個缺點是即使字符在屏幕以外,也會嘗試在緩沖中查找這個字符,如果沒找到,還會重新生成這個字符。

    讓我們先來看看第一個問題,換行。所謂換行其實就是把光標移動到下一行的開頭,如果知道每一行開頭的位置的話,只需要很短的代碼就可以實現。
    不過,OpenGL顯示文字的時候并不會保存每一行開頭的位置,所以這個需要我們自己動手來做。
    第二個問題是關于性能的,如果字符本身不會顯示出來,那么為它產生顯示列表和紋理就是一種浪費,如果為了容納它的顯示列表或者紋理,而把緩沖區中其它有用的字符的顯示列表或者紋理給刪除了,那就更加得不償失。
    所以,判斷字符是否會顯示也是很重要的。像我們的瀏覽器,如果顯示一個巨大的網頁,其實它也只繪制最必要的部分。
    為了解決上面兩個問題,我們再單獨的編寫一個模塊。初始化的時候指定顯示區域的大小、每行多少個字符、每列多少個字符,在模塊內部判斷是否需要換行,以及判斷每個文字是否真的需要顯示。

    呃,小小的感慨一下,為什么每當我做好一份代碼,就發現它實在太長,長到我不想貼出來呢?唉……
    先看看圖:


    注意觀察就可以發現,歌詞分為多行,只有必要的行才會顯示,不會從頭到尾的顯示出來。
    代碼中主要是算法和C語言基本功,跟OpenGL關系并不大。還是照舊,把主要的代碼放到附件里,文件名為textarea.h和textarea.c,使用時要與前面的ctbuf.h和ctbuf.c一起使用。
    這里僅給出調用部分的代碼。 

    const char* g_string =
        "《合金裝備》(Metal Gear Solid)結尾曲歌詞\n"
        // 歌詞很多很長
        "因為。。。。。。。。 \n"
        "美好即將到來\n";

    textarea_t* p_textarea = NULL;

    void display(void) {
        static int isFirstCall = 1;

        if( isFirstCall ) {
            isFirstCall = 0;
            ctbuf_init(24, 256, "隸書");
            p_textarea = ta_create(-0.7f, -0.5f, 0.7f, 0.5f,
                20, 10, g_string);
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        }

        glClear(GL_COLOR_BUFFER_BIT);

        // 顯示歌詞文字
        glEnable(GL_TEXTURE_2D);
        ta_display(p_textarea);

        // 用半透明的效果顯示一個方框
        // 這個框是實際需要顯示的范圍
        glEnable(GL_BLEND);
        glDisable(GL_TEXTURE_2D);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
        glRectf(-0.7f, -0.5f, 0.7f, 0.5f);
        glDisable(GL_BLEND);

        // 顯示一些幫助信息
        glEnable(GL_TEXTURE_2D);
        glPushMatrix();
        glTranslatef(-1.0f, 0.9f, 0.0f);
        ctbuf_drawString("歌詞顯示程序", 0.1f, 0.1f);
        glTranslatef(0.0f, -0.1f, 0.0f);
        ctbuf_drawString("按W/S鍵實現上、下翻頁", 0.1f, 0.1f);
        glTranslatef(0.0f, -0.1f, 0.0f);
        ctbuf_drawString("按ESC退出", 0.1f, 0.1f);
        glPopMatrix();

        glutSwapBuffers();
    }




    輪廓字體
    其實上面我們所講那么多,只講了一類字體,即像素字體,此外還有輪廓字體。所以,這個看似已經很長的課程,其實只講了“顯示文字”這個課題的一半。估計大家已經看不下去了,其實我也寫不下去了。好長……
    那么,本課就到這里吧。有種虎頭蛇尾的感覺:(
    小結

    本課的內容不可謂不多。列表如下:
    1. 以Hello, World開始,說明英文字符(ASCII字符)是如何繪制的。
    2. 給出了一個設置字體的函數selectFont。
    3. 講了如何顯示中文字符。
    4. 講了如何把字符保存到紋理中。
    5. 給出了一個大的例子,繪制一面“曹”字旗。(附件flag.c)
    6. 講解了緩沖機制,其實跟內存的置換算法原理是一樣的。我們給出了一個最簡單的緩沖實現,采用隨機的置換算法。(做成了模塊,附件ctbuf.h,ctbuf.c,調用的例子在本課正文中可以找到)
    7. 通過緩沖機制,實現顯示大段的文字。主要是注意換行的處理,還有就是只顯示必要的行。(做成了模塊,附件textarea.h,textarea.c,調用的例子在本課正文中可以找到)
    最后兩個模塊雖然是以附件形式給出的,但是原理我想我已經說清楚了,并且這些內容跟OpenGL關系并不大,主要還是相關專業的知識,或者C語言基本功。主要是讓大家弄清楚原理,附件代碼只是作為參考用。
    說說我的感受:顯示文字很簡單,顯示文字很復雜。除了最基本的顯示列表、紋理等OpenGL常識外,更多的會涉及到數學、數據結構與算法、操作系統等各個領域。一個大型的程序通常都要實現一些文字特殊效果,僅僅是調用幾個顯示列表當然是不行的,需要大量的相關知識來支撐。

    轉自:http://www.cnblogs.com/crazyxiaom/articles/2073586.html
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色