OpenGL入門學習(八)

OpenGL入門學習[八]


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

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

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


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

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

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

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

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

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

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

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


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

#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

#include <math.h>
#define ColoredVertex(c, v) do{ glColor3fv(c); glVertex3fv(v); }while(0)

GLfloat angle = 0.0f;

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

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

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

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

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

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

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

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

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


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

轉自http://www.cppblog.com/doing5552/archive/2009/01/08/71532.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章