我們今天來介紹一下B樣條曲線。相比較Beizer曲線來說,B樣條有着兩個優點:(1)k次B樣條曲線具有良好的局部性,它只與k+1個控制點有關;(2)B樣條曲線拼接較為簡單。不過B樣條曲線的公式比較難懂,網上介紹原理的也着實不多,這里詳細分享一下。
圖1
我們先來看看什么是B樣條曲線,如圖1,我們以三次B樣條曲線為例。由於k次B樣條曲線的控制點有k+1個,所以P0P1P2P3控制u1u2段曲線,P1P2P3P4控制u2u3段曲線,P2P3P4P5控制u3u4段曲線。所以這樣就不會像beizer曲線那樣,改變任一控制點,都會對整個曲線產生影響。接下來我們看一下B樣條曲線的公式:
圖2
S(t)表示的是uaua+1段曲線,k表示的k次B樣條曲線,所以S(t)就是控制點與基函數的乘積之和,其中控制點是從Pj點到Pi點,一共有i-j+1個。結合圖1中的u1u2段曲線舉個例子,Su1u2(t) = P0N0,3(t) + P1N1,3(t) + P2N2,3(t) + P3N3,3(t)。這個公式比較抽象,我們換種寫法,同時我們再給出N(t)的公式:
圖3
圖3、圖2的公式參數意義略有不同。圖3中,j表示的是起始的控制點,k表示的是k次B樣條曲線,i表示的是迭代參數,相當於控制點是從Pj到Pj+k。基函數N(t)中參數i,k跟其上面的公式保持同步。這個公式看起來很復雜,光用文字說明並不能解釋太清楚,我們舉一個二次B樣條曲線和一個三次B樣條曲線的例子來說明一下。
(1)二次B樣條曲線:k=2,下面是二次B樣條曲線的三個基函數(建議大家拿筆算一算,這樣對公示理解的更深):
圖4
這里我們跟據上面的基函數給出P0,2(t)的公式及相關性質:
圖5
這條曲線的端點位置和端點切失如下:
圖6
根據上面的公式和性質可以得到如下曲線:
圖7
(2)三次B樣條曲線:k=3,這個也是我們今天代碼展示的曲線。下面是三次B樣條曲線的四個基函數:
圖8
這里我們跟據上面的基函數給出P0,3(t)的公式及相關性質:
圖9
這條曲線的端點位置和端點切失如下:
圖10
根據上面的公式和性質可以得到如下曲線:
圖11
我們這里就講完三種最基本也是最常用的曲線,我們在貼B樣條曲線的代碼之前,先討論一下三種曲線的優缺點(這只是個人觀點)。首先,Hermite曲線和Beizer曲線基本是一致的,雖然原理上有着一定的差異,但從性質以及參數的影響程度來說,大致上是相似的。而且這兩種曲線在表達復雜的曲線時,都是利用低次曲線拼接的方式來表達。相比這兩種曲線,B樣條曲線就顯得比較具有優勢,具體優勢開頭也有提到,這里就不重復了,缺點也非常明顯,控制點不在曲線上導致不好容易控制。
說到這,OpenGL繪制曲線就正式完結了。最后貼出三次B樣條曲線的代碼(效果就是圖1,不過可以拖動頂點調整曲線),大家可以試試。
#include <math.h> #include <gl/glut.h> #include <iostream> using namespace std; #define NUM_POINTS 6 #define NUM_SEGMENTS (NUM_POINTS-3) struct Point2 { double x; double y; Point2() { ; } Point2(int px, int py) { x = px; y = py; } void SetPoint2(int px, int py) { x = px; y = py; } }; /*全局變量*/ Point2 vec[NUM_POINTS]; bool mouseLeftDown = false; /*繪制B樣條曲線*/ void Bspline(int n) { float f1, f2, f3, f4; float deltaT = 1.0 / n; float T; glBegin(GL_LINE_STRIP); for (int num = 0; num < NUM_SEGMENTS; num++) { for (int i = 0; i <= n; i++) { T = i * deltaT; f1 = (-T*T*T + 3*T*T - 3*T + 1) / 6.0; f2 =(3*T*T*T - 6*T*T + 4) / 6.0; f3 = (-3*T*T*T +3*T*T + 3*T + 1) / 6.0; f4 = (T*T*T) / 6.0; glVertex2f( f1*vec[num].x + f2*vec[num+1].x + f3*vec[num+2].x + f4*vec[num+3].x, f1*vec[num].y + f2*vec[num+1].y + f3*vec[num+2].y + f4*vec[num+3].y); } } glEnd(); } void display() { glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); glLineWidth(1.5f); glColor3f(1.0,0.0,0.0); glBegin(GL_LINE_STRIP); for(int i = 0;i < NUM_POINTS; i++) { glVertex2f(vec[i].x, vec[i].y); } glEnd(); glPointSize(10.0f); glColor3f(0.0, 0.0, 1.0); glBegin(GL_POINTS); for(int i = 0;i < NUM_POINTS; i++) { glVertex2f(vec[i].x, vec[i].y); } glEnd(); Bspline(20); glFlush(); glutSwapBuffers(); } void init() { glClearColor(1.0, 1.0, 1.0, 0.0); glShadeModel(GL_FLAT); vec[0].SetPoint2(200, 400); vec[1].SetPoint2(100, 300); vec[2].SetPoint2(200, 200); vec[3].SetPoint2(250, 300); vec[4].SetPoint2(400, 200); vec[5].SetPoint2(400, 400); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei)w, (GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, (GLsizei)w, (GLsizei)h, 0.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void mouse(int button, int state, int x, int y) { if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) { mouseLeftDown = true; } if (button == GLUT_LEFT_BUTTON && state == GLUT_UP) { mouseLeftDown = false; } } double distance(int x1, int y1, int x2, int y2) { return sqrt((x1-x2) * (x1 -x2) + (y1-y2) * (y1-y2)); } void motion(int x, int y) { if (mouseLeftDown) { for (int i = 0; i < NUM_POINTS; i++) { if (distance(vec[i].x, vec[i].y, x, y) < 20) { vec[i].SetPoint2(x, y); } } } glutPostRedisplay(); } int main(int argc,char** argv) { glutInit(&argc,argv); glutInitDisplayMode(GLUT_RGBA|GLUT_DOUBLE); glutInitWindowSize(500, 500); glutInitWindowPosition (200, 200); glutCreateWindow("B-Spline Curve"); init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMouseFunc(mouse); glutMotionFunc(motion); glutMainLoop(); return 0; }