貝塞爾曲線的缺點是當我們增加很多控制點的時候,曲線變得不可控,其連續性會變差差。如果控制點很多(高階曲線),當我們調整一個控制點的位置,對整個曲線的影響是很大的。要獲得更高級的控制,可以使用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