opengl入門學習

提及編程做圖,大概還有不少人想起TC的#include <graphics.h>吧?html

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

本帖的目的是讓你們放棄TC的老舊圖形接口,讓你們接觸一些新事物。程序員

OpenGL做爲當前主流的圖形API之一,它在一些場合具備比DirectX更優越的特性。算法

一、與C語言緊密結合。數據庫

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

二、強大的可移植性。小程序

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

三、高性能的圖形渲染。數組

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的步驟:

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

二、在「個人電腦」中搜索「gl.h」,並找到其所在文件夾(若是是VisualStudio2005,則應該是其安裝目錄下面的「VC\PlatformSDK\include\gl文件夾」)。把解壓獲得的glut.h放到這個文件夾。

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

四、把解壓獲得的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工具包所提供的函數,下面對用到的幾個函數進行介紹。

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

二、 glutInitDisplayMode,設置顯示方式,其中GLUT_RGB表示使用RGB顏色,與之對應的還有GLUT_INDEX(表示使用索引顏色)。GLUT_SINGLE表示使用單緩衝,與之對應的還有GLUT_DOUBLE(使用雙緩衝)。更多信息,請本身Google。固然之後的教程也會有一些講解。

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

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

五、glutCreateWindow,根據前面設置的信息建立窗口。參數將被做爲窗口的標題。注意:窗口被建立後,並不當即顯示到屏幕上。須要調用glutMainLoop才能看到窗口。

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

七、glutMainLoop,進行一個消息循環。(這個可能初學者也不太明白,如今只須要知道這個函數能夠顯示窗口,而且等待窗口關閉後纔會返回,這就足夠了。)

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

一、glClear,清除。GL_COLOR_BUFFER_BIT表示清除顏色,glClear函數還能夠清除其它的東西,但這裏不做介紹。

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

三、glFlush,保證前面的OpenGL命令當即執行(而不是讓它們在緩衝區中等待)。其做用跟fflush(stdout)相似。

OpenGL入門學習[二]

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

1、點、直線和多邊形
咱們知道數學(具體的說,是幾何學)中有點、直線和多邊形的概念,但這些概念在計算機中會有所不一樣。
數學上的點,只有位置,沒有大小。但在計算機中,不管計算精度如何提升,始終不能表示一個無窮小的點。另外一方面,不管圖形輸出設備(例如,顯示器)如何精確,始終不能輸出一個無窮小的點。通常狀況下,OpenGL中的點將被畫成單個的像素(像素的概念,請本身搜索之~),雖然它可能足夠小,但並不會是無窮小。同一像素上,OpenGL能夠繪製許多座標只有稍微不一樣的點,但該像素的具體顏色將取決於OpenGL的實現。固然,過分的注意細節就是鑽牛角尖,咱們大可沒必要花費過多的精力去研究「多個點如何畫到同一像素上」。
一樣的,數學上的直線沒有寬度,但OpenGL的直線則是有寬度的。同時,OpenGL的直線必須是有限長度,而不是像數學概念那樣是無限的。能夠認爲,OpenGL的「直線」概念與數學上的「線段」接近,它能夠由兩個端點來肯定。
多邊形是由多條線段首尾相連而造成的閉合區域。OpenGL規定,一個多邊形必須是一個「凸多邊形」(其定義爲:多邊形內任意兩點所肯定的線段都在多邊形內,由此也能夠推導出,凸多邊形不能是空心的)。多邊形能夠由其邊的端點(這裏可稱爲頂點)來肯定。(注意:若是使用的多邊形不是凸多邊形,則最後輸出的效果是未定義的——OpenGL爲了效率,放寬了檢查,這可能致使顯示錯誤。要避免這個錯誤,儘可能使用三角形,由於三角形都是凸多邊形)

能夠想象,經過點、直線和多邊形,就能夠組合成各類幾何圖形。甚至於,你能夠把一段弧當作是不少短的直線段相連,這些直線段足夠短,以致於其長度小於一個像素的寬度。這樣一來弧和圓也能夠表示出來了。經過位於不一樣平面的相連的小多邊形,咱們還能夠組成一個「曲面」。

2、在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的不少函數都是採用這樣的形式,一個相同的前綴再加上參數說明標記,這一點會隨着學習的深刻而有更多的體會。


3、開始繪製
假設如今我已經指定了若干頂點,那麼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函數,編譯後便可運行。



