4. 复杂曲面构建:贝塞尔曲线、B样条曲线、NURBS曲面原理与实现

曲面建模这块,说实话,是很多工程师的坎。我刚入行那会儿,总觉得曲线曲面就是数学公式堆出来的,离实际工程很远。直到有一次做汽车A级曲面评审,甲方拿着光顺检查图,指着我说“这里曲率变化太突兀”……嗯,从那以后我才真正开始啃曲线曲面的底层逻辑。

今天咱们就把贝塞尔、B样条、NURBS这三兄弟彻底捋清楚。你想想看,它们本质上都是在解决同一个问题:如何用有限的控制点,描述一条光滑的曲线或一张光滑的曲面。只是每往上走一步,解决的能力就更强一些。

核心认知:贝塞尔是基础,B样条解决了局部控制,NURBS统一了自由曲面与解析曲面的表达。这三者构成了现代CAD/CAM系统的几何内核。

4.1 贝塞尔曲线:从伯恩斯坦多项式说起

贝塞尔曲线的数学基础是伯恩斯坦多项式。说白了,它就是一组基函数,用来给控制点分配权重。n次贝塞尔曲线由n+1个控制点定义,公式长这样:

C(t) = Σᵢ₌₀ⁿ Pᵢ · Bᵢ,ₙ(t),  t ∈ [0, 1]

其中 Bᵢ,ₙ(t) = C(n, i) · tⁱ · (1-t)ⁿ⁻ⁱ

我个人习惯把贝塞尔曲线理解成“控制点的加权平均”。t从0走到1,曲线从第一个控制点出发,被中间的点“拉”向某个方向,最后落在最后一个控制点上。注意,曲线一般不经过中间的控制点——这是很多初学者容易搞混的地方。

实用技巧:三次贝塞尔曲线(4个控制点)是工程中最常用的。我在做刀具路径规划时,就用它来插值关键路径点,效果比线性插值平滑得多。

贝塞尔曲线有个明显的短板:全局性。你动任何一个控制点,整条曲线都会跟着变。这在精细调整时非常痛苦。我曾经在做一个logo的曲线轮廓,只是想微调中间一个点,结果两端全跑了,气得我重画了三遍。

4.2 B样条曲线:局部控制的革命

B样条曲线解决了贝塞尔的全局性问题。它的核心思路是:把曲线分成若干段,每段由几个控制点决定,段与段之间保持连续。这样你移动一个控制点,影响的只是附近几段曲线,远处的纹丝不动。

B样条的基函数是递归定义的,也就是著名的Cox-de Boor公式:

Nᵢ,₀(t) = 1  if tᵢ ≤ t < tᵢ₊₁, else 0
Nᵢ,ₖ(t) = (t - tᵢ)/(tᵢ₊ₖ - tᵢ) · Nᵢ,ₖ₋₁(t) + (tᵢ₊ₖ₊₁ - t)/(tᵢ₊ₖ₊₁ - tᵢ₊₁) · Nᵢ₊₁,ₖ₋₁(t)

看着有点绕,但你记住一句话就行:B样条的基函数只在局部区间非零。这就是它能实现局部控制的根本原因。

B样条还有一个重要参数——节点向量(knot vector)。它决定了曲线段之间的拼接方式。均匀节点向量让曲线等距分布,非均匀节点向量则可以在某些区域集中更多控制点,实现更精细的局部形状。

特性 贝塞尔曲线 B样条曲线
控制点影响范围 全局 局部
阶数限制 控制点数-1 可独立设置
连续性控制 隐式 通过节点向量显式控制
工程适用性 简单形状、字体轮廓 复杂自由曲面、汽车/船舶设计

4.3 NURBS曲面:统一自由曲面与解析曲面

NURBS(非均匀有理B样条)是B样条的升级版。它引入了权重(weight)这个维度。每个控制点除了位置坐标,还带一个权重值。权重越大,曲线越“靠近”那个控制点。

为什么需要权重?因为B样条只能表示自由曲面,但工程中经常需要精确的圆弧、椭圆、球面等解析曲面。NURBS通过有理分式,把圆锥曲线也纳入了统一框架。你想想看,一个CAD系统里,如果自由曲面和解析曲面用两套不同的数学表示,那布尔运算、求交、渲染都会变得极其复杂。NURBS一统天下,这才是它真正的价值。

NURBS曲面公式:
S(u, v) = Σᵢ Σⱼ Nᵢ,ₚ(u) · Nⱼ,ₚ(v) · wᵢⱼ · Pᵢⱼ / Σᵢ Σⱼ Nᵢ,ₚ(u) · Nⱼ,ₚ(v) · wᵢⱼ

