1. 程式人生 > >【待完善】OpenGL入門學習

【待完善】OpenGL入門學習

嚴正宣告:本文轉載自網路,但具體出處未知。如果有讀者瞭解,請聯絡我更正。
為了閱讀方便,我對文字格式進行了修改,並填補了缺少的圖片。
我尊重每位作者的權益,如果本文存在侵權行為,請聯絡我刪除並道歉。


OpenGL入門學習【一】

說起程式設計作圖,大概還有很多人想起TC的#include <graphics.h>吧?但是各位是否想過,那些畫面絢麗的PC遊戲是如何編寫出來的?就靠TC那可憐的640*480解析度、16色來做嗎?顯然是不行的。本帖的目的是讓大家放棄TC的老舊圖形介面,讓大家接觸一些新事物。

OpenGL作為當前主流的圖形API之一,它在一些場合具有比DirectX更優越的特性。
1、與C語言緊密結合。


OpenGL命令最初就是用C語言函式來進行描述的,對於學習過C語言的人來講,OpenGL是容易理解和學習的。如果你曾經接觸過TC的graphics.h,你會發現,使用OpenGL作圖甚至比TC更加簡單。
2、強大的可移植性。
微軟的Direct3D雖然也是十分優秀的圖形API,但它只用於Windows系統(現在還要加上一個XBOX遊戲機)。而OpenGL不僅用於 Windows,還可以用於Unix/Linux等其它系統,它甚至在大型計算機、各種專業計算機(如:醫療用顯示裝置)上都有應用。並且,OpenGL 的基本命令都做到了硬體無關,甚至是平臺無關。
3、高效能的圖形渲染。
OpenGL是一個工業標準,它的技術緊跟時代,現今各個顯示卡廠家無一不對OpenGL提供強力支援,激烈的競爭中使得OpenGL效能一直領先。
總之,OpenGL是一個很NB的圖形軟體介面。至於究竟有多NB,去看看DOOM3和QUAKE4等專業遊戲就知道了。

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

下面將對Windows下的OpenGL程式設計進行簡單介紹。

學習OpenGL前的準備工作

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

第二步,安裝GLUT工具包
GLUT不是OpenGL所必須的,但它會給我們的學習帶來一定的方便,推薦安裝。
Windows環境下的GLUT下載地址:(大小約為150k)

http://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip
無法從以上地址下載的話請使用下面的連線: http://upload.programfan.com/upfile/200607311626279.zip

Windows環境下安裝GLUT的步驟:
1、將下載的壓縮包解開,將得到5個檔案
2、在“我的電腦”中搜索“gl.h”,並找到其所在資料夾(如果是VisualStudio2005,則應該是其安裝目錄下面的“VC\PlatformSDK\include\gl資料夾”)。把解壓得到的glut.h放到這個資料夾。
3、把解壓得到的glut.lib和glut32.lib放到靜態函式庫所在資料夾(如果是VisualStudio2005,則應該是其安裝目錄下面的“VC\lib”資料夾)。
4、把解壓得到的glut.dll和glut32.dll放到作業系統目錄下面的system32資料夾內。(典型的位置為:C:\Windows\System32)

第三步,建立一個OpenGL工程
這裡以VisualStudio2005為例。
選擇File->New->Project,然後選擇Win32 Console Application,選擇一個名字,然後按OK。
在談出的對話方塊左邊點Application Settings,找到Empty project並勾上,選擇Finish。
然後向該工程新增一個程式碼檔案,取名為“OpenGL.c”,注意用.c來作為檔案結尾。
搞定了,就跟平時的工程沒什麼兩樣的。

第一個OpenGL程式

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

#include <GL/glut.h>
void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
     glFlush();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("第一個OpenGL程式");
     glutDisplayFunc(&myDisplay);
     glutMainLoop();
     return 0;
}

該程式的作用是在一個黑色的視窗中央畫一個白色的矩形。

下面對各行語句進行說明。

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

