登陆后访问



H
I
S
T
O
R
Y

数学之美|贝塞尔曲线方程

摘要:本文介绍了贝塞尔曲线及其基本方程,体验贝塞尔曲线之美。
一、贝塞尔曲线

图1
贝塞尔曲线(The Bézier Curves),是一种在计算机图形学中相当重要的参数曲线(2D、3D的称为曲面)。贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所发表,他运用贝塞尔曲线来为汽车的主体进行设计。
贝塞尔曲线是应用于二维图形应用程序的数学曲线。曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线。
B(t)为t时间下点的坐标,P0为起点,Pn为终点,Pi为控制点。
1. 一阶贝塞尔曲线(线段)
\(B\left ( t \right )=\left ( 1-t \right )P_{0}+tP_{1}\)\(t\in \left [ 0,1 \right ]\)
意义:由 P0 至 P1 的连续点, 描述的一条线段。

图2
2. 二阶贝塞尔曲线(抛物线)
\(B\left ( t \right )=\left ( 1-t \right )^{2}P_{0}+2t\left ( 1-t \right )P_{1}+t^{2}P_{2}\)\(t\in \left [ 0,1 \right ]\)

图3
原理:由P0至P1的连续点Q0,描述一条线段;由P1至P2的连续点Q1,描述一条线段;由Q0至Q1的连续点B(t),描述一条二次贝塞尔曲线。
经验:P1-P0为曲线在P0处的切线。
3. 三阶贝塞尔曲线
\(B\left ( t \right )=\left ( 1-t \right )^{3}P_{0}+3t\left ( 1-t \right )^{2}P_{1}+3t^{2}\left ( 1-t \right )P_{2}+t^{3}P_{3}\)\(t\in \left [ 0,1 \right ]\)

图4
4. 通用公式
\(P_{i}^{k}= \begin{cases} P_{i}& k=0 \\ \left ( 1-t \right )P_{i}^{k-1}+tP_{i+1}^{k-1}& k=1,2,...,n,i=0,1,...,n-k \end{cases}\)
5. 高阶贝塞尔曲线
(1) 4阶曲线

图5
(2) 5阶曲线

图6
二、贝塞尔曲线的数学原理
贝塞尔曲线是“参数”方程的一种形式。从数学上讲,参数方程作弊了:“方程”实际上是一个从输入到唯一输出的、良好定义的映射关系。几个输入进来,一个输出返回。改变输入变量,还是只有一个输出值。参数方程在这里作弊了。它们基本上干了这么件事,“好吧,我们想要更多的输出值,所以我们用了多个方程”。举个例子:假如我们有一个方程,通过一些计算,将假设为x的一些值映射到另外的值:
\(f\left ( x \right )=\cos \left (x \right )\)
记号f(x)是表示函数的标准方式(为了方便起见,如果只有一个的话,我们称函数为f),函数的输出根据一个变量(本例中是x)变化。改变x,f(x)的输出值也会变。
到目前没什么问题。现在,让我们来看一下参数方程,以及它们是怎么作弊的。我们取以下两个方程:
\(f\left ( a \right )=\cos \left (a \right )\)
\(f\left ( b \right )=\sin \left (b \right )\)

这俩方程没什么让人印象深刻的,只不过是正弦函数和余弦函数,但正如你所见,输入变量有两个不同的名字。如果我们改变了a的值,f(b)的输出不会有变化,因为这个方程没有用到a。参数方程通过改变这点来作弊。在参数方程中,所有不同的方程共用一个变量,如下所示:
\(\begin{cases} f_{a} \left ( t \right )=\cos\left ( t \right )\\ f_{b} \left ( t \right )=\sin\left ( t \right ) \end{cases}\)
多个方程,但只有一个变量。如果我们改变了t的值,fa(t)和fb(t)的输出都会发生变化。你可能会好奇这有什么用,答案其实很简单:对于参数曲线,如果我们用常用的标记来替代fa(t)和fb(t),看起来就有些明朗了:
\(\begin{cases} x=\cos\left ( t \right )\\ y=\sin\left ( t \right ) \end{cases}\)
好了,通过一些神秘的t值将x/y坐标系联系起来。
所以,参数曲线不像一般函数那样,通过x坐标来定义y坐标,而是用一个“控制”变量将它们连接起来。如果改变t的值,每次变化时我们都能得到两个值,这可以作为图形中的(x,y)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使t在正负极值间变化,得到的输出(x,y)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出t从0到5时的值,将得到如下图像(你可以用上下键来改变画的点和值):

