贝塞尔曲线

贝塞尔曲线

贝塞尔曲线得名于法国工程师贝塞尔(Pierre Bézier)。他从1962年开始大力推广其应用。最初应用于汽车造型设计中车身曲线拟合。

##一阶贝塞尔曲线

一阶贝塞尔曲线需要两个控制点 $P_{0} , P_{1} $, 它的参数方程如下所示:
B ( t ) = P 0 + t ( P 1 − P 0 ) = ( 1 − t ) P 0 + t P 1 ,    t ∈ [ 0 , 1 ] B(t) = P_{0} + t(P_{1} - P_{0}) = (1-t)P_{0} + t P_{1}, ~~ t\in[0,1] B(t)=P0+t(P1P0)=(1t)P0+tP1,  t[0,1]

其中 t t t 为参数。一阶贝塞尔曲线上各点是两个控制点 P 0 P_{0} P0 和 $ P_{1} $之间的线性插值计算得出, 实际上就是连接两控制点的直线段。

##二阶贝塞尔曲线

二阶贝塞尔曲线需要三个控制点 $P_{0} , P_{1}, P_{2} $. 二阶贝塞尔曲线的解析表达式如下:
B ( t ) = ( 1 − t ) 2 P 0 + 2 t ( 1 − t ) P 1 + t 2 P 2 ,   t ∈ [ 0 , 1 ] B(t) = (1-t)^{2} P_{0} + 2 t (1-t) P_{1} + t^{2} P_{2} , ~~ t\in[0,1] B(t)=(1t)2P0+2t(1t)P1+t2P2  t[0,1]
= ( 1 − t ) 2 P 0 + t ( 1 − t ) P 1 + t ( 1 − t ) P 1 + t 2 P 2 = (1-t)^{2} P_{0} + t (1-t) P_{1} + t (1-t) P_{1}+ t^{2} P_{2} =(1t)2P0+t(1t)P1+t(1t)P1+t2P2
= ( 1 − t ) [ ( 1 − t ) P 0 + t P 1 ] + t [ ( 1 − t ) P 1 + t P 2 ] = (1-t)[(1-t) P_{0} + t P_{1}] + t[ (1-t) P_{1}+ t P_{2} ] =(1t)[(1t)P0+tP1]+t[(1t)P1+tP2]
= [ ( 1 − t ) 2 2 ( 1 − t ) t t 2 ] [ P 0 P 1 P 2 ] = \begin{bmatrix} (1-t)^2 & 2(1-t)t & t^2 \end{bmatrix} \begin{bmatrix} P_{0} \\ P_{1} \\ P_{2} \end{bmatrix} =[(1t)22(1t)tt2] P0P1P2
= [ 1 t t 2 ] [ 1 0 0 − 2 2 0 1 − 2 1 ] [ P 0 P 1 P 2 ] = \begin{bmatrix} 1 & t & t^2 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0\\ -2 & 2& 0 \\1& -2 & 1\end{bmatrix} \begin{bmatrix} P_{0} \\ P_{1} \\ P_{2} \end{bmatrix} =[1tt2] 121022001 P0P1P2

二阶贝塞尔曲线绘制过程如图所示[1]
2nd-order-Bezier

由二阶贝塞尔曲线参数方程
B ( t ) = ( 1 − t ) [ ( 1 − t ) P 0 + t P 1 ] + t [ ( 1 − t ) P 1 + t P 2 ] B(t) = (1-t)[(1-t) P_{0} + t P_{1}] + t[ (1-t) P_{1}+ t P_{2} ] B(t)=(1t)[(1t)P0+tP1]+t[(1t)P1+tP2]
可以看出,二阶贝塞尔曲线是两个一阶贝塞尔曲线的线性插值:

  • 首先计算出 P 0 P_{0} P0 P 1 P_{1} P1两个控制点之间的插值点 P 01 = ( 1 − t ) P 0 + t P 1 P_{01} = (1-t) P_{0}+ t P_{1} P01=(1t)P0+tP1,
  • 然后计算出 P 1 P_{1} P1 P 2 P_{2} P2两个控制点之间的插值点 P 12 = ( 1 − t ) P 1 + t P 2 P_{12} = (1-t) P_{1}+ t P_{2} P12=(1t)P1+tP2,
  • 最后再取 P 01 P_{01} P01 P 02 P_{02} P02两点之间的插值点$P = (1-t) P_{01} + t P_{12} , 点 , 点 ,P$ 即二阶贝塞尔曲线上的点。

