简单粗暴:B-样条曲线入门

前言

这段时间公司有B-样条曲线的研发要求,因此研究了一发B-样条曲线,在自己理解好的几天以内把思路记录下来,也算是趁热打铁。

前面的直线和曲线两段其实是初中数学级别的,写上去除了是为了做一个难度的铺垫,也是为了能有一点图文搭配的效果,让全文的可读性稍微增强一点,不至于看起来通篇都是枯燥的公式。

我最开始列了一个大纲,尽可能的思考了我这样子写能不能让比数学水平相对不高的人也能看懂,而不是望着一大堆奇怪的符号发愣。但知识点确实相对越讲越难,到后面想要同时保证自己说清楚和别人能看懂,确实要花些精力_(:з)∠)_所以如果有读着感到奇怪,晦涩或不通的地方。也欢迎以任何途径找我交流,我会尽力改改,多谢。


第一条插值线段

先在平面上取两点,A和B。问题:如何在A、B之间做插值线段?如何把它们连起来?

嗯……当然是直接连起来就好了。

在将AB连起来以后,我们需要搞清线段AB上每一点的含义。换言之,如何用已知的A、B位置,来表明线段上任意一点?

比如,怎么表示AB正中间的点?

显而易见,答案是 \frac{1}{2}A+\frac{1}{2}B

然后加点难度,如何表示偏向A一侧七分之一处的点?

没错, \frac{6}{7}A+\frac{1}{7}B 。因为P更靠近A一侧,所以A的值更大一些。

很简单,对吧。我们可以按照这个规律,将这种表述方式,推广到上的任意一点:从A向B慢慢移动,最一开始由于从A出发,A占完全的比重1,而B是0。随着点P缓慢向右推移,B的占比逐渐增大,A的占比逐渐减小。最终到达B点的时候,A成为了0,而B占完全的比重1。我们将这个比重值抽象成一个值t,可以获得下列公式:

A*t+B*(1-t)

这样,我们在已经知道AB位置的情况下,就可以通过修改t值,来简单的表述线段AB上的任意点了。

变弯的过程:第一条插值曲线

两个点的插值再怎么变化也只能做出一条直线。所以接下来,我们要引入一个新点C。

C的出现,使得线段的状态变得不确定了起来。

为什么这么说呢?我们先考虑比较简单的情况,A和B的比值保持不变,因此还是t和1-t,这个比值,更多的时候我们将其称之为权重。同样的,C也需要一个权重。然后C的值——我们设为y——想要表示成这个样子:一开始y=0,随着t值增高,y先缓慢升高,在t=0.5的时候达到1,然后逐渐降低,在t=1的时候达到0。最终得到的是一个二次函数图像的形状。

使用公式的话其实就是: y=-4t^2+4t, \space 0\leq t\leq1

结合A的比值t,B的比值1-t,曲线整体的公式为:

tA+(1-t)B+(-4t^2+4t)C

最终的整体曲线效果类似下图,最初A的比值为1,后面随着A减少和B、C的逐渐增大,A、B、C各占0.5,再之后B的比值逐渐增大,A、C开始减少,最终曲线轨迹完全贴合B。

上述公式对应的曲线形状。抱歉做的比较渣,过渡段表示的不太精确

好了,现在我们有了一条插值曲线,只需要通过修改t值,我们就可以表述曲线上的任意一点。

不过,真正的B-样条曲线的规则没有这么简单,它的插值规则是不确定的。不过不管怎么变,目标是一样的:最终,只需要通过修改t值,就可以表述任意曲线上的任意点。

做曲线也要讲基本法(大雾)

做曲线也要按照数学的基本规律啊,对不对?当然我们提供的基本参数也是很重要的……

到目前为止,我们讲的思路是:首先做两点直线,然后做三点曲线,按这个规律接下来我们应该逐步推理多点曲线的情况,再从中获取B样条的表达方式,但那就太复杂了。所以接下来我们的思路会反过来:首先介绍B样条曲线的一些基本参数,通过这些参数就可以生成B样条曲线。不论从哪种角度出发,最终的目标都是一样的,只需要修改t值,就可以表述曲线上的任意点。

好了,接下来开始说B样条曲线的基本参数。

B样条曲线的基本参数中其实就几样,t,阶数,控制点列表,节点表,基本函数表。

t值的作用前面已经说了很多了,在此不提。控制点列表代表一系列需要用户提供的顶点。实际上上面直线中的AB,曲线中的ABC,都可以理解为控制点。

我们还没接触的,实际上只有阶数,节点值和基本函数表了。

阶数

剩下的参数中最基本的当然是阶数。每个t值都是通过控制点和权重相乘计算得出的结果。而阶数越高,生成每个t值所需要的控制点数越多。