兩個例子
例1、畫一個圓
/*
正四邊形,正五邊形,正六邊形,……,直到正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();
}


例2、畫一個五角星
/*
設五角星的五個頂點分佈位置關係以下:
      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();
}


例3、畫出正弦函數的圖形
/*
因爲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個像素,但也能夠改變之。改變的命令爲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();

}

二、關於直線

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

}

三、關於多邊形

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

(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表示最大的使用。
這些規則看似麻煩,但熟悉後實際使用中不會有什麼障礙。

二、索引顏色
在索引顏色模式中,OpenGL須要一個顏色表。這個表就至關於畫家的調色板:雖然能夠調出不少種顏色,但同時存在於調色板上的顏色種數將不會超過調色板的格數。試將顏色表的每一項想象成調色板上的一個格子:它保存了一種顏色。
在使用索引顏色模式畫圖時,我說「我把第i種顏色設置爲某某」,其實就至關於將調色板的第i格調爲某某顏色。「我須要第k種顏色來畫圖」,那麼就用畫筆去蘸一下第k格調色板。
顏色表的大小是頗有限的,通常在256~4096之間,且老是2的整數次冪。在使用索引顏色方式進行繪圖時,老是先設置顏色表,而後選擇顏色。

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

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、手機等,索引顏色仍是有它的用武之地。


三、指定清除屏幕用的顏色
咱們寫: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();
}
呵,這個還真簡單~


四、指定着色模型
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軸垂直屏幕。這些限制給咱們的繪圖帶來了不少不便。

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

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


一、模型變換和視圖變換
從「相對移動」的觀點來看,改變觀察點的位置與方向和改變物體自己的位置與方向具備等效性。在OpenGL中,實現這兩種功能甚至使用的是一樣的函數。
因爲模型和視圖的變換都經過矩陣運算來實現,在進行變換前,應先設置當前操做的矩陣爲「模型視圖矩陣」。設置的方法是以GL_MODELVIEW爲參數調用glMatrixMode函數,像這樣:
glMatrixMode(GL_MODELVIEW);
一般,咱們須要在進行變換前把當前矩陣設置爲單位矩陣。這也只須要一行代碼:
glLoadIdentity();

而後,就能夠進行模型變換和視圖變換了。進行模型和視圖變換,主要涉及到三個函數:
glTranslate*,把當前矩陣和一個表示移動物體的矩陣相乘。三個參數分別表示了在三個座標上的位移值。
glRotate*,把當前矩陣和一個表示旋轉物體的矩陣相乘。物體將繞着(0,0,0)到(x,y,z)的直線以逆時針旋轉,參數angle表示旋轉的角度。
glScale*,把當前矩陣和一個表示縮放物體的矩陣相乘。x,y,z分別表示在該方向上的縮放比例。

注意我都是說「與XX相乘」,而不是直接說「這個函數就是旋轉」或者「這個函數就是移動」,這是有緣由的,立刻就會講到。
假設當前矩陣爲單位矩陣,而後先乘以一個表示旋轉的矩陣R,再乘以一個表示移動的矩陣T,最後獲得的矩陣再乘上每個頂點的座標矩陣v。因此,通過變換獲得的頂點座標就是((RT)v)。因爲矩陣乘法的結合率,((RT)v) = (R(Tv)),換句話說,其實是先進行移動,而後進行旋轉。即: 實際變換的順序與代碼中寫的順序是相反的。因爲「先移動後旋轉」和「先旋轉後移動」獲得的結果極可能不一樣,初學的時候須要特別注意這一點。
OpenGL之因此這樣設計,是爲了獲得更高的效率。但在繪製複雜的三維圖形時,若是每次都去考慮如何把變換倒過來,也是很痛苦的事情。這裏介紹另外一種思路,可讓代碼看起來更天然(寫出的代碼其實徹底同樣,只是考慮問題時用的方法不一樣了)。
讓咱們想象,座標並非固定不變的。 旋轉的時候,座標系統隨着物體旋轉。移動的時候,座標系統隨着物體移動。如此一來,就不須要考慮代碼的順序反轉的問題了。

以上都是針對改變物體的位置和方向來介紹的。若是要改變觀察點的位置,除了配合使用glRotate*和glTranslate*函數之外,還能夠使用這個函數:gluLookAt。它的參數比較多,前三個參數表示了觀察點的位置,中間三個參數表示了觀察目標的位置,最後三個參數表明從(0,0,0)到 (x,y,z)的直線,它表示了觀察者認爲的「上」方向。


二、投影變換

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

透視投影所產生的結果相似於照片,有近大遠小的效果,好比在火車頭內向前照一個鐵軌的照片,兩條鐵軌彷佛在遠處相交了。
使用glFrustum函數能夠將當前的可視空間設置爲透視投影空間。其參數的意義以下圖:
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。


三、視口變換
當一切工做已經就緒,只須要把像素繪製到屏幕上了。這時候還剩最後一個問題:應該把像素繪製到窗口的哪一個區域呢?一般狀況下,默認是完整的填充整個窗口,但咱們徹底能夠只填充一半。(即:把整個圖象填充到一半的窗口內)
http://blog.programfan.com/upfile/200610/20061007151639.gif
聲明:該圖片來自www.opengl.org,該圖片是《OpenGL編程指南》一書的附圖,因爲該書的舊版(初版,1994年)已經流傳於網絡,我但願沒有觸及到版權問題。

使用glViewport來定義視口。其中前兩個參數定義了視口的左下腳(0,0表示最左下方),後兩個參數分別是寬度和高度。

四、操做矩陣堆棧
介因而入門教程,先簡單介紹一下堆棧。你能夠把堆棧想象成一疊盤子。開始的時候一個盤子也沒有,你能夠一個一個往上放,也能夠一個一個取下來。每次取下的,都是最後一次被放上去的盤子。一般,在計算機實現堆棧時,堆棧的容量是有限的,若是盤子過多,就會出錯。固然,若是沒有盤子了,再要求取一個盤子,也會出錯。
咱們在進行矩陣操做時,有可能須要先保存某個矩陣,過一段時間再恢復它。當咱們須要保存時,調用glPushMatrix函數,它至關於把矩陣(至關於盤子)放到堆棧上。當須要恢復最近一次的保存時,調用glPopMatrix函數,它至關於把矩陣從堆棧上取下。OpenGL規定堆棧的容量至少能夠容納32個矩陣,某些OpenGL實現中,堆棧的容量實際上超過了32個。所以沒必要過於擔憂矩陣的容量問題。
一般,用這種先保存後恢復的措施,比先變換再逆變換要更方便,更快速。
注意:模型視圖矩陣和投影矩陣都有相應的堆棧。使用glMatrixMode來指定當前操做的到底是模型視圖矩陣仍是投影矩陣。

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

咱們要製做的是一個三維場景,包括了太陽、地球和月亮。假定一年有12個月,每月30天。每一年,地球繞着太陽轉一圈。每月,月亮圍着地球轉一圈。即一年有360天。如今給出日期的編號(0~359),要求繪製出太陽、地球、月亮的相對位置示意圖。(這是爲了編程方便才這樣設計的。若是須要製做更現實的狀況,那也只是一些數值處理而已,與OpenGL關係不大)
首先,讓咱們認定這三個天體都是球形,且他們的運動軌跡處於同一水平面,創建如下座標系:太陽的中心爲原點,天體軌跡所在的平面表示了X軸與Y軸決定的平面,且每一年第一天,地球在X軸正方向上,月亮在地球的正X軸方向。
下一步是確立可視空間。注意:太陽的半徑要比太陽到地球的距離短得多。若是咱們直接使用天文觀測獲得的長度比例,則當整個窗口表示地球軌道大小時,太陽的大小將被忽略。所以,咱們只能成倍的放大幾個天體的半徑,以適應咱們觀察的須要。(百度一下,獲得太陽、地球、月亮的大體半徑分別是:696000km, 6378km,1738km。地球到太陽的距離約爲1.5億km=150000000km,月亮到地球的距離約爲380000km。)
讓咱們假想一些數據,將三個天體的半徑分別「修改」爲:69600000(放大100倍),15945000(放大2500倍),4345000(放大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);


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

咱們要製做的是一個三維場景,包括了太陽、地球和月亮。假定一年有12個月,每月30天。每一年,地球繞着太陽轉一圈。每月,月亮圍着地球轉一圈。即一年有360天。如今給出日期的編號(0~359),要求繪製出太陽、地球、月亮的相對位置示意圖。(這是爲了編程方便才這樣設計的。若是須要製做更現實的狀況,那也只是一些數值處理而已,與OpenGL關係不大)
首先,讓咱們認定這三個天體都是球形,且他們的運動軌跡處於同一水平面,創建如下座標系:太陽的中心爲原點,天體軌跡所在的平面表示了X軸與Y軸決定的平面,且每一年第一天,地球在X軸正方向上,月亮在地球的正X軸方向。
下一步是確立可視空間。注意:太陽的半徑要比太陽到地球的距離短得多。若是咱們直接使用天文觀測獲得的長度比例,則當整個窗口表示地球軌道大小時,太陽的大小將被忽略。所以,咱們只能成倍的放大幾個天體的半徑,以適應咱們觀察的須要。(百度一下,獲得太陽、地球、月亮的大體半徑分別是:696000km, 6378km,1738km。地球到太陽的距離約爲1.5億km=150000000km,月亮到地球的距離約爲380000km。)
讓咱們假想一些數據,將三個天體的半徑分別「修改」爲:69600000(放大100倍),15945000(放大2500倍),4345000(放大2500倍)。將地球到月亮的距離「修改」爲38000000(放大100倍)。地球到太陽的距離保持不變。
爲了讓地球和月亮在離咱們很近時,咱們仍然不須要變換觀察點和觀察方向就能夠觀察它們,咱們把觀察點放在這個位置:(0, -200000000, 0) ——由於地球軌道半徑爲150000000,我們就湊個整,取-200000000就能夠了。觀察目標設置爲原點(即太陽中心),選擇Z軸正方向做爲 「上」方。固然咱們還能夠把觀察點往「上」方移動一些,獲得(0, -200000000, 200000000),這樣能夠獲得45度角的俯視效果。
爲了獲得透視效果,咱們使用gluPerspective來設置可視空間。假定可視角爲60度(若是調試時發現該角度不合適,可修改之。我在最後選擇的數值是75。),高寬比爲1.0。最近可視距離爲1.0,最遠可視距離爲200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);


如今咱們來看看如何繪製這三個天體。
爲了簡單起見,咱們把三個天體都想象成規則的球體。而咱們所使用的glut實用工具中,正好就有一個繪製球體的現成函數:glutSolidSphere,這個函數在「原點」繪製出一個球體。因爲座標是能夠經過glTranslate*和glRotate*兩個函數進行隨意變換的,因此咱們就能夠在任意位置繪製球體了。函數有三個參數:第一個參數表示球體的半徑,後兩個參數表明了「面」的數目,簡單點說就是球體的精確程度,數值越大越精確,固然代價就是速度越緩慢。這裏咱們只是簡單的設置後兩個參數爲20。
太陽在座標原點,因此不須要通過任何變換,直接繪製就能夠了。
地球則要複雜一點,須要變換座標。因爲今年已經通過的天數已知爲day,則地球轉過的角度爲day/一年的天數*360度。前面已經假定每一年都是360天,所以地球轉過的角度剛好爲day。因此能夠經過下面的代碼來解決:
glRotatef(day, 0, 0, -1);
/* 注意地球公轉是「自西向東」的,所以是饒着Z軸負方向進行逆時針旋轉 */
glTranslatef(地球軌道半徑, 0, 0);
glutSolidSphere(地球半徑, 20, 20);
月亮是最複雜的。由於它不只要繞地球轉,還要隨着地球繞太陽轉。但若是咱們選擇地球做爲參考,則月亮進行的運動就是一個簡單的圓周運動了。若是咱們先繪製地球,再繪製月亮,則只須要進行與地球相似的變換:
glRotatef(月亮旋轉的角度, 0, 0, -1);
glTranslatef(月亮軌道半徑, 0, 0);
glutSolidSphere(月亮半徑, 20, 20);
但這個「月亮旋轉的角度」,並不能簡單的理解爲day/一個月的天數30*360度。由於咱們在繪製地球時,這個座標已是旋轉過的。如今的旋轉是在之前的基礎上進行旋轉,所以還須要處理這個「差值」。咱們能夠寫成:day/30*360 - day,即減去原來已經轉過的角度。這只是一種簡單的處理,固然也能夠在繪製地球前用glPushMatrix保存矩陣,繪製地球后用glPopMatrix恢復矩陣。再設計一個跟地球位置無關的月亮位置公式,來繪製月亮。一般後一種方法比前一種要好,由於浮點的運算是不精確的,便是說咱們計算地球自己的位置就是不精確的。拿這個不精確的數去計算月亮的位置,會致使 「不精確」的成分累積,過多的「不精確」會形成錯誤。咱們這個小程序沒有去考慮這個,但並非說這個問題不重要。
還有一個須要注意的細節: OpenGL把三維座標中的物體繪製到二維屏幕,繪製的順序是按照代碼的順序來進行的。所以後繪製的物體會遮住先繪製的物體,即便後繪製的物體在先繪製的物體的「後面」也是如此。使用深度測試能夠解決這一問題。使用的方法是:一、以GL_DEPTH_TEST爲參數調用glEnable函數,啓動深度測試。二、在必要時(一般是每次繪製畫面開始時),清空深度緩衝,即:glClear(GL_DEPTH_BUFFER_BIT);其中,glClear (GL_COLOR_BUFFER_BIT)與glClear(GL_DEPTH_BUFFER_BIT)能夠合併寫爲:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
且後者的運行速度可能比前者快。


