摘要: 本文主要討論了在VC++中使用OpenGL繪制Bezier、NURBS等典型曲面的一般性方法。
關鍵詞: OpenGL;Bezier;NURBS;曲面繪制
OpenGL中對復雜物體的建摸
基本幾何圖元是OpenGL進行建模的最基本的方法,但其對較復雜真實物體的建模則比較困難。對于這些復雜物體的建模,需要用到OpenGL基本庫和功能庫函數(gl庫和glu庫)以對圖元進行擴展并完成法向計算、曲線生成和曲面構造等內容。這種對基本圖元的擴展實際也就是對點、線及多邊形的擴展。OpenGL中定義的點可具有不同大小的尺寸,其擴展的函數形式為:
void glPointSize(GLfloat size);
其參數size以象素為單位設置了點的寬度,其值必須為正,缺省值1.0。對于線的擴展,可通過下面的函數來分別指定其寬度和繪制類型:
void glLineWidth(GLfloat width); void glLineStipple(GLint factor,GLushort pattern);
glLineWidth()的參數width以象素為單位指定線寬,其值必須為正,缺省值為1.0。glLineStipple()的參數factor為對模式進行拉伸的比例因子,參數pattern指定了線的模式(例如11001100將繪制一條虛線,為1時繪制,為0時不繪制)。該函數只有在啟用了函數glEnable(GL_LINE_STIPPLE)后才可以使用,當不再使用時調用glDisable(GL_LINE_STIPPLE)將其關閉。擴展多邊形的繪制模式包括全填充式、輪廓點式、輪廓線式及圖案填充式等幾種。使用時,首先調用glPolygonMode()設置多邊形的模式設置:
void glPolygonMode(GLenum face,GLenum mode); 參數face為GL_FRONT、GL_BACK或GL_FRONT_AND BACK;mode取值可以是GL_POINT、GL_LINE或GL_FILL,分別表示多邊型的輪廓點、輪廓線和填充模式的繪制方式。缺省設置為填充模式。設置完成后可進行圖案填充的設置:
void glPolygonStipple(const GLubyte *mask);
其參數mask必須為一指向32×32大小的位圖的指針,值為1時繪制、為0不繪制。該函數的使用同樣也需要進行如下啟動、關閉設置:
glEnable(GL_POLYGON-STIPPLE); glDisable(GL_POLYGON_STIPPLE);
復雜模型的建模不同與簡單模型的建模,在簡單模型中一個平面上各點的法向(mormal vector)是一樣的,均等于此平面的法向。對于復雜模型中由眾多小的平面多邊形逼近而成的曲面,其每個頂點的法向量都不一樣,因此曲面上每個點的法向計算結果根據采取的不同算法而有不同的結果。OpenGL只提供賦予當前頂點法向量的函數,而不提供對法向量計算的方法,法向量的計算需要由開發者來完成。下面給出一種簡單的計算方法:
void getNormal(GLfloat gx[3],GLfloat gy[3],GLfloat gz[3],GLfloat *ddnv) { GLfloat w0,w1,w2,v0,v1,v2,nr,nx,ny,nz; w0=gx[0]-gx[1]; w1=gy[0]-gy[1]; w2=gz[0]-gz[1]; v0=gx[2]-gx[1]; v1=gy[2]-gy[1]; v2=gz[2]-gz[1]; nx=(w1*v2-w2*v1); ny=(w2*v0-w0*v2); nz=(w0*v1-w1*v0); nr=sqrt(nx*nx+ny*ny+nz*nz); ddnv[0]=nx/nr; ddnv[1]=ny/nr; ddnv[2]=nz/nr; } 其參數gx[3],gy[3]和gz[3]為逼近曲面的一個三角形的三個頂點P0,P1和P2。通過計算矢量P0-P1與矢量P2-P1的叉乘而得到其平面法向量,并在歸一化后保存到由參數ddnv所指向的數組中。至于頂點法向的計算則多是取鄰近平面法向量的均值。OpenGL提供的法向定義函數為:
void glNormal3{bsifd}(TYPE nx,TYPE ny,TYPE nz); void glNormal3{bsifd}v(const TYPE *v);
通過這兩個函數可以設置當前法向值。對于非向量形式的定義采用前一種方式,通過參數nx、ny和nz分別給出法向三個分量值;對于向量形式的定義采取后一種方式,將v設置為指向法向三分量的指針。在應用時,通常要對法向進行歸一化處理。
構造曲線、曲面
在進行復雜物體建模時,使用的光滑曲線、曲面都是由一些線段和多邊形逼近而成,并通過少數幾個控制點對其進行描述。曲線的定義由glMap1*()函數完成:
void glMap1{fd}(GLenum target,TYPE u1,TYPE u2,GLint stride, GLint order,const TYPE *points);
參數target指出了控制頂點的意義以及在points參數中需要提供多少值;points指針可以指向控制點集、RGBA顏色值或是紋理坐標串等。參數u1和u2限定了變量U的取值范圍,通常是從0變化到1;stride表示跨度(在每塊存儲區內浮點數或雙精度數的個數,即兩個控制點間的偏移量);最后的參數order為階數,是次數加1,與控制點數一致。曲線定義后必須再glEnable()函數顯式啟動后才能起作用,其參數與target保持一致。在使用完畢后通過glDisable()函數將其關閉。曲線坐標可通過glEvalCoord1*()函數進行計算:
void glEvalCoord1{fd}[v](TYPE u);
該函數將產生曲線坐標值并將其繪制。參數u為定義域內的任意值,每調用一次將只產生一個坐標,此坐標值也是任意的。但目前較多采用的是定義均勻間隔曲線坐標值,依次調用glMapGrid1*()和glEvalMesh1()可以獲得等間隔值。這兩個函數分別用來定義一個一維網格和計算相應的坐標值。
曲面的構造可以是網格線和填充曲面形式,與曲線的構造很類似只是將其擴展為二維而已。下面給出曲面的定義函數:
void glMap2{fd}(GLenum target,TYPE u1,TYPE u2,GLint ustride,GLint uorder,TYPE v1,TYPE v2,GLint vstride,GLint vorder,TYPE points); 這里target的意義與在glMap1*()中的意義相同;(u1,u2),(v1,v2)是二維曲面坐標;其他參數如uorder,vorder,ustride和vstride等的定義都類似于在曲線中的定義;points為控制點坐標。對曲面任意一點的計算可通過函數
void glEvalCoord2{fd}[v](TYPE u,TYPE v);
來完成,通過在定義域內的曲線坐標值u,v來計算曲面內任意一點的世界坐標位置。對于曲面,也可以象曲線一樣通過函數來定義均勻間隔的曲面坐標值:
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()將此網格應用到已經啟動的曲面計算上。glEvalMesh2()的mode參數除了可以是GL_POINT和GL_LINE外,也可以是GL_FILL(生成填充空間曲面)。
Bezier曲面的繪制
下面給出一個通過定義曲面和均勻網格繪制一個具有光照和明暗處理效果的Bezier曲面(圖1)的部分主要代碼:
GLfloat ctrlpoints[4][4][3] = {// 控制點坐標 {{-2.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}, {-1.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 Init() { 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); // 定義參數空間的均勻網格 GLfloat ambient[4] = {0.4, 0.6, 0.2, 1.0}; // 初始化光照、材質的過程 GLfloat position[4] = {0.0, 1.0, 3.0, 1.0}; GLfloat mat_diffuse[4] = {0.8, 0.6, 0.3, 1.0}; GLfloat mat_specular[4] = {0.8, 0.6, 0.3, 1.0}; GLfloat mat_shininess[1] = {45.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); }
圖1 繪制的Bezier曲面
NURBS曲面的繪制
上面的例程通過給定少量控制點就可以很好的控制曲面的形狀。在實際應用時也可以通過程序來動態生成或調整控制點。在本例中,對曲面的定義等操作均是顯示調用本節前面介紹的glMap2f()、glMapGrid2f()等函數來實現的。除了這種方式,還可以利用OpenGL的功能庫提供的繪制非均勻有理B樣條曲面(NURBS曲面)的函數進行曲面繪制,下面給出實現此功能的部分示例代碼:
GLfloat ctlpoints[4][4][3]; // 控制點的存儲空間 GLUnurbsObj *theNurb; // 指向NURBS曲面對象的指針 void InitSurface() { int u, v; for (u = 0; u < 4; u++) { for (v = 0; v < 4; v++) { ctlpoints[u][v][0] = 2.0 * ((GLfloat)u - 1.5); ctlpoints[u][v][1] = 2.0 * ((GLfloat)v - 1.5); if ((u == 1 || u == 2) && (v == 1 || v == 2)) ctlpoints[u][v][2] = 6; else ctlpoints[u][v][2] = -6; } } } void Init(void) { GLfloat mat_diffuse[] = {0.8, 0.6, 0.3, 1.0}; // 定義曲面材質 GLfloat mat_specular[] = {0.8, 0.6, 0.3, 1.0}; GLfloat mat_shininess[] = {45.0}; glClearColor(0.0, 0.0, 0.0, 1.0); glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse); glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glDepthFunc(GL_LESS); glEnable(GL_DEPTH_TEST); glEnable(GL_AUTO_NORMAL); glEnable(GL_NORMALIZE); InitSurface(); // 初始化控制點 theNurb = gluNewNurbsRenderer(); // 創建一個NURBS曲面對象 // 修改NURBS曲面對象的屬性 gluNurbsProperty(theNurb, GLU_SAMPLING_TOLERANCE, 5.0); gluNurbsProperty(theNurb, GLU_DISPLAY_MODE, GLU_FILL); } void CALLBACK Display() { GLfloat knots[8] = {0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0}; // NURBS曲面的控制向量 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清屏 glPushMatrix(); // 入棧 glRotatef(30.0, -1.0, 0.0, 0.0); // 旋轉變換 glScalef (0.5, 0.5, 0.5); // 縮放變換 gluBeginSurface(theNurb); // 開始曲面繪制 gluNurbsSurface(theNurb, 8, knots, 8, knots, 4 * 3, 3, &ctlpoints[0][0][0], 4, 4, GL_MAP2_VERTEX_3); // 定義曲面的數學模型,確定其形狀 gluEndSurface(theNurb); // 結束曲面繪制 glPopMatrix(); // 出棧 glFlush(); // 強制刷新 }
圖2 繪制的NURBS曲面
該示例中對控制點的設定即是通過InitSurface()函數動態計算的,在初始化控制點后通過glu庫的gluNewNurbsRenderer()函數創建一個NURBS曲面對象theNurb,并可通過gluNurbsProperty()函數修改其屬性。在繪制NURBUS曲面時,通過gluBeginSurface()和gluEndSurface()進行界定,在其中通過gluNurbsSurface()函數完成對曲面數學模型的定義并確定曲面的形狀,圖2給出了上述代碼的繪制結果。
|