OpenGL入門學習

這個入門教程實在是高大全啊,所以我必須把它收藏起來,轉載自:http://www.cppblog.com/doing5552/archive/2009/01/08/71532.html

說起編程作圖,大概還有很多人想起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等,每種方式的大致效果見下圖:
http://blog.programfan.com/upfile/200607/200607311604018.gif
聲明:該圖片來自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(R*cos(2*Pi/n*i), R*sin(2*Pi/n*i));
     glEnd();
     glFlush();
}


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

    D    C
首先,根據餘弦定理列方程,計算五角星的中心到頂點的距離a
(假設五角星對應正五邊形的邊長爲.0)
a = 1 / (2-2*cos(72*Pi/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-2*cos(72*Pi/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(x*factor, sin(x)*factor);
     }
     glEnd();
     glFlush();
}


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

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

OpenGL入門學習[三]

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

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

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

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(i*Pi/4), sin(i*Pi/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(i*Pi/4), sin(i*Pi/4));
     }
     glEnd();
     glFlush();
}
在默認情況下,OpenGL會計算兩點頂點之間的其它點,併爲它們填上“合適”的顏色,使相鄰的點的顏色值都比較接近。如果使用的是RGB模式,看起來就具有漸變的效果。如果是使用顏色索引模式,則其相鄰點的索引值是接近的,如果將顏色表中接近的項設置成接近的顏色,則看起來也是漸變的效果。但如果顏色表中接近的項顏色卻差距很大,則看起來可能是很奇怪的效果。
使用glShadeModel函數可以關閉這種計算,如果頂點的顏色不同,則將頂點之間的其它點全部設置爲與某一個點相同。(直線以後指定的點的顏色爲準,而多邊形將以任意頂點的顏色爲準,由實現決定。)爲了避免這個不確定性,儘量在多邊形中使用同一種顏色。
glShadeModel的使用方法:
glShadeModel(GL_SMOOTH);    // 平滑方式,這也是默認方式
glShadeModel(GL_FLAT);      // 單色方式

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

=====================    第四課 完    =====================
=====================TO BE CONTINUED=====================


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函數可以將當前的可視空間設置爲透視投影空間。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/20061007151547.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。
也可以使用更常用的gluPerspective函數。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/2006100715161.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。

正投影相當於在無限遠處觀察得到的結果,它只是一種理想狀態。但對於計算機來說,使用正投影有可能獲得更好的運行速度。
使用glOrtho函數可以將當前的可視空間設置爲正投影空間。其參數的意義如下圖:
http://blog.programfan.com/upfile/200610/20061007151619.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網絡,我希望沒有觸及到版權問題。

如果繪製的圖形空間本身就是二維的,可以使用gluOrtho2D。他的使用類似於glOrgho。


3、視口變換
當一切工作已經就緒,只需要把像素繪製到屏幕上了。這時候還剩最後一個問題:應該把像素繪製到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半。(即:把整個圖象填充到一半的窗口內)
http://blog.programfan.com/upfile/200610/20061007151639.gif
聲明:該圖片來自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);
最後的一步,也被我們解決了——雖然做法不太雅觀,沒關係,以後我們還會改善它的。


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

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.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();
     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;
}



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

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

OpenGL入門學習[七]


今天要講的是OpenGL光照的基本知識。雖然內容顯得有點多,但條理還算比較清晰,理解起來應該沒有困難。即使對於一些內容沒有記住,問題也不大——光照部分是一個比較獨立的內容,它的學習與其它方面的學習可以分開,不像視圖變換那樣,影響到許多方面。課程的最後給出了一個有關光照效果的動畫演示程序,我想大家會喜歡的。
從生理學的角度上講,眼睛之所以看見各種物體,是因爲光線直接或間接的從它們那裏到達了眼睛。人類對於光線強弱的變化的反應,比對於顏色變化的反應來得靈敏。因此對於人類而言,光線很大程度上表現了物體的立體感。
請看圖1,圖中繪製了兩個大小相同的白色球體。其中右邊的一個是沒有使用任何光照效果的,它看起來就像是一個二維的圓盤,沒有立體的感覺。左邊的一個是使用了簡單的光照效果的,我們通過光照的層次,很容易的認爲它是一個三維的物體。
http://blog.programfan.com/upfile/200702/2007022315149.jpg
圖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度,即不使用聚光燈,向全周圍發射。
http://blog.programfan.com/upfile/200702/20070223151415.gif
圖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入門學習[八]

今天介紹關於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;
}