这个公式看着复杂,但拆开看就三部分:B样条基函数、权重、控制点。分子是加权求和,分母是归一化因子。

避坑指南:我曾经在做一个螺旋桨叶片建模时,把权重设得过大(超过100),结果曲面出现了严重的“拉扯”变形,局部曲率直接爆表。后来查资料才发现,权重一般建议在0.5到5之间,超过10就要非常小心了。

4.4 三种曲面的对比与选型

实际项目中怎么选?我一般按这个原则来:

  • 简单轮廓、图标、字体:用贝塞尔就够了,计算量小,实现简单。
  • 复杂自由曲面、需要局部微调:上B样条,尤其是汽车、飞机、消费电子产品的造型。
  • 需要精确表示圆弧、球面,同时又有自由曲面:必须用NURBS,这是工业级CAD的标准。

下面这张图展示了三者的递进关系:

曲线曲面技术演进 贝塞尔曲线 全局控制 伯恩斯坦基函数 B样条曲线 局部控制 Cox-de Boor递归 NURBS曲面 权重 + 有理分式 统一自由/解析曲面 选型建议 • 简单形状 → 贝塞尔(计算快,实现简单) • 复杂自由曲面 → B样条(局部控制灵活) • 工业级CAD/精确解析曲面 → NURBS(统一表达)

4.5 实现要点与代码示例

理论说完了,咱们看看代码。下面是一个三次贝塞尔曲线的Python实现,核心就是伯恩斯坦多项式的计算:

import numpy as np

def bernstein(n, i, t):
    """计算伯恩斯坦基函数值"""
    from math import comb
    return comb(n, i) * (t ** i) * ((1 - t) ** (n - i))

def bezier_curve(points, num=100):
    """生成贝塞尔曲线上的点"""
    n = len(points) - 1
    curve = []
    for t in np.linspace(0, 1, num):
        x = sum(bernstein(n, i, t) * points[i][0] for i in range(n+1))
        y = sum(bernstein(n, i, t) * points[i][1] for i in range(n+1))
        curve.append((x, y))
    return curve

# 示例:4个控制点
ctrl_pts = [(0, 0), (1, 3), (4, 2), (5, 0)]
curve_pts = bezier_curve(ctrl_pts)

B样条的实现稍微复杂一些,核心是Cox-de Boor递归。我建议你先把节点向量和基函数计算封装好,再去做曲线求值:

def bspline_basis(i, k, t, knots):
    """计算B样条基函数 N_{i,k}(t)"""
    if k == 0:
        return 1.0 if knots[i] <= t < knots[i+1] else 0.0
    else:
        d1 = knots[i+k] - knots[i]
        d2 = knots[i+k+1] - knots[i+1]
        c1 = (t - knots[i]) / d1 * bspline_basis(i, k-1, t, knots) if d1 != 0 else 0
        c2 = (knots[i+k+1] - t) / d2 * bspline_basis(i+1, k-1, t, knots) if d2 != 0 else 0
        return c1 + c2

性能提示:递归实现基函数在阶数高时效率很低。我在实际项目中会用查表法或动态规划来优化,把基函数值预先计算好存成矩阵,求值时直接查表。

4.6 曲面构建:从曲线到曲面的张量积

曲线扩展到曲面,用的是张量积(tensor product)的思路。简单说,就是沿着u方向和v方向各做一次曲线,然后组合成网格。控制点从一维数组变成二维网格,基函数从一维变成两个方向的乘积。

NURBS曲面的构建步骤:

  1. 定义u方向和v方向的节点向量
  2. 定义控制点网格(m+1行 × n+1列)
  3. 为每个控制点分配权重
  4. 对每个(u, v)参数对,计算两个方向的基函数乘积
  5. 加权求和并归一化,得到曲面上的点

我记得有一次做船体曲面建模,控制点网格有50×80个点,权重调整了整整两天。最后发现,船首部分曲率变化剧烈,需要加密节点向量;船尾部分比较平缓,节点可以稀疏一些。这就是非均匀节点向量的典型应用场景。

关键认知:NURBS的强大之处不在于它能表示多么复杂的形状,而在于它用一套统一的数学框架,同时表达了自由曲面和解析曲面。这意味着你的CAD内核只需要实现一套求值、求导、求交算法,就能处理所有几何形状。

好了,这一章的内容就到这里。贝塞尔、B样条、NURBS这三者的关系,你只要记住一句话:贝塞尔是基础,B样条是局部化,NURBS是统一化。下次做曲面建模时,先想清楚你的需求,再选对工具,能省下不少调试时间。