OpenGL入門學習

環境搭建

  OpenGL作爲當前主流的圖形API之一,它在一些場合具有比DirectX更優越的特性。
1、與C語言緊密結合。
  OpenGL命令最初就是用C語言函數來進行描述的,對於學習過C語言的人來講,OpenGL是容易理解和學習的。如果你曾經接觸過TC的graphics.h,你會發現,使用OpenGL作圖甚至比TC更加簡單。

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

3、高性能的圖形渲染。
  OpenGL是一個工業標準,它的技術緊跟時代,現今各個顯卡廠家無一不對OpenGL提供強力支持,激烈的競爭中使得OpenGL性能一直領先。
  總之,OpenGL是一個很NB的圖形軟件接口。至於究竟有多NB,去看看DOOM3和QUAKE4等專業遊戲就知道了。

OpenGL官方網站(英文)
http://www.opengl.org

  下面以linux爲例,簡介下opengl編程。這裏不選擇window因爲不想下載龐大的winIDE工具包,而且可以看到以前在win下的示例代碼幾乎不用修改就可以在linux下運行。
  必不可少的編譯器與基本的函式庫,如果系統沒有安裝的話,依照下面的方式安裝:

$ sudo apt-get install build-essential

安裝OpenGL Library

$ sudo apt-get install libgl1-mesa-dev

安裝OpenGL Utilities

$ sudo apt-get install libglu1-mesa-dev

  OpenGL Utilities 是一組建構於 OpenGL Library 之上的工具組,提供許多很方便的函式,使 OpenGL 更強大且更容易使用。

  安裝OpenGL Utility Toolkit

$ sudo apt-get install libglut-dev

  OpenGL Utility Toolkit 是建立在 OpenGL Utilities 上面的工具箱,除了強化了 OpenGL Utilities 的不足之外,也增加了 OpenGL 對於視窗介面支援。
  注意:在這一步的時候,可能會出現以下情況,shell提示:

Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package libglut-dev

將上述

$ sudo apt-get install libglut-dev

命令改成

$ sudo apt-get install freeglut3-dev

即可。
測試代碼test.c :

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

編譯

$ gcc -o test test.c -lGL -lGLU -lglut

這裏寫圖片描述

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

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

然後看main函數。
  int main(int argc, char *argv[]),這個是帶命令行參數的main函數,各位應該見過吧?沒見過的同志們請多翻翻書,等弄明白了再往下看。注意main函數中的各語句,除了最後的return之外,其餘全部以glut開頭。這種以glut開頭的函數都是GLUT工具包所提供的函數,下面對用到的幾個函數進行介紹。

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

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

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

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

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

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

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

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

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

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

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

OpenGL入門學習[二]

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

點、直線和多邊形

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

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

在OpenGL中指定頂點

  由以上的討論可以知道,“點”是一切的基礎。
  如何指定一個點呢?OpenGL提供了一系列函數。它們都以glVertex開頭,後面跟一個數字和1~2個字母。例如:

glVertex2d
glVertex2f
glVertex3f
glVertex3fv

  數字表示參數的個數,2表示有兩個參數,3表示三個,4表示四個(我知道有點羅嗦~)。
  字母表示參數的類型:

s表示16位整數(OpenGL中將這個類型定義爲GLshort),
i表示32位整數(OpenGL中將這個類型定義爲GLint和GLsizei),
f表示32位浮點數(OpenGL中將這個類型定義爲GLfloat和GLclampf),
d表示64位浮點數(OpenGL中將這個類型定義爲GLdouble和GLclampd)。
v表示傳遞的幾個參數將使用指針的方式,見下面的例子。

  這些函數除了參數的類型和個數不同以外,功能是相同的。例如,以下五個代碼段的功能是等效的:

(一)glVertex2i(1, 3);
(二)glVertex2f(1.0f, 3.0f);
(三)glVertex3f(1.0f, 3.0f, 0.0f);
(四)glVertex4f(1.0f, 3.0f, 0.0f, 1.0f);
(五)GLfloat VertexArr3[] = {1.0f, 3.0f, 0.0f};
               glVertex3fv(VertexArr3);

以後我們將用glVertex*來表示這一系列函數。

  • 注意:OpenGL的很多函數都是採用這樣的形式,一個相同的前綴再加上參數說明標記,這一點會隨着學習的深入而有更多的體會。

開始繪製

  假設現在我已經指定了若干頂點,那麼OpenGL是如何知道我想拿這些頂點來幹什麼呢?是一個一個的畫出來,還是連成線?或者構成一個多邊形?或者做其它什麼事情?
  爲了解決這一問題,OpenGL要求:指定頂點的命令必須包含在glBegin函數之後,glEnd函數之前(否則指定的頂點將被忽略)。並由glBegin來指明如何使用這些點
例如我寫:

glBegin(GL_POINTS);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.5f, 0.0f);
glEnd();

  則這兩個點將分別被畫出來。如果將GL_POINTS替換成GL_LINES,則兩個點將被認爲是直線的兩個端點,OpenGL將會畫出一條直線。
我們還可以指定更多的頂點,然後畫出更復雜的圖形。
另一方面,glBegin支持的方式除了GL_POINTS和GL_LINES,還有GL_LINE_STRIP,GL_LINE_LOOP,GL_TRIANGLES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN等,每種方式的大致效果見下圖:
這裏寫圖片描述
這裏寫圖片描述

這裏是對每種圖形進行測試

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

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin( /* 在這裏填上你所希望的模式 */ );
    /* 在這裏使用glVertex*系列函數 */
    /* 指定你所希望的頂點位置 */
    glEnd();
    glFlush();
}

  把這段代碼改成你喜歡的樣子,然後用它替換第一課中的myDisplay函數,編譯後即可運行。
幾個小例子:

例一、畫一個圓

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

#include <math.h>
const int n = 20;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
    int i;
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(R*cos(2*Pi/n*i), R*sin(2*Pi/n*i));
    glEnd();
    glFlush();
}

編譯命令:

$ gcc -o test test.c -lGL -lGLU -lglut -lm

生成圖形:

這裏寫圖片描述

如果是 GL_LINE_STRIP 生成的圖形:
這裏寫圖片描述

如果是 GL_TRIANGLES 生成的圖形:
這裏寫圖片描述

例二、畫一個五角星

  首先,根據餘弦定理列方程,計算五角星的中心到頂點的距離a (爲什麼這麼計算我想了很久都無法想通,高中的數學喂狗了!!TODO)

a = 1 / (2-2*cos(72*Pi/180));

  然後,根據正弦和餘弦的定義,計算B的x座標bx和y座標by,以及C的y座標