然後看main函式。int main(int argc, char *argv[]),這個是帶命令列引數的main函式,各位應該見過吧?沒見過的同志們請多翻翻書,等弄明白了再往下看。注意main函式中的各語句,除了最後的return之外,其餘全部以glut開頭。這種以glut開頭的函式都是GLUT工具包所提供的函式,下面對用到的幾個函式進行介紹。
1、glutInit,對GLUT進行初始化,這個函式必須在其它的GLUT使用之前呼叫一次。其格式比較死板,一般照抄這句glutInit(&argc, argv)就可以了。
2、glutInitDisplayMode,設定顯示方式,其中GLUT_RGB表示使用RGB顏色,與之對應的還有GLUT_INDEX(表示使用索引顏色)。GLUT_SINGLE表示使用單緩衝,與之對應的還有GLUT_DOUBLE(使用雙緩衝)。更多資訊,請自己Google。當然以後的教程也會有一些講解。
3、glutInitWindowPosition,這個簡單,設定視窗在螢幕中的位置。
4、glutInitWindowSize,這個也簡單,設定視窗的大小。
5、glutCreateWindow,根據前面設定的資訊建立視窗。引數將被作為視窗的標題。注意:視窗被建立後,並不立即顯示到螢幕上。需要呼叫glutMainLoop才能看到視窗。
6、glutDisplayFunc,設定一個函式,當需要進行畫圖時,這個函式就會被呼叫。(這個說法不夠準確,但準確的說法可能初學者不太好理解,暫時這樣說吧)。
7、glutMainLoop,進行一個訊息迴圈。(這個可能初學者也不太明白,現在只需要知道這個函式可以顯示視窗,並且等待視窗關閉後才會返回,這就足夠了。)

在glutDisplayFunc函式中,我們設定了“當需要畫圖時,請呼叫myDisplay函式”。於是myDisplay函式就用來畫圖。觀察myDisplay中的三個函式呼叫,發現它們都以gl開頭。這種以gl開頭的函式都是OpenGL的標準函式,下面對用到的函式進行介紹。
1、glClear,清除。GL_COLOR_BUFFER_BIT表示清除顏色,glClear函式還可以清除其它的東西,但這裡不作介紹。
2、glRectf,畫一個矩形。四個引數分別表示了位於對角線上的兩個點的橫、縱座標。
3、glFlush,保證前面的OpenGL命令立即執行(而不是讓它們在緩衝區中等待)。其作用跟fflush(stdout)類似。


OpenGL入門學習【二】

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

一、點、直線和多邊形
我們知道數學(具體的說,是幾何學)中有點、直線和多邊形的概念,但這些概念在計算機中會有所不同。
數學上的點,只有位置,沒有大小。但在計算機中,無論計算精度如何提高,始終不能表示一個無窮小的點。另一方面,無論圖形輸出裝置(例如,顯示器)如何精確,始終不能輸出一個無窮小的點。一般情況下,OpenGL中的點將被畫成單個的畫素(畫素的概念,請自己搜尋之~),雖然它可能足夠小,但並不會是無窮小。同一畫素上,OpenGL可以繪製許多座標只有稍微不同的點,但該畫素的具體顏色將取決於OpenGL的實現。當然,過度的注意細節就是鑽牛角尖,我們大可不必花費過多的精力去研究“多個點如何畫到同一畫素上”。

同樣的,數學上的直線沒有寬度,但OpenGL的直線則是有寬度的。同時,OpenGL的直線必須是有限長度,而不是像數學概念那樣是無限的。可以認為,OpenGL的“直線”概念與數學上的“線段”接近,它可以由兩個端點來確定。

多邊形是由多條線段首尾相連而形成的閉合區域。OpenGL規定,一個多邊形必須是一個“凸多邊形”(其定義為:多邊形內任意兩點所確定的線段都在多邊形內,由此也可以推匯出,凸多邊形不能是空心的)。多邊形可以由其邊的端點(這裡可稱為頂點)來確定。(注意:如果使用的多邊形不是凸多邊形,則最後輸出的效果是未定義的——OpenGL為了效率,放寬了檢查,這可能導致顯示錯誤。要避免這個錯誤,儘量使用三角形,因為三角形都是凸多邊形)

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

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

等等。
數字表示引數的個數,2表示有兩個引數,3表示三個,4表示四個(我知道有點囉嗦~)。
字母表示引數的型別:
                   s表示16位整數(OpenGL中將這個型別定義為GLshort),
                   i表示32位整數(OpenGL中將這個型別定義為GLint和GLsizei),
                   f表示32位浮點數(OpenGL中將這個型別定義為GLfloat和GLclampf),
                   d表示64位浮點數(OpenGL中將這個型別定義為GLdouble和GLclampd)。
                   v表示傳遞的幾個引數將使用指標的方式,見下面的例子。

