貝塞爾曲線的缺點是當我們增加很多控制點的時候,曲線變得不可控,其連續性會變差差。如果控制點很多(高階曲線),當我們調整一個控制點的位置,對整個曲線的影響是很大的。要獲得更高級的控制,可以使用GLU庫提供的NURBS(非均勻有理B樣條)。通過這些函數我們可以在求值器中調整控制點的影響力,在有大量控制點的情況下,依然可以產生平滑的曲線。
貝塞爾曲線由起點、終點和其他控制點來影響曲線的形狀。在二次貝塞爾曲線和三次貝塞爾曲線中,可以通過調整控制點的位置而得到很好的平滑性(C2級連續性 曲率級)的曲線。當增加更多的控制點的時候,這種平滑性就被破壞了。如下圖所示,前兩個曲線很平滑(曲率級的連續性),第三個曲線在增加了一個控制點之後,曲線被拉伸了,其平滑性遭到了破壞。
B樣條的工作方式類似於貝塞爾曲線,但不同的是曲線被分成很多段。每段曲線的形狀只受到最近的四個控制點的影響,這樣曲線就像是4階的貝塞爾曲線拼接起來的。這樣很長的有很多控制點的曲線就會有固定的連續性,平滑性(每一段都是c2級的連續性)。
NURBS(非均勻有理B樣條)的真正威力在於,可以調整任意一段曲線中的四個控制點的影響力,來產生較好的平滑性。這是通過一系列結點來控制的。每個控制點都定義了兩個結點的值。結點的取值范圍是u或v的定義域,而且必須是非遞減的。
結點的值決定了落在u、v參數定義域內的控制點的影響力。下圖的曲線表示控制點對一條在u參數定義域內的具有四個單位的曲線的影響。下圖表示中間點對曲線的影響更大,而且只有在[0,3]范圍內的控制點才會對曲線產生影響。
在u、v參數定義域內的控制點對曲線的形狀會有有影響,而且我們可以通過結點來控制控制點的影響力。非均勻性就是指一個控制點的影響力的范圍是可以改變的。
以下內容及節選自 http://www.rhino3d.com/cn/nurbs
節點 ( Knot ) 是一個 ( 階數 + N - 1 ) 的數字列表,N 代表控制點數目。有時候這個列表上的數字也稱為節點矢量 ( Knot Vector ),這裡的矢量並不是指 3D 方向。
節點列表上的數字必須符合幾個條件,確定條件是否符合的標准方式是在列表上往下時,數字必需維持不變或變大,而且數字重復的次數不可以比階數大。例如,階數 3 有 15 個控制點的 NURBS 曲線,列表數字為 0,0,0,1,2,2,2,3,7,7,9,9,9 是一個符合條件的節點列表。列表數字為 0,0,0,1,2,2,2,2,7,7,9,9,9 則不符合,因為此列表中有四個 2,而四比階數大 ( 階數為 3 )。
節點值重復的次數稱為節點的重數 ( Multiplicity ),在上面例子中符合條件的節點列表中,節點值 0 的重數值為三;節點值 1 的重數值為一;節點值 2 的重數為三;節點值 7 的重數值為二;節點值 9 的重數值為三。如果節點值重復的次數和階數一樣,該節點值稱為全復節點 ( Full-Multiplicity Knot )。在上面的例子中,節點值 0、2、9 有完整的重數,只出現一次的節點值稱為單純節點 ( Simple Knot ),節點值 1 和 3 為單純節點。
如果在節點列表中是以全復節點開始,接下來是單純節點,再以全復節點結束,而且節點值為等差,稱為均勻 ( Uniform )。例如,如果階數為 3 有 7 個控制點的 NURBS 曲線,其節點值為 0,0,0,1,2,3,4,4,4,那麼該曲線有均勻的節點。如果節點值是 0,0,0,1,2,5,6,6,6 不是均勻的,稱為非均勻 ( Non-Uniform )。在 NURBS 的 NU 代表“非均勻”,意味著在一條 NURBS 曲線中節點可以是非均勻的。
在節點值列表中段有重復節點值的 NURBS 曲線比較不平滑,最不平滑的情形是節點列表中段出現全復節點,代表曲線有銳角。因此,有些設計師喜歡在曲線插入或移除節點,然後調整控制點,使曲線的造型變得平滑或尖銳。因為節點數等於 ( N + 階數 - 1 ),N 代表控制點的數量,所以插入一個節點會增加一個控制點,移除一個節點也會減少一個控制點。插入節點時可以不改變 NURBS 曲線的形狀,但通常移除節點必定會改變 NURBS 曲線的形狀。
控制點和節點是一對一成對的是常見的錯誤概念,這種情形只發生在 1 階的 NURBS ( 多重直線 )。較高階數的 NURBS 的每 ( 2 x 階數 ) 個節點是一個群組,每 ( 階數 + 1 ) 個控制點是一個群組。例如,一條 3 階 7 個控制點的 NURBS 曲線,節點是 0,0,0,1,2,5,8,8,8,前四個控制點是對應至前六個節點;第二至第五個控制點是對應至第二至第七個節點 0,0,1,2,5,8;第三至第六個控制點是對應至第三至第八個節點 0,1,2,5,8,8;最後四個控制點是對應至最後六個節點
GLU庫中提供了易用高級的繪制NURBS表面的函數。我們不需要顯示地調用求值函數或建立網格。渲染一個NURBS表面的步驟如下:
我們通過gluNewNurbsRenderer函數來為NURBS創建一個渲染器對象,在最後使用gluDeleteNurbsRenderer銷毀它。代碼如下:
// NURBS 對象指針 GLUnurbsObj * pNurb = NULL; ... ... // 創建NURBS對象 pNurb = gluNewNurbsRenderer(); ... if (pNurb) gluDeleteNurbsRenderer(pNurb);在創建了NURBS渲染器之後,我們需要設置NURBS的屬性。
//設置采樣容差
gluNurbsProperty(pNurb, GLU_SAMPLING_TOLERANCE, 25.0f);
//填充一個實體的表面
gluNurbsProperty(pNurb, GLU_DISPLAY_MODE, (GLfloat)GLU_FILL);
GLU_SAMPLING_TOLERANCE決定了網格的精細程度。GLU_FILL表示使用填充模式,相應的GLU_OUTLINE_POLYGON是線框模式。
表面通過一組控制點和一個結點序列來定義。使用gluNurbsSurface函數來定義表面,這個函數要在gluBeginSurface和gluEndSurface中間:
// 渲染NURB // 開始NURB表面的定義 gluBeginSurface(pNurb); gluNurbsSurface(pNurb, // 指針指向NURBS渲染器 8 , Knots, // u定義域內的結點個數,以及結點序列 8 , Knots, // v定義域內的結點個數,以及結點序列 4 * 3 , // u方向上控制點的間隔 3 , // v方向上控制點的間隔 & ctrlPoints[ 0 ][ 0 ][ 0 ], // 控制點數組 4 , 4 , // u,v 的次數 GL_MAP2_VERTEX_3); // 表面的類型 // 完成定義 gluEndSurface(pNurb);我們可以通過gluNurbsSurface來定義多個NURBS表面,但NURBS渲染器的屬性不會改變。一般情況下我們連續畫兩個不同的屬性的表面,比如很少需要相鄰的兩個曲面一個是填充型的一個是線框性的。控制點和結點的序列如下:
GLint nNumPoints = 4; // 4 X 4 // u v (x,y,z) GLfloat ctrlPoints[4][4][3]= {{{ -6.0f, -6.0f, 0.0f}, // u = 0, v = 0 { -6.0f, -2.0f, 0.0f}, // v = 1 { -6.0f, 2.0f, 0.0f}, // v = 2 { -6.0f, 6.0f, 0.0f}}, // v = 3 {{ -2.0f, -6.0f, 0.0f}, // u = 1 v = 0 { -2.0f, -2.0f, 8.0f}, // v = 1 { -2.0f, 2.0f, 8.0f}, // v = 2 { -2.0f, 6.0f, 0.0f}}, // v = 3 {{ 2.0f, -6.0f, 0.0f }, // u =2 v = 0 { 2.0f, -2.0f, 8.0f }, // v = 1 { 2.0f, 2.0f, 8.0f }, // v = 2 { 2.0f, 6.0f, 0.0f }},// v = 3 {{ 6.0f, -6.0f, 0.0f}, // u = 3 v = 0 { 6.0f, -2.0f, 0.0f}, // v = 1 { 6.0f, 2.0f, 0.0f}, // v = 2 { 6.0f, 6.0f, 0.0f}}};// v = 3
效果圖:
修剪的功能常用語消減NURBS表面的銳利的邊緣。我們也可以使用修剪的功能在表面上剪一個洞。下面的示例在表面剪一個三角形的洞:
static GLUnurbsObj *pNurb = NULL; GLint nNumPoints = 4; // 4 X 4 // u v (x,y,z) GLfloat ctrlPoints[4][4][3]= {{{ -6.0f, -6.0f, 0.0f}, // u = 0, v = 0 { -6.0f, -2.0f, 0.0f}, // v = 1 { -6.0f, 2.0f, 0.0f}, // v = 2 { -6.0f, 6.0f, 0.0f}}, // v = 3 {{ -2.0f, -6.0f, 0.0f}, // u = 1 v = 0 { -2.0f, -2.0f, 8.0f}, // v = 1 { -2.0f, 2.0f, 8.0f}, // v = 2 { -2.0f, 6.0f, 0.0f}}, // v = 3 {{ 2.0f, -6.0f, 0.0f }, // u =2 v = 0 { 2.0f, -2.0f, 8.0f }, // v = 1 { 2.0f, 2.0f, 8.0f }, // v = 2 { 2.0f, 6.0f, 0.0f }},// v = 3 {{ 6.0f, -6.0f, 0.0f}, // u = 3 v = 0 { 6.0f, -2.0f, 0.0f}, // v = 1 { 6.0f, 2.0f, 0.0f}, // v = 2 { 6.0f, 6.0f, 0.0f}}};// v = 3 // Knot sequence for the NURB GLfloat Knots[8] = {0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; void DrawPoints(void) { glColor3ub(255, 0, 0); glPointSize(5.0f); glBegin(GL_POINTS); for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { glVertex3fv(ctrlPoints[i][j]); } } glEnd(); } // NURBS 出錯時的回調函數 void CALLBACK NurbsErrorHandler(GLenum nErrorCode) { char cMessage[100] = {0,}; strcpy(cMessage, "NURBS error : "); strcat(cMessage, (char*)gluErrorString(nErrorCode)); glutSetWindowTitle(cMessage); } void RenderScene(void) { glColor3ub(0,0,220); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glRotatef(330.0f, 1.0f, 0.0f, 0.0f); //修剪框是一個閉合的環 //外修剪框 GLfloat outSidePts[5][2] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}, {0.0f, 0.0f}}; //內修剪框 GLfloat inSidePts[4][2] = {{0.25f, 0.25f}, { 0.5f, 0.5f}, {0.75f, 0.25f},{0.25f, 0.25f} }; gluBeginSurface(pNurb); //定義NURBS表面 gluNurbsSurface(pNurb, 8, Knots, //u定義域內的結點個數,以及結點序列 8, Knots,//v定義域內的結點個數,以及結點序列 4 * 3, //u方向上的控制點間隔 3, //v方向上的控制點間隔 &ctrlPoints[0][0][0], //控制點數組 4, 4, //u v的次數 GL_MAP2_VERTEX_3);//產生的類型 //開始修剪 gluBeginTrim(pNurb); gluPwlCurve(pNurb, 5, //修剪點的個數 &outSidePts[0][0], //修剪點數組 2, //點之間的間隔 GLU_MAP1_TRIM_2);//修剪的類型 gluEndTrim(pNurb); gluBeginTrim(pNurb); gluPwlCurve(pNurb, 4, &inSidePts[0][0], 2, GLU_MAP1_TRIM_2); gluEndTrim(pNurb); gluEndSurface(pNurb); DrawPoints(); glPopMatrix(); glutSwapBuffers(); }
在一對gluBeginSurface/gluEndSurface調用內部,調用gluBeginTrim函數開始修剪,調用gluPwlCurve函數指定一條修剪曲線,然後調用gluEndTrim完成曲線的修剪。這些修剪曲線必須根據單位參數方程u和v空間指定。這意味著u/v定義域被縮放到0.0到1.0之間。
gluPwlCurve函數定義一條由片段拼接成的線性曲線,實質上就是一些首尾相連的點。對曲線進行修剪時,順時針方向修剪的曲線將會丟棄它的內部。一般情況下,應該指定一條外部修剪曲線,它包圍了整個NURBS參數空間,然後在這個區域內部指定一個較小的修剪區域(順時針圍繞)。
為了使OpenGl的速度盡可能快,所有的幾何圖元都必須是凸的。如果我們遇到復雜的幾何圖形(如下圖),要把它手工切分為多個凸多邊形工作量較大。OpenGL的GLU庫提供了曲面細分的特性,幫助我們處理復雜的圖形。
對上面的兩個幾何圖形我們可以進行如下圖的細分(這種分法不是唯一的)
曲面細分通過鑲嵌器對象來工作。鑲嵌器對象類似於二次方程狀態對象需要創建以及銷毀。
GLUtesselator *pTess = gluNewTes(); //注意不是GLUtessellator 少了個l
…
gluDeleteTess(pTess);
所有的曲面細分函數的第一個參數都是鑲嵌器對象。這樣就允許我們構造多個鑲嵌器,並在需要的時候很方便的進行切換,這也使得鑲嵌器只影響當前的工作對象。
鑲嵌器分解多邊形並渲染的步驟如下:
每個多邊形由一個或多個輪廓組成。上圖左邊的多邊形就只有一個輪廓,右邊的有個洞所以有兩個輪廓。曲線細分的工作是到步驟8才進行的。曲線細分會帶來一定性能的開銷。如果圖形是靜態的最好是把函數調用放到顯示列表中。
在曲面細分的過程中,鑲嵌器會調用一系列你提供的回調函數。這些回調函數指定了頂點的信息以及開始和結束圖元。注冊回調函數的原型如下:
void gluTessCallback(GLUTesselator *tobj, GLenum which, void (*fn)());
第一個參數是鑲嵌器對象,第二個指定回調函數的類型,第三個是回調函數指針。
例如:
typedef GLvoid (_stdcall *CallBack)();
gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin);
gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv);
gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd);
上面三個函數分別指定了,新圖元開始的回調函數,為每一個頂點調用glVertex3dv,以及結束的回調函數。上面只是簡單的指定了OpenGL的函數,你也可以自定一個回調函數,裡面實現你想要的功能。還可以注冊出錯時的回調函數:
void tessError(GLenum code) { const char *str = (const char*)gluErrorString(code); glutSetWindowTitle(str); } gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError);
void gluTessBeginPolygon(GLUTesselator *tobj, void *data);
第一個參數為鑲嵌器對象指針,第二個參數是指向曲面細分處理相關聯的用戶自定義數據,這個數據可以用回調函數在細分的過程中發送回來。一般情況下,這個參數常常設置為NULL. 完成一個多邊形後調用下面的函數開始細分。
void gluTessEndPolygon(GLUTesslator *tobj);
下面兩個函數開始和結束輪廓,對應於步驟4和6:
void gluTessBeginContour(GLUTesselator *tobj);
void gluTessEndContour(GLUTesselator *tobj);
在這兩個函數中,添加輪廓的頂點:
void gluTessVertex(GLUTesselator *tobj, GLdouble v[3], void *data);
v參數包含了用於鑲嵌器計算的真實的頂點數據,data參數是指向頂點數據的指針,傳給指定的GLU_VERTEX的回調函數。第二個參數可以包含除了頂點之外的一些信息如顏色,法線等。如果我們自己定義了GLU_VERTEX的回調函數,那麼就可以使用data的數據了。
一個佛羅裡達州的簡單輪廓,這個州裡面還有個奧基喬比湖的輪廓。通過右鍵菜單,我們可以在簡單的畫線環繞模式,外圍輪廓曲線細分模式,和復雜模式之間切換。
這裡面有個新函數gluTessProperty(pTess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);這個指定了輪廓線為奇數的是填充的,輪廓線是偶數的是镂空的。我們的湖的輪廓線在裡面,是第二個輪廓線,所以是镂空的。
代碼如下:
#include "gltools.h" #include <math.h> //外圍輪廓線 #define COAST_POINTS 24 GLdouble vCoast[COAST_POINTS][3] = {{-70.0, 30.0, 0.0 }, {-50.0, 30.0, 0.0 }, {-50.0, 27.0, 0.0 }, { -5.0, 27.0, 0.0 }, { 0.0, 20.0, 0.0 }, { 8.0, 10.0, 0.0 }, { 12.0, 5.0, 0.0 }, { 10.0, 0.0, 0.0 }, { 15.0,-10.0, 0.0 }, { 20.0,-20.0, 0.0 }, { 20.0,-35.0, 0.0 }, { 10.0,-40.0, 0.0 }, { 0.0,-30.0, 0.0 }, { -5.0,-20.0, 0.0 }, {-12.0,-10.0, 0.0 }, {-13.0, -5.0, 0.0 }, {-12.0, 5.0, 0.0 }, {-20.0, 10.0, 0.0 }, {-30.0, 20.0, 0.0 }, {-40.0, 15.0, 0.0 }, {-50.0, 15.0, 0.0 }, {-55.0, 20.0, 0.0 }, {-60.0, 25.0, 0.0 }, {-70.0, 25.0, 0.0 }}; //湖的輪廓線 #define LAKE_POINTS 4 GLdouble vLake[LAKE_POINTS][3] = {{ 10.0, -20.0, 0.0 }, { 15.0, -25.0, 0.0 }, { 10.0, -30.0, 0.0 }, { 5.0, -25.0, 0.0 }}; #define LINE_LOOP 1 #define TESS 2 #define COMPLEX 3 static int iMode = LINE_LOOP; void SetupRC() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); } void tessError(GLenum code) { const char *str = (const char*)gluErrorString(code); glutSetWindowTitle(str); } void RenderScene() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0f, 0.0f, 1.0f); glPushMatrix(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); switch(iMode) { case LINE_LOOP: { glBegin(GL_LINE_LOOP); for (int i = 0; i < COAST_POINTS; ++i) { glVertex3dv(vCoast[i]); } glEnd(); } break; case TESS: { //創建鑲嵌器對象 GLUtesselator *pTess = gluNewTess(); //設置回調函數 gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin); gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd); gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv); gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError); //開始一個多邊形 gluTessBeginPolygon(pTess, NULL); //開始一個輪廓 gluTessBeginContour(pTess); //設置輪廓的頂點 for (int i = 0; i < COAST_POINTS; ++i) { gluTessVertex(pTess, vCoast[i], vCoast[i]); } gluTessEndContour(pTess); gluTessEndPolygon(pTess); gluDeleteTess(pTess); } break; case COMPLEX: { GLUtesselator *pTess = gluNewTess(); gluTessCallback(pTess, GLU_TESS_BEGIN, (CallBack)glBegin); gluTessCallback(pTess, GLU_TESS_END, (CallBack)glEnd); gluTessCallback(pTess, GLU_TESS_VERTEX, (CallBack)glVertex3dv); gluTessCallback(pTess, GLU_TESS_ERROR, (CallBack)tessError); //指定奇數的輪廓為填充,偶數的輪廓是镂空的。這也是默認的設置 gluTessProperty(pTess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD); gluTessBeginPolygon(pTess, NULL); gluTessBeginContour(pTess); for (int i = 0; i < COAST_POINTS; ++i) { gluTessVertex(pTess, vCoast[i], vCoast[i]); } gluTessEndContour(pTess); gluTessBeginContour(pTess); for (int i = 0; i < LAKE_POINTS; ++i) { gluTessVertex(pTess, vLake[i], vLake[i]); } gluTessEndContour(pTess); gluTessEndPolygon(pTess); gluDeleteTess(pTess); } break; default: break; } glPopMatrix(); glutSwapBuffers(); } void ChangeSize(GLsizei w, GLsizei h) { if (h == 0) { h = 1; } glViewport(0, 0, w, h); GLfloat aspect = (GLfloat)w/(GLfloat)h; glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(-100.0, 100.0, -100.0, 100.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void ProcessMenu(int value) { iMode = value; glutPostRedisplay(); } int main(int args, char *argv[]) { glutInit(&args, argv); glutInitWindowSize(800, 600); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH); glutCreateWindow("florida"); glutDisplayFunc(RenderScene); glutReshapeFunc(ChangeSize); glutCreateMenu(ProcessMenu); glutAddMenuEntry("LINE_LOOP", LINE_LOOP); glutAddMenuEntry("Tess", TESS); glutAddMenuEntry("Complex", COMPLEX); glutAttachMenu(GLUT_RIGHT_BUTTON); SetupRC(); glutMainLoop(); return 0; }
用線環繞的模式:
單輪廓模式:
包含湖的輪廓
OpenGL超級寶典 第4版 中文版PDF+英文版+源代碼 見 http://www.linuxidc.com/Linux/2013-10/91413.htm
OpenGL編程指南(原書第7版)中文掃描版PDF 下載 http://www.linuxidc.com/Linux/2012-08/67925.htm
OpenGL 渲染篇 http://www.linuxidc.com/Linux/2011-10/45756.htm
Ubuntu 13.04 安裝 OpenGL http://www.linuxidc.com/Linux/2013-05/84815.htm
OpenGL三維球體數據生成與繪制【附源碼】 http://www.linuxidc.com/Linux/2013-04/83235.htm
Ubuntu下OpenGL編程基礎解析 http://www.linuxidc.com/Linux/2013-03/81675.htm
如何在Ubuntu使用eclipse for c++配置OpenGL http://www.linuxidc.com/Linux/2012-11/74191.htm
更多《OpenGL超級寶典學習筆記》相關知識 見 http://www.linuxidc.com/search.aspx?where=nkey&keyword=34581