(假設五角星的中心在座標原點)

bx = a * cos(18 * Pi/180);
by = a * sin(18 * Pi/180);
cy = -a * cos(18 * Pi/180);

  五個點的座標就可以通過以上四個量和一些常數簡單的表示出來

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

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

這裏寫圖片描述

例三、畫出正弦函數的圖形

  由於OpenGL默認座標值只能從-1到1,(可以修改,但方法留到以後講)所以我們設置一個因子factor,把所有的座標值等比例縮小,這樣就可以畫出更多個正弦週期。
試修改factor的值,觀察變化情況

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

圖示如下:
這裏寫圖片描述

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

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個點將被畫爲虛的。
示例代碼:

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

這裏寫圖片描述

關於這個這裏介紹的還不是很詳細,可以看博文:opengl 直線的線型(各種虛線)

關於多邊形

  多邊形的內容較多,我們將講述以下四個方面。
(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();
}

運行結果
這裏寫圖片描述

glFrontFace(GL_CCW)修改爲glFrontFace(GL_CW)之後運行結果:

這裏寫圖片描述

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

結果如下:
這裏寫圖片描述

小結
本課學習了繪製幾何圖形的一些細節。
點可以設置大小。
直線可以設置寬度;可以將直線畫成虛線。
多邊形的兩個面的繪製方法可以分別設置;在三維空間中,不可見的多邊形可以被剔除;可以將填充多邊形繪製成鏤空的樣式。
瞭解這些細節會使我們在一些圖象繪製中更加得心應手。
另外,把一些數據寫到程序之外的文件中,並用專門的工具編輯之,有時可以顯得更方便。

OpenGL入門學習[四]

  本次學習的是顏色的選擇。終於要走出黑白的世界了~~
OpenGL支持兩種顏色模式:一種是RGBA,一種是顏色索引模式。
  無論哪種顏色模式,計算機都必須爲每一個像素保存一些數據。不同的是,RGBA模式中,數據直接就代表了顏色;而顏色索引模式中,數據代表的是一個索引,要得到真正的顏色,還必須去查索引表。

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的整數次冪。在使用索引顏色方式進行繪圖時,總是先設置顏色表,然後選擇顏色。

選擇顏色
  使用glIndex*系列函數可以在顏色表中選擇顏色。其中最常用的可能是glIndexi,它的參數是一個整形。

void glIndexi(GLint c);

是的,這的確很簡單。

設置顏色表
  OpenGL 並沒有直接提供設置顏色表的方法,因此設置顏色表需要使用操作系統的支持。原文的例子是在windows下設置顏色表,這裏略過。

指定清除屏幕用的顏色

  我們寫: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清除後屏幕所剩的顏色。
可以設置顏色填充方式:平滑方式或單色方式。

OpenGL入門學習[五]

  今天要講的是三維變換的內容,課程比較枯燥。主要是因爲很多函數在單獨使用時都不好描述其效果,我只好在最後舉一個比較綜合的例子。希望大家能一口氣看到底了。只看一次可能不夠,如果感覺到迷糊,不妨多看兩遍。有疑問可以在下面跟帖提出。
  我也使用了若干圖形,希望可以幫助理解。

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

  我們生活在一個三維的世界——如果要觀察一個物體,我們可以:

1、從不同的位置去觀察它。(視圖變換)
2、移動或者旋轉它,當然了,如果它只是計算機裏面的物體,我們還可以放大或縮小它。(模型變換)
3、如果把物體畫下來,我們可以選擇:是否需要一種“近大遠小”的透視效果。另外,我們可能只希望看到物體的一部分,而不是全部(剪裁)。(投影變換)
4、我們可能希望把整個看到的圖形畫下來,但它只佔據紙張的一部分,而不是全部。(視口變換)

這些,都可以在OpenGL中實現。

  OpenGL變換實際上是通過矩陣乘法來實現。無論是移動、旋轉還是縮放大小,都是通過在當前矩陣的基礎上乘以一個新的矩陣來達到目的。關於矩陣的知識,這裏不詳細介紹,有興趣的朋友可以看看線性代數(PS 當年線性代數本學渣就逃課弄了個專門解線性代數題的程序,居然讓我弄成了,整本書幾乎所有的向量,矩陣,行列式都能解答,成就感啊!)。
  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函數可以將當前的可視空間設置爲透視投影空間。也可以使用更常用的gluPerspective函數。

  正投影相當於在無限遠處觀察得到的結果,它只是一種理想狀態。但對於計算機來說,使用正投影有可能獲得更好的運行速度。使用glOrtho函數可以將當前的可視空間設置爲正投影空間。

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

視口變換

  當一切工作已經就緒,只需要把像素繪製到屏幕上了。這時候還剩最後一個問題:應該把像素繪製到窗口的哪個區域呢?通常情況下,默認是完整的填充整個窗口,但我們完全可以只填充一半。(即:把整個圖象填充到一半的窗口內)
  使用glViewport來定義視口。其中前兩個參數定義了視口的左下腳(0,0表示最左下方),後兩個參數分別是寬度和高度。

操作矩陣堆棧

  鑑於是入門教程,先簡單介紹一下堆棧。你可以把堆棧想象成一疊盤子。開始的時候一個盤子也沒有,你可以一個一個往上放,也可以一個一個取下來。每次取下的,都是最後一次被放上去的盤子。通常,在計算機實現堆棧時,堆棧的容量是有限的,如果盤子過多,就會出錯。當然,如果沒有盤子了,再要求取一個盤子,也會出錯。(最基本的數據結構了,就是一個先進後出)
  我們在進行矩陣操作時,有可能需要先保存某個矩陣,過一段時間再恢復它。當我們需要保存時,調用glPushMatrix函數,它相當於把矩陣(相當於盤子)放到堆棧上。當需要恢復最近一次的保存時,調用glPopMatrix函數,它相當於把矩陣從堆棧上取下。OpenGL規定堆棧的容量至少可以容納32個矩陣,某些OpenGL實現中,堆棧的容量實際上超過了32個。因此不必過於擔心矩陣的容量問題。
  通常,用這種先保存後恢復的措施,比先變換再逆變換要更方便,更快速。

注意:模型視圖矩陣和投影矩陣都有相應的堆棧。使用glMatrixMode來指定當前操作的究竟是模型視圖矩陣還是投影矩陣。