這些函式除了引數的型別和個數不同以外,功能是相同的。例如,以下五個程式碼段的功能是等效的:
(一)glVertex2i(1, 3);
(二)glVertex2f(1.0f, 3.0f);
(三)glVertex3f(1.0f, 3.0f, 0.0f);
(四)glVertex4f(1.0f, 3.0f, 0.0f, 1.0f);
(五)GLfloat VertexArr3[] = {1.0f, 3.0f, 0.0f}; glVertex3fv(VertexArr3);
以後我們將用glVertex*來表示這一系列函式。
注意:OpenGL的很多函式都是採用這樣的形式,一個相同的字首再加上引數說明標記,這一點會隨著學習的深入而有更多的體會。

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

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

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

把這段程式碼改成你喜歡的樣子,然後用它替換第一課中的myDisplay函式,編譯後即可執行。

幾個例子
例一、畫一個圓

/*
正四邊形,正五邊形,正六邊形,……,直到正n邊形,當n越大時,這個圖形就越接近圓
當n大到一定程度後,人眼將無法把它跟真正的圓相區別
這時我們已經成功的畫出了一個“圓”
(注:畫圓的方法很多,這裡使用的是比較簡單,但效率較低的一種)
試修改下面的const int n的值,觀察當n=3,4,5,8,10,15,20,30,50等不同數值時輸出的變化情況
將GL_POLYGON改為GL_LINE_LOOP、GL_POINTS等其它方式,觀察輸出的變化情況
*/
#include <math.h>
const int n = 20;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     int i;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_POLYGON);
     for(i=0; i<n; ++i)
         glVertex2f(R*cos(2*Pi/n*i), R*sin(2*Pi/n*i));
     glEnd();
     glFlush();
}

例二、畫一個五角星

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

    D    C
首先,根據餘弦定理列方程,計算五角星的中心到頂點的距離a
(假設五角星對應正五邊形的邊長為.0)
a = 1 / (2-2*cos(72*Pi/180));
然後,根據正弦和餘弦的定義,計算B的x座標bx和y座標by,以及C的y座標
(假設五角星的中心在座標原點)
bx = a * cos(18 * Pi/180);
by = a * sin(18 * Pi/180);
cy = -a * cos(18 * Pi/180);
五個點的座標就可以通過以上四個量和一些常數簡單的表示出來
*/
#include <math.h>
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     GLfloat a = 1 / (2-2*cos(72*Pi/180));
     GLfloat bx = a * cos(18 * Pi/180);
     GLfloat by = a * sin(18 * Pi/180);
     GLfloat cy = -a * cos(18 * Pi/180);
     GLfloat
         PointA[2] = { 0, a },
         PointB[2] = { bx, by },
         PointC[2] = { 0.5, cy },
         PointD[2] = { -0.5, cy },
         PointE[2] = { -bx, by };

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

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

/*
由於OpenGL預設座標值只能從-1到1,(可以修改,但方法留到以後講)
所以我們設定一個因子factor,把所有的座標值等比例縮小,
這樣就可以畫出更多個正弦週期
試修改factor的值,觀察變化情況
*/
#include <math.h>
const GLfloat factor = 0.1f;
void myDisplay(void)
{
     GLfloat x;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_LINES);
         glVertex2f(-1.0f, 0.0f);
         glVertex2f(1.0f, 0.0f);         // 以上兩個點可以畫x軸
         glVertex2f(0.0f, -1.0f);
         glVertex2f(0.0f, 1.0f);         // 以上兩個點可以畫y軸
     glEnd();
     glBegin(GL_LINE_STRIP);
     for(x=-1.0f/factor; x<1.0f/factor; x+=0.01f)
     {
         glVertex2f(x*factor, sin(x)*factor);
     }
     glEnd();
     glFlush();
}

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


OpenGL入門學習【三】

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

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

下面就點、直線、多邊形分別討論。
1、關於點
點的大小預設為1個畫素,但也可以改變之。改變的命令為glPointSize,其函式原型如下:
void glPointSize(GLfloat size);
size必須大於0.0f,預設值為1.0f,單位為“畫素”。
注意:對於具體的OpenGL實現,點的大小都有個限度的,如果設定的size超過最大值,則設定可能會有問題。

例子:

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glPointSize(5.0f);
     glBegin(GL_POINTS);
         glVertex2f(0.0f, 0.0f);
         glVertex2f(0.5f, 0.5f);
     glEnd();
     glFlush();
}