在程序中,我們將繪製正四面體的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)當然了,這樣的效果還不夠。如果在各表面的交界處設置很多細小的平面,進行平滑處理,則光照後的效果將更真實。但這已經遠離本課的內容了。
http://blog.programfan.com/upfile/200703/20070303005337.jpg圖一
http://blog.programfan.com/upfile/200703/20070303005342.jpg圖二
小結
本課介紹了顯示列表的知識和簡單的應用。
可以把各種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,所以兩個矩形看上去都是半透明的,乃至於看到黑色背景。第三種是將顏色相加,紅色和綠色相加得到黃色。
http://blog.programfan.com/upfile/200704/20070406022726.jpghttp://blog.programfan.com/upfile/200704/20070406022731.jpghttp://blog.programfan.com/upfile/200704/20070406022735.jpg

三、實現三維混合
也許你迫不及待的想要繪製一個三維的帶有半透明物體的場景了。但是現在恐怕還不行,還有一點是在進行三維場景的混合時必須注意的,那就是深度緩衝。
深度緩衝是這樣一段數據,它記錄了每一個像素距離觀察者有多近。在啓用深度緩衝測試的情況下,如果將要繪製的像素比原來的像素更近,則像素將被繪製。否則,像素就會被忽略掉,不進行繪製。這在繪製不透明的物體時非常有用——不管是先繪製近的物體再繪製遠的物體,還是先繪製遠的物體再繪製近的物體,或者乾脆以混亂的順序進行繪製,最後的顯示結果總是近的物體遮住遠的物體。
然而在你需要實現半透明效果時,發現一切都不是那麼美好了。如果你繪製了一個近距離的半透明物體,則它在深度緩衝區內保留了一些信息,使得遠處的物體將無法再被繪製出來。雖然半透明的物體仍然半透明,但透過它看到的卻不是正確的內容了。
要解決以上問題,需要在繪製半透明物體時將深度緩衝區設置爲只讀,這樣一來,雖然半透明物體被繪製上去了,深度緩衝區還保持在原來的狀態。如果再有一個物體出現在半透明物體之後,在不透明物體之前,則它也可以被繪製(因爲此時深度緩衝區中記錄的是那個不透明物體的深度)。以後再要繪製不透明物體時,只需要再將深度緩衝區設置爲可讀可寫的形式即可。嗯?你問我怎麼繪製一個一部分半透明一部分不透明的物體?這個好辦,只需要把物體分爲兩個部分,一部分全是半透明的,一部分全是不透明的,分別繪製就可以了。
即使使用了以上技巧,我們仍然不能隨心所欲的按照混亂順序來進行繪製。必須是先繪製不透明的物體,然後繪製透明的物體。否則,假設背景爲藍色,近處一塊紅色玻璃,中間一個綠色物體。如果先繪製紅色半透明玻璃的話,它先和藍色背景進行混合,則以後繪製中間的綠色物體時,想單獨與紅色玻璃混合已經不能實現了。
總結起來,繪製順序就是:首先繪製所有不透明的物體。如果兩個物體都是不透明的,則誰先誰後都沒有關係。然後,將深度緩衝區設置爲只讀。接下來,繪製所有半透明的物體。如果兩個物體都是半透明的,則誰先誰後只需要根據自己的意願(注意了,先繪製的將成爲“目標顏色”,後繪製的將成爲“源顏色”,所以繪製的順序將會對結果造成一些影響)。最後,將深度緩衝區設置爲可讀可寫形式。
調用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刪去,結果會看到最近的藍色球雖然是半透明的,但它的背後直接就是紅色球了,中間的綠色球沒有被正確繪製。

http://blog.programfan.com/upfile/200704/20070406022744.jpghttp://blog.programfan.com/upfile/200704/20070406022749.jpg
小結:
本課介紹了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);
}



把這段代碼複製到以前任何課程的樣例程序中,在繪製函數的最後調用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窗口,並繪製像素。
繪製像素本來是很簡單的過程,但是這個程序在骨架上與前面的各種示例程序稍有不同,所以我還是打算給出一份完整的代碼。

#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();
}



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



OpenGL入門學習[十一]


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

http://blog.programfan.com/upfile/200707/20070730074740.jpg
http://blog.programfan.com/upfile/200707/20070730074746.jpg
http://blog.programfan.com/upfile/200707/20070730074751.jpg

紋理的使用是非常複雜的。因此即使是入門教程,在編寫時我也多次進行刪改,很多東西都被精簡掉了,但本課的內容仍然較多,大家要有一點心理準備~
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);
}

第二段代碼是我們的重點。它包括兩個函數。其中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;
}

第三段代碼是關於顯示的部分,以及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;
}