綜合舉例

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

  我們要製作的是一個三維場景,包括了太陽、地球和月亮。假定一年有12個月,每個月30天。每年,地球繞着太陽轉一圈。每個月,月亮圍着地球轉一圈。即一年有360天。

  現在給出日期的編號(0~359),要求繪製出太陽、地球、月亮的相對位置示意圖。(這是爲了編程方便才這樣設計的。如果需要製作更現實的情況,那也只是一些數值處理而已,與OpenGL關係不大)

  首先,讓我們認定這三個天體都是球形,且他們的運動軌跡處於同一水平面,建立以下座標系:太陽的中心爲原點,天體軌跡所在的平面表示了X軸與Y軸決定的平面,且每年第一天,地球在X軸正方向上,月亮在地球的正X軸方向。

  下一步是確立可視空間。注意:太陽的半徑要比太陽到地球的距離短得多。如果我們直接使用天文觀測得到的長度比例,則當整個窗口表示地球軌道大小時,太陽的大小將被忽略。因此,我們只能成倍的放大幾個天體的半徑,以適應我們觀察的需要。(百度一下,得到太陽、地球、月亮的大致半徑分別是:696000km,6378km,1738km。地球到太陽的距離約爲1.5億km=150000000km,月亮到地球的距離約爲380000km。)

  讓我們假想一些數據,將三個天體的半徑分別“修改”爲:69600000(放大100倍),15945000(放大2500倍),4345000(放大2500倍)。將地球到月亮的距離“修改”爲38000000(放大100倍)。地球到太陽的距離保持不變。

  爲了讓地球和月亮在離我們很近時,我們仍然不需要變換觀察點和觀察方向就可以觀察它們,我們把觀察點放在這個位置:(0, -200000000, 0) ——因爲地球軌道半徑爲150000000,咱們就湊個整,取-200000000就可以了。觀察目標設置爲原點(即太陽中心),選擇Z軸正方向作爲 “上”方。當然我們還可以把觀察點往“上”方移動一些,得到(0, -200000000, 200000000),這樣可以得到45度角的俯視效果。

  爲了得到透視效果,我們使用gluPerspective來設置可視空間。假定可視角爲60度(如果調試時發現該角度不合適,可修改之。我在最後選擇的數值是75。)高寬比爲1.0。最近可視距離爲1.0,最遠可視距離爲200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);現在我們來看看如何繪製這三個天體。

  爲了簡單起見,我們把三個天體都想象成規則的球體。而我們所使用的glut實用工具中,正好就有一個繪製球體的現成函數:glutSolidSphere,這個函數在“原點”繪製出一個球體。由於座標是可以通過glTranslate*glRotate*兩個函數進行隨意變換的,所以我們就可以在任意位置繪製球體了。函數有三個參數:第一個參數表示球體的半徑,後兩個參數代表了“面”的數目,簡單點說就是球體的精確程度,數值越大越精確,當然代價就是速度越緩慢。這裏我們只是簡單的設置後兩個參數爲20。
  太陽在座標原點,所以不需要經過任何變換,直接繪製就可以了。

  地球則要複雜一點,需要變換座標。由於今年已經經過的天數已知爲day,則地球轉過的角度爲day/一年的天數*360度。前面已經假定每年都是360天,因此地球轉過的角度恰好爲day。所以可以通過下面的代碼來解決:

glRotatef(day, 0, 0, -1);
/* 注意地球公轉是“自西向東”的,因此是饒着Z軸負方向進行逆時針旋轉 */
glTranslatef(地球軌道半徑, 0, 0);
glutSolidSphere(地球半徑, 20, 20);

  月亮是最複雜的。因爲它不僅要繞地球轉,還要隨着地球繞太陽轉。但如果我們選擇地球作爲參考,則月亮進行的運動就是一個簡單的圓周運動了。如果我們先繪製地球,再繪製月亮,則只需要進行與地球類似的變換:

glRotatef(月亮旋轉的角度, 0, 0, -1);
glTranslatef(月亮軌道半徑, 0, 0);
glutSolidSphere(月亮半徑, 20, 20);

  但這個“月亮旋轉的角度”,並不能簡單的理解爲day/一個月的天數30*360度。因爲我們在繪製地球時,這個座標已經是旋轉過的。現在的旋轉是在以前的基礎上進行旋轉,因此還需要處理這個“差值”。我們可以寫成:day/30*360 - day,即減去原來已經轉過的角度。這只是一種簡單的處理,當然也可以在繪製地球前用glPushMatrix保存矩陣,繪製地球后用glPopMatrix恢復矩陣。再設計一個跟地球位置無關的月亮位置公式,來繪製月亮。通常後一種方法比前一種要好,因爲浮點的運算是不精確的,即是說我們計算地球本身的位置就是不精確的。拿這個不精確的數去計算月亮的位置,會導致 “不精確”的成分累積,過多的“不精確”會造成錯誤。我們這個小程序沒有去考慮這個,但並不是說這個問題不重要。

  還有一個需要注意的細節: OpenGL把三維座標中的物體繪製到二維屏幕,繪製的順序是按照代碼的順序來進行的。因此後繪製的物體會遮住先繪製的物體,即使後繪製的物體在先繪製的物體的“後面”也是如此。使用深度測試可以解決這一問題。使用的方法是:

1、以GL_DEPTH_TEST爲參數調用glEnable函數,啓動深度測試。
2、在必要時(通常是每次繪製畫面開始時),清空深度緩衝,即:glClear(GL_DEPTH_BUFFER_BIT);其中,glClear (GL_COLOR_BUFFER_BIT)與glClear(GL_DEPTH_BUFFER_BIT)可以合併寫爲:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 且後者的運行速度可能比前者快。

  到此爲止,我們終於可以得到整個“太陽,地球和月亮”系統的完整代碼。

// 太陽、地球和月亮
// 假設每個月都是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*
  我們可以定義可視空間,這個空間可以是“正投影”的(使用glOrthogluOrtho2D),也可以是“透視投影”的(使用glFrustumgluPerspective)。
  我們可以定義繪製到窗口的範圍,使用的函數是glViewport。
  矩陣有自己的“堆棧”,方便進行保存和恢復。這在繪製複雜圖形時很有幫助。使用的函數是glPushMatrix和glPopMatrix。

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

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還提供了一些別的函數,例如“在鍵盤按下時做某事”等。

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

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

// 太陽、地球和月亮
// 假設每個月都是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();

    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的內容,所以我不打算詳細講述它。

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

  最後的一步,也被我們解決了——雖然做法不太雅觀,沒關係,以後我們還會改善它的。
  時間過得太久,每次給的程序都只是一小段,一些朋友難免會出問題。
  現在,我給出一個比較完整的程序,供大家參考。

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