所以,阶数到底是什么?阶数=所有权重中t值的最高次幂。

觉得费劲?有个更简单的:阶数=生成t值所需要的控制点数-1。

像前面所提到的曲线,生成t值,需要A、B、C三个控制点。因此它是一个3-1=2阶的曲线(不过它不是B-样条)。而用比较绕的最高幂次方式理解,同样是该曲线,A的权重是 t ,B的权重是 1-t ,C的权重是 -4t^2+2t ,最高幂次是 -4t^2 中的2,因此同样可以得出是2阶曲线。

节点表

在理解了阶数的概念以后,就可以说节点表了。

节点表是生成基本函数表的关键参数,大小严格等于控制点数量+阶数+1。节点表的参数是人为设置的,如果不是今天在讲B-样条曲线,你想怎么设置都可以。但B-样条还是要遵守一些规律的。

一般设置的方法有两种:顺序方法和Clamped方法。前者用于制作标准的B-样条开曲线和闭曲线,后者用于制作一种比较实用的B-样条曲线。

顺序列表只需要从0-1线性递增设置即可,Clamped列表则需要将前后各阶数+1个节点设置成0。

举个例子就能讲的很清楚:假设曲线有6个控制点,阶数是3阶,那么节点表大小=6+3+1=10。

如果是顺序列表,只需要按顺序设置: 0, \frac{1}{9},\frac{2}{9},\frac{3}{9},\frac{4}{9},\frac{5}{9},\frac{6}{9},\frac{7}{9},\frac{8}{9},1

如果是Clamped列表,由于是3阶,前面3+1个参数均设置为0,后面3+1个参数均设置为1,然后剩余参数均匀递增: 0, 0, 0, 0, \frac{1}{3}, \frac{2}{3}, 1, 1, 1,1

基本函数表

基本函数表本质上是一个递归方程,但同时它也是一个中间参数。

我想首先将基本函数表的公式列出来,看了十有八九会头大,不过没关系,后面我会一点一点解剖这个公式:B_{i,deg}(t)=\frac{t-knot_i}{knot_{i+deg}-knot_i}B_{i,deg-1}(t)+\frac{knot_{i+deg+1}-t}{knot_{i+deg+1}-knot_{i+1}}B_{i+1,deg-1}(t)

t的话最容易理解,就是前面反复提到的t值。

knot代表节点,也就是我们前面提到的节点表。而 knot_i 代表节点表中的第 i 个元素。

B_{i,deg}(t) 就是基本函数表的参数了。没错,基本函数表是一个二维数组。参数i和deg分别表示第几个元素和阶数。 所以B_{i,deg}(t) 的意思就是用户输入值为t时,基本函数表在第deg阶的第i个元素的值。

这时候我们再纵观整个长公式,发现了吗?整个公式实际上是两个部分相加。而加号两侧的公式格式一致:一个通过节点表 knott 经过一系列计算得出的权重值和一个比当前更低一阶的基本函数表值乘积

所以为什么说基本函数表是递归方程,这就是原因。当前deg阶的元素需要通过两个deg-1阶的元素获得,deg-1阶的元素则需要通过deg-2阶的元素获得……以此类推,直到递归deg次以后,回退到0阶为止。

但是,回退到0阶的时候怎么算呢?

B-样条算法规定,回退到0阶时使用以下公式:

B_{i,0} =  \begin{equation} \left\{     \begin{array}{l}     1, &knot_i\leq t \leq knot_{i+1}\\     0, &t<knot_i \space 或 \space t > knot_{i+1}\\     \end{array} \right. \end{equation}

即,0阶的时候,t处于第i个knot值和第i+1个knot值之间的时候,才会等于1,其他时候都等于0.

怎么样,是不是突然发现了knot节点表的意义?实际上节点表的作用就是确认一下t在元素表的哪个位置,然后我们只需要找出基本函数表在deg中能够递归到该位置的值,就可以在后续的最终计算中,得到t值在B-样条曲线的对应位置了。

最后一步:最终计算

设最终t值在B-样条曲线的对应位置为 C(t) ,则最终B-样条曲线计算公式为:

C(t)=\Sigma_{i=0}^{n-1}B_{i,deg}(t)P_i

其中,第deg阶的第i个元素 B_{i,deg}(t) 我们已经提过,不再赘述。

P_i 便是我们前面一直提到的控制点,而且在这里是第i个控制点。求和符号上的 n 表示控制点的总数。

所以公式的意思就是,最终的曲线位置 C(t) ,是基本函数表中第 deg 阶下所有元素的权重值,和对应位置控制点坐标乘积,即 B_{0,deg} \times P_0B_{1,deg} \times P_1,……, B_{n-1,deg} \times P_{n-1} 一一相加后得到的总和。

编辑于 2019-05-20

文章被以下专栏收录