第三章 贝塞尔曲线:德卡斯特里奥算法、伯恩斯坦多项式、曲线拼接

各位同学,欢迎来到第三章。前两章我们聊了NURBS的来龙去脉,今天咱们把目光聚焦在贝塞尔曲线上。说实话,贝塞尔曲线是理解NURBS的基石,也是我入行时啃得最久的一块骨头。

为什么这么说?因为贝塞尔曲线虽然数学上看着简单,但实际工程里坑不少。我记得刚做数控系统那会儿,用贝塞尔曲线做刀具路径规划,结果在拼接处出现了速度突变,差点把工件干废了。嗯,今天咱们就把这些坑一个个填平。

3.1 德卡斯特里奥算法:几何直觉的胜利

先问大家一个问题:给你三个点,怎么画出一条平滑的曲线?

德卡斯特里奥算法给出了一个非常直观的答案——递归插值。说白了,就是不断在线段上取比例点,然后把这些点连起来。

咱们看一个三次贝塞尔曲线的例子。控制点是P0、P1、P2、P3。算法分三步走:

  1. 在P0P1、P1P2、P2P3上分别取比例t的点,得到Q0、Q1、Q2
  2. 在Q0Q1、Q1Q2上取比例t的点,得到R0、R1
  3. 在R0R1上取比例t的点,得到B(t)——这就是曲线上的点

我习惯把这个过程叫做「剥洋葱」。每剥一层,控制点就少一个,直到剩下最后一个点。你想想看,这个算法多适合在嵌入式系统里实现——只需要循环加法和乘法,没有复杂的三角函数。

核心要点:德卡斯特里奥算法的时间复杂度是O(n²),其中n是控制点数量。对于三次曲线(n=3),只需要6次乘法和6次加法。这在数控系统的实时插补中非常友好。

下面是我当年在运动控制卡上写的一段C代码,专门做德卡斯特里奥递推:

// 德卡斯特里奥算法实现
// points: 控制点数组,n: 控制点数量,t: 参数值
Point deCasteljau(Point* points, int n, float t) {
    Point* temp = (Point*)malloc(n * sizeof(Point));
    memcpy(temp, points, n * sizeof(Point));
    
    for (int k = 1; k < n; k++) {
        for (int i = 0; i < n - k; i++) {
            temp[i].x = (1 - t) * temp[i].x + t * temp[i+1].x;
            temp[i].y = (1 - t) * temp[i].y + t * temp[i+1].y;
        }
    }
    
    Point result = temp[0];
    free(temp);
    return result;
}

工程小技巧:实际项目中,我建议把temp数组预分配好,避免每次调用都malloc/free。数控系统里每毫秒要算几百个点,动态内存分配会带来不可预测的延迟。

3.2 伯恩斯坦多项式:数学公式背后的工程意义

德卡斯特里奥算法是几何视角,伯恩斯坦多项式是代数视角。两者殊途同归,但理解后者对后续学习NURBS至关重要。

伯恩斯坦多项式长这样:

B_{i,n}(t) = C(n,i) * t^i * (1-t)^(n-i)

其中C(n,i)是组合数。n次贝塞尔曲线就是控制点与伯恩斯坦多项式的线性组合:

C(t) = Σ P_i * B_{i,n}(t),  i=0..n

为什么会这样?说白了,伯恩斯坦多项式就是「权重函数」。每个控制点P_i对曲线的影响程度由B_{i,n}(t)决定。t=0时,只有P0起作用;t=1时,只有Pn起作用。中间的点轮流「掌权」。

我在项目中遇到过一个问题:当控制点数量很多时(比如n>10),直接计算伯恩斯坦多项式会非常慢,因为组合数会爆炸。怎么办?

答案是:用递推公式。伯恩斯坦多项式满足:

B_{i,n}(t) = (1-t) * B_{i,n-1}(t) + t * B_{i-1,n-1}(t)

这个递推关系,其实就是德卡斯特里奥算法的代数形式。你看,几何和代数在这里完美统一了。

数值稳定性警告:当n较大(比如n>20)且t接近0或1时,直接计算伯恩斯坦多项式会遇到严重的数值问题。我曾经在计算15次贝塞尔曲线时,因为浮点数精度问题,曲线末端出现了肉眼可见的抖动。解决方案:要么用德卡斯特里奥算法,要么用分段贝塞尔曲线。

3.3 曲线拼接:C0连续和C1连续

实际加工中,很少用单条高次贝塞尔曲线。为什么?因为高次曲线计算量大,而且容易振荡。我见过有人用20次贝塞尔曲线拟合一个复杂轮廓,结果曲线在中间疯狂抖动,根本没法用。

正确的做法是:用多条低次贝塞尔曲线拼接。这就引出了连续性的概念。

连续性类型 数学含义 工程意义
C0连续 两条曲线在连接点处位置相同 曲线不断开,但可能有尖角
C1连续 位置相同且一阶导数相同 曲线光滑,速度连续
C2连续 位置、一阶、二阶导数都相同 加速度连续,适合高速加工

对于三次贝塞尔曲线,C1连续的拼接条件很简单。假设曲线1的控制点是P0、P1、P2、P3,曲线2的控制点是Q0、Q1、Q2、Q3。要让它们在P3=Q0处C1连续,需要满足:

P3 - P2 = Q1 - Q0
// 即:P2、P3(=Q0)、Q1三点共线,且距离相等

我习惯把这个条件叫做「三点一线等距」。你想想看,这其实就是在说:曲线离开P3的方向和进入Q0的方向必须一致,而且「速度」大小也要一样。

实战经验:在数控系统中,我通常只保证C1连续。C2连续虽然理论上更光滑,但会过度约束控制点的位置,导致曲线形状难以调整。而且对于大多数加工场景,C1连续已经足够。除非是做模具精加工,我才会考虑C2。

下面是我用SVG画的一张图,展示了三条贝塞尔曲线拼接时的控制点关系:

P0 P1 P2 P3=Q0 Q1 Q2 Q3=R0 R1 R2 R3 贝塞尔曲线拼接示意图(C1连续) 曲线1 曲线2 曲线3 连接点

图中红色标记的是连接点。注意看P2、P3、Q1这三个点——它们必须共线,而且P2到P3的距离等于P3到Q1的距离。这就是C1连续的几何条件。

我的调试习惯:在数控系统的上位机软件里,我会把控制多边形也画出来。一旦发现曲线拼接处有尖角,十有八九是控制多边形在连接点处「折」了。这时候调整控制点位置,让它们共线,问题就解决了。

最后说一句:贝塞尔曲线虽然基础,但它是通往B样条和NURBS的必经之路。下一章我们会看到,B样条其实就是把贝塞尔曲线的控制点「共享」了,从而实现了更灵活的局部控制。嗯,今天就到这里,大家回去可以试试用德卡斯特里奥算法画一条五次贝塞尔曲线,看看控制点多了之后曲线会有什么变化。


公众号:蓝海资料掘金营,微信deep3321