2、關於直線
(1)直線可以指定寬度:
void glLineWidth(GLfloat width);
其用法跟glPointSize類似。

(2)畫虛線。
首先,使用glEnable(GL_LINE_STIPPLE);來啟動虛線模式(使用glDisable(GL_LINE_STIPPLE);可以關閉之)。
然後,使用glLineStipple來設定虛線的樣式。
void glLineStipple(GLint factor, GLushort pattern);
pattern是由1和0組成的長度為16的序列,從最低位開始看,如果為1,則直線上接下來應該畫的factor個點將被畫為實的;如果為0,則直線上接下來應該畫的factor個點將被畫為虛的。

例如:
glLineStipple(1, 0x3F07);
glEnable(GL_LINE_STIPPLE);
此時模式為Ox3F07(二進位制形式為0011111100000111),它所畫出來的直線是這樣的:先連續繪製3個畫素,然後連續5個畫素留空,再連續繪製6個畫素,最後兩個畫素留空(注意,首先是從低位開始的)。如果factor是2,那麼這個模式便被擴充套件為:先連續繪製6個畫素,然後連續10個畫素留空,再連續繪製12個畫素,最後4個畫素留空。[百度百科]

以下是一些例子:

示例程式碼:

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glEnable(GL_LINE_STIPPLE);
     glLineStipple(2, 0x0F0F);
     glLineWidth(10.0f);
     glBegin(GL_LINES);
         glVertex2f(0.0f, 0.0f);
         glVertex2f(0.5f, 0.5f);
     glEnd();
     glFlush();
}

3、關於多邊形

多邊形的內容較多,我們將講述以下四個方面。
(1)多邊形的兩面以及繪製方式。
雖然我們目前還沒有真正的使用三維座標來畫圖,但是建立一些三維的概念還是必要的。
從三維的角度來看,一個多邊形具有兩個面。每一個面都可以設定不同的繪製方式:填充、只繪製邊緣輪廓線、只繪製頂點,其中“填充”是預設的方式。可以為兩個面分別設定不同的方式。
glPolygonMode(GL_FRONT, GL_FILL);// 設定正面為填充方式
glPolygonMode(GL_BACK, GL_LINE);// 設定反面為邊緣繪製方式
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT); // 設定兩面均為頂點繪製方式

(2)反轉
一般約定為“頂點以逆時針順序出現在螢幕上的面”為“正面”,另一個面即成為“反面”。生活中常見的物體表面,通常都可以用這樣的“正面”和“反面”,“合理的”被表現出來(請找一個比較透明的礦泉水瓶子,在正對你的一面沿逆時針畫一個圓,並標明畫的方向,然後將背面轉為正面,畫一個類似的圓,體會一下“正面”和“反面”。你會發現正對你的方向,瓶的外側是正面,而背對你的方向,瓶的內側才是正面。正對你的內側和背對你的外側則是反面。這樣一來,同樣屬於“瓶的外側”這個表面,但某些地方算是正面,某些地方卻算是反面了)。
但也有一些表面比較特殊。例如“麥比烏斯帶”(請自己Google一下),可以全部使用“正面”或全部使用“背面”來表示。
可以通過glFrontFace函式來交換“正面”和“反面”的概念。
glFrontFace(GL_CCW);   // 設定CCW方向為“正面”,CCW即CounterClockWise,逆時針
glFrontFace(GL_CW);    // 設定CW方向為“正面”,CW即ClockWise,順時針
下面是一個示例程式,請用它替換第一課中的myDisplay函式,並將glFrontFace(GL_CCW)修改為glFrontFace(GL_CW),並觀察結果的變化。

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glPolygonMode(GL_FRONT, GL_FILL); // 設定正面為填充模式
     glPolygonMode(GL_BACK, GL_LINE);   // 設定反面為線形模式
     glFrontFace(GL_CCW);               // 設定逆時針方向為正面
     glBegin(GL_POLYGON);               // 按逆時針繪製一個正方形,在左下方
         glVertex2f(-0.5f, -0.5f);
         glVertex2f(0.0f, -0.5f);
         glVertex2f(0.0f, 0.0f);
         glVertex2f(-0.5f, 0.0f);
     glEnd();
     glBegin(GL_POLYGON);               // 按順時針繪製一個正方形,在右上方
         glVertex2f(0.0f, 0.0f);
         glVertex2f(0.0f, 0.5f);
         glVertex2f(0.5f, 0.5f);
         glVertex2f(0.5f, 0.0f);
     glEnd();
     glFlush();
}