OpenGL入門學習[七]

  今天要講的是OpenGL光照的基本知識。雖然內容顯得有點多,但條理還算比較清晰,理解起來應該沒有困難。即使對於一些內容沒有記住,問題也不大——光照部分是一個比較獨立的內容,它的學習與其它方面的學習可以分開,不像視圖變換那樣,影響到許多方面。課程的最後給出了一個有關光照效果的動畫演示程序,我想大家會喜歡的。

  從生理學的角度上講,眼睛之所以看見各種物體,是因爲光線直接或間接的從它們那裏到達了眼睛。人類對於光線強弱的變化的反應,比對於顏色變化的反應來得靈敏。因此對於人類而言,光線很大程度上表現了物體的立體感

  OpenGL對於光照效果提供了直接的支持,只需要調用某些函數,便可以實現簡單的光照效果。但是在這之前,我們有必要了解一些基礎知識。

建立光照模型

  在現實生活中,某些物體本身就會發光,例如太陽、電燈等,而其它物體雖然不會發光,但可以反射來自其它物體的光。這些光通過各種方式傳播,最後進入我們的眼睛——於是一幅畫面就在我們的眼中形成了。

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

法線向量

  根據光的反射定律,由光的入射方向和入射點的法線就可以得到光的出射方向。因此,對於指定的物體,在指定了光源後,即可計算出光的反射方向,進而計算出光照效果的畫面。在OpenGL中,法線的方向是用一個向量來表示。
  不幸的是,OpenGL並不會根據你所指定的多邊形各個頂點來計算出這些多邊形所構成的物體的表面的每個點的法線(這話聽着有些迷糊),通常,爲了實現光照效果,需要在代碼中爲每一個頂點指定其法線向量。
  指定法線向量的方式與指定顏色的方式有雷同之處。在指定顏色時,只需要指定每一個頂點的顏色,OpenGL就可以自行計算頂點之間的其它點的顏色。並且,顏色一旦被指定,除非再指定新的顏色,否則以後指定的所有頂點都將以這一顏色作爲自己的顏色。在指定法線向量時,只需要指定每一個頂點的法線向量,OpenGL會自行計算頂點之間的其它點的法線向量。並且,法線向量一旦被指定,除非再指定新的法線向量,否則以後指定的所有頂點都將以這一向量作爲自己的法線向量。使用glColor*函數可以指定顏色,而使用glNormal*函數則可以指定法線向量。

注意:使用glTranslate*函數或者glRotate*函數可以改變物體的外觀,但法線向量並不會隨之改變。然而,使用glScale*函數,對每一座標軸進行不同程度的縮放,很有可能導致法線向量的不正確,雖然OpenGL提供了一些措施來修正這一問題,但由此也帶來了各種開銷。因此,在使用了法線向量的場合,應儘量避免使用glScale*函數。即使使用,也最好保證各座標軸進行等比例縮放。

控制光源

  在OpenGL中,僅僅支持有限數量的光源。使用GL_LIGHT0表示第0號光源,GL_LIGHT1表示第1號光源,依次類推,OpenGL至少會支持8個光源,即GL_LIGHT0到GL_LIGHT7。使用glEnable函數可以開啓它們。例如,glEnable(GL_LIGHT0);可以開啓第0號光源。使用glDisable函數則可以關閉光源。一些OpenGL實現可能支持更多數量的光源,但總的來說,開啓過多的光源將會導致程序運行速度的嚴重下降,玩過3D Mark的朋友可能多少也有些體會。一些場景中可能有成百上千的電燈,這時可能需要採取一些近似的手段來進行編程,否則以目前的計算機而言,是無法運行這樣的程序的。
  每一個光源都可以設置其屬性,這一動作是通過glLight*函數完成的。glLight*函數具有三個參數,第一個參數指明是設置哪一個光源的屬性,第二個參數指明是設置該光源的哪一個屬性,第三個參數則是指明把該屬性值設置成多少。光源的屬性衆多,下面將分別介紹。

