我们今天来介绍一下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;
}