图7:(一部分的)圆:x=sin(t),y=cos(t)
贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中x值和y值使用了不同的方程,与此不同的是,贝塞尔曲线的x和y都用了“二项多项式”。那什么是二项多项式呢?
你可能记得高中所学的多项式,看起来像这样:
\(f\left ( x \right )=a\cdot x^{3}+b\cdot x^{2}+c\cdot x+d\)
如果它的最高次项是x³就称为“三次”多项式,如果最高次项是x²,称为“二次”多项式,如果只含有x的项,它就是一条线(不过不含任何x的项它就不是一个多项式!)
贝塞尔曲线不是x的多项式,它是t的多项式,t的值被限制在0和1之间,并且含有a,b等参数。它采用了二次项的形式,听起来很神奇但实际上就是混合不同值的简单描述:
\(linear=\left ( 1-t \right )+t\)
\(square=\left ( 1-t \right )^{2}+2\cdot \left ( 1-t \right )\cdot t+t^{2}\)
\(cubic=\left ( 1-t \right )^{3}+3\cdot \left ( 1-t \right )^{2}\cdot t+3\cdot \left ( 1-t \right ) \cdot t^{2}+t^{3}\)

我明白你在想什么,这看起来并不简单,但如果我们拿掉t并让系数乘以1,事情就会立马简单很多,看看这些二次项:
\(linear=1+1\)
\(square=1+2+1\)
\(cubic=1+3+3+1\)

\(hypercubic=1+4+6+4+1\)
需要注意的是,2与1+1相同,3相当于2+1或1+2,6相当于3+3......如你所见,每次我们增加一个维度,只要简单地将头尾置为1,中间的操作都是“将上面的两个数字相加”。现在就能很容易地记住了。
还有一个简单的办法可以弄清参数项怎么工作的,如果我们将(1-t)重命名为a,将t重命名为b,暂时把权重删掉,可以得到这个:
\(linear=a+b\)
\(square=a\cdot a+a\cdot b+b\cdot b\)
\(cubic=a\cdot a\cdot a+a\cdot a\cdot b+a\cdot b\cdot b+b\cdot b\cdot b\)

基本上它就是“每个a和b结合项”的和,在每个加号后面逐步的将a换成b。因此这也很简单。现在你已经知道了二次多项式,为了叙述的完整性,我将给出一般方程:
\(Bézier\left ( n,t \right )=\sum_{i=0}^{n}\binom{n}{i}\cdot \left ( 1-t \right )^{n-i}\cdot t^{i}\)
这就是贝塞尔曲线完整的描述。在这个函数中的Σ表示了这是一系列的加法(用Σ下面的变量i,从i=0开始,直到n结束)。

图8
三、如何实现基本方程
我们可以用之前说过的方程,来简单地实现基本方程作为数学构造,如下:
function Bezier(n,t):
  sum = 0
  for(k=0; k<n; k++):
    sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
  return sum
我说我们“可以用”是因为我们不会这么去做:因为阶乘函数开销非常大。并且,正如我们在上面所看到的,我们不用阶乘也能够很容易地构造出帕斯卡三角形:一开始是[1],接着是[1,2,1],然后是[1,3,3,1]等等。下一行都比上一行多一个数,首尾都为1,中间的数字是上一行两边元素的和。
我们可以很快的生成这个列表,并在之后使用这个查找表而不用再计算二次多项式的系数:
lut = [      [1],           // n=0
            [1,1],          // n=1
           [1,2,1],         // n=2
          [1,3,3,1],        // n=3
         [1,4,6,4,1],       // n=4
        [1,5,10,10,5,1],    // n=5
       [1,6,15,20,15,6,1]]  // n=6
binomial(n,k):
  while(n >= lut.length):
    s = lut.length
    nextRow = new array(size=s+1)
    nextRow[0] = 1
    for(i=1, prev=s-1; i<prev; i++):
      nextRow[i] = lut[prev][i-1] + lut[prev][i]
    nextRow[s] = 1
    lut.add(nextRow)
  return lut[n][k]
这里做了些什么?首先,我们声明了一个足够大的查找表。然后,我们声明了一个函数来获取我们想要的值,并且确保当一个请求的n/k对不在LUT查找表中时,先将表扩大。我们的基本函数如下所示:
function Bezier(n,t):
  sum = 0
  for(k=0; k<=n; k++):
    sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
  return sum
完美。当然我们可以进一步优化。为了大部分的计算机图形学目的,我们不需要任意的曲线。我们需要二次曲线和三次曲线(实际上这篇文章没有涉及任意次的曲线,因此你会在其他地方看到与这些类似的代码),这说明我们可以彻底简化代码:
function Bezier(2,t):
  t2 = t * t
  mt = 1-t
  mt2 = mt * mt
  return mt2 + 2*mt*t + t2
function Bezier(3,t):
  t2 = t * t
  t3 = t2 * t
  mt = 1-t
  mt2 = mt * mt
  mt3 = mt2 * mt
  return mt3 + 3*mt2*t + 3*mt*t2 + t3