到此爲止,咱們終於能夠獲得整個「太陽,地球和月亮」系統的完整代碼。


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


一、雙緩衝技術
在計算機上的動畫與實際的動畫有些不一樣:實際的動畫都是先畫好了,播放的時候直接拿出來顯示就行。計算機動畫則是畫一張,就拿出來一張,再畫下一張,再拿出來。若是所須要繪製的圖形很簡單,那麼這樣也沒什麼問題。但一旦圖形比較複雜,繪製須要的時間較長,問題就會變得突出。
讓咱們把計算機想象成一個畫圖比較快的人,假如他直接在屏幕上畫圖,而圖形比較複雜,則有可能在他只畫了某幅圖的一半的時候就被觀衆看到。然後面雖然他把畫補全了,但觀衆的眼睛卻又沒有反應過來,還停留在原來那個殘缺的畫面上。也就是說,有時候觀衆看到完整的圖象,有時卻又只看到殘缺的圖象,這樣就形成了屏幕的閃爍。
如何解決這一問題呢?咱們設想有兩塊畫板,畫圖的人在旁邊畫,畫好之後把他手裏的畫板與掛在屏幕上的畫板相交換。這樣以來,觀衆就不會看到殘缺的畫了。這一技術被應用到計算機圖形中,稱爲雙緩衝技術。即:在存儲器(頗有多是顯存)中開闢兩塊區域,一塊做爲發送到顯示器的數據,一塊做爲繪畫的區域,在適當的時候交換它們。因爲交換兩塊內存區域實際上只須要交換兩個指針,這一方法效率很是高,因此被普遍的採用。
注意:雖然絕大多數平臺都支持雙緩衝技術,但這一技術並非OpenGL標準中的內容。OpenGL爲了保證更好的可移植性,容許在實現時不使用雙緩衝技術。固然,咱們經常使用的PC都是支持雙緩衝技術的。
要啓動雙緩衝功能,最簡單的辦法就是使用GLUT工具包。咱們之前在main函數裏面寫:
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
其中GLUT_SINGLE表示單緩衝,若是改爲GLUT_DOUBLE就是雙緩衝了。
固然還有須要更改的地方——每次繪製完成時,咱們須要交換兩個緩衝區,把繪製好的信息用於屏幕顯示(不然不管怎麼繪製,仍是什麼都看不到)。若是使用GLUT工具包,也能夠很輕鬆的完成這一工做,只要在繪製完成時簡單的調用glutSwapBuffers函數就能夠了。