(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR屬性。這三個屬性表示了光源所發出的光的反射特性(以及顏色)。每個屬性由四個值表示,分別代表了顏色的R, G, B, A值。GL_AMBIENT表示該光源所發出的光,經過非常多次的反射後,最終遺留在整個光照環境中的強度(顏色)。GL_DIFFUSE表示該光源所發出的光,照射到粗糙表面時經過漫反射,所得到的光的強度(顏色)。GL_SPECULAR表示該光源所發出的光,照射到光滑表面時經過鏡面反射,所得到的光的強度(顏色)。

(2)GL_POSITION屬性。表示光源所在的位置。由四個值(X, Y, Z, W)表示。如果第四個值W爲零,則表示該光源位於無限遠處,前三個值表示了它所在的方向。這種光源稱爲方向性光源,通常,太陽可以近似的被認爲是方向性光源。如果第四個值W不爲零,則X/W, Y/W, Z/W表示了光源的位置。這種光源稱爲位置性光源。對於位置性光源,設置其位置與設置多邊形頂點的方式相似,各種矩陣變換函數例如:glTranslate*、glRotate*等在這裏也同樣有效。方向性光源在計算時比位置性光源快了不少,因此,在視覺效果允許的情況下,應該儘可能的使用方向性光源。

(3)GL_SPOT_DIRECTION、GL_SPOT_EXPONENT、GL_SPOT_CUTOFF屬性。表示將光源作爲聚光燈使用(這些屬性只對位置性光源有效)。很多光源都是向四面八方發射光線,但有時候一些光源則是隻向某個方向發射,比如手電筒,只向一個較小的角度發射光線。GL_SPOT_DIRECTION屬性有三個值,表示一個向量,即光源發射的方向。GL_SPOT_EXPONENT屬性只有一個值,表示聚光的程度,爲零時表示光照範圍內向各方向發射的光線強度相同,爲正數時表示光照向中央集中,正對發射方向的位置受到更多光照,其它位置受到較少光照。數值越大,聚光效果就越明顯。

(4)GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION屬性。這三個屬性表示了光源所發出的光線的直線傳播特性(這些屬性只對位置性光源有效)。現實生活中,光線的強度隨着距離的增加而減弱,OpenGL把這個減弱的趨勢抽象成函數:

衰減因子 = 1 / (k1 + k2 * d + k3 * k3 * d)

  其中d表示距離,光線的初始強度乘以衰減因子,就得到對應距離的光線強度。k1,k2,k3分別就是GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION。通過設置這三個常數,就可以控制光線在傳播過程中的減弱趨勢。屬性還真是不少。當然了,如果是使用方向性光源,(3)(4)這兩類屬性就不會用到了,問題就變得簡單明瞭。

控制材質

  材質與光源相似,也需要設置衆多的屬性。不同的是,光源是通過glLight*函數來設置的,而材質則是通過glMaterial*函數來設置的。

  glMaterial*函數有三個參數。第一個參數表示指定哪一面的屬性。可以是GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK。分別表示設置“正面”“背面”的材質,或者兩面同時設置。(關於“正面”“背面”的內容需要參看前些課程的內容)第二、第三個參數與glLight*函數的第二、三個參數作用類似。下面分別說明glMaterial*函數可以指定的材質屬性。

(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR屬性。這三個屬性與光源的三個對應屬性類似,每一屬性都由四個值組成。GL_AMBIENT表示各種光線照射到該材質上,經過很多次反射後最終遺留在環境中的光線強度(顏色)。GL_DIFFUSE表示光線照射到該材質上,經過漫反射後形成的光線強度(顏色)。GL_SPECULAR表示光線照射到該材質上,經過鏡面反射後形成的光線強度(顏色)。通常,GL_AMBIENT和GL_DIFFUSE都取相同的值,可以達到比較真實的效果。使用GL_AMBIENT_AND_DIFFUSE可以同時設置GL_AMBIENT和GL_DIFFUSE屬性。

(2)GL_SHININESS屬性。該屬性只有一個值,稱爲“鏡面指數”,取值範圍是0到128。該值越小,表示材質越粗糙,點光源發射的光線照射到上面,也可以產生較大的亮點。該值越大,表示材質越類似於鏡面,光源照射到上面後,產生較小的亮點。

(3)GL_EMISSION屬性。該屬性由四個值組成,表示一種顏色。OpenGL認爲該材質本身就微微的向外發射光線,以至於眼睛感覺到它有這樣的顏色,但這光線又比較微弱,以至於不會影響到其它物體的顏色。

(4)GL_COLOR_INDEXES屬性。該屬性僅在顏色索引模式下使用,由於顏色索引模式下的光照比RGBA模式要複雜,並且使用範圍較小,這裏不做討論。

選擇光照模型

  這裏所說的“光照模型”是OpenGL的術語,它相當於我們在前面提到的“光照環境”。在OpenGL中,光照模型包括四個部分的內容:全局環境光線(即那些充分散射,無法分清究竟來自哪個光源的光線)的強度、觀察點位置是在較近位置還是在無限遠處、物體正面與背面是否分別計算光照、鏡面顏色(即GL_SPECULAR屬性所指定的顏色)的計算是否從其它光照計算中分離出來,並在紋理操作以後在進行應用。
  以上四方面的內容都通過同一個函數glLightModel*來進行設置。該函數有兩個參數,第一個表示要設置的項目,第二個參數表示要設置成的值。

GL_LIGHT_MODEL_AMBIENT表示全局環境光線強度,由四個值組成。
GL_LIGHT_MODEL_LOCAL_VIEWER表示是否在近處觀看,若是則設置爲GL_TRUE,否則(即在無限遠處觀看)設置爲GL_FALSE。
GL_LIGHT_MODEL_TWO_SIDE表示是否執行雙面光照計算。如果設置爲GL_TRUE,則OpenGL不僅將根據法線向量計算正面的光照,也會將法線向量反轉並計算背面的光照。
GL_LIGHT_MODEL_COLOR_CONTROL表示顏色計算方式。如果設置爲GL_SINGLE_COLOR,表示按通常順序操作,先計算光照,再計算紋理。如果設置爲GL_SEPARATE_SPECULAR_COLOR,表示將>GL_SPECULAR屬性分離出來,先計算光照的其它部分,待紋理操作完成後再計算GL_SPECULAR。後者通常可以使畫面效果更爲逼真(當然,如果本身就沒有執行任何紋理操作,這樣的分離就沒有任何意義)。

最後的準備

  到現在可以說是完事俱備了。不過,OpenGL默認是關閉光照處理的。要打開光照處理功能,使用下面的語句:

glEnable(GL_LIGHTING);

  要關閉光照處理功能,使用glDisable(GL_LIGHTING);即可。

示例程序

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

下面給出具體的代碼:

#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

static GLfloat angle = 0.0f;

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

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

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

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

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

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

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

        glutSolidSphere(2.0, 40, 32);
    }

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

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

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

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

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(WIDTH, HEIGHT);
    glutCreateWindow("OpenGL光照演示");
    glutDisplayFunc(&myDisplay);
    glutIdleFunc(&myIdle);
    glutMainLoop();
    return 0;
}

小結:
  本課介紹了OpenGL光照的基本知識。OpenGL把光照分解爲光源、材質、光照模式三個部分,根據這三個部分的各種信息,以及物體表面的法線向量,可以計算得到最終的光照效果。
  光源、材質和光照模式都有各自的屬性,儘管屬性種類繁多,但這些屬性都只用很少的幾個函數來設置。使用glLight*函數可設置光源的屬性,使用glMaterial*函數可設置材質的屬性,使用glLightModel*函數可設置光照模式。
  GL_AMBIENTGL_DIFFUSEGL_SPECULAR這三種屬性是光源和材質所共有的,如果某光源發出的光線照射到某材質的表面,則最終的漫反射強度由兩個GL_DIFFUSE屬性共同決定,最終的鏡面反射強度由兩個GL_SPECULAR屬性共同決定。

  可以使用多個光源來實現各種逼真的效果,然而,光源數量的增加將造成程序運行速度的明顯下降。

  在使用OpenGL光照過程中,屬性的種類和數量都非常繁多,通常,需要很多的經驗纔可以熟練的設置各種屬性,從而形成逼真的光照效果。(各位也看到了,其實這個課程的示例程序中,屬性設置也不怎麼好)。然而,設置這些屬性的藝術性遠遠超過了技術性,往往是一些美術製作人員設置好各種屬性(並保存爲文件),然後由程序員編寫的程序去執行繪製工作。因此,即使目前無法熟練運用各種屬性,也不必過於擔心。如果條件允許,可以玩玩類似3DS MAX之類的軟件,對理解光照、熟悉各種屬性設置會有一些幫助。
  在課程的最後,我們給出了一個樣例程序,演示了太陽和地球模型中的光照效果。

