65.9K
CodeProject 正在变化。 阅读更多。
Home

生成和理解 NURBS 曲线

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2016 年 4 月 24 日

CPOL

6分钟阅读

viewsIcon

49701

downloadIcon

1572

创建 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 的混合函数的通用写法是

$y(t)_{\text{$x_0$ 到 $x_1$}} = \frac{t_1 - t}{t_1 - t_0} \cdot y_0 + \frac{t-t_0}{t_1 - t_0} \cdot y_1 \text{ 对于 } t \in [ t_0, t_1 ]$

t 的起点和终点的常见选择是 \(t_0 = 0\)\(t_1 = 1\),从而得到一个更易读的公式:

$y(t)_{\text{$x_0$ 到 $x_1$}} = (1-t) \cdot y_0 + t \cdot y_1 $

稍后,当我想处理 B 样条混合函数时,我将需要 \(t_0\)\(t_1\) 值的通用形式。您还应该知道,将两个函数相加总是等于

$\frac{t_1 - t}{t_1 - t_0} + \frac{t-t_0}{t_1 - t_0} = 1$

在数学中,这被称为 \(y_0\)\(y_1\) 的凸组合。

乍一看,这似乎并没有带来任何好处。但是,让我们再添加一个点,称为 \((x_2,y_2)\),并设置 \((x_1,y_1)\)\((x_2,y_2)\) 之间的混合公式:

$y(t)_{\text{$x_1$ 到 $x_2$}} = (1-t) \cdot y_1+ t \cdot y_2$

我现在可以将两个结果方程组合成一个新的混合函数:

$y(t)_{\text{$x_0$ 到 $x_2$}} = (1-t) \cdot y(t)_{\text{$x_0$ 到 $x_1$}} + t \cdot y(t)_{\text{$x_1$ 到 $x_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 样条开始:

$N_{i,0}(t) = \begin{cases} 1 & \text{如果 $t_i \le t \lt t_{i+1}$} \\[2ex] 0 & \text{否则} \end{cases}$

连接低次 B 样条函数和高次 B 样条函数的递归公式如下:

$N_{i,p}(t)=\frac{t - t_i}{t_{i+p}-t_i}N_{i, p-1}(t) + \frac{t_{i+p+1}-t}{t_{i+p+1}-t_{i+1}}N_{i+1,p-1}$

因此,可以构建 \(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\) 实际上是一个贝塞尔曲线,更高的次数将没有足够的控制点来生成它。贝塞尔样条的节点向量是:

$U = \{ \underbrace{0, \ldots , 0}_{p+1} , \underbrace{1, \ldots , 1}_{p+1} \}$

任何低于该次数且通过起点和终点(当它们被固定时,或者开放时,遗憾的是文献中的术语有所不同)的曲线,都将具有通用形式:

$U = \{ \underbrace{0, \ldots , 0}_{p+1} , u_{p+1}, \ldots , u_{m-p-1}, \underbrace{1, \ldots , 1}_{p+1} \}$

有许多不同的方法可以创建具有不同节点向量系数的 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

本文还包含一些对其他网站的引用,我从中获取了一些图片。

 

 

© . All rights reserved.