二、實現連續動畫
彷佛沒有任何疑問,咱們應該把繪製動畫的代碼寫成下面這個樣子:
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;
}


三、關於垂直同步
代碼是寫好了,但相信你們還有疑問。某些朋友可能在運行時發現,雖然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關係也不太大。這裏就不作介紹了。


四、計算幀速
不知道你們玩過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對於光照效果提供了直接的支持,只須要調用某些函數,即可以實現簡單的光照效果。可是在這以前,咱們有必要了解一些基礎知識。
1、創建光照模型
在現實生活中,某些物體自己就會發光,例如太陽、電燈等,而其它物體雖然不會發光,但能夠反射來自其它物體的光。這些光經過各類方式傳播,最後進入咱們的眼睛——因而一幅畫面就在咱們的眼中造成了。
就目前的計算機而言,要準確模擬各類光線的傳播,這是沒法作到的事情。好比一個四面都是粗糙牆壁的房間,一盞電燈所發出的光線在很短的時間內就會通過很是屢次的反射,最終幾乎佈滿了房間的每個角落,這一過程即便使用目前運算速度最快的計算機,也沒法精確模擬。不過,咱們並不須要精確的模擬各類光線,只須要找到一種近似的計算方式,使它的最終結果讓咱們的眼睛認爲它是真實的,這就能夠了。
OpenGL在處理光照時採用這樣一種近似:把光照系統分爲三部分,分別是光源、材質和光照環境。光源就是光的來源,能夠是前面所說的太陽或者電燈等。材質是指接受光照的各類物體的表面,因爲物體如何反射光線只由物體表面決定(OpenGL中沒有考慮光的折射),材質特色就決定了物體反射光線的特色。光照環境是指一些額外的參數,它們將影響最終的光照畫面,好比一些光線通過屢次反射後,已經沒法分清它到底是由哪一個光源發出,這時,指定一個「環境亮度」參數,能夠使最後造成的畫面更接近於真實狀況。
在物理學中,光線若是射入理想的光滑平面,則反射後的光線是很規則的(這樣的反射稱爲鏡面反射)。光線若是射入粗糙的、不光滑的平面,則反射後的光線是雜亂的(這樣的反射稱爲漫反射)。現實生活中的物體在反射光線時,並非絕對的鏡面反射或漫反射,但能夠當作是這兩種反射的疊加。對於光源發出的光線,能夠分別設置其通過鏡面反射和漫反射後的光線強度。對於被光線照射的材質,也能夠分別設置光線通過鏡面反射和漫反射後的光線強度。這些因素綜合起來,就造成了最終的光照效果。

2、法線向量
根據光的反射定律,由光的入射方向和入射點的法線就能夠獲得光的出射方向。所以,對於指定的物體,在指定了光源後,便可計算出光的反射方向,進而計算出光照效果的畫面。在OpenGL中,法線的方向是用一個向量來表示。
不幸的是,OpenGL並不會根據你所指定的多邊形各個頂點來計算出這些多邊形所構成的物體的表面的每一個點的法線(這話聽着有些迷糊),一般,爲了實現光照效果,須要在代碼中爲每個頂點指定其法線向量。
指定法線向量的方式與指定顏色的方式有雷同之處。在指定顏色時,只須要指定每個頂點的顏色,OpenGL就能夠自行計算頂點之間的其它點的顏色。而且,顏色一旦被指定,除非再指定新的顏色,不然之後指定的全部頂點都將以這一貫量做爲本身的顏色。在指定法線向量時,只須要指定每個頂點的法線向量,OpenGL會自行計算頂點之間的其它點的法線向量。而且,法線向量一旦被指定,除非再指定新的法線向量,不然之後指定的全部頂點都將以這一貫量做爲本身的法線向量。使用glColor*函數能夠指定顏色,而使用glNormal*函數則能夠指定法線向量。
注意:使用glTranslate*函數或者glRotate*函數能夠改變物體的外觀,但法線向量並不會隨之改變。然而,使用glScale*函數,對每一座標軸進行不一樣程度的縮放,頗有可能致使法線向量的不正確,雖然OpenGL提供了一些措施來修正這一問題,但由此也帶來了各類開銷。所以,在使用了法線向量的場合,應儘可能避免使用glScale*函數。即便使用,也最好保證各座標軸進行等比例縮放。
3、控制光源
在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)這兩類屬性就不會用到了,問題就變得簡單明瞭。
4、控制材質
材質與光源類似,也須要設置衆多的屬性。不一樣的是,光源是經過glLight*函數來設置的,而材質則是經過glMaterial*函數來設置的。
glMaterial*函數有三個參數。第一個參數表示指定哪一面的屬性。能夠是GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK。分別表示設置「正面」「背面」的材質,或者兩面同時設置。(關於「正面」「背面」的內容須要參看前些課程的內容)第2、第三個參數與glLight*函數的第2、三個參數做用相似。下面分別說明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模式要複雜,而且使用範圍較小,這裏不作討論。
5、選擇光照模型
這裏所說的「光照模型」是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。後者一般能夠使畫面效果更爲逼真(固然,若是自己就沒有執行任何紋理操做,這樣的分離就沒有任何意義)。