OpenGL入門學習[八]

  今天介紹關於OpenGL顯示列表的知識。本課內容並不多,但需要一些理解能力。在學習時,可以將顯示列表與C語言的“函數”進行類比,加深體會。
  我們已經知道,使用OpenGL其實只要調用一系列的OpenGL函數就可以了。然而,這種方式在一些時候可能導致問題。比如某個畫面中,使用了數千個多邊形來表現一個比較真實的人物,OpenGL爲了產生這數千個多邊形,就需要不停的調用glVertex*函數,每一個多邊形將至少調用三次(因爲多邊形至少有三個頂點),於是繪製一個比較真實的人物就需要調用上萬次的glVertex*函數。更糟糕的是,如果我們需要每秒鐘繪製60幅畫面,則每秒調用的glVertex*函數次數就會超過數十萬次,乃至接近百萬次。這樣的情況是我們所不願意看到的。

  同時,考慮這樣一段代碼:

const int segments = 100;
const GLfloat pi = 3.14f;
int i;

glLineWidth(10.0);
glBegin(GL_LINE_LOOP);

for(i=0; i<segments; ++i)
{
    GLfloat tmp = 2 * pi * i / segments;
    glVertex2f(cos(tmp), sin(tmp));
}
glEnd();

  這段代碼將繪製一個圓環。如果我們在每次繪製圖象時調用這段代碼,則雖然可以達到繪製圓環的目的,但是cos、sin等開銷較大的函數被多次調用,浪費了CPU資源。如果每一個頂點不是通過cos、sin等函數得到,而是使用更復雜的運算方式來得到,則浪費的現象就更加明顯。
  經過分析,我們可以發現上述兩個問題的共同點:程序多次執行了重複的工作,導致CPU資源浪費和運行速度的下降。使用顯示列表可以較好的解決上述兩個問題。
  在編寫程序時,遇到重複的工作,我們往往是將重複的工作編寫爲函數,在需要的地方調用它。類似的,在編寫OpenGL程序時,遇到重複的工作,可以創建一個顯示列表,把重複的工作裝入其中,並在需要的地方調用這個顯示列表。
  使用顯示列表一般有四個步驟:分配顯示列表編號、創建顯示列表、調用顯示列表、銷燬顯示列表。

分配顯示列表編號

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

創建顯示列表

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

glNewList(list, GL_COMPILE);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();

  注意:顯示列表只能裝入OpenGL函數,而不能裝入其它內容。例如:

int i = 3;
glNewList(list, GL_COMPILE);
if( i > 20 )
     glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();

  其中if這個判斷就沒有被裝入到顯示列表。以後即使修改i的值,使i>20的條件成立,則glColor3f這個函數也不會被執行。因爲它根本就不存在於顯示列表中。

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

調用顯示列表

  使用glCallList函數可以調用一個顯示列表。該函數有一個參數,表示要調用的顯示列表的編號。例如,要調用編號爲10的顯示列表,直接使用glCallList(10);就可以了。
  使用glCallLists函數可以調用一系列的顯示列表。該函數有三個參數,第一個參數表示了要調用多少個顯示列表。第二個參數表示了這些顯示列表的編號的儲存格式,可以是GL_BYTE(每個編號用一個GLbyte表示),GL_UNSIGNED_BYTE(每個編號用一個GLubyte表示),GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT。第三個參數表示了這些顯示列表的編號所在的位置。在使用該函數前,需要用glListBase函數來設置一個偏移量。假設偏移量爲k,且glCallLists中要求調用的顯示列表編號依次爲l1, l2, l3, …,則實際調用的顯示列表爲l1+k, l2+k, l3+k, …。
例如:

GLuint lists[] = {1, 3, 4, 8};
glListBase(10);
glCallLists(4, GL_UNSIGNED_INT, lists);

  則實際上調用的是編號爲11, 13, 14, 18的四個顯示列表。

注:“調用顯示列表”這個動作本身也可以被裝在另一個顯示列表中。

銷燬顯示列表

  銷燬顯示列表可以回收資源。使用glDeleteLists來銷燬一串編號連續的顯示列表。
  例如,使用glDeleteLists(20, 4);將銷燬20,21,22,23這四個顯示列表。
  使用顯示列表將會帶來一些開銷,例如,把各種動作保存到顯示列表中會佔用一定數量的內存資源。但如果使用得當,顯示列表可以提升程序的性能。這主要表現在以下方面:

  • 1 明顯的減少OpenGL函數的調用次數。如果函數調用是通過網絡進行的(Linux等操作系統支持這樣的方式,即由應用程序在客戶端發出OpenGL請求,由網絡上的另一臺服務器進行實際的繪圖操作),將顯示列表保存在服務器端,可以大大減少網絡負擔。
  • 2 保存中間結果,避免一些不必要的計算。例如前面的樣例程序中,cos、sin函數的計算結果被直接保存到顯示列表中,以後使用時就不必重複計算。
  • 3 便於優化。我們已經知道,使用glTranslate*glRotate*glScale*等函數時,實際上是執行矩陣乘法操作,由於這些函數經常被組合在一起使用,通常會出現矩陣的連乘。這時,如果把這些操作保存到顯示列表中,則一些複雜的OpenGL版本會嘗試先計算出連乘的一部分結果,從而提高程序的運行速度。在其它方面也可能存在類似的例子。

  同時,顯示列表也爲程序的設計帶來方便。我們在設置一些屬性時,經常把一些相關的函數放在一起調用,(比如,把設置光源的各種屬性的函數放到一起)這時,如果把這些設置屬性的操作裝入到顯示列表中,則可以實現屬性的成組的切換。
  當然了,即使使用顯示列表在某些情況下可以提高性能,但這種提高很可能並不明顯。畢竟,在硬件配置和大致的軟件算法都不變的前提下,性能可提升的空間並不大
  顯示列表的內容就是這麼多了,下面我們看一個例子。
  假設我們需要繪製一個旋轉的彩色正四面體,則可以這樣考慮:設置一個全局變量angle,然後讓它的值不斷的增加(到達360後又恢復爲0,周而復始)。每次需要繪製圖形時,根據angle的值進行旋轉,然後繪製正四面體。這裏正四面體採用顯示列表來實現,即把繪製正四面體的若干OpenGL函數裝到一個顯示列表中,然後每次需要繪製時,調用這個顯示列表即可。
  將正四面體的四個頂點顏色分別設置爲紅、黃、綠、藍,通過數學計算,將座標設置爲:

    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>
#include <stdio.h>
#include <stdlib.h>
#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);
}

  限於篇幅,這裏就不給出完整的程序了。不過,大家可以自行嘗試,看看使用光照後效果有何種改觀。尤其是注意四面體各個表面交界的位置,在未使用光照前,幾乎看不清輪廓,在使用光照後,可比較容易的區分各個平面,因此立體感得到加強。

  當然了,這樣的效果還不夠。如果在各表面的交界處設置很多細小的平面,進行平滑處理,則光照後的效果將更真實。但這已經遠離本課的內容了。

