第三章 贝塞尔曲线:德卡斯特里奥算法、伯恩斯坦多项式、曲线拼接
各位同学,欢迎来到第三章。前两章我们聊了NURBS的来龙去脉,今天咱们把目光聚焦在贝塞尔曲线上。说实话,贝塞尔曲线是理解NURBS的基石,也是我入行时啃得最久的一块骨头。
为什么这么说?因为贝塞尔曲线虽然数学上看着简单,但实际工程里坑不少。我记得刚做数控系统那会儿,用贝塞尔曲线做刀具路径规划,结果在拼接处出现了速度突变,差点把工件干废了。嗯,今天咱们就把这些坑一个个填平。
3.1 德卡斯特里奥算法:几何直觉的胜利
先问大家一个问题:给你三个点,怎么画出一条平滑的曲线?
德卡斯特里奥算法给出了一个非常直观的答案——递归插值。说白了,就是不断在线段上取比例点,然后把这些点连起来。
咱们看一个三次贝塞尔曲线的例子。控制点是P0、P1、P2、P3。算法分三步走:
- 在P0P1、P1P2、P2P3上分别取比例t的点,得到Q0、Q1、Q2
- 在Q0Q1、Q1Q2上取比例t的点,得到R0、R1
- 在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画的一张图,展示了三条贝塞尔曲线拼接时的控制点关系:
图中红色标记的是连接点。注意看P2、P3、Q1这三个点——它们必须共线,而且P2到P3的距离等于P3到Q1的距离。这就是C1连续的几何条件。
我的调试习惯:在数控系统的上位机软件里,我会把控制多边形也画出来。一旦发现曲线拼接处有尖角,十有八九是控制多边形在连接点处「折」了。这时候调整控制点位置,让它们共线,问题就解决了。
最后说一句:贝塞尔曲线虽然基础,但它是通往B样条和NURBS的必经之路。下一章我们会看到,B样条其实就是把贝塞尔曲线的控制点「共享」了,从而实现了更灵活的局部控制。嗯,今天就到这里,大家回去可以试试用德卡斯特里奥算法画一条五次贝塞尔曲线,看看控制点多了之后曲线会有什么变化。
公众号:蓝海资料掘金营,微信deep3321