(3)剔除多邊形表面
在三維空間中,一個多邊形雖然有兩個面,但我們無法看見背面的那些多邊形,而一些多邊形雖然是正面的,但被其他多邊形所遮擋。如果將無法看見的多邊形和可見的多邊形同等對待,無疑會降低我們處理圖形的效率。在這種時候,可以將不必要的面剔除。
首先,使用glEnable(GL_CULL_FACE);來啟動剔除功能(使用glDisable(GL_CULL_FACE);可以關閉之)
然後,使用glCullFace來進行剔除。
glCullFace的引數可以是GL_FRONT,GL_BACK或者GL_FRONT_AND_BACK,分別表示剔除正面、剔除反面、剔除正反兩面的多邊形。
注意:剔除功能隻影響多邊形,而對點和直線無影響。例如,使用glCullFace(GL_FRONT_AND_BACK)後,所有的多邊形都將被剔除,所以看見的就只有點和直線。

(4)鏤空多邊形
直線可以被畫成虛線,而多邊形則可以進行鏤空。
首先,使用glEnable(GL_POLYGON_STIPPLE);來啟動鏤空模式(使用glDisable(GL_POLYGON_STIPPLE);可以關閉之)。
然後,使用glPolygonStipple來設定鏤空的樣式。
void glPolygonStipple(const GLubyte *mask);
其中的引數mask指向一個長度為128位元組的空間,它表示了一個32*32的矩形應該如何鏤空。其中:第一個位元組表示了最左下方的從左到右(也可以是從右到左,這個可以修改)8個畫素是否鏤空(1表示不鏤空,顯示該畫素;0表示鏤空,顯示其後面的顏色),最後一個位元組表示了最右上方的8個畫素是否鏤空。
但是,如果我們直接定義這個mask陣列,像這樣:

static GLubyte Mask[128] =
{
     0x00, 0x00, 0x00, 0x00,    //   這是最下面的一行
     0x00, 0x00, 0x00, 0x00,
     0x03, 0x80, 0x01, 0xC0,    //   麻
     0x06, 0xC0, 0x03, 0x60,    //   煩
     0x04, 0x60, 0x06, 0x20,    //   的
     0x04, 0x30, 0x0C, 0x20,    //   初
     0x04, 0x18, 0x18, 0x20,    //   始
     0x04, 0x0C, 0x30, 0x20,    //   化
     0x04, 0x06, 0x60, 0x20,    //   ,
     0x44, 0x03, 0xC0, 0x22,    //   不
     0x44, 0x01, 0x80, 0x22,    //   建
     0x44, 0x01, 0x80, 0x22,    //   議
     0x44, 0x01, 0x80, 0x22,    //   使
     0x44, 0x01, 0x80, 0x22,    //   用
     0x44, 0x01, 0x80, 0x22,
     0x44, 0x01, 0x80, 0x22,
     0x66, 0x01, 0x80, 0x66,
     0x33, 0x01, 0x80, 0xCC,
     0x19, 0x81, 0x81, 0x98,
     0x0C, 0xC1, 0x83, 0x30,
     0x07, 0xE1, 0x87, 0xE0,
     0x03, 0x3F, 0xFC, 0xC0,
     0x03, 0x31, 0x8C, 0xC0,
     0x03, 0x3F, 0xFC, 0xC0,
     0x06, 0x64, 0x26, 0x60,
     0x0C, 0xCC, 0x33, 0x30,
     0x18, 0xCC, 0x33, 0x18,
     0x10, 0xC4, 0x23, 0x08,
     0x10, 0x63, 0xC6, 0x08,
     0x10, 0x30, 0x0C, 0x08,
     0x10, 0x18, 0x18, 0x08,
     0x10, 0x00, 0x00, 0x08    // 這是最上面的一行
};

這樣一堆資料非常缺乏直觀性,我們需要很費勁的去分析,才會發現它表示的竟然是一隻蒼蠅。
如果將這樣的資料儲存成圖片,並用專門的工具進行編輯,顯然會方便很多。下面介紹如何做到這一點。
首先,用Windows自帶的畫筆程式新建一副圖片,取名為mask.bmp,注意儲存時,應該選擇“單色點陣圖”。在“圖象”->“屬性”對話方塊中,設定圖片的高度和寬度均為32。
用放大鏡觀察圖片,並編輯之。黑色對應二進位制零(鏤空),白色對應二進位制一(不鏤空),編輯完畢後儲存。
然後,就可以使用以下程式碼來獲得這個Mask陣列了。