现在我们知道如何代用码实现基本方程了。
既然我们已经知道基本函数的样子,是时候添加一些魔法来使贝塞尔曲线变得特殊了:控制点。
四、控制贝塞尔的曲率
贝塞尔曲线是插值方程(就像所有曲线一样),这表示它们取一系列的点,生成一些处于这些点之间的值。(一个推论就是你永远无法生成一个位于这些控制点轮廓线外面的点,更普遍是称为曲线的外壳。这信息很有用!)实际上,我们可以将每个点对方程产生的曲线做出的贡献进行可视化,因此可以看出曲线上哪些点是重要的,它们处于什么位置。
下面的图形显示了二次曲线和三次曲线的差值方程,“S”代表了点对贝塞尔方程总和的贡献。点击或拖动点来看看在特定的t值时,每个曲线定义的点的插值百分比。

图9
上面有一张是15阶的插值方程。如你所见,在所有控制点中,起点和终点对曲线形状的贡献比其他点更大些。
如果我们要改变曲线,就需要改变每个点的权重,有效地改变插值。可以很直接地做到这个:只要用一个值乘以每个点,来改变它的强度。这个值照惯例称为“权重”,我们可以将它加入我们原始的贝塞尔函数:
\(Bézier\left ( n,t \right )=\sum_{i=0}^{n}\binom{n}{i}\cdot \left ( 1-t \right )^{n-i}\cdot t^{i}\cdot \omega_{i}\)
看起来很复杂,但实际上“权重”只是我们想让曲线所拥有的坐标值:对于一条n阶曲线,ω0是起始坐标,ωn是终点坐标,中间的所有点都是控制点坐标。假设说一条曲线的起点为(120,160),终点为(220,40),并受点(35,200)和点(220,260)的控制,贝塞尔曲线方程就为:
\(\begin{cases} x=120\cdot \left ( 1-t \right )^{3}+35\cdot 3\cdot \left ( 1-t \right )^{2}\cdot t+220\cdot 3\cdot \left ( 1-t \right ) \cdot t^{2}+220\cdot t^{3}\\ y=160\cdot \left ( 1-t \right )^{3}+200\cdot 3\cdot \left ( 1-t \right )^{2}\cdot t+260\cdot 3\cdot \left ( 1-t \right ) \cdot t^{2}+40\cdot t^{3} \end{cases}\)
这就是我们在文章开头看到的曲线:

图10:我们的三次贝塞尔曲线
我们还能对贝塞尔曲线做些什么?实际上还有很多。文章接下来涉及到我们可能运用到的一系列操作和算法,以及它们可以完成的任务。
五、如何实现权重基本函数
鉴于我们已经知道怎样实现基本函数,在其加入控制点是非常简单的:
function Bezier(n,t,w[]):
  sum = 0
  for(k=0; k<n; k++):
    sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
  return sum
下面是优化过的版本:
function Bezier(2,t,w[]):
  t2 = t * t
  mt = 1-t
  mt2 = mt * mt
  return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
function Bezier(3,t,w[]):
  t2 = t * t
  t3 = t2 * t
  mt = 1-t
  mt2 = mt * mt
  mt3 = mt2 * mt
  return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3
现在我们知道如何编程实现基本权重函数了。
六、贝塞尔曲线Matlab代码
另外让我们一起来看一下贝塞尔曲线的Matlab代码:
function [ Bx By ] = cubic_Bezier_curve( p1,p2,p3,p4)
 % Cubic Bezier Curve
   % Input: four points
   % p1 = [x1,y1]
 % p2 = [x2,y2]
 % p3 = [x3,y3] 
 % p4 = [x4,y4]
   % Output: the Bezier curve (spline) between p1 and p4 
 % using the control points p2 and p3
   % The Interval
 t = [0:0.001:1]; % <---- Adjust for finer resolution if needed
 Lt = length(t);
   % The curve
 % B(t) = (1-t)^3*p1 + 3*(1-t)^2*t*p2 + 3*(1-t)*t^2*p3 + t^3*p4
 x = 1; % x coordinate index
 y = 2; % y coordinate index
 Bx = zeros(1,Lt);
 By = Bx;
 Bx = ((1-t).^3).*p1(x)+((((1-t).^2).*t).*3).*p2(x)+(((1-t).*(t.^2)).*3).*p3(x)+(t.^3).*p4(x);
 By = ((1-t).^3).*p1(y)+((((1-t).^2).*t).*3).*p2(y)+(((1-t).*(t.^2)).*3).*p3(y)+(t.^3).*p4(y);
     end

 

声明:文章转自微信公众号【YaK芽课】,版权归原作者所有,转载仅供学习使用,不用于任何商业用途,如有侵权请联系删除,谢谢。

相关文章

奇妙的数字:巧合数

Posted by - November 10, 2017 425
世界上,无时无刻不在发生着一些看似偶然的巧合,有让人遗憾的,也有让人庆幸的,在数学上,也有很多的巧合。

数学之美|填色游戏

Posted by - November 09, 2017 473
人们提起数学之“美”时常意指其抽象涵义,罗素称之为“朴素冷峻之美……庄严纯净,能够达到严格的完美”。然而,人类也一向从数学中发现审美上的...