t = 0.5 t=0.5 t=0.5时, 计算过程如下图所示:
2nd-order-Bezier-t-0.5

将二阶贝塞尔曲线公式对参数 t t t 求导,
B ′ ( t ) = 2 ( 1 − t ) ( P 1 − P 0 ) + 2 t ( P 2 − P 1 ) B'(t) = 2(1-t)(P_{1} - P_{0}) + 2t( P_{2} - P_{1}) B(t)=2(1t)(P1P0)+2t(P2P1)
由上式和二阶贝塞尔曲线图可看出, 贝塞尔曲线在端点$P_{0} 处切线为 处切线为 处切线为 P_{0} P_{1} , 在端点 , 在端点 ,在端点P_{2} 处切线为 处切线为 处切线为 P_{1} P_{2} , 贝塞尔曲线在两端点 , 贝塞尔曲线在两端点 ,贝塞尔曲线在两端点P_{0} , P_{2} 处切线相交于 处切线相交于 处切线相交于 P_{1} $. 二阶贝塞尔曲线上每一点的导数是两个端点处 曲线导数的线性插值。

二阶贝塞尔曲线公式对参数 t t t 的二阶导数为,
B ′ ′ ( t ) = 2 ( P 2 − 2 P 1 + P 0 ) B''(t) = 2( P_{2} - 2P_{1} + P_{0}) B′′(t)=2(P22P1+P0)
由上式可知,贝塞尔曲线从端点$P_{0} 处开始脱离 处开始脱离 处开始脱离 P_{0} P_{1} ,并逐渐在端点 , 并逐渐在端点 ,并逐渐在端点P_{2} $ 处逼近 $ P_{1} P_{2} $ .

##三阶贝塞尔曲线

需要四个控制点 $P_{0} , P_{1}, P_{2}, P_{3} $

B ( t ) = ( 1 − t ) 3 P 0 + 3 t ( 1 − t ) 2 P 1 + 3 t 2 ( 1 − t ) P 2 + t 3 P 3 ,    t ∈ [ 0 , 1 ] B(t) = (1-t)^{3} P_{0} +3 t (1-t)^{2} P_{1} +3 t^{2}(1-t) P_{2 } + t^{3} P_{3} , ~~ t\in[0,1] B(t)=(1t)3P0+3t(1t)2P1+3t2(1t)P2+t3P3,  t[0,1]

##n阶贝塞尔曲线

需要(n+1)个控制点 $P_{0} , P_{1}, P_{2}, \cdots,P_{n} $
B ( t ) = ∑ i = 0 n C i n P i t i ( 1 − t ) n − i ,    t ∈ [ 0 , 1 ] B(t) = \sum_{i=0}^{n}C_{i}^{n} P_{i}t^{i}(1-t)^{n-i}, ~~ t\in[0,1] B(t)=i=0nCinPiti(1t)ni,  t[0,1]
其中 C i n = n ! i ! ( n − i ) ! C_{i}^{n} = \frac{n!}{i!(n-i)!} Cin=i!(ni)!n! .