6、最後的準備
到如今能夠說是完事俱備了。不過,OpenGL默認是關閉光照處理的。要打開光照處理功能,使用下面的語句:
glEnable(GL_LIGHTING);
要關閉光照處理功能,使用glDisable(GL_LIGHTING);便可。
7、示例程序
到如今,咱們已經能夠編寫簡單的使用光照的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程序時,遇到重複的工做,能夠建立一個顯示列表,把重複的工做裝入其中,並在須要的地方調用這個顯示列表。
使用顯示列表通常有四個步驟:分配顯示列表編號、建立顯示列表、調用顯示列表、銷燬顯示列表。

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

2、建立顯示列表
建立顯示列表實際上就是把各類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函數都不知道如何處理這些返回值。在網絡方式下,設置客戶端狀態的函數也沒法被裝入到顯示列表,這是由於顯示列表被保存到服務器端,各類設置客戶端狀態的函數在發送到服務器端之前就被執行了,而服務器端沒法執行這些函數。分配、建立、刪除顯示列表的動做也沒法被裝入到另外一個顯示列表,但調用顯示列表的動做則能夠被裝入到另外一個顯示列表。

3、調用顯示列表
使用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的四個顯示列表。
注:「調用顯示列表」這個動做自己也能夠被裝在另外一個顯示列表中。

4、銷燬顯示列表
銷燬顯示列表能夠回收資源。使用glDeleteLists來銷燬一串編號連續的顯示列表。
例如,使用glDeleteLists(20, 4);將銷燬20,21,22,23這四個顯示列表。
使用顯示列表將會帶來一些開銷,例如,把各類動做保存到顯示列表中會佔用必定數量的內存資源。但若是使用得當,顯示列表能夠提高程序的性能。這主要表如今如下方面:
一、明顯的減小OpenGL函數的調用次數。若是函數調用是經過網絡進行的(Linux等操做系統支持這樣的方式,即由應用程序在客戶端發出OpenGL請求,由網絡上的另外一臺服務器進行實際的繪圖操做),將顯示列表保存在服務器端,能夠大大減小網絡負擔。
二、保存中間結果,避免一些沒必要要的計算。例如前面的樣例程序中,cos、sin函數的計算結果被直接保存到顯示列表中,之後使用時就沒必要重複計算。
三、便於優化。咱們已經知道,使用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模式下,才能夠使用混合功能,顏色索引模式下是沒法使用混合功能的。
1、源因子和目標因子
前面咱們已經提到,混合須要把原來的顏色和將要畫上去的顏色找出來,通過某種方式處理後獲得一種新的顏色。這裏把將要畫上去的顏色稱爲「源顏色」,把原來的顏色稱爲「目標顏色」。
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),結果爲黃色。
注意:
所謂源顏色和目標顏色,是跟繪製的順序有關的。假如先繪製了一個紅色的物體,再在其上繪製綠色的物體。則綠色是源顏色,紅色是目標顏色。若是順序反過來,則紅色就是源顏色,綠色纔是目標顏色。在繪製時,應該注意順序,使得繪製的源顏色與設置的源因子對應,目標顏色與設置的目標因子對應。不要被混亂的順序搞暈了。
2、二維圖形混合舉例
下面看一個簡單的例子,實現將兩種不一樣的顏色混合在一塊兒。爲了便於觀察,咱們繪製兩個矩形: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

3、實現三維混合
也許你火燒眉毛的想要繪製一個三維的帶有半透明物體的場景了。可是如今恐怕還不行,還有一點是在進行三維場景的混合時必須注意的,那就是深度緩衝。
深度緩衝是這樣一段數據,它記錄了每個像素距離觀察者有多近。在啓用深度緩衝測試的狀況下,若是將要繪製的像素比原來的像素更近,則像素將被繪製。不然,像素就會被忽略掉,不進行繪製。這在繪製不透明的物體時很是有用——無論是先繪製近的物體再繪製遠的物體,仍是先繪製遠的物體再繪製近的物體,或者乾脆以混亂的順序進行繪製,最後的顯示結果老是近的物體遮住遠的物體。
然而在你須要實現半透明效果時,發現一切都不是那麼美好了。若是你繪製了一個近距離的半透明物體,則它在深度緩衝區內保留了一些信息,使得遠處的物體將沒法再被繪製出來。雖然半透明的物體仍然半透明,但透過它看到的卻不是正確的內容了。
要解決以上問題,須要在繪製半透明物體時將深度緩衝區設置爲只讀,這樣一來,雖然半透明物體被繪製上去了,深度緩衝區還保持在原來的狀態。若是再有一個物體出如今半透明物體以後,在不透明物體以前,則它也能夠被繪製(由於此時深度緩衝區中記錄的是那個不透明物體的深度)。之後再要繪製不透明物體時,只須要再將深度緩衝區設置爲可讀可寫的形式便可。嗯?你問我怎麼繪製一個一部分半透明一部分不透明的物體?這個好辦,只須要把物體分爲兩個部分,一部分全是半透明的,一部分全是不透明的,分別繪製就能夠了。
即便使用了以上技巧,咱們仍然不能爲所欲爲的按照混亂順序來進行繪製。必須是先繪製不透明的物體,而後繪製透明的物體。不然,假設背景爲藍色,近處一塊紅色玻璃,中間一個綠色物體。若是先繪製紅色半透明玻璃的話,它先和藍色背景進行混合,則之後繪製中間的綠色物體時,想單獨與紅色玻璃混合已經不能實現了。
總結起來,繪製順序就是:首先繪製全部不透明的物體。若是兩個物體都是不透明的,則誰先誰後都沒有關係。而後,將深度緩衝區設置爲只讀。接下來,繪製全部半透明的物體。若是兩個物體都是半透明的,則誰先誰後只須要根據本身的意願(注意了,先繪製的將成爲「目標顏色」,後繪製的將成爲「源顏色」,因此繪製的順序將會對結果形成一些影響)。最後,將深度緩衝區設置爲可讀可寫形式。
調用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來繪製幾何體,咱們經過重複的繪製許多幾何體,能夠繪製出一幅矢量圖。那麼,應該如何繪製像素圖呢?這就是咱們今天要學習的內容了。
一、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;   // 數據總長 = 每行長度 * 圖象高度

