3. B样条曲线原理:从递推到实战
B样条曲线,说白了就是贝塞尔曲线的「升级版」。贝塞尔曲线有个硬伤——移动一个控制点,整条曲线都会跟着动。这在工程里简直要命。我当年做汽车A级曲面时,就因为这个特性被折腾得够呛。
B样条曲线解决了这个问题。它把曲线分成一段一段的,每段只受局部控制点影响。你调整一个点,只有附近几段会变,其他地方纹丝不动。这种「局部支撑」特性,让它成了自由曲面设计的基石。
核心思想: B样条曲线 = 分段多项式 + 局部控制 + 连续光滑
3.1 B样条基函数的递推定义
B样条基函数不是用显式公式定义的,而是用递推。这个递推公式叫 Cox-de Boor 公式。我第一次看到这个公式时觉得挺绕,但用多了就发现它其实很优雅。
零阶基函数(阶数 k=1)很简单:
N_{i,0}(u) = 1 如果 u_i ≤ u < u_{i+1}
N_{i,0}(u) = 0 否则
高阶基函数由低阶递推得到:
N_{i,k}(u) = ((u - u_i) / (u_{i+k} - u_i)) * N_{i,k-1}(u)
+ ((u_{i+k+1} - u) / (u_{i+k+1} - u_{i+1})) * N_{i+1,k-1}(u)
嗯,这里要注意:分母可能为零。如果出现 0/0,我们约定该项为 0。这是标准做法,我在代码里也这么处理。
我的习惯: 写代码实现递推时,先判断分母是否接近零(比如 < 1e-10),如果是就直接返回 0。否则浮点误差会搞出 NaN,排查起来很头疼。
3.2 节点向量:B样条的「骨架」
节点向量(Knot Vector)是B样条曲线的灵魂。它是一个非递减的实数序列:
U = {u_0, u_1, ..., u_m}
其中 m = n + k + 1,n 是控制点数量,k 是阶数。节点向量把参数域分成若干区间,每个区间对应一段多项式。
节点向量的类型决定了B样条曲线的性质:
| 类型 | 节点分布 | 特点 | 应用场景 |
|---|---|---|---|
| 均匀 | 等间距,如 [0,1,2,3,4,5] | 基函数形状相同,计算简单 | 通用曲线、教学示例 |
| 准均匀 | 两端重复 k+1 次,中间均匀 | 曲线经过首尾控制点 | CAD 系统默认设置 |
| 非均匀 | 任意非递减序列 | 可控制曲线形状局部变化 | 复杂曲面、逆向工程 |
| 贝塞尔端点 | 两端重复 k+1 次,中间无重复 | 每段都是贝塞尔曲线 | 与旧系统兼容 |
我在做车身曲面时,最常用的是准均匀节点向量。它让曲线首尾固定,中间又保持B样条的局部性,非常实用。
3.3 均匀B样条 vs 非均匀B样条
这两者的区别,说白了就是节点向量是否等距。
均匀B样条
节点等距分布,比如 [0, 1, 2, 3, 4, 5]。基函数形状完全一样,只是平移。计算简单,但有个问题——曲线不经过首尾控制点。这在工程里不太方便,你总希望曲线起点和终点是可控的。
// 均匀三次B样条节点向量示例
double knots_uniform[] = {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0};
非均匀B样条
节点可以任意分布。通过调整节点位置,你可以让曲线在某些区域变化更剧烈,在另一些区域更平缓。我曾经用非均匀B样条做过一个螺旋桨叶片,靠近轮毂处曲率变化大,节点就加密;靠近叶尖处变化平缓,节点就稀疏。
// 非均匀三次B样条节点向量示例
double knots_nonuniform[] = {0.0, 0.0, 0.0, 0.0, 0.5, 0.7, 0.9, 1.0, 1.0, 1.0, 1.0};
避坑指南: 我曾经在非均匀节点向量里不小心把节点顺序搞反了(递减序列),结果曲线形状完全失控。节点向量必须是非递减的,否则基函数定义会出问题。写代码时一定要加个断言检查。
3.4 B样条曲线的重要性质
这些性质我几乎每天都在用,记牢了能省很多事:
- 局部支撑: 移动一个控制点,只影响附近 k+1 段曲线。这是B样条最大的优势。
- 凸包性: 曲线完全落在控制多边形的凸包内。这可以用来快速判断曲线的大致范围。
- 连续性: k 阶B样条曲线在节点处至少 C^{k-1} 连续。三次B样条就是 C^2 连续,足够光滑。
- 仿射不变性: 对控制点做仿射变换,曲线也跟着做同样的变换。这在做旋转平移时很方便。
- 变差缩减性: 曲线与任意直线的交点数不超过控制多边形与同一直线的交点数。说白了就是曲线比控制多边形更「顺」。
3.5 知识结构图
下面这张图把B样条曲线的核心知识串起来了,方便你建立整体认知:
3.6 实战:如何选择节点向量
我总结了一套选择节点向量的经验法则:
- 曲线要经过首尾点? 用准均匀节点向量,两端重复 k+1 次。
- 需要局部加密? 用非均匀节点向量,在曲率大的区域插入更多节点。
- 只是做教学演示? 用均匀节点向量,简单直观。
- 要兼容旧系统? 用贝塞尔端点节点向量,每段都是贝塞尔曲线。
我的经验: 实际项目中,我通常先用准均匀节点向量跑通流程,再根据曲率分布调整节点位置。不要一上来就搞非均匀,容易把自己绕晕。
3.7 代码实现要点
实现B样条曲线时,有几个关键点要注意:
- 基函数计算: 用递归实现,但要注意缓存中间结果,避免重复计算。
- 节点向量边界: 参数 u 必须在 [u_k, u_{n+1}] 范围内,否则基函数未定义。
- 曲线求值: 先找到 u 所在的节点区间,再计算该区间内的非零基函数。
- 导数计算: 基函数的导数也有递推公式,可以直接复用基函数代码。
// 计算B样条基函数值(C语言风格伪代码)
double basis(int i, int k, double u, double* knots) {
if (k == 0) {
return (u >= knots[i] && u < knots[i+1]) ? 1.0 : 0.0;
}
double left = 0.0, right = 0.0;
double d1 = knots[i+k] - knots[i];
double d2 = knots[i+k+1] - knots[i+1];
if (d1 > 1e-10) left = ((u - knots[i]) / d1) * basis(i, k-1, u, knots);
if (d2 > 1e-10) right = ((knots[i+k+1] - u) / d2) * basis(i+1, k-1, u, knots);
return left + right;
}
这段代码虽然简单,但递归效率不高。实际项目中我会用迭代方式计算,或者用动态规划填表。不过作为理解原理,递归版本最清晰。
好了,B样条曲线的核心内容就这些。记住:基函数是「怎么算」,节点向量是「怎么分」,性质是「怎么用」。把这三块吃透,后面学NURBS就轻松多了。