計算機圖形學中,所有的光滑曲線、曲面都采用線段或三角形逼近來模擬,但為了精確地表現曲線,通常需要成千上萬個線段或三角形來逼近,這種方法對於計算機的硬件資源有相當高的要求。然而,許多有用的曲線、曲面在數學上只需要用少數幾個參數(如控制點等)來描述。這種方法所需要的存儲空間比線段、三角形逼近的方法來所需要的空間要小得多,並且控制點方法描述的曲線、曲面比線段、三角形逼近的曲線、曲面更精確。
為了說明如何在OpenGL中繪制復雜曲線和曲面,我們對上述兩類比方法都進行了介紹。下面我們先來介紹有關基礎知識,然後再看是如何實現的吧。
一、曲線的繪制
OpenGL通過一種求值器的機制來產生曲線和曲面,該機制非常靈活,可以生成任意角度的多項式曲線,並可以將其他類型的多邊形曲線和曲面轉換成貝塞爾曲線和曲面。這些求值器能在任何度的曲線及曲面上計算指定數目的點。隨後,OpenGL利用曲線和曲面上的點生成標准OpenGL圖元,例如與曲線或曲面近似的線段和多邊形。由於可讓OpenGL計算在曲線上所需的任意數量的點,因此可以達到應用所需的精度。
對於曲線,OpenGL中使用glMap1*()函數來創建一維求值器,該函數原型為:
void glMap1{fd}(GLenum target,TYPE u1,TYPE u2,
GLint stride, GLint order,const TYPE *points);
函數的第一個參數target指出控制頂點的意義以及在參數points中需要提供多少值,具體值見表一所示。參數points指針可以指向控制點集、RGBA顏色值或紋理坐標串等。例如若target是GL_MAP1_COLOR_4,則就能在RGBA四維空間中生成一條帶有顏色信息的曲線,這在數據場可視化中應用極廣。參數u1和u2,指明變量U的范圍,U一般從0變化到1。參數stride是跨度,表示在每塊存儲區內浮點數或雙精度數的個數,即兩個控制點間的偏移量,比如上例中的控制點集ctrpoint[4][3]的跨度就為3,即單個控制點的坐標元素個數。函數參數order是次數加 1,叫階數,與控制點數一致。
參數
意義
GL_MAP1_VERTEX_3
x,y,z頂點坐標
GL_MAP1_VERTEX_4
x,y,z,w 頂點坐標
GL_MAP1_INDEX
顏色表
GL_MAP1_COLOR_4
R,G,B,A
GL_MAP1_NORMAL
法向量
GL_MAP1_TEXTURE_COORD_1
s 紋理坐標
GL_MAP1_TEXTURE_COORD_2
s,t 紋理坐標
GL_MAP1_TEXTURE_COORD_3
s,t,r 紋理坐標
GL_MAP1_TEXTURE_COORD_4
s,t,r,q 紋理坐標
表一、參數target的取值表
使用求值器定義曲線後,必須要啟動求值器,才能進行下一步的繪制工作。啟動函數仍是glEnable(),其中參數與glMap1*()的第一個參數一致。同樣,關閉函數為glDisable(),參數也一樣。
一旦啟動一個或多個求值器,我們就可以構造近似曲線了。最簡單的方法是通過調用計算坐標函數glEvalcoord1*()替換所有對函數glVertex*()的調用。與glVertex*()使用二維、三維和四維坐標不同,glEvalcoord1*()將u值傳給所有已啟動的求值器,然後由這些已啟動的求值器生成坐標、法向量、顏色或紋理坐標。OpenGL曲線坐標計算的函數形式如下:
void glEvalCoord1{fd}[v](TYPE u);
該函數產生曲線坐標值並繪制。參數u是定義域內的值,這個函數調用一次只產生一個坐標。在使用glEvalCoord1*()計算坐標,因為u可取定義域內的任意值,所以由此計算出的坐標值也是任意的。
使用glEvalCoord1*()函數的優點是,可以對U使用任意值,然而,假如想對u使用N個不同的值,就必須對glEvalCoord1*()函數執行N次調用,為此,OpenGL提供了等間隔值取值法,即先調用glMapGrid1*()定義一個間隔相等的一維網格,然後再用glEvalMesh1()通過一次函數執行,將求值器應用在網格上,計算相應的坐標值。下面具體解釋這兩個函數:
1、void glMapGrid1{fd}(GLint n,TYPE u1,TYPE u2);
定義一個網格,從u1到u2分為n步,它們是等間隔的。實際上,這個函數定義的是參數空間網格。
2、void glEvalMesh1(GLenum mode,GLint p1,GLint p2);
計算並繪制坐標點。參數mode可以是GL_POINT或GL_LINE,即沿曲線繪制點或沿曲線繪制相連的線段。這個函數的調用效果同在p1和p2之間的每一步給出一個glEvalCoord1()的效果一樣。從編程角度來說,除了當i=0或i=n,它准確以u1或u2作為參數調用glEvalCoord1()之外,它等價於一下代碼:
glBegin(GL_POINT); /* glBegin(GL_LINE_STRIP); */
for(i=p1;i
<=p2;i++)
glEvalCoord1(u1+i*(u2-u1)/n);
glEnd();
為了進一步說明OpenGL中曲線的繪制方法。下面我們來看一個簡單的例子,這是用四個控制頂點來畫一條三次Bezier曲線。程序如下(注:這是本講座中提供的第一個完整的OpenGL實例代碼,假如讀者朋友對整個程序結構有些迷惑的話,也不要緊,慢慢地往下看,先有一個感官上的印象,主要是把握如何實現曲線繪制這一部分。關於OpenGL的程序整體結構實現,筆者將在第五講中專門闡述):
#include "glos.h"
#include
#include
#include
void myinit(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
GLfloat ctrlpoints[4][3] = {
{ -4.0, -4.0, 0.0 }, { -2.0, 4.0, 0.0 },
{ 2.0, -4.0, 0.0 }, { 4.0, 4.0, 0.0 }
};
void myinit(void)
{
glClearColor(0.0, 0.0, 0.0, 1.0);
glMap1f(GL_MAP1_VERTEX_3, 0.0, 1.0, 3, 4,
&ctrlpoints[0][0]);
glEnable(GL_MAP1_VERTEX_3);
glShadeModel(GL_FLAT);
}
void CALLBACK display(void)
{
int i;
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_LINE_STRIP);
for (i = 0; i
<= 30; i++)
glEvalCoord1f((GLfloat) i/30.0);
glEnd();
/* 顯示控制點 */
glPointSize(5.0);
glColor3f(1.0, 1.0, 0.0);
glBegin(GL_POINTS);
for (i = 0; i
< 4; i++)
glVertex3fv(&ctrlpoints[i][0]);
glEnd();
glFlush();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w
<= h)
glOrtho(-5.0, 5.0, -5.0*(GLfloat)h/(GLfloat)w,
5.0*(GLfloat)h/(GLfloat)w, -5.0, 5.0);
else
glOrtho(-5.0*(GLfloat)w/(GLfloat)h, 5.0*(GLfloat)w/(GLfloat)h, -5.0, 5.0, -5.0, 5.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
二、曲面構造
曲面的繪制方法基本上與曲線的繪制方法是相同的,所不同的是曲面使用二維求值器,並且控制點連接起來形成一個網格。
對於曲面,求值器除了使用二個參數U、V之外,其余與一維求值器基本相同。頂點坐標 、顏色、法線矢量和紋理坐標都對應於曲面而不是曲線。在OpenGL中定義二維求值器的函數是:
void glMap2{fd}(GLenum target,TYPE u1,TYPE u2,GLint ustride,GLint uorder,TYPE v1,TYPE v2,
GLint vstride,GLint vorder,TYPE points);
參數target可以是表一中任意值,不過需將MAP1改為MAP2。同樣,啟動曲面的函數仍是glEnable(),關閉是glDisable()。u1、u2為u的最大值和最小值;v1、v2為v的最大值和最小值。參數ustride和vstride指出在控制點數組中u和v向相鄰點的跨度,即可從一個非常大的數組中選擇一塊控制點長方形。例如,若數據定義成如下形式:
GLfloat ctlpoints[100][100][3];
並且,要用從ctlpoints[20][30]開始的4x4子集,選擇ustride為100*3,vstride為3,初始點設置為ctlpoints[20][30][0]。最後的參數都是階數,uorder和vorder,二者可以不同。
曲面坐標計算函數為:
void glEvalCoord2{fd}[v](TYPE u,TYPE v);
該函數產生曲面坐標並繪制。參數u和v是定義域內的值。下面看一個繪制Bezier曲面的例子:
/* 控制點的坐標 */
GLfloat ctrlpoints[4][4][3] = {
{{-1.5, -1.5, 2.0}, {-0.5, -1.5, 2.0},
{0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}},
{{-1.5, -0.5, 1.0}, {-0.5, 1.5, 2.0},
{0.5, 0.5, 1.0}, {1.5, -0.5, -1.0}},
{{-1.5, 0.5, 2.0}, {-0.5, 0.5, 1.0},
{0.5, 0.5, 3.0}, {1.5, -1.5, 1.5}},
{{-1.5, 1.5, -2.0}, {-0.5, 1.5, -2.0},
{0.5, 0.5, 1.0}, {1.5, 1.5, -1.0}}
};
void myinit(void)
{
glClearColor (0.0, 0.0, 0.0, 1.0);
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4,
&ctrlpoints[0][0][0]);
glEnable(GL_MAP2_VERTEX_3);
glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0);
glEnable(GL_DEPTH_TEST);
}
void CALLBACK display(void)
{
int i, j;
glClear(GL_COLOR_BUFFER_BIT GL_DEPTH_BUFFER_BIT);
glColor3f(0.3, 0.6, 0.9);
glPushMatrix ();
glRotatef(35.0, 1.0, 1.0, 1.0);
for (j = 0; j
<= 8; j++)
{
glBegin(GL_LINE_STRIP);
for (i = 0; i
<= 30; i++)
glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0);
glEnd();
glBegin(GL_LINE_STRIP);
for (i = 0; i
<= 30; i++)
glEvalCoord2f((GLfloat)j/8.0,
(GLfloat)i/30.0);
glEnd();
}
glPopMatrix ();
glFlush();
}
OpenGL中定義均勻間隔的曲面坐標值的函數與曲線的類似,其函數形式為:
void glMapGrid2{fd}(GLenum nu,TYPE u1,TYPE u2,GLenum nv,TYPE v1,TYPE v2);
void glEvalMesh2(GLenum mode,GLint p1,GLint p2,GLint q1,GLint q2);
第一個函數定義參數空間的均勻網格,從u1到u2分為等間隔的nu步,從v1到v2分為等間隔的nv步,然後glEvalMesh2()把這個網格應用到已經啟動的曲面計算上。第二個函數參數mode除了可以是GL_POINT和GL_LINE外,還可以是GL_FILL,即生成填充空間曲面。
下面舉出一個用網格繪制一個經過光照和明暗處理的Bezier曲面的例程:
#include "glos.h"
#include
#include
#include
void myinit(void);
void initlights(void);
void CALLBACK myReshape(GLsizei w, GLsizei h);
void CALLBACK display(void);
/* 控制點坐標 */
GLfloat ctrlpoints[4][4][3] = {
{{-1.5, -1.5, 2.0}, {-0.5, -1.5, 2.0},
{0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}},
{{-1.5, -0.5, 1.0}, {-0.5, 1.5, 2.0},
{0.5, 0.5, 1.0}, {1.5, -0.5, -1.0}},
{{-1.5, 0.5, 2.0}, {-0.5, 0.5, 1.0},
{0.5, 0.5, 3.0}, {1.5, -1.5, 1.5}},
{{-1.5, 1.5, -2.0}, {-0.5, 1.5, -2.0},
{0.5, 0.5, 1.0}, {1.5, 1.5, -1.0}}
};
void initlights(void)
{
GLfloat ambient[] = { 0.4, 0.6, 0.2, 1.0 };
GLfloat position[] = { 0.0, 1.0, 3.0, 1.0 };
GLfloat mat_diffuse[] = { 0.2, 0.4, 0.8, 1.0 };
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_shininess[] = { 80.0 };
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
glLightfv(GL_LIGHT0, GL_POSITION, position);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
}
void CALLBACK display(void)
{
glClear(GL_COLOR_BUFFER_BIT GL_DEPTH_BUFFER_BIT);
glPushMatrix();
glRotatef(35.0, 1.0, 1.0, 1.0);
glEvalMesh2(GL_FILL, 0, 20, 0, 20);
glPopMatrix();
glFlush();
}
void myinit(void)
{
glClearColor (0.0, 0.0, 0.0, 1.0);
glEnable (GL_DEPTH_TEST);
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12,
4, &ctrlpoints[0][0][0]);
glEnable(GL_MAP2_VERTEX_3);
glEnable(GL_AUTO_NORMAL);
glEnable(GL_NORMALIZE);
glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0);
initlights();
}
void CALLBACK myReshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w
<= h)
glOrtho(-4.0, 4.0, -4.0*(GLfloat)h/(GLfloat)w,
4.0*(GLfloat)h/(GLfloat)w, -4.0, 4.0);
else
glOrtho(-4.0*(GLfloat)w/(GLfloat)h,
4.0*(GLfloat)w/(GLfloat)h, -4.0, 4.0, -4.0, 4.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main(void)
{
auxInitDisplayMode (AUX_SINGLE AUX_RGBA);
auxInitPosition (0, 0, 500, 500);
auxInitWindow ("Lighted and Filled Bezier Surface");
myinit();
auxReshapeFunc (myReshape);
auxMainLoop(display);
}
三、圖元逼近法繪制三維物體
在OpenGL的輔助庫中,提供了繪制11種基本幾何圖形的函數,具體參考第一講的有關內容,在此不再贅述。這裡我們討論用另外一種方法來繪制三維物體 。
需要注重的是,這裡我們用來近似曲面的多邊形最好選擇三角形,而不是四邊形或其他外形的多邊形,這是因為三角形的三個頂點在任何時候都位於同一平面內,它一定是非常簡單的非凹多邊形,而四邊形或其他多邊形的頂點可能不在同一平面內,也就有可能不是簡單多邊形,對於這樣的多邊形,OpenGL是不能正常處理的。假設我們繪制一個球體,球體表面用很多個小三角形拼接而成,顯然,用來近似球面的三角形越小、三角形越多,那麼球面就越光滑。為了簡要地說明如何用三角形逼近球體,這裡我們使用三角形來構造一個20面體,二十面體的頂點坐標定義在vdata[][]數組中,tindinces[][]數組定義了構成二十面體的二十個三角形頂點的繪制順序。下面是主要實現代碼:
#define x 5.25731
#define z 8.50651
static GLfloat vdata[12][3]={
{x,0.0,z},{x,0.0,z},{-x,0.0,-z},{x,0.0,-z},
{0.0,z,x},{0.0,z,-x},{0.0,-z-x},{0.0,-z,-x},
{z,x,0.0},{-z,x,0.0},{z,-x,0.0},{-z,-x,0.0}
};
static GLint tindices[20][3]={
{0,4,1},{0,9,4},{9,5,4},{4,5,8},{4,8,1},
{8,10,1},{8,3,10},{5,3,8},{5,2,3},{2,7,3},
{7,10,3},{7,6,10},{7,11,6},{11,0,6},
{6,1,10},{9,0,11},{9,11,2},{9,2,5},{7,2,11}
};
glColor3f(1.0, 0.0, 0.0);
for(int i=0;i
<20;i++){
glBegin(GL_TRIANGLES);
glVertex3fv(&vdata[tindices[i][0]][0]);
glVertex3fv(&vdata[tindices[i][1]][0]);
glVertex3fv(&vdata[tindices[i][2]][0]);
glEnd();
}
顯然,用正二十面體來表示一個球體顯得過於粗糟,可以通過增加面數的方法使正多面體和求更為接近,一種簡單的方法是剖分法,即將前面定義的三角形面分成幾個面,例如,一分為四,形成4個多邊形等,具體實現方法這裡就不再贅述了。