生成和理解 NURBS 曲线





5.00/5 (10投票s)
创建 B 样条和 NURBS 曲线,并学习如何使用和操作它们。
引言
每次遇到需要根据一些点创建平滑曲线的情况时,总会有一个东西出现,那就是 NURBS。网上的教程几乎总是过于理论化,让我难以理解,我很难弄清楚我关于它们的疑问。总的来说,我想了解两件事。第一,如何创建它们;第二,如何操作和使用它们。我很难找到任何简单的、带有工作代码并且解释清楚的教程。
背景
为了解释 B 样条背后的基本概念,我将暂时回顾贝塞尔曲线。如果您还没有看过我关于贝塞尔曲线的文章,我强烈建议您阅读它(样条插值 - 历史、理论和实现)。
假设我有两个点,分别是 \((x_0,y_0)\) 和 \((x_1,y_1)\),并希望用一条简单的直线连接它们,即一度贝塞尔曲线。为了混合这两个点,我还需要一个值 t,它告诉我是否接近第一个点 \((x_0,y_0)\),我将该点对应的 t 值称为 \(t_0\),并将点 \((x_1,y_1)\) 的 t 值称为 \(t_1\)。我还要求 \(t_1 > t_0\),并且当函数值低于 \(t_0\) 时等于零,高于 \(t_1\) 时等于零。x 和 y 的混合函数的通用写法是
t 的起点和终点的常见选择是 \(t_0 = 0\) 和 \(t_1 = 1\),从而得到一个更易读的公式:
稍后,当我想处理 B 样条混合函数时,我将需要 \(t_0\) 和 \(t_1\) 值的通用形式。您还应该知道,将两个函数相加总是等于
在数学中,这被称为 \(y_0\) 和 \(y_1\) 的凸组合。
乍一看,这似乎并没有带来任何好处。但是,让我们再添加一个点,称为 \((x_2,y_2)\),并设置 \((x_1,y_1)\) 和 \((x_2,y_2)\) 之间的混合公式:
我现在可以将两个结果方程组合成一个新的混合函数:
这三个点现在将组合成一个二次分段多项式贝塞尔曲线,并且上述公式可以通过下图直观地说明(来自 Wikipedia):