小結:
本課介紹了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格式)
http://blog.programfan.com/upfile/200710/2007100711109.jpghttp://blog.programfan.com/upfile/200710/20071007111014.jpg
注:第一幅圖片是著名網絡遊戲《魔獸世界》的一幅桌面背景,用在這裏希望沒有涉及版權問題。如果有什麼不妥,請及時指出,我會立即更換。

在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(width*height*4);
    if( pixels == 0 )
        return;
     glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);

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

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



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

 

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

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的像素纔會被繪製,因此只有“水面”的像素纔有可能被倒影的像素替換,而其它像素則保持不變。

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

 

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

繪製這個場景的思路跟前面提到的湖面倒影是接近的。
假設平面鏡所在的平面正好是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*函數重新設定。

可以使用glTexCoord*函數來設置一個紋理座標,以後繪製的所有物體都是採用這種紋理座標,除非再次使用glTexCoord*函數重新設置。

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

可以使用glLight*函數來指定光源的位置、顏色,以後繪製的所有物體都是採用這個光源的位置、顏色,除非再次使用glBlendFunc函數重新指定。

……

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

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();



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

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

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


則繪製出來的圖形就只有正面,並且只顯示邊線,不進行填充。
效果如圖:
http://blog.programfan.com/upfile/200805/20080505132612.gif
頂點數組
(提示:頂點數組是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();
}



效果如圖:
http://blog.programfan.com/upfile/200805/20080505132619.gif

指定字體
在產生顯示列表前,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表示默認字符集)、字體名稱。
效果如圖:
http://blog.programfan.com/upfile/200805/20080505132624.gif


顯示中文
原則上,顯示中文和顯示英文並無不同,同樣是把要顯示的字符做成顯示列表,然後進行調用。
但是有一個問題,英文字母很少,最多隻有幾百個,爲每個字母創建一個顯示列表,沒有問題。但是漢字有非常多個,如果每個漢字都產生一個顯示列表,這是不切實際的。
我們不能在初始化時就爲每個字符建立一個顯示列表,那就只有在每次繪製字符時創建它了。當我們需要繪製一個字符時,創建對應的顯示列表,等繪製完畢後,再將它銷燬。
這裏還經常涉及到中文亂碼的問題,我對這個問題也不甚瞭解,但是網上流傳的版本中,使用了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();
}


效果如圖:
http://blog.programfan.com/upfile/200805/20080505132632.gif
紋理字體
把文字放到紋理中有很多好處,例如,可以任意修改字符的大小(而不必重新指定字體)。
對一面飄動的旗幟使用帶有文字的紋理,則文字也會隨着飄動。這個技術在“三國志”系列遊戲中經常用到,比如關羽的部隊,旗幟上就飄着個“關”字,張飛的部隊,旗幟上就飄着個“張”字,曹操的大營,旗幟上就飄着個“曹”字。三國人物何其多,不可能爲每種姓氏都單獨製作一面旗幟紋理,如果能夠把文字放到紋理上,則可以解決這個問題。(參見後面的例子:繪製一面“曹”字旗)
如何把文字放到紋理中呢?自然的想法就是:“如果前面所用的顯示列表,可以直接往紋理裏面繪製,那就好了”。不過,“繪製到紋理”這種技術要涉及的內容可不少,足夠我們專門拿一課的篇幅來講解了。這裏我們不是直接繪製到紋理,而是用簡單一點的辦法:先把漢字繪製出來,成爲像素,然後用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尤其明顯。見下圖:
http://blog.programfan.com/upfile/200805/20080505132638.gif

所以,需要把繪製的位置往上移一點,具體來說就是移動下面一欄的高度。這個高度是多少像素呢?這個我也不知道有什麼好辦法來計算,根據我的經驗,移動整個字符高度的八分之一是比較合適的。例如字符大小爲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。


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

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

爲了完成上面的效果,我們需要具備以下的知識:
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();
}



http://blog.programfan.com/upfile/200805/20080505132715.gif

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

顯示大段的文字


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

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

呃,小小的感慨一下,爲什麼每當我做好一份代碼,就發現它實在太長,長到我不想貼出來呢?唉……
先看看圖:
http://blog.programfan.com/upfile/200805/20080505132721.gif

注意觀察就可以發現,歌詞分爲多行,只有必要的行纔會顯示,不會從頭到尾的顯示出來。
代碼中主要是算法和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常識外,更多的會涉及到數學、數據結構與算法、操作系統等各個領域。一個大型的程序通常都要實現一些文字特殊效果,僅僅是調用幾個顯示列表當然是不行的,需要大量的相關知識來支撐。
本課的門檻突然提高,搞得我都不知道這還算不算是“入門教程”了,希望各位不要退縮哦。祝大家愉快。 

發佈了22 篇原創文章 · 獲贊 5 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章