這並非效率最高的方法,但因爲這個修正自己運算量並不大,使用頻率也不高,咱們就不須要再考慮更快的方法了。
二、簡單的OpenGL像素操做
OpenGL提供了簡潔的函數來操做像素:
glReadPixels:讀取一些像素。當前能夠簡單理解爲「把已經繪製好的像素(它可能已經被保存到顯卡的顯存中)讀取到內存」。
glDrawPixels:繪製一些像素。當前能夠簡單理解爲「把內存中一些數據做爲像素數據,進行繪製」。
glCopyPixels:複製一些像素。當前能夠簡單理解爲「把已經繪製好的像素從一個位置複製到另外一個位置」。雖然從功能上看,好象等價於先讀取像素再繪製像素,但實際上它不須要把已經繪製的像素(它可能已經被保存到顯卡的顯存中)轉換爲內存數據,而後再由內存數據進行從新的繪製,因此要比先讀取後繪製快不少。
這三個函數能夠完成簡單的像素讀取、繪製和複製任務,但實際上也能夠完成更復雜的任務。當前,咱們僅討論一些簡單的應用。因爲這幾個函數的參數數目比較多,下面咱們分別介紹。
三、glReadPixels的用法和舉例
3.1 函數的參數說明
該函數總共有七個參數。前四個參數能夠獲得一個矩形,該矩形所包括的像素都會被讀取出來。(第1、二個參數表示了矩形的左下角橫、縱座標,座標以窗口最左下角爲零,最右上角爲最大值;第3、四個參數表示了矩形的寬度和高度)
第五個參數表示讀取的內容,例如: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文件了。(在我寫這個教程的時候,很多地方都用這樣的代碼進行截圖工做,這段代碼一旦寫好,運行起來是很方便的。)
四、glDrawPixels的用法和舉例
glDrawPixels函數與glReadPixels函數相比,參數內容大體相同。它的第1、2、3、四個參數分別對應於glReadPixels函數的第3、4、5、六個參數,依次表示圖象寬度、圖象高度、像素數據內容、像素數據在內存中的格式。兩個函數的最後一個參數也是對應的,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*函數設置的「開始位置」不必定就是矩形的左下角)。
五、glCopyPixels的用法和舉例
從效果上看,glCopyPixels進行像素複製的操做,等價於把像素讀取到內存,再從內存繪製到另外一個區域,所以能夠經過glReadPixels和glDrawPixels組合來實現複製像素的功能。然而咱們知道,像素數據一般數據量很大,例如一幅1024*768的圖象,若是使用24位BGR方式表示,則須要至少1024*768*3字節,即2.25兆字節。這麼多的數據要進行一次讀操做和一次寫操做,而且由於在glReadPixels和glDrawPixels中設置的數據格式不一樣,極可能涉及到數據格式的轉換。這對CPU無疑是一個不小的負擔。使用glCopyPixels直接從像素數據複製出新的像素數據,避免了多餘的數據的格式轉換,而且也可能減小一些數據複製操做(由於數據可能直接由顯卡負責複製,不須要通過主內存),所以效率比較高。
glCopyPixels函數也經過glRasterPos*系列函數來設置繪製的位置,由於不須要涉及到主內存,因此不須要指定數據在內存中的格式,也不須要使用任何指針。
glCopyPixels函數有五個參數,第1、二個參數表示複製像素來源的矩形的左下角座標,第3、四個參數表示複製像素來源的舉行的寬度和高度,第五個參數一般使用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

紋理的使用是很是複雜的。所以即便是入門教程,在編寫時我也屢次進行刪改,不少東西都被精簡掉了,但本課的內容仍然較多,你們要有一點心理準備~
一、啓用紋理和載入紋理
就像咱們曾經學習過的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將沒法識別這個參數,形成錯誤)
第4、五個參數是二維紋理像素的寬度和高度。這裏有一個很須要注意的地方: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。把這些像素數據從主內存傳送到專門的圖形硬件,這個過程當中還可能須要把程序中所指定的像素格式轉化爲圖形硬件所能識別的格式(或最能發揮圖形硬件性能的格式),這些操做都須要較多時間。
二、紋理座標
咱們先來回憶一下以前學過的一點內容:
當咱們繪製一個三角形時,只須要指定三個頂點的顏色。三角形中其它各點的顏色不須要咱們指定,這些點的顏色是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*等操做矩陣的函數就能夠用來處理「對紋理座標進行轉換」的工做了。在簡單應用中,可能不會對矩陣進行任何變換,這樣考慮問題會比較簡單。
三、紋理參數
到這裏,入門所須要掌握的全部難點都被咱們掌握了。可是,咱們的知識仍然是不夠的,若是僅利用現有的知識去使用紋理的話,你可能會發現紋理徹底不起做用。這是由於在使用紋理前還有某些參數是必須設置的。
使用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);

四、紋理對象
前面已經提到過,載入一幅紋理所須要的時間是比較多的。所以應該儘可能減小載入紋理的次數。若是隻有一幅紋理,則應該在第一次繪製前就載入它,之後就不須要再次載入了。這點與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版或更高版本),因此這個問題也就不算是問題了。
五、示例程序
紋理入門所須要掌握的知識點就介紹到這裏了。可是若是不實際動手操做的話,也是不可能真正掌握的。下面咱們來看看本課開頭的那個紋理效果是如何實現的吧。
由於代碼比較長,我把它拆分紅了三段,你們若是要編譯的話,應該把三段代碼按順序連在一塊兒編譯。若是要運行的話,除了要保證有一個名稱爲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測試和模板測試。

一、剪裁測試
剪裁測試用於限制繪製區域。咱們能夠指定一個矩形的剪裁窗口,當啓用剪裁測試後,只有在這個窗口以內的像素才能被繪製,其它像素則會被丟棄。換句話說,不管怎麼繪製,剪裁窗口之外的像素將不會被修改。
有的朋友可能玩過《魔獸爭霸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節中有介紹)。但視口變換和剪裁測試是不一樣的。視口變換是將全部內容縮放到合適的大小後,放到一個矩形的區域內;而剪裁測試不會進行縮放,超出矩形範圍的像素直接忽略掉。

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