B P 0 , ⋯   , P n ( t ) = ( 1 − t ) B P 0 , ⋯   , P n − 1 ( t ) + t B P 1 , ⋯   , P n ( t ) ,    t ∈ [ 0 , 1 ] B_{P_{0}, \cdots, P_{n}} (t) = (1-t)B_{P_{0}, \cdots, P_{n-1}} (t) + t B_{P_{1}, \cdots, P_{n}}(t) ,~~ t\in[0,1] BP0,,Pn(t)=(1t)BP0,,Pn1(t)+tBP1,,Pn(t),  t[0,1]
如上式所示, n n n阶贝塞尔曲线是两个 ( n − 1 ) (n-1) (n1)阶贝塞尔曲线的线性插值.

#Code

//
//   Author: Chunfeng Yang
//   Version: 0.2.0
//
// Parameters: 
//    controlPoints  -- control points array of the Bezier curve
//             It contains the coordinates of control points
//             data type: Array
//
//    t     -- parameter t of the Biezier curve
//             data type: float
//
//    start -- the index of start control point in the strP array 
//             data type: int
//
//    end   -- the index of end point in the strP array  
//             data type: int
//
function BezierCurve( controlPoints, t, start, end )
{

  if( undefined === controlPoints )
  {
    console.error('ERROR: undefined point array ');
    return;
  }

  if( Object.prototype.toString.call( controlPoints ) !== '[object Array]' ) 
  {
    console.error('ERROR: invalided point array ');
    return;
  }

  if( undefined === t )
  { 
    console.log('Warning: t is undefined, using default value: t = 0.0 ');
    t = 0.0;
  } 
  if( undefined === start )
  {
    console.log('Warning: start is undefined, using default value: start = 0');
    start = 0;
  }
  if( undefined === end )
  {
    console.log('Warning: end is undefined, using default value: end = controlPoints.length - 1');
    end = controlPoints.length - 1;
  }

  if( parseInt(start) > parseInt(end) - 1 )
  {
    console.error('ERROR: start > end - 1 ');
    return;
  }
 
  var len = controlPoints.length;
  if( 0 === parseInt(len) )
  {
    console.error('ERROR: point array length is ZERO');
    return;
  }

  if( parseInt(start) < 0 )
  {
    console.log('Warning: start is invalided, using default value: start = 0');
    start = 0;
  }
  if( parseInt(end) < 1 )
  {
    console.log('Warning: end is invalided, using default value: end = controlPoints.length - 1');
    end = controlPoints.length - 1;
  }
  if( parseInt(start) > parseInt(len) - 1 )
  {
    console.log('Warning: start is invalided, using default value: start = 0');
    start = 0;
  }
  if( parseInt(end) > parseInt(len) - 1 )
  {
    console.log('Warning: end is invalided, using default value: end = controlPoints.length - 1');
    end = controlPoints.length - 1;
  }

     if( 1 == ( parseInt(end) - parseInt(start) ) )
     {
       var p1 = controlPoints[start];  
       var p2 = controlPoints[end];  

       var result = [];
       for( var item in p1 )  
       {
            var delta =  parseFloat( p2[item] ) -  parseFloat( p1[item] )
            result.push( parseFloat( p1[item] ) + t * parseFloat( delta ) );
        }
        return result;

      }else {
       var p1 = BezierCurve( controlPoints, t, start, parseInt(end) -1 ) ; 
       var p2 = BezierCurve( controlPoints, t, parseInt(start) + 1, end  ); 
       var result = [];
       for( var item in p1 )  
       {
            result.push(( 1-parseFloat(t)) * parseFloat( p1[item] ) + t * parseFloat(p2[item]));
        }
        return result;
     }
     
  return;
}

Testing Scenario
取一平面二阶Bezier曲线,其控制点有三个,分别为[10, 40 ], [20, 30 ]和 [30, 40]。当参数t=0.5时,曲线中点坐标应为[20,35].

var f;
var result = 0;

//
// Test 1 
//
var a = "Hello world"
result = BezierCurve( a, 0.5, 0, 2 )
if( undefined == result )
{
  console.log( "Test 1: controlPoints type is String test -- OK" );
}


