贝塞尔曲线
贝塞尔曲线得名于法国工程师贝塞尔(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(P1−P0)=(1−t)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)=(1−t)2P0+2t(1−t)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}
=(1−t)2P0+t(1−t)P1+t(1−t)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} ]
=(1−t)[(1−t)P0+tP1]+t[(1−t)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}
=[(1−t)22(1−t)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]
1−2102−2001
P0P1P2
二阶贝塞尔曲线绘制过程如图所示[1]
由二阶贝塞尔曲线参数方程
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)=(1−t)[(1−t)P0+tP1]+t[(1−t)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=(1−t)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=(1−t)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时, 计算过程如下图所示:
将二阶贝塞尔曲线公式对参数
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(1−t)(P1−P0)+2t(P2−P1)
由上式和二阶贝塞尔曲线图可看出, 贝塞尔曲线在端点$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(P2−2P1+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)=(1−t)3P0+3t(1−t)2P1+3t2(1−t)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=0∑nCinPiti(1−t)n−i, t∈[0,1]
其中
C
i
n
=
n
!
i
!
(
n
−
i
)
!
C_{i}^{n} = \frac{n!}{i!(n-i)!}
Cin=i!(n−i)!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)=(1−t)BP0,⋯,Pn−1(t)+tBP1,⋯,Pn(t), t∈[0,1]
如上式所示,
n
n
n阶贝塞尔曲线是两个
(
n
−
1
)
(n-1)
(n−1)阶贝塞尔曲线的线性插值.
#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/