B 样条
B 样条允许您调整曲线的次数,但为了能够做到这一点,您需要能够调整 t 的实现时机,这一点非常清楚。事实上,我们确实需要一个单独的向量来存储 t 值。
我们还必须对节点向量 U 施加一些限制,其中元素数量为 \(m\),排列方式为 \({t_0}, \cdots , t_i , \cdots, t_{m-1}\),并且序列必须满足 \(t_i \le t_{i+1}\)。此外,我还选择区间为 \(t_0 = 0\) 和 \(t_{m-1}=1\)。节点跨度是半开的,意味着区间包含 \([u_i, u_{i+1})\),在此区间之外(如果两个值相等),相关的节点跨度也将为 0。此外,节点数 \(m\)、次数 \(p\) 和控制点数 \(n\) 通过公式 \(n = m - p -1\) 相关联。
为了使用 Cox-de Boor 公式计算次数为 p 的 i-th B 样条函数,首先从 0 次的 B 样条开始:
连接低次 B 样条函数和高次 B 样条函数的递归公式如下:
因此,可以构建 \(N_{i,p}(t)\) 的代码。
/// <summary>
/// This code is translated to C# from the original C++ code given on page 74-75 in "The NURBS Book" by Les Piegl and Wayne Tiller
/// </summary>
/// <param name="i">Current control pont</param>
/// <param name="p">The picewise polynomial degree</param>
/// <param name="U">The knot vector</param>
/// <param name="u">The value of the current curve point. Valid range from 0 <= u <=1 </param>
/// <returns>N_{i,p}(u)</returns>
private double Nip(int i, int p, double[] U, double u)
{
double[] N = new double[p + 1];
double saved, temp;
int m = U.Length - 1;
if ((i == 0 && u == U[0]) || (i == (m - p - 1) && u == U[m]))
return 1;
if (u < U[i] || u >= U[i + p + 1])
return 0;
for (int j = 0; j <= p; j++)
{
if (u >= U[i + j] && u < U[i + j + 1])
N[j] = 1d;
else
N[j] = 0d;
}
for (int k = 1; k <= p; k++)
{
if (N[0] == 0)
saved = 0d;
else
saved = ((u - U[i]) * N[0]) / (U[i + k] - U[i]);
for (int j = 0; j < p - k + 1; j++)
{
double Uleft = U[i + j + 1];
double Uright = U[i + j + k + 1];
if (N[j + 1] == 0)
{
N[j] = saved;
saved = 0d;
}
else
{
temp = N[j + 1] / (Uright - Uleft);
N[j] = saved + (Uright - u) * temp;
saved = (u - Uleft) * temp;
}
}
}
return N[0];
}
开头有一些特殊情况,以及对非零节点向量的预检查,使得代码看起来比实际情况更复杂。第二个 for 循环实现了递归的 Cox-de Boor 公式。
也可以为 B 样条的任何导数创建 B 样条函数。尽管我在这里不提供代码,但可以在《NURBS 书》中找到,其中的 Nip 代码也是出自那里。
假设我们有一个有效的 B 样条函数节点向量和次数,通过计算 t 从 0 到 1 的所有值,可以轻松生成曲线。
Point BSplinePoint(PointCollection Points, int degree, double[] KnotVector, double t)
{
double x, y;
x = 0;
y = 0;
for (int i = 0; i < Points.Count; i++)
{
double temp = Nip(i, degree, KnotVector, t);
x += Points[i].X * temp;
y += Points[i].Y * temp;
}
return new Point(x, y);
}
创建 B 样条节点向量
B 样条曲线的最高次数 \(p\) 实际上是一个贝塞尔曲线,更高的次数将没有足够的控制点来生成它。贝塞尔样条的节点向量是:
任何低于该次数且通过起点和终点(当它们被固定时,或者开放时,遗憾的是文献中的术语有所不同)的曲线,都将具有通用形式:
有许多不同的方法可以创建具有不同节点向量系数的 B 样条,唯一的要求是下一个值等于或大于前一个值。当节点向量被固定时,它被称为非周期或非均匀 B 样条,原因是它在生成样条时包含的控制点跨度从 1 到选定的次数不等,如下例所示:
程序提供了两种生成节点向量的方法。一种具有均匀的内部节点跨度,并在开头和结尾处固定;另一种对所有点都具有均匀的节点跨度。
private void CalcualteKnotVector(int Degree,int ContolPointCount, bool IsUniform)
{
if (Degree + 1 > ContolPointCount || ContolPointCount == 0)
return;
StringBuilder outText = new StringBuilder();
int n = ContolPointCount;
int m = n + Degree + 1;
int divisor = m - 1 - 2 * Degree;
if (IsUniform)
{
outText.Append("0");
for (int i = 1; i < m; i++)
{
if (i >= m - 1)
outText.Append(", 1");
else
{
int dividend = m-1;
outText.Append(", " + i.ToString() + "/" + dividend.ToString());
}
}
}
else
{
outText.Append("0");
for (int i = 1; i < m; i++)
{
if (i <= Degree)
outText.Append(", 0");
else if (i >= m - Degree - 1)
outText.Append(", 1");
else
{
int dividend = i - Degree;
outText.Append(", " + dividend.ToString() + "/" + divisor.ToString());
}
}
}
txtKnotVector.Text= outText.ToString();
}
它们产生的系列当然可以根据您的需要进行修改,但以这些为起点对我来说在开始时帮助很大。您还应该注意,您可以在节点向量中以负数开始,并在大于 1 的数字处结束,这通常用于创建多边形形状。
非均匀有理 B 样条 (NURBS) 曲线
一旦您理解了 B 样条曲线,有理 B 样条就非常简单了。每个控制点都有一个权重与之关联,如果所有点的权重都相等,那么您将得到标准的 B 样条曲线。这使您可以控制曲线围绕各个点的形状,而无需更改次数或控制点位置,从而从人类的角度使操作非常直观。代码非常简单:
Point RationalBSplinePoint(ObservableCollection<RationalBSplinePoint> Points, int degree, double[] KnotVector, double t)
{
double x, y;
x = 0;
y = 0;
double rationalWeight = 0d;
for (int i = 0; i < Points.Count; i++)
{
double temp = Nip(i, degree, KnotVector, t)*Points[i].Weight;
rationalWeight += temp;
}
for (int i = 0; i < Points.Count; i++)
{
double temp = Nip(i, degree, KnotVector, t);
x += Points[i].MyPoint.X*Points[i].Weight * temp/rationalWeight;
y += Points[i].MyPoint.Y * Points[i].Weight * temp / rationalWeight;
}
return new Point(x, y);
}
参考
本文的主要资源是:
- 《NURBS 书》,第二版,作者 Les Piegl 和 Wayne Tiller
- 《样条实用指南》,作者 Carl de Boor
本文还包含一些对其他网站的引用,我从中获取了一些图片。