//
// Test 2 
//
var a = 3.2 
result = BezierCurve( a, 0.5, 0, 2 )
if( undefined == result )
{
  console.log( "Test 2: controlPoints type is Number test -- OK" );
}


//
// Test 3 
//
var a = {"Helloworld":3}
result = BezierCurve( a, 0.5, 0, 2 )
if( undefined == result )
{
  console.log( "Test 3: controlPoints type is JSON test -- OK" );
}


//
// Test 4 
//
var a = {"Helloworld":3}
result = BezierCurve( f, 0.5, 0, 2 )
if( undefined == result )
{
  console.log( "Test 4: undefined controlPoints test -- OK" );
}

//
// Test 5 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, f, 0, 2 )
if( undefined !== result )
{
  if( ( 10 == parseInt(result[0]) ) & ( 40 == parseInt(result[1]) ) )
  {
    console.log( "Test 5: undefined t test -- OK" );
  }
}


//
// Test 6 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, 0, f, 2 )
if( undefined !== result )
{
  if( ( 10 == parseInt(result[0]) ) & ( 40 == parseInt(result[1]) ) )
  {
    console.log( "Test 6: undefined start test -- OK" );
  }
}


//
// Test 7 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, 0, 0, f )
if( undefined !== result )
{
  if( ( 10 == parseInt(result[0]) ) & ( 40 == parseInt(result[1]) ) )
  {
    console.log( "Test 7: undefined end test -- OK" );
  }
}


//
// Test 8 
//
var a = [ ];
result = BezierCurve( a, 0, 0, 2 )
if( undefined == result )
{
  console.log( "Test 8: point array length is ZERO test -- OK" );
}


//
// Test 9 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, 0, 2, 2 )
if( undefined == result )
{
  console.log( "Test 9: start > end - 1 test -- OK" );
}


//
// Test 10 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, 0, 9, 10 )
if( undefined !== result )
{
  if( ( 10 == parseInt(result[0]) ) & ( 40 == parseInt(result[1]) ) )
  {
    console.log( "Test 10: invalided end test -- OK" );
  }
}


//
// Test 11 
//
var a = [[10, 40 ], [20, 30 ], [30, 40], [40, 30]];
result = BezierCurve( a, 0.5, 0, 2 )
if( undefined !== result )
{
  if( ( 20 == parseInt(result[0]) ) & ( 35 == parseInt(result[1]) ) )
  {
    console.log( "Test 11:  calculation test -- OK" );
  }
}
console.log(result);

Results:

ERROR: invalided point array
Test 1: controlPoints type is String test -- OK
ERROR: invalided point array
Test 2: controlPoints type is Number test -- OK
ERROR: invalided point array
Test 3: controlPoints type is JSON test -- OK
ERROR: undefined point array
Test 4: undefined controlPoints test -- OK
Warning: t is undefined, using default value: t = 0.0
Test 5: undefined t test -- OK
Warning: start is undefined, using default value: start = 0
Test 6: undefined start test -- OK
Warning: end is undefined, using default value: end = controlPoints.length - 1
Test 7: undefined end test -- OK
ERROR: point array length is ZERO
Test 8: point array length is ZERO test -- OK
ERROR: start > end - 1
Test 9: start > end - 1 test -- OK
Warning: start is invalided, using default value: start = 0
Warning: end is invalided, using default value: end = controlPoints.length - 1
Test 10: invalided end test -- OK
Test 11:  calculation test -- OK
[ 20, 35 ]


[1] https://en.wikipedia.org/wiki/B%C3%A9zier_curve
[2] http://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/
[3] https://www.zhihu.com/question/29565629
[4] https://www.scratchapixel.com/lessons/advanced-rendering/bezier-curve-rendering-utah-teapot/bezier-curve
[5] Bezier.js https://github.com/Pomax/bezierjs
[6] http://web.cs.wpi.edu/~matt/courses/cs563/talks/surface/bez_surf.html
[7] https://www.scratchapixel.com/lessons/advanced-rendering/bezier-curve-rendering-utah-teapot
[8] http://paulbourke.net/geometry/bezier/
[9] https://pomax.github.io/bezierinfo/
[10] https://www.particleincell.com/2012/bezier-splines/
[11] https://www.particleincell.com/2013/cubic-line-intersection/
[12] https://pomax.github.io/bezierinfo/