二、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測試而不是採用混合操做。當須要繪製半透明像素時,才採用混合操做。

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

 

三、模板測試
模板測試是全部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,以至於在繪製後其顏色不會有任何變化,可是這個像素的深度值有可能會被修改,而若是是使用模板測試,沒有經過測試的像素其深度值不會發生任何變化。並且,模板測試和混合功能中,像素模板值的修改方式是不同的。

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

 

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

深度測試須要深度緩衝區,跟模板測試須要模板緩衝區是相似的。若是使用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常識外,更多的會涉及到數學、數據結構與算法、操做系統等各個領域。一個大型的程序一般都要實現一些文字特殊效果,僅僅是調用幾個顯示列表固然是不行的,須要大量的相關知識來支撐。
本課的門檻忽然提升,搞得我都不知道這還算不算是「入門教程」了,但願各位不要退縮哦。祝你們愉快。 


posted on 2009-01-08 21:40 doing5552 閱讀(235301) 評論(81)   編輯  收藏 引用 所屬分類: OpenGL

Feedback

#  re: OpenGL入門學習 2009-02-07 21:58 Polygon
你好,我也在學習OpenGL,請問你是否是pfan的eastcowboy   回覆   更多評論
  

 

#  re: OpenGL入門學習 2009-06-20 19:31 Zoltin
樓主太感謝了   回覆   更多評論
  

 

#  re: OpenGL入門學習 2009-06-25 00:18 wsj
太感謝了哦,最近也在學opengl,解開了我很多惑啊,哈哈   回覆   更多評論
  

 

#  re: OpenGL入門學習 2009-10-18 15:53 江偉新
OpenGL入門學習總結的真好,謝謝!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2009-10-30 10:15 OpenGL
感謝   回覆   更多評論
  

 

#  re: OpenGL入門學習 2009-12-01 22:12 cww
我想問一下
opengl在 TC下的配置方法

還有就是 我我的感受在學習起初 是否應該 重點學習 Graphics   回覆   更多評論
  

 

#  re: OpenGL入門學習 2010-04-30 15:38 飛天川
這篇文章寫的至關好了,謝謝了   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2010-05-05 19:12 wang
一口氣全複製了,謝謝   回覆   更多評論
  

 

#  re: OpenGL入門學習 2010-12-29 16:48
感謝你的無私貢獻,讓我獲得了一個美好的明天!!
要否則我還真不知道本身怎樣完成畢業畢業設計啊!!
多謝'
多謝!!    回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-01-14 15:05 靜一
謝謝樓主,太有用了,我決定看完您的看紅包書!
請問附件哪裏下載?能夠發給我麼?謝謝!
kingpotter1990@gmail.com   回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-04-12 14:54 李廣宇
很是好,對C++下OpenGL編程作了很是清晰、有條理和理論與實踐相結合的介紹。篇幅很少,但切合要領,一看就明白,上手就能用。優於大多數同類教程。
感謝樓主。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-04-22 15:18 slam
頂起!!樓主幫了大忙了!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-05-22 22:58 lue
附件源碼在哪呢?   回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-05-22 23:03 mary
附件flag.c 哪裏呢    回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-08-26 11:00 Lewis
很不錯的教程。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2011-10-04 10:45 快樂小沖沖
您好!我想知道關於多邊形填充的相關內容   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2011-12-15 15:52 123
很好的資料,學習了,謝謝   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-02-12 20:47 xue
講地太好了,不過水平有限,一時很難掌握到位   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-04-17 20:37 屈秋竹
我在學習第五課的時候,發現所舉的綜合例子沒有獲得預期的顯示結果,最後只有一個黑色的框。另外就這個問題,我想問一下是否是座標的問題,您在開始說能夠座標擴展,再也不是隻能從-1到1了,那這個擴展的方法又是什麼?我沒有看到啊,看到求解答!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-06-19 16:32 BigMoon
好文章 寫的很齊全 謝謝!!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-07-01 21:02 xiao
樓主很細心,感謝樓主的無私奉獻!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-07-02 21:15 BigMoon
講的很好 花了兩個星期看完了 如今要好好消化!!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-07-18 15:05 禪定中的蟬
很好很強大   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-08-09 18:45 thomasy
很是好的入門,謝謝樓主   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2012-09-21 10:34 TW
真的很是棒   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-09-21 15:27 ylb615334975@qq.com
高手哦。吼吼,至關給力,大力感謝。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-10-07 02:03 friend
好文,太給力了!謝謝!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-10-17 22:30 lsy
請教您一個問題能夠嗎?我用圖形計算學計算法向量的時候,總會有一個很小的偏差,不知道這個偏差是否是因爲初始設置形成的?   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2012-11-08 20:21 hh
好   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2012-11-25 14:25 Alvin
真是太厲害了 , 歎爲觀止。 不知道能夠請教問題麼。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2012-11-30 10:02 哈登
無比給力,無比感謝,但願樓主能分享更多,但願本身學成後也能分享給更多人   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-04-28 17:25 未命名
原帖在這裏
http://bbs.pfan.cn/showbbs.asp?id=184355&read=df   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-06-29 15:00 qsd
我在學習第五課的時候,發現所舉的例子沒有獲得預期的顯示結果,最後只有一個黑色的框。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-06-29 15:00 qsd
我也是這樣的~請問你解決了嗎@屈秋竹
   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-07-16 12:55 ream
@qsd
把裏面的大數字全減2個零   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-08-25 12:00 bwa
是繪製球體時,半徑太大,還有透視距離太遠!只要將那幾個看起來好幾萬的數字按比例縮小就能夠了!@qsd
   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-09-26 19:02 Zauther
