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样条曲线的核心知识串起来了,方便你建立整体认知:

B样条曲线知识结构图 B样条曲线 基函数递推定义 节点向量 曲线性质 Cox-de Boor 公式 0/0 处理约定 均匀 / 非均匀 重复度控制 局部支撑 凸包性 应用:自由曲面设计 / CAD / 逆向工程 💡 学习路径建议 先理解递推公式 → 掌握节点向量类型 → 用均匀B样条练手 → 再深入非均匀B样条 → 最后结合性质做实际项目

3.6 实战:如何选择节点向量

我总结了一套选择节点向量的经验法则:

  1. 曲线要经过首尾点? 用准均匀节点向量,两端重复 k+1 次。
  2. 需要局部加密? 用非均匀节点向量,在曲率大的区域插入更多节点。
  3. 只是做教学演示? 用均匀节点向量,简单直观。
  4. 要兼容旧系统? 用贝塞尔端点节点向量,每段都是贝塞尔曲线。

我的经验: 实际项目中,我通常先用准均匀节点向量跑通流程,再根据曲率分布调整节点位置。不要一上来就搞非均匀,容易把自己绕晕。

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就轻松多了。