贝塞尔曲线(Bezier Curve)在计算机图形领域应用非常广泛,比如我们 CSS 动画、 Canvas 以及 Photoshop 等都可以看到贝塞尔曲线的身影。
贝塞尔曲线类型
贝塞尔曲线根据_控制点_的数量分为:
- 一阶贝塞尔曲线(2 个控制点)
- 二阶贝塞尔曲线(3 个控制点)
- 三阶贝塞尔曲线(4 个控制点)
- n阶贝塞尔曲线(n+1个控制点)
如何绘制出贝塞尔曲线
下图为一个三阶的贝塞尔曲线,包括四个控制点,分别为 P 0 , P 1 , P 2 , P 3 P_0,P_1,P_2,P_3 P0,P1,P2,P3
那我们通过控制点是怎么绘制出贝塞尔曲线的呢?
通过上图的三阶贝塞尔曲线举例,基本的步骤如下:
- 四个控制点通过先后顺序进行连接,形成了三条线段,也就是上图中的 P 0 P 1 , P 1 P 2 , P 2 P 3 P_0P_1,P_1P_2,P_2P_3 P0P1,P1P2,P2P3,然后通过一个参数 t , 其中 t ∈ [ 0 , 1 ] t,其中 t\in[0,1] t,其中t∈[0,1]该参数的值等于线段上某一个点距离起点的长度除以线段长度。就比如 P 0 P 1 P_0P_1 P0P1线段上有一个点 P 0 ′ ,此时 t 的值就是 P_0^{'},此时t的值就是 P0′,此时t的值就是 P 0 P ′ P 0 P 1 \frac{P_0P_{'}}{P_0P_1} P0P1P0P′,其中 P 0 ′ P_0^{'} P0′位置如下图所示。
- 接下来对每一条线段做同样的操作,得到三个控制点 P 0 ′ , P 1 ′ , P 2 ′ P_0^{'},P_1^{'},P_2^{'} P0′,P1′,P2′,如下图所示。
- 然后对这三个控制点重复第1步操作,得出两个控制点 P 0 ′ ′ , P 1 ′ ′ P_0^{''},P_1^{''} P0′′,P1′′,如下图所示。
- 最后再使用同样的方法可以得到,最终的一个点 P 0 ′ ′ ′ P_0^{'''} P0′′′,如下图所示,此时这个点就是贝塞尔曲线上的一个点。
通过控制t的值,由 0 增加至 1,就绘制出了一条由起点P_0至终点P_1的贝塞尔曲线。
通过下面这个动画直观感受一下绘制的过程:
求贝塞尔曲线上的点坐标
贝塞尔曲线的定义
给定n+1个控制点,则n次贝塞尔曲线的定义为
p ( t ) = ∑ i = 0 n P i B i , n ( t ) , t ∈ [ 0 , 1 ] p(t)=\sum_{i=0}^{n} {P_iB_{i,n}(t)},t\in[0,1] p(t)=i=0∑nPiBi,n(t),t∈[0,1]式子中 B i , n ( t ) B_{i,n}(t) Bi,n(t)是贝塞尔基数,表达式为 B i , n ( t ) = C n i t i ( 1 − t ) n − i , i = 0 , 1 , 2... , n B_{i,n}(t)=C_n^it^i(1-t)^{n-i},i=0,1,2...,n Bi,n(t)=Cniti(1−t)n−i,i=0,1,2...,n
1、一阶贝塞尔曲线
对于一阶贝塞尔曲线,我们可以通过几何知识,很容易根据t的值得出线段上那个点的坐标:
p ( t ) = P 0 + ( P 1 − P 0 ) t p(t) = P_0 + (P_1 - P_0)t p(t)=P0+(P1−P0)t
然后可以得出:
p ( t ) = ( 1 − t ) P 0 + t P 1 , t ∈ [ 0 , 1 ] p(t) = (1 - t)P_0 + tP_1,t\in[0,1] p(t)=(1−t)P0+tP1,t∈[0,1]
也可以直接根据定义得出 p ( t ) = ∑ i = 0 1 P i B i , 1 ( t ) = ( 1 − t ) P 0 + t P 1 p(t)=\sum_{i=0}^1{P_iB_{i,1}(t)}=(1-t)P_0+tP_1 p(t)=i=0∑1PiBi,1(t)=(1−t)P0+tP1
2、二阶贝塞尔曲线
对于二阶贝塞尔曲线,其实你可以理解为:在 P 0 P 1 P_0P_1 P0P1上利用一阶公式求出点 P 0 ′ P_0^{'} P0′,然后在
P_1P_2上利用一阶公式求出点 P 1 ′ P_1^{'} P1′,最后在 P 0 ′ P 1 ′ P_0^{'}P_1^{'} P0′P1′ 上再利用一阶公式就可以求出最终贝塞尔曲线上的点 P 0 ′ ′ P_0{''} P0′′。具体推导过程如下:
P 0 ′ = ( 1 − t ) P 0 + t P 1 P_0^{'} = (1 - t)P_0 + tP_1 P0′=(1−t)P0+tP1
P 1 ′ = ( 1 − t ) P 1 + t P 2 P_1^{'} = (1 - t)P_1 + tP_2 P1′=(1−t)P1+tP2
P 0 ′ ′ = ( 1 − t ) P 0 ′ + t P 1 ′ P^{''}_0 = (1 - t)P_0^{'} + tP_1^{'} P0′′=(1−t)P0′+tP1′
整理得到 P 0 ′ ′ = ( 1 − t ) 2 P 0 + 2 t ( 1 − t ) P 1 + t 2 P 2 P_{0}^{''}=(1-t)^2P_0+2t(1-t)P_1+t^2P_2 P0′′=(1−t)2P0+2t(1−t)P1+t2P2
3、三阶贝塞尔曲线
!
与二阶贝塞尔曲线类似,可以通过相同的几何方法得出以下坐标公式:
p ( 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 ] p(t) = (1 - t)^3P_0 + 3t(1 - t)^2P_1 + 3t^2(1 - t)P_2 + t^3P_3 , t\in[0, 1] p(t)=(1−t)3P0+3t(1−t)2P1+3t2(1−t)P2+t3P3,t∈[0,1]
应用
三次贝塞尔曲线绘制圆
如图所示,使用三次贝塞尔曲线可以绘制出1/4个圆,那么通过二维变换就可以绘制出一个圆。
假设点
P
0
0
(
0
,
1
)
点
P
1
0
(
m
,
1
)
点
P
2
0
(
1
,
m
)
点
P
3
0
(
1
,
0
)
假设点P_0^0(0,1)点P_1^0(m,1)点P_2^0(1,m)点P_3^0(1,0)
假设点P00(0,1)点P10(m,1)点P20(1,m)点P30(1,0)
三次贝塞尔曲线的表达式为
p
(
t
)
=
(
1
−
t
)
3
P
0
+
3
t
(
1
−
t
)
2
P
1
+
3
t
2
(
1
−
t
)
P
2
+
t
3
P
3
p(t) = (1 - t)^3P_0 + 3t(1 - t)^2P_1 + 3t^2(1 - t)P_2 + t^3P_3
p(t)=(1−t)3P0+3t(1−t)2P1+3t2(1−t)P2+t3P3
对于圆弧的中点,t=0.5有
(
2
2
,
2
2
)
=
1
8
P
0
0
+
3
8
P
1
0
+
3
8
P
2
2
+
1
8
P
3
0
(\frac{\sqrt{2}}{2},\frac{\sqrt{2}}{2})=\frac{1}{8}P_0^0+\frac{3}{8}P_1^0+\frac{3}{8}P_2^2+\frac{1}{8}P_3^0
(22,22)=81P00+83P10+83P22+81P30
带入点的坐标可以得到
m
=
4
(
2
−
1
)
3
≈
0.5523
m=\frac{4(\sqrt{2}-1)}{3}\approx0.5523
m=34(2−1)≈0.5523
/*曲线的轨迹可以用直线去拟合也可以用一个个像素点*/
void CBezierCurve3::DrawCurve(CDC* pDC)
{
CPen NewPen, *OldPen;
NewPen.CreatePen(PS_SOLID, 3, RGB(255, 0,0));
OldPen=pDC->SelectObject(&NewPen);
CPoint2 p00 = m_p[0], p10 = m_p[1], p20 = m_p[2], p30 = m_p[3];//控制点
CPoint2 p01, p11, p21, p02, p12, p03;//插值点
double step = 0.001;
/*pDC->MoveTo(int(m_p[0].m_x + 0.5), int(m_p[0].m_y + 0.5));*/
for (double t = 0.00; t < 1; t += step) {
p01 = (1 - t) * p00 + t * p10;
p11 = (1 - t) * p10 + t * p20;
p21 = (1 - t) * p20 + t * p30;
p02 = (1 - t) * p01 + t * p11;
p12 = (1 - t) * p11 + t * p21;
p03 = (1 - t) * p02 + t * p12;
/*pDC->LineTo(int(p03.m_x + 0.5), int(p03.m_y + 0.5));*/
auto c = LinearInterP(t, CRGB(1, 0, 0), CRGB(1.0, 0, 1.0));
pDC->SetPixelV(p03.m_x, p03.m_y, COLOR(c));
}
/*pDC->LineTo(int(m_p[3].m_x + 0.5), int(m_p[3].m_y + 0.5));*/
pDC->SelectObject(OldPen);
NewPen.DeleteObject();
}
#define M 4.0/3*(sqrt(2)-1)
C贝塞尔曲线View::C贝塞尔曲线View() noexcept
{
CPoint2 p[4] = { {0,1},{M,1} ,{1,M},{1,0} };
for (int i = 0; i < 4; i++) {
curve[i].ReadPoint(p);
transform[i].SetMatrix(curve[i].m_p, 4);
}
transform[0].Scale(100, 100);
transform[1].Scale(-100,100);
transform[2].Scale(-100,-100);
transform[3].Scale(100,-100);
}
多条贝塞尔曲线拼接
假设有两段三贝塞尔曲线p(t)和q(t)。曲线的控制点分别是
P
0
,
P
1
,
P
2
,
P
3
,
Q
0
,
Q
1
,
Q
2
,
Q
3
P_0,P_1,P_2,P_3,Q_0,Q_1,Q_2,Q_3
P0,P1,P2,P3,Q0,Q1,Q2,Q3如果这两段曲线要拼接,那么就要求
P
2
,
Q
0
(
P
3
)
,
Q
1
P_2,Q_0(P_3),Q_1
P2,Q0(P3),Q1三点共线。
- 二次贝塞尔曲线绘制爱心
void CBezierCurve2::DrawCurve(CDC* pDC)
{
CPen NewPen, * OldPen;
NewPen.CreatePen(PS_SOLID, 3, RGB(255, 0, 0));
OldPen = pDC->SelectObject(&NewPen);
CPoint2 p00 = m_p[0], p10 = m_p[1], p20 = m_p[2];//控制点
CPoint2 p01, p11, p02;//插值点
double step = 0.00001;
/*pDC->MoveTo(int(m_p[0].m_x + 0.5), int(m_p[0].m_y + 0.5));*/
for (double t = 0.00; t < 1; t += step) {
p01 = (1 - t) * p00 + t * p10;
p11 = (1 - t) * p10 + t * p20;
p02 = (1 - t) * p01 + t * p11;
/*pDC->LineTo(int(p03.m_x + 0.5), int(p03.m_y + 0.5));*/
auto c = LinearInterP(t, CRGB(1, 0, 0), CRGB(1.0, 0, 1.0));
pDC->SetPixelV(p02.m_x, p02.m_y, COLOR(c));
}
/*pDC->LineTo(int(m_p[3].m_x + 0.5), int(m_p[3].m_y + 0.5));*/
pDC->SelectObject(OldPen);
NewPen.DeleteObject();
}
C贝塞尔曲线View::C贝塞尔曲线View() noexcept
{
CPoint2 P2[3] = { {0,0},{1.4,1.8},{2,0} };
CPoint2 P3[3] = { {2,0},{2.3,-3 * 2.3 + 6},{0,-2.6} };\\P2[1],P2[2](P3[0]),P3[1]三点共线
curve2[0].ReadPoint(P2);
transform[0].SetMatrix(curve2[0].m_p, 3);
transform[0].Scale(100, 100);
curve2[1].ReadPoint(P2);
transform[1].SetMatrix(curve2[1].m_p, 3);
transform[1].Scale(-100, 100);
curve2[2].ReadPoint(P3);
transform[2].SetMatrix(curve2[2].m_p, 3);
transform[2].Scale(100, 100);
curve2[3].ReadPoint(P3);
transform[3].SetMatrix(curve2[3].m_p, 3);
transform[3].Scale(-100, 100);
}
需要项目源代码请评论区留言或私信