今天介紹關于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.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)當然了,這樣的效果還不夠。如果在各表面的交界處設定很多細小的平面,進行平滑處理,則光照後的效果将更真實。但這已經遠離本課的内容了。

圖一
圖二
小結
本課介紹了顯示清單的知識和簡單的應用。
可以把各種OpenGL函數調用的動作裝到顯示清單中,以後調用顯示清單,就相當于調用了其中的OpenGL函數。顯示清單中除了存放對OpenGL函數的調用外,不會存放其它内容。
使用顯示清單的過程是:配置設定一個未使用的顯示清單編号,把OpenGL函數調用裝入顯示清單,調用顯示清單,銷毀顯示清單。
使用顯示清單有可能帶來程式運作速度的提升,但是這種提升并不一定會很明顯。顯示清單本身也存在一定的開銷。
把繪制固定的物體的OpenGL函數放到一個顯示清單中,是一種不錯的程式設計思路。本課最後的例子中使用了這種思路。