static GLubyte Mask[128];
FILE *fp;
fp = fopen("mask.bmp", "rb");
if( !fp )
     exit(0);
// 移動檔案指標到這個位置,使得再讀sizeof(Mask)個位元組就會遇到檔案結束
// 注意-(int)sizeof(Mask)雖然不是什麼好的寫法,但這裡它確實是正確有效的
// 如果直接寫-sizeof(Mask)的話,因為sizeof取得的是一個無符號數,取負號會有問題
if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )
     exit(0);
// 讀取sizeof(Mask)個位元組到Mask
if( !fread(Mask, sizeof(Mask), 1, fp) )
     exit(0);
fclose(fp);

好的,現在請自己編輯一個圖片作為mask,並用上述方法取得Mask陣列,執行後觀察效果。
說明:繪製虛線時可以設定factor因子,但多邊形的鏤空無法設定factor因子。請用滑鼠改變視窗的大小,觀察鏤空效果的變化情況。

#include <stdio.h>
#include <stdlib.h>
void myDisplay(void)
{
     static GLubyte Mask[128];
     FILE *fp;
     fp = fopen("mask.bmp", "rb");
     if( !fp )
         exit(0);
     if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )
         exit(0);
     if( !fread(Mask, sizeof(Mask), 1, fp) )
         exit(0);
     fclose(fp);
     glClear(GL_COLOR_BUFFER_BIT);
     glEnable(GL_POLYGON_STIPPLE);
     glPolygonStipple(Mask);
     glRectf(-0.5f, -0.5f, 0.0f, 0.0f);   // 在左下方繪製一個有鏤空效果的正方形
     glDisable(GL_POLYGON_STIPPLE);
     glRectf(0.0f, 0.0f, 0.5f, 0.5f);     // 在右上方繪製一個無鏤空效果的正方形
     glFlush();
}

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


OpenGL入門學習【四】

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

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

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

大家可以通過改變下面程式碼中glColor3f的引數值,繪製不同顏色的矩形。

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glColor3f(0.0f, 1.0f, 1.0f);
     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
     glFlush();
}

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

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

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

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

#include <windows.h>
#include <GL/gl.h>
#include <GL/glaux.h>

#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glaux.lib")

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

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

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

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

3、指定清除螢幕用的顏色
我們寫:glClear(GL_COLOR_BUFFER_BIT);意思是把螢幕上的顏色清空。
但實際上什麼才叫“空”呢?在宇宙中,黑色代表了“空”;在一張白紙上,白色代表了“空”;在信封上,信封的顏色才是“空”。
OpenGL用下面的函式來定義清楚屏幕後螢幕所擁有的顏色。
在RGB模式下,使用glClearColor來指定“空”的顏色,它需要四個引數,其引數的意義跟glColor4f相似。
在索引顏色模式下,使用glClearIndex來指定“空”的顏色所在的索引,它需要一個引數,其意義跟glIndexi相似。

void myDisplay(void)
{
     glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
     glClear(GL_COLOR_BUFFER_BIT);
     glFlush();
}

呵,這個還真簡單~

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

#include <math.h>
const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
     int i;
     // glShadeModel(GL_FLAT);
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLE_FAN);
     glColor3f(1.0f, 1.0f, 1.0f);
     glVertex2f(0.0f, 0.0f);
     for(i=0; i<=8; ++i)
     {
         glColor3f(i&0x04, i&0x02, i&0x01);
         glVertex2f(cos(i*Pi/4), sin(i*Pi/4));
     }
     glEnd();
     glFlush();
}

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

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


OpenGL入門學習【五】

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

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

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

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

1、模型變換和檢視變換
從“相對移動”的觀點來看,改變觀察點的位置與方向和改變物體本身的位置與方向具有等效性。在OpenGL中,實現這兩種功能甚至使用的是同樣的函式。
由於模型和檢視的變換都通過矩陣運算來實現,在進行變換前,應先設定當前操作的矩陣為“模型檢視矩陣”。設定的方法是以GL_MODELVIEW為引數呼叫glMatrixMode函式,像這樣:
glMatrixMode(GL_MODELVIEW);
通常,我們需要在進行變換前把當前矩陣設定為單位矩陣。這也只需要一行程式碼:
glLoadIdentity();

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

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