小結
  本課介紹了顯示列表的知識和簡單的應用。
  可以把各種OpenGL函數調用的動作裝到顯示列表中,以後調用顯示列表,就相當於調用了其中的OpenGL函數。顯示列表中除了存放對OpenGL函數的調用外,不會存放其它內容。
  使用顯示列表的過程是:分配一個未使用的顯示列表編號,把OpenGL函數調用裝入顯示列表,調用顯示列表,銷燬顯示列表。
  使用顯示列表有可能帶來程序運行速度的提升,但是這種提升並不一定會很明顯。顯示列表本身也存在一定的開銷。
  把繪製固定的物體的OpenGL函數放到一個顯示列表中,是一種不錯的編程思路。本課最後的例子中使用了這種思路。

OpenGL入門學習[九]

  今天介紹關於OpenGL混合的基本知識。混合是一種常用的技巧,通常可以用來實現半透明。但其實它也是十分靈活的,你可以通過不同的設置得到不同的混合結果,產生一些有趣或者奇怪的圖象。
  混合是什麼呢?混合就是把兩種顏色混在一起。具體一點,就是把某一像素位置原來的顏色和將要畫上去的顏色,通過某種方式混在一起,從而實現特殊的效果。
  假設我們需要繪製這樣一個場景:透過紅色的玻璃去看綠色的物體,那麼可以先繪製綠色的物體,再繪製紅色玻璃。在繪製紅色玻璃的時候,利用“混合”功能,把將要繪製上去的紅色和原來的綠色進行混合,於是得到一種新的顏色,看上去就好像玻璃是半透明的。

  要使用OpenGL的混合功能,只需要調用:glEnable(GL_BLEND);即可。
  要關閉OpenGL的混合功能,只需要調用:glDisable(GL_BLEND);即可。

注意:只有在RGBA模式下,纔可以使用混合功能,顏色索引模式下是無法使用混合功能的。

源因子和目標因子

  前面我們已經提到,混合需要把原來的顏色和將要畫上去的顏色找出來,經過某種方式處理後得到一種新的顏色。這裏把將要畫上去的顏色稱爲“源顏色”,把原來的顏色稱爲“目標顏色”。
  OpenGL會把源顏色和目標顏色各自取出,並乘以一個係數(源顏色乘以的係數稱爲“源因子”,目標顏色乘以的係數稱爲“目標因子”),然後相加,這樣就得到了新的顏色。(也可以不是相加,新版本的OpenGL可以設置運算方式,包括加、減、取兩者中較大的、取兩者中較小的、邏輯運算等,但我們這裏爲了簡單起見,不討論這個了)
  下面用數學公式來表達一下這個運算方式。假設源顏色的四個分量(指紅色,綠色,藍色,alpha值)是(Rs, Gs, Bs, As),目標顏色的四個分量是(Rd, Gd, Bd, Ad),又設源因子爲(Sr, Sg, Sb, Sa),目標因子爲(Dr, Dg, Db, Da)。則混合產生的新顏色可以表示爲:

(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)

  當然了,如果顏色的某一分量超過了1.0,則它會被自動截取爲1.0,不需要考慮越界的問題。

  源因子和目標因子是可以通過glBlendFunc函數來進行設置的。glBlendFunc有兩個參數,前者表示源因子,後者表示目標因子。這兩個參數可以是多種值,下面介紹比較常用的幾種。

GL_ZERO: 表示使用0.0作爲因子,實際上相當於不使用這種顏色參與混合運算。
GL_ONE: 表示使用1.0作爲因子,實際上相當於完全的使用了這種顏色參與混合運算。
GL_SRC_ALPHA:表示使用源顏色的alpha值來作爲因子。
GL_DST_ALPHA:表示使用目標顏色的alpha值來作爲因子。
GL_ONE_MINUS_SRC_ALPHA:表示用1.0減去源顏色的alpha值來作爲因子。
GL_ONE_MINUS_DST_ALPHA:表示用1.0減去目標顏色的alpha值來作爲因子。

  除此以外,還有GL_SRC_COLOR(把源顏色的四個分量分別作爲因子的四個分量)、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前兩個在OpenGL舊版本中只能用於設置目標因子,後兩個在OpenGL舊版本中只能用於設置源因子。新版本的OpenGL則沒有這個限制,並且支持新的GL_CONST_COLOR(設定一種常數顏色,將其四個分量分別作爲因子的四個分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、GL_ONE_MINUS_CONST_ALPHA。另外還有GL_SRC_ALPHA_SATURATE。新版本的OpenGL還允許顏色的alpha值和RGB值採用不同的混合因子。但這些都不是我們現在所需要了解的。畢竟這還是入門教材,不需要整得太複雜~

舉例來說:
  如果設置了glBlendFunc(GL_ONE, GL_ZERO); 則表示完全使用源顏色,完全不使用目標顏色,因此畫面效果和不使用混合的時候一致(當然效率可能會低一點點)。如果沒有設置源因子和目標因子,則默認情況就是這樣的設置。
  如果設置了glBlendFunc(GL_ZERO, GL_ONE);,則表示完全不使用源顏色,因此無論你想畫什麼,最後都不會被畫上去了。(但這並不是說這樣設置就沒有用,有些時候可能有特殊用途)

  如果設置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,則表示源顏色乘以自身的alpha值,目標顏色乘以1.0減去源顏色的alpha值,這樣一來,源顏色的alpha值越大,則產生的新顏色中源顏色所佔比例就越大,而目標顏色所佔比例則減小。這種情況下,我們可以簡單的將源顏色的alpha值理解爲“不透明度”。這也是混合時最常用的方式。
  如果設置了glBlendFunc(GL_ONE, GL_ONE);則表示完全使用源顏色和目標顏色,最終的顏色實際上就是兩種顏色的簡單相加。例如紅色(1, 0, 0)和綠色(0, 1, 0)相加得到(1, 1, 0),結果爲黃色。

注意:
  所謂源顏色和目標顏色,是跟繪製的順序有關的。假如先繪製了一個紅色的物體,再在其上繪製綠色的物體。則綠色是源顏色,紅色是目標顏色。如果順序反過來,則紅色就是源顏色,綠色纔是目標顏色。在繪製時,應該注意順序,使得繪製的源顏色與設置的源因子對應,目標顏色與設置的目標因子對應。不要被混亂的順序搞暈了。

二維圖形混合舉例

  下面看一個簡單的例子,實現將兩種不同的顏色混合在一起。爲了便於觀察,我們繪製兩個矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,這兩個矩形有一個重疊的區域,便於我們觀察混合的效果。
  先來看看使用glBlendFunc(GL_ONE, GL_ZERO);的它的結果與不使用混合時相同。

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);

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

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

    glutSwapBuffers();
}

  嘗試把glBlendFunc的參數修改爲

glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glBlendFunc(GL_ONE, GL_ONE);

  觀察效果。第一種情況下,效果與沒有使用混合時相同,後繪製的圖形會覆蓋先繪製的圖形。第二種情況下,alpha被當作“不透明度”,由於被設置爲0.5,所以兩個矩形看上去都是半透明的,乃至於看到黑色背景。第三種是將顏色相加,紅色和綠色相加得到黃色。
第一種情況:
這裏寫圖片描述

第二種情況:

這裏寫圖片描述

第三種情況:
這裏寫圖片描述

實現三維混合

  也許你迫不及待的想要繪製一個三維的帶有半透明物體的場景了。但是現在恐怕還不行,還有一點是在進行三維場景的混合時必須注意的,那就是深度緩衝。

  深度緩衝是這樣一段數據,它記錄了每一個像素距離觀察者有多近。在啓用深度緩衝測試的情況下,如果將要繪製的像素比原來的像素更近,則像素將被繪製。否則,像素就會被忽略掉,不進行繪製。這在繪製不透明的物體時非常有用——不管是先繪製近的物體再繪製遠的物體,還是先繪製遠的物體再繪製近的物體,或者乾脆以混亂的順序進行繪製,最後的顯示結果總是近的物體遮住遠的物體。

  然而在你需要實現半透明效果時,發現一切都不是那麼美好了。如果你繪製了一個近距離的半透明物體,則它在深度緩衝區內保留了一些信息,使得遠處的物體將無法再被繪製出來。雖然半透明的物體仍然半透明,但透過它看到的卻不是正確的內容了。

  要解決以上問題,需要在繪製半透明物體時將深度緩衝區設置爲只讀,這樣一來,雖然半透明物體被繪製上去了,深度緩衝區還保持在原來的狀態。如果再有一個物體出現在半透明物體之後,在不透明物體之前,則它也可以被繪製(因爲此時深度緩衝區中記錄的是那個不透明物體的深度)。以後再要繪製不透明物體時,只需要再將深度緩衝區設置爲可讀可寫的形式即可。嗯?你問我怎麼繪製一個一部分半透明一部分不透明的物體?這個好辦,只需要把物體分爲兩個部分,一部分全是半透明的,一部分全是不透明的,分別繪製就可以了。
  即使使用了以上技巧,我們仍然不能隨心所欲的按照混亂順序來進行繪製。必須是先繪製不透明的物體,然後繪製透明的物體。否則,假設背景爲藍色,近處一塊紅色玻璃,中間一個綠色物體。如果先繪製紅色半透明玻璃的話,它先和藍色背景進行混合,則以後繪製中間的綠色物體時,想單獨與紅色玻璃混合已經不能實現了。

  總結起來,繪製順序就是:首先繪製所有不透明的物體。如果兩個物體都是不透明的,則誰先誰後都沒有關係。然後,將深度緩衝區設置爲只讀。接下來,繪製所有半透明的物體。如果兩個物體都是半透明的,則誰先誰後只需要根據自己的意願(注意了,先繪製的將成爲“目標顏色”,後繪製的將成爲“源顏色”,所以繪製的順序將會對結果造成一些影響)。最後,將深度緩衝區設置爲可讀可寫形式。

  調用glDepthMask(GL_FALSE);可將深度緩衝區設置爲只讀形式。調用glDepthMask(GL_TRUE);可將深度緩衝區設置爲可讀可寫形式。

  一些網上的教程,包括大名鼎鼎的NeHe教程,都在使用三維混合時直接將深度緩衝區禁用,即調用glDisable(GL_DEPTH_TEST);。這樣做並不正確。如果先繪製一個不透明的物體,再在其背後繪製半透明物體,本來後面的半透明物體將不會被顯示(被不透明的物體遮住了),但如果禁用深度緩衝,則它仍然將會顯示,並進行混合。NeHe提到某些顯卡在使用glDepthMask函數時可能存在一些問題,但可能是由於我的閱歷有限,並沒有發現這樣的情況。

  那麼,實際的演示一下吧。我們來繪製一些半透明和不透明的球體。假設有三個球體,一個紅色不透明的,一個綠色半透明的,一個藍色半透明的。紅色最遠,綠色在中間,藍色最近。根據前面所講述的內容,紅色不透明球體必須首先繪製,而綠色和藍色則可以隨意修改順序。這裏爲了演示不注意設置深度緩衝的危害,我們故意先繪製最近的藍色球體,再繪製綠色球體。

  爲了讓這些球體有一點立體感,我們使用光照。在(1, 1, -1)處設置一個白色的光源。代碼如下:

void setLight(void)
{
    static const GLfloat light_position[] = {1.0f, 1.0f, -1.0f, 1.0f};
    static const GLfloat light_ambient[]   = {0.2f, 0.2f, 0.2f, 1.0f};
    static const GLfloat light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
    static const GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

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

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

  每一個球體顏色不同。所以它們的材質也都不同。這裏用一個函數來設置材質。

void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)
{
    static const GLfloat mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
    static const GLfloat mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR,   mat_specular);
    glMaterialfv(GL_FRONT, GL_EMISSION,   mat_emission);
    glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}

  有了這兩個函數,我們就可以根據前面的知識寫出整個程序代碼了。這裏只給出了繪製的部分,其它部分大家可以自行完成。

void myDisplay(void)
{
    // 定義一些材質顏色
    const static GLfloat red_color[] = {1.0f, 0.0f, 0.0f, 1.0f};
    const static GLfloat green_color[] = {0.0f, 1.0f, 0.0f, 0.3333f};
    const static GLfloat blue_color[] = {0.0f, 0.0f, 1.0f, 0.5f};

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

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

    // 設置光源
    setLight();

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

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

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

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

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

    glutSwapBuffers();
}

  大家也可以將上面兩處glDepthMask刪去,結果會看到最近的藍色球雖然是半透明的,但它的背後直接就是紅色球了,中間的綠色球沒有被正確繪製。(我經過測試發現不是這樣的,和原來的顯示一樣,是不是因爲版本改變了??)

這裏寫圖片描述

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

/*****************************************************************************/
轉來的東西,原文是在windows上開發,這裏在linux下稍作修改就能跑起來。

這裏不是爲了弄出絢麗的3D效果,而是讓opengl也能在skyworth平臺上跑起來。
/*****************************************************************************/

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