### 回答1: 三维空间曲线的参数方程一般形如: x = x(t) y = y(t) z = z(t) 其中 t 是参数。 曲率半径公式如下: R = [x'(t)^2 + y'(t)^2 + z'(t)^2]^(3/2) / [x''(t)y'(t) - x'(t)y''(t)] 其中 x', y', z' 分别表示 x, y, z 的一阶导数,x'', y'', z'' 分别表示 x, y, z 的二阶导数。 可以发现,上述公式中包含了偏导数,这是因为三维空间曲线在不同方向上的曲率不同。 ### 回答2: 三维空间中的曲线参数方程可以表示为x=f(t),y=g(t),z=h(t),其中t为参数。曲率是描述曲线弯曲程度的一个重要性质,可以通过曲线的参数方程来计算。 曲率半径公式是描述曲线曲率与参数关系的公式。对于三维空间曲线参数方程,曲率半径公式为: ρ = |(dx/dt × dy/dt × dz/dt)| / |(dx/dt)² + (dy/dt)² + (dz/dt)²|^(3/2) 其中,ρ表示曲线在某一点处的曲率半径,dx/dt、dy/dt、dz/dt代表曲线参数方程中x、y、z分别对参数t的导数。 曲率半径公式的分子部分表示向量(dx/dt × dy/dt × dz/dt)的模长,即曲线切向量和法向量的叉乘结果的模长。分母部分表示切向量的模长的立方。 曲率半径公式可以用来计算曲线在某一点处的曲率半径,并能判断曲线在该点处是否为直线、圆、抛物线等。如果曲率半径为正数,表示曲线是凹向外的曲线,即局部地呈现出曲面的凸性质;如果曲率半径为负数,表示曲线是凹向内的曲线,即局部地呈现出曲面的凹性质。当曲率半径趋近于无穷大时,表示曲线是直线;当曲率半径为常数时,表示曲线是圆。曲率半径公式的应用广泛,可以在计算机图形学、机械设计、物理学等领域中得到应用。 ### 回答3: 三维空间曲线的参数方程描述了曲线在三维空间中的位置。曲线的曲率是描述曲线弯曲程度的一个重要指标,用于衡量曲线在某点的弯曲程度。根据参数方程可求得曲线的切线向量和法向量,通过计算切线向量和法向量的夹角可以求得曲线在该点的曲率。 对于三维空间曲线的参数方程 x(t),y(t),z(t) ,其中 t 为参数,曲线的切线向量可以表示为: T(t) = (dx/dt, dy/dt, dz/dt) 曲线的切向量的模长可以表示为: |T(t)| = sqrt((dx/dt)² + (dy/dt)² + (dz/dt)²) 从而曲线的切线向量可以表示为: T(t) = (dx/dt, dy/dt, dz/dt) / |T(t)| 曲线的法向量 N(t) 可以通过对切向量 T(t) 求导得到: N(t) = (d²x/dt², d²y/dt², d²z/dt²) / |T(t)| 曲线在参数 t 对应的点的曲率可以表示为: k(t) = |dN/dt| / |T(t)| 其中 dN/dt 表示对法向量 N(t) 对参数 t 的导数。 根据以上公式,我们可以计算出曲线在每个参数 t 对应点的曲率。曲率半径的定义为曲率的倒数,即: R(t) = 1 / k(t) 曲率半径表示了曲线在某点的曲率大小,值越大表示曲线越平缓,值越小表示曲线越陡峭。通过计算曲率半径,我们可以了解到曲线在不同点上的变化情况,有助于对曲线的形状进行分析和研究。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值