以上都是針對改變物體的位置和方向來介紹的。如果要改變觀察點的位置,除了配合使用glRotate*和glTranslate*函式以外,還可以使用這個函式:gluLookAt。它的引數比較多,前三個引數表示了觀察點的位置,中間三個引數表示了觀察目標的位置,最後三個引數代表從(0,0,0)到 (x,y,z)的直線,它表示了觀察者認為的“上”方向。

2、投影變換
投影變換就是定義一個可視空間,可視空間以外的物體不會被繪製到螢幕上。(注意,從現在起,座標可以不再是-1.0到1.0了!)
OpenGL支援兩種型別的投影變換,即透視投影和正投影。投影也是使用矩陣來實現的。如果需要操作投影矩陣,需要以GL_PROJECTION為引數呼叫glMatrixMode函式。
glMatrixMode(GL_PROJECTION);
通常,我們需要在進行變換前把當前矩陣設定為單位矩陣。
glLoadIdentity();

透視投影所產生的結果類似於照片,有近大遠小的效果,比如在火車頭內向前照一個鐵軌的照片,兩條鐵軌似乎在遠處相交了。
使用glFrustum函式可以將當前的可視空間設定為透視投影空間。其引數的意義如下圖:
http://blog.programfan.com/upfile/200610/20061007151547.gif
宣告:該圖片來自www.opengl.org,該圖片是《OpenGL程式設計指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網路,我希望沒有觸及到版權問題。
也可以使用更常用的gluPerspective函式。其引數的意義如下圖:
http://blog.programfan.com/upfile/200610/2006100715161.gif
宣告:該圖片來自www.opengl.org,該圖片是《OpenGL程式設計指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網路,我希望沒有觸及到版權問題。

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

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

3、視口變換
當一切工作已經就緒,只需要把畫素繪製到螢幕上了。這時候還剩最後一個問題:應該把畫素繪製到視窗的哪個區域呢?通常情況下,預設是完整的填充整個視窗,但我們完全可以只填充一半。(即:把整個圖象填充到一半的視窗內)
http://blog.programfan.com/upfile/200610/20061007151639.gif
宣告:該圖片來自www.opengl.org,該圖片是《OpenGL程式設計指南》一書的附圖,由於該書的舊版(第一版,1994年)已經流傳於網路,我希望沒有觸及到版權問題。

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

4、操作矩陣堆疊
介於是入門教程,先簡單介紹一下堆疊。你可以把堆疊想象成一疊盤子。開始的時候一個盤子也沒有,你可以一個一個往上放,也可以一個一個取下來。每次取下的,都是最後一次被放上去的盤子。通常,在計算機實現堆疊時,堆疊的容量是有限的,如果盤子過多,就會出錯。當然,如果沒有盤子了,再要求取一個盤子,也會出錯。
我們在進行矩陣操作時,有可能需要先儲存某個矩陣,過一段時間再恢復它。當我們需要儲存時,呼叫glPushMatrix函式,它相當於把矩陣(相當於盤子)放到堆疊上。當需要恢復最近一次的儲存時,呼叫glPopMatrix函式,它相當於把矩陣從堆疊上取下。OpenGL規定堆疊的容量至少可以容納32個矩陣,某些OpenGL實現中,堆疊的容量實際上超過了32個。因此不必過於擔心矩陣的容量問題。
通常,用這種先儲存後恢復的措施,比先變換再逆變換要更方便,更快速。
注意:模型檢視矩陣和投影矩陣都有相應的堆疊。使用glMatrixMode來指定當前操作的究竟是模型檢視矩陣還是投影矩陣。

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

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

現在我們來看看如何繪製這三個天體。
為了簡單起見,我們把三個天體都想象成規則的球體。而我們所使用的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*。
我們可以定義可視空間,這個空間可以是“正投影”的(使用glOrtho或gluOrtho2D),也可以是“透視投影”的(使用glFrustum或gluPerspective)。
我們可以定義繪製到視窗的範圍,使用的函式是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();
}


1、雙緩衝技術
在計算機上的動畫與實際的動畫有些不同:實際的動畫都是先畫好了,播放的時候直接拿出來顯示就行。計算機動畫則是畫一張,就拿出來一張,再畫下一張,再拿出來。如果所需要繪製的