樓主幫了大忙,很適合我這種初學者,謝謝樓主   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-10-24 09:44 xphcyh
恰好要學習中   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2013-10-26 20:20 noname
由於要研究光線跟蹤算法,因此須要opengl很耐心的看完了,而後的感謝樓主啊   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-11-01 16:56 西工大
好長的博文,先收藏着吧   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-12-04 08:32 yesl
頂一個   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-12-09 15:08 晨禱
好。這麼好的文章真是不多見了,多謝做者。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2013-12-24 17:03 zhiming
寫的很好!頂一個 謝謝!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-01-10 18:00 蘇宛如
一邊看 OpenGL Programming Guide 紅寶一邊看您的博客,相比簡潔許多,贊一個   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-02-18 16:10 咿呀
贊啊啊啊~~寫的真好~   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-03-18 16:03 allen
講得很仔細很好,提幾個意見和疑問:
1,應該開頭的時候就想一下OpenGL的座標系,否則-1.0f,0.5f是什麼概念都不清楚;
2,OpenGL是否有相似早期D3D同樣的硬件檢測,以肯定某些API特性是否被支持?
3,OpenGL是命令式的,linux的圖形系統也是命令式的,這是不是OpenGL/Linux的圖形部分都沒法與D3D/windows匹敵的緣由呢?

Thanks !   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2014-03-26 14:42 Nathan
thank u ~ very much   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-04-25 18:02 lambda
樓主真是用心在講一個教程,寫的很是通俗易懂,很是感謝。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-11-06 16:36 Lucky
會好好學習的,感謝分享   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-11-16 17:42 kbs
很棒!! 感謝分享~~~   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-12-01 16:24 艾青
請問使用OpenGL導入obj文件能夠對其中部分圖形操做嗎?不行的話能夠把每一部分分開讀取多個obj文件而後同時顯示嗎?   回覆   更多評論
  

 

#  re: OpenGL入門學習 2014-12-08 11:02 Mr Hu
很是感謝樓主,這篇教程我已經仔細看過了,內容豐富,講解仔細,很是適合咱們初學者學習!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-02-07 22:00 顧學喬
初學者很適合,但願樓主繼續更新   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-02-09 19:47 liky
很不錯,謝謝!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-03-23 21:06 cjz
謝謝博主   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-04-10 13:51 小保
很是感謝了,不是用話語能夠表達你這種分享的精神的   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-04-17 14:57 hcw
2015了,還GLUT,已經很老了   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-04-27 18:21 h2
寫的不錯, 收藏了。。   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2015-06-10 19:11 Fox
不知道樓主還看不看這篇文章,我想請教一個問題,是這樣的,個人模型座標很大,如何讓模型繞中心軸旋轉呢?由於若是隻是Rotatef(angle,0,1,0)這樣的話,模型一旋轉就沒有了。   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-06-18 08:11 wl
謝謝樓主   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-07-09 21:40 彤彤123
附件在哪裏下載啊??怎麼沒看到   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-07-09 21:48 彤彤123
請問附件在哪裏下載呢?   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2015-07-23 16:02 lee
樓主,能教下怎麼畫一個B樣條曲面麼   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-09-04 10:24 wangzhione
第二課 畫五角形能夠有個通用的方式
仍是採用畫圓的方式 只打5個點
#define _USE_MATH_DEFINES
int i=-1,n=5;//打點索引,打點個數
double r = 0.5f;//圓半徑

glClear(GL_COLOR_BUFFER_BIT);

//開始畫圓
glBegin(GL_POLYGON);

while (++i < n) { //按照角度開始畫 總體 是2π角度
double jd = 2 * M_PI*i / n;
glVertex2f(r*cos(jd), r*sin(jd));
}

glEnd();

glFlush();   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-09-09 18:26 鍾恩芳
今天上午剛學這門課程,樓主寫的OpenGL入門學習總結的真好,謝謝   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-09-27 16:05 openglStudy
太陽地球月球畫不出來   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-09-30 11:15 路過
@openglStudy
記得在display函數中加上
glDepthFunc(GL_LEQUAL);這一句   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2015-10-20 15:22 LJ
截圖那裏保存的全是黑的 求解   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-10-21 15:38 四夕
樓主辛苦了   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2015-10-27 14:53 LJ
我發現把雙緩衝改爲單緩衝就能保存正確的屏幕截圖了,可是OPENGL窗口卻白屏了……求解啊T.T   回覆   更多評論
  

 

#  re: OpenGL入門學習[未登陸] 2015-10-27 15:28 LJ
原來是單緩衝要加個glflush().同時我發現雙緩衝下把grab()放在glutSwapBuffers();前面就能正常截圖和顯示了,是否是我哪裏理解錯了?   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-04 15:23 zhuxiangfeicool
@openglStudy


博主的gluPerspective函數參數有誤!近距設置過小了,都映射到一個像素上面了,能看到纔怪!,第三個參數能夠設置大一點,像這樣:gluPerspective(45, 1, 10000000, 400000000);   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-04 16:04 zhuxiangfeicool
@zhuxiangfeicool
並且參數大一些,能夠避免圖形上產生的雪花!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-06 00:17 zhuxiangfeicool
@zhuxiangfeicool

// 建立透視效果視圖
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);

在這裏,
glMatrixMode(GL_MODELVIEW);這一句的位置也錯了,
應該放在gluLookAt()以後吧,求解答!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-09 18:41 zhuxiangfeicool

void myDisplay()
{
glClearColor(0.02f, 0.02f, 0.02f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
//glTranslatef(0.0, 0.0, -2.0);
drawSphere();
glPopMatrix();
if (isGrabed);
else
grab();
glutSwapBuffers();


}

抓取屏幕的操做grab(),要放在glutSwapBuffers()以前,才能抓到內容   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-09 18:42 zhuxiangfeicool
@zhuxiangfeicool
第十課裏面的   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-10 19:32 yuxinxin
我在努力學習這方面的知識,感謝樓主分享資源!樓主萬歲   回覆   更多評論
  

 

#  re: OpenGL入門學習 2015-11-23 11:18 pengpeng
真的很棒,樓主辛苦了!!!   回覆   更多評論
  

 

#  re: OpenGL入門學習 2016-07-14 21:50 三胖子
請教,在第五課中,我按照樓主的程序,運行以後爲何窗口裏面什麼也沒顯示?前面的例子都是能夠顯示的   回覆   更多評論
  

 

#  re: OpenGL入門學習 2016-07-14 22:51 三胖子
@zhuxiangfeicool
樓主那個設置確實什麼都看不到,按您的設置,能顯示   回覆   更多評論
  

 

#  re: OpenGL入門學習 2016-07-16 10:53 喻馨

@三胖子
我也遇到了那個問題,可否請教一下是怎麼解決的  回覆  更多評論  

相關文章
相關標籤/搜索