在数学的数值分析领域中,贝济埃曲线(Bézier curve)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝济埃曲线就称作贝济埃曲面,其中贝济埃三角是一种特殊的实例。
贝济埃曲线于1962年,由法国工程师皮埃尔·贝济埃(Pierre Bézier)所广泛发表,他运用贝济埃曲线来为汽车的主体进行设计。贝济埃曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝济埃曲线。
目录[隐藏]
|
[编辑]实例说明
[编辑]线性贝济埃曲线
给定点P0、P1,线性贝济埃曲线只是一条两点之间的直线。这条线由下式给出:
且其等同于线性插值。
[编辑]二次方贝济埃曲线
二次方贝济埃曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
。
TrueType字型就运用了以贝济埃样条组成的二次贝济埃曲线。
[编辑]三次方贝济埃曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝济埃曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前,走向P2方向的“长度有多长”。
曲线的参数形式为:
。
现代的成象系统,如PostScript、Asymptote和Metafont,运用了以贝济埃样条组成的三次贝济埃曲线,用来描绘曲线轮廓。
[编辑]一般化
n阶贝济埃曲线可如下推断。给定点P0、P1、…、Pn,其贝济埃曲线即
。
例如n = 5:
。
如上公式可如下递归表达: 用
表示由点P0、P1、…、Pn所决定的贝济埃曲线。则
用平常话来说,n阶的贝济埃曲线,即双n - 1阶贝济埃曲线之间的插值。
[编辑]术语
一些关于参数曲线的术语,有
即多项式
又称作n阶的伯恩斯坦基底多项式,定义00 = 1。
点Pi称作贝济埃曲线的控制点。多边形以带有线的贝济埃点连接而成,起始于P0并以Pn终止,称作贝济埃多边形(或控制多边形)。贝济埃多边形的凸包(convex hull)包含有贝济埃曲线。
[编辑]注解
- 开始于P0并结束于Pn的曲线,即所谓的端点插值法属性。
- 曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝济埃曲线是直线的充分必要条件是控制点共线。
- 曲线的起始点(结束点)相切于贝济埃多边形的第一节(最后一节)。
- 一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝济埃曲线。
- 一些看似简单的曲线(如圆)无法以贝济埃曲线精确的描述,或分段成贝济埃曲线(虽然当每个内部控制点对单位圆上的外部控制点水平或垂直的的距离为时,分成四段的贝济埃曲线,可以小于千分之一的最大半径误差近似于圆)。
- 位于固定偏移量的曲线(来自给定的贝济埃曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝济埃曲线精确的形成(某些琐屑实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值。
[编辑]建构贝济埃曲线
[编辑]线性曲线
线性贝济埃曲线演示动画,t in [0,1] |
线性贝济埃曲线函数中的t会经过由P0至P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线。
[编辑]二次曲线
为建构二次贝济埃曲线,可以中介点Q0和Q1作为由0至1的t:
- 由P0至P1的连续点Q0,描述一条线性贝济埃曲线。
- 由P1至P2的连续点Q1,描述一条线性贝济埃曲线。
- 由Q0至Q1的连续点B(t),描述一条二次贝济埃曲线。
| ||
二次贝济埃曲线的结构 | | 二次贝济埃曲线演示动画,t in [0,1] |
[编辑]高阶曲线
为建构高阶曲线,便需要相应更多的中介点。对于三次曲线,可由线性贝济埃曲线描述的中介点Q0、Q1、Q2,和由二次曲线描述的点R0、R1所建构:
| ||
三次贝济埃曲线的结构 | | 三次贝济埃曲线演示动画,t in [0,1] |
对于四次曲线,可由线性贝济埃曲线描述的中介点Q0、Q1、Q2、Q3,由二次贝济埃曲线描述的点R0、R1、R2,和由三次贝济埃曲线描述的点S0、S1所建构:
| ||
四次贝济埃曲线的结构 | | 四次贝济埃曲线演示动画,t in [0,1] |
(还可参阅五阶贝济埃曲线的构成。)
[编辑]应用
[编辑]电脑绘图
贝济埃曲线被广泛地在计算机图形中用来为平滑曲线建立模型。
二次和三次贝济埃曲线最为常见
[编辑]程式范例
下列程式码为一简单的实际运用范例,展示如何使用C标出三次方贝济埃曲线。注意,此处仅简单的计算多项式系数,并读尽一系列由0至1的t值;实践中一般不会这么做,递归求解通常会更快速——以更多的内存为代价,花费较少的处理器时间。不过直接的方法较易于理解并产生相同结果。以下程式码已使运算更为清晰。实践中的最佳化会先计算系数一次,并在实际计算曲线点的循环中反复使用。此处每次都会重新计算,损失了效率,但程式码更清楚易读。
曲线的计算可在曲线阵列上将相连点画上直线——点越多,曲线越平滑。
在部分架构中,下以程式码也可由动态程式设计进行最佳化。举例来说,dt是一个常数,cx * t则等同于每次反复就修改一次常数。经反复应用这种最佳化后,循环可被重写为没有任何乘法(虽然这个过程不是稳定数值的)。
/*
產生三次方貝茲曲線的程式碼
*/
typedef struct
{
float x;
float y;
}
Point2D;
/*
cp在此是四個元素的陣列:
cp[0]為起始點,或上圖中的P0
cp[1]為第一個控制點,或上圖中的P1
cp[2]為第二個控制點,或上圖中的P2
cp[3]為結束點,或上圖中的P3
t為參數值,0 <= t <= 1
*/
Point2D PointOnCubicBezier( Point2D* cp, float t )
{
float ax, bx, cx;
float ay, by, cy;
float tSquared, tCubed;
Point2D result;
/*計算多項式係數*/
cx = 3.0 * (cp[1].x - cp[0].x);
bx = 3.0 * (cp[2].x - cp[1].x) - cx;
ax = cp[3].x - cp[0].x - cx - bx;
cy = 3.0 * (cp[1].y - cp[0].y);
by = 3.0 * (cp[2].y - cp[1].y) - cy;
ay = cp[3].y - cp[0].y - cy - by;
/*計算位於參數值t的曲線點*/
tSquared = t * t;
tCubed = tSquared * t;
result.x = (ax * tCubed) + (bx * tSquared) + (cx * t) + cp[0].x;
result.y = (ay * tCubed) + (by * tSquared) + (cy * t) + cp[0].y;
return result;
}
/*
ComputeBezier以控制點cp所產生的曲線點,填入Point2D結構的陣列。
呼叫者必須分配足夠的記憶體以供輸出結果,其為<sizeof(Point2D) numberOfPoints>
*/
void ComputeBezier( Point2D* cp, int numberOfPoints, Point2D* curve )
{
float dt;
int i;
dt = 1.0 / ( numberOfPoints - 1 );
for( i = 0; i < numberOfPoints; i++)
curve[i] = PointOnCubicBezier( cp, i*dt );
}
另一种贝济埃曲线的应用是在动画中,描述物件的运动路径等等。此处,曲线的x、y位置不用来标示曲线,但用来表示图形位置。当用在这种形式时,连续点之间的距离会变的更为重要,且大多不是平均比例。点将会串的更紧密,控制点更接近每一个点,而更为稀疏的控制点会散的更开。如果需要线性运动速度,进一步处理时就需要循所需路径将点平均分散。
[编辑]有理贝济埃曲线
有理贝济埃增加可调节的权重,以提供更近似于随意的形状。分子是加权的伯恩斯坦形式贝济埃曲线,而分母是加权的伯恩斯坦多项式的总和。
给定n + 1控制点Pi,有理贝济埃曲线可如下描述:
或简单的
[编辑]参阅
- de Casteljau算法
- 样条
- 贝济埃样条
- 贝济埃曲面
- 贝济埃三角
- NURBS
- string art,Bézier curves are also formed by many common forms of string art, where strings are looped across a frame of nails.
- 埃尔米特曲线
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
以前有人发表过类似的文章,是用Texture2D模拟贝赛尔曲线 ,而本文是基于顶点绘制的。
在XNA中,使用DrawUserPrimitives方法可以绘制直线,但是没有直接绘制贝塞尔曲线的方法。其实绘制贝塞尔曲线就是绘制一组与贝赛尔曲线近似的折线段。
面向对象编程,首先定义贝赛尔曲线类。三次贝赛尔曲线由两端的锚点和中间两个控制点组成,没什么好说的,上代码:
Bezier.cs
using
System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace DrawBezier
{
///
<summary>
/// 定义一段三次贝塞尔曲线。
///
</summary>
public
class
Bezier
{
///
<summary>
/// 储存构成该贝赛尔曲线的点的对应顶点结构。
///
</summary>
List <
Vector2
>
points;
///
<summary>
/// 获取或设置贝赛尔曲线的第一个锚点。
///
</summary>
public
Vector2 Anchor1
{
get
{
return anchor1;
}
set
{
anchor1 = value;
needUpdate =
true
;
}
}
Vector2 anchor1;
///
<summary>
/// 获取或设置贝赛尔曲线的第二个锚点。
///
</summary>
public
Vector2 Anchor2
{
get
{
return anchor2;
}
set
{
anchor2 = value;
needUpdate =
true
;
}
}
Vector2 anchor2;
///
<summary>
/// 获取或设置贝赛尔曲线的第一个控制点。
///
</summary>
public
Vector2 Control1
{
get
{
return control1;
}
set
{
control1 = value;
needUpdate =
true
;
}
}
Vector2 control1;
///
<summary>
/// 获取或设置贝赛尔曲线的第二个控制点。
///
</summary>
public
Vector2 Control2
{
get
{
return control2;
}
set
{
control2 = value;
needUpdate =
true
;
}
}
Vector2 control2;
///
<summary>
/// 用于判断曲线的作用点是否改变。
///
</summary>
bool
needUpdate
=
false
;
///
<summary>
/// 用指定参数创建贝赛尔曲线的新实例。
///
</summary>
///
<param name="anchor1">
指定贝赛尔曲线的第一个锚点。
</param>
///
<param name="control1">
指定贝赛尔曲线的第一个控制点。
</param>
///
<param name="control2">
指定贝赛尔曲线的第二个控制点。
</param>
///
<param name="anchor2">
指定贝赛尔曲线的第二个锚点。
</param>
public
Bezier(Vector2 anchor1, Vector2 control1, Vector2 control2, Vector2 anchor2)
{
this .Anchor1
=
anchor1;
this .Anchor2
=
anchor2;
this .Control1
=
control1;
this .Control2
=
control2;
}
}
}
设贝塞尔曲线的作用点分别为P1(第一个锚点)、P2(第一个控制点)、P3(第二个控制点)、P4(第二个锚点)。首先确定P1和P2、P2和P3、P3和P4的中心点,分别设为P12、P23、P34;然后找到P12和P23、P23和P34的中心点,设为P123、P234;这两个点的中心点P为该贝塞尔曲线上的一点(这是贝塞尔曲线的一个重要性质,其原理网上到处都是)。此点将原贝塞尔曲线一分为二,生成两段新的贝塞尔曲线。
根据这个原理,可以递归拆分贝赛尔曲线直到生成的曲线与直线近似。判断是否近似的标准就是:新生成贝塞尔曲线的两个控制点到经过两锚点的直线的距离小于指定容限(注:容限一般设为0.1414)。
代码如下:
Bezier.cs方法
///
<summary>
/// 用指定参数创建贝赛尔曲线的新实例。
/// </summary>
/// <param name="anchor1">
指定贝赛尔曲线的第一个锚点。
</param>
/// <param name="control1">
指定贝赛尔曲线的第一个控制点。
</param>
/// <param name="control2">
指定贝赛尔曲线的第二个控制点。
</param>
/// <param name="anchor2">
指定贝赛尔曲线的第二个锚点。
</param>
public Bezier(Vector2 anchor1, Vector2 control1, Vector2 control2, Vector2 anchor2)
{
this .Anchor1 =
anchor1;
this .Anchor2 =
anchor2;
this .Control1 =
control1;
this .Control2 =
control2;
}
/// <summary>
/// 获取该贝赛尔曲线的近似折线的点集。
/// </summary>
/// <returns>
该贝赛尔曲线的近似折线的点集。
</returns>
public List
<
Vector2
>
GetPoints()
{
if (needUpdate)
{
points = new
List
<
Vector2
>
();
points.Add( this .Anchor1);
CubicBezierToPoints( this .Anchor1, this
.Control1,
this
.Control2,
this
.Anchor2, points);
needUpdate = false
;
}
return points;
}
/// <summary>
/// 将指定参数的贝赛尔曲线转换为近似折线的点集。
/// </summary>
/// <param name="p0">
指定贝赛尔曲线的第一个锚点。
</param>
/// <param name="p1">
指定贝赛尔曲线的第二个控制点。
</param>
/// <param name="p2">
指定贝赛尔曲线的第一个控制点。
</param>
/// <param name="p3">
指定贝赛尔曲线的第二个锚点。
</param>
/// <param name="points">
转换后的近似折线的点集。
</param>
void CubicBezierToPoints(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, List
<
Vector2
>
points)
{
if (Distance(p1, p0, p3) <
0.02
&&
Distance(p2, p0, p3)
<
0.02
)
{
points.Add(p3);
}
else
{
Vector2 p01 = new
Vector2((p0.X
+
p1.X)
/
2
, (p0.Y
+
p1.Y)
/
2
);
Vector2 p12 = new
Vector2((p1.X
+
p2.X)
/
2
, (p1.Y
+
p2.Y)
/
2
);
Vector2 p23 = new
Vector2((p2.X
+
p3.X)
/
2
, (p2.Y
+
p3.Y)
/
2
);
Vector2 p012 = new
Vector2((p01.X
+
p12.X)
/
2
, (p01.Y
+
p12.Y)
/
2
);
Vector2 p123 = new
Vector2((p12.X
+
p23.X)
/
2
, (p12.Y
+
p23.Y)
/
2
);
Vector2 p = new
Vector2((p012.X
+
p123.X)
/
2
, (p012.Y
+
p123.Y)
/
2
);
CubicBezierToPoints(p0, p01, p012, p, points);
CubicBezierToPoints(p, p123, p23, p3, points);
}
}
/// <summary>
/// 计算指定点 p 到指定直线 ab 的距离的平方。
/// </summary>
/// <param name="p">
指定点 p。
</param>
/// <param name="a">
指定直线的第一个点 a。
</param>
/// <param name="b">
指定直线的第二个点 b。
</param>
/// <returns>
点 p 到直线 ab 的距离的平方。
</returns>
float Distance(Vector2 p, Vector2 a, Vector2 b)
{
float paX =
p.X
-
a.X;
float paY =
p.Y
-
a.Y;
float baX =
b.X
-
a.X;
float baY =
b.Y
-
a.Y;
float pa2 =
paX
*
paX
+
paY
*
paY;
float ba2 =
baX
*
baX
+
baY
*
baY;
float apab =
paX
*
baX
+
paY
*
baY;
float apab2 =
apab
*
apab;
return pa2 -
apab2
/
ba2;
}
得到这组点集后可以做很多事,这里提供我自己的绘制方法,以供参考。
定义简单的顶点结构:VertexPosition2D
VertexPosition2D.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace DrawBezier
{
public struct VertexPosition2D
{
public Vector2 Position;
public static
readonly
int
SizeInBytes
=
sizeof
(
float
)
*
2
;
public static
readonly
VertexElement[] VertexElements
=
{
new VertexElement( 0 ,
0
, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.Position,
0
),
};
public VertexPosition2D(Vector2 position)
{
Position = position;
}
}
}
编写简单的绘制直线的Shader:
Line.fx
float4 Color;
float4 VS_Main(float2 Position : POSITION0) : POSITION0
{
return float4(Position.xy, 0.5 , 1
);
}
float4 PS_Main(float2 Position : POSITION0) : COLOR0
{
return Color;
}
technique Technique1
{
pass Pass1
{
VertexShader = compile vs_1_1 VS_Main();
PixelShader = compile ps_1_1 PS_Main();
}
}
在游戏主程序中运用:
Game1.cs
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace DrawBezier
{
/// <summary>
/// 测试贝塞尔曲线。
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
Effect lineEffect;
VertexDeclaration declaration;
Bezier bezier;
VertexPosition2D[] vertices;
public Game1()
{
graphics = new GraphicsDeviceManager( this );
Content.RootDirectory = " Content " ;
graphics.PreferredBackBufferWidth = 400 ;
graphics.PreferredBackBufferHeight = 300 ;
graphics.PreferMultiSampling = true ;
graphics.PreparingDeviceSettings += new EventHandler < PreparingDeviceSettingsEventArgs
>
(graphics_PreparingDeviceSettings);
}
void graphics_PreparingDeviceSettings( object sender, PreparingDeviceSettingsEventArgs e)
{
// Xbox 360 and most PCs support FourSamples/0
// (4x) and TwoSamples/0 (2x) antialiasing.
PresentationParameters pp = e.GraphicsDeviceInformation.PresentationParameters;
int quality = 0 ;
GraphicsAdapter adapter = e.GraphicsDeviceInformation.Adapter;
SurfaceFormat format = adapter.CurrentDisplayMode.Format;
// Check for 4xAA
if (adapter.CheckDeviceMultiSampleType(DeviceType.Hardware, format,
false , MultiSampleType.FourSamples, out quality))
{
// even if a greater quality is returned, we only want quality 0
pp.MultiSampleQuality = 0 ;
pp.MultiSampleType =
MultiSampleType.FourSamples;
}
// Check for 2xAA
else if (adapter.CheckDeviceMultiSampleType(DeviceType.Hardware,
format, false , MultiSampleType.TwoSamples, out quality))
{
// even if a greater quality is returned, we only want quality 0
pp.MultiSampleQuality = 0 ;
pp.MultiSampleType =
MultiSampleType.TwoSamples;
}
return ;
}
/// <summary>
/// 初始化。
/// </summary>
protected override void
Initialize()
{
declaration = new VertexDeclaration(GraphicsDevice, VertexPosition2D.VertexElements);
bezier = new Bezier( new Vector2(
-
100
,
-
50
),
new
Vector2(
-
200
,
150
),
new
Vector2(
250
,
200
),
new
Vector2(
100
,
-
100
));
List < Vector2 > positions = bezier.GetPoints();
vertices = new VertexPosition2D[positions.Count];
int i = 0 ;
foreach (Vector2 position in positions)
{
Vector2 pos = new Vector2(position.X / GraphicsDevice.Viewport.Width
*
2
, position.Y
/
GraphicsDevice.Viewport.Height
*
2
);
vertices[i] = new VertexPosition2D(pos);
i ++ ;
}
base .Initialize();
}
/// <summary>
/// 加载资源。
/// </summary>
protected override void
LoadContent()
{
lineEffect = Content.Load < Effect > (
"
Effects/Line
"
);
}
/// <summary>
/// 绘制。
/// </summary>
/// <param name="gameTime"> Provides a snapshot of timing values. </param>
protected override void
Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
lineEffect.Parameters[ " Color " ].SetValue(Color.White.ToVector4());
lineEffect.Begin();
foreach (EffectPass pass in lineEffect.CurrentTechnique.Passes)
{
pass.Begin();
GraphicsDevice.VertexDeclaration = declaration;
GraphicsDevice.DrawUserPrimitives < VertexPosition2D > (PrimitiveType.LineStrip, vertices, 0 , vertices.Length
-
1
);
pass.End();
}
lineEffect.End();
base .Draw(gameTime);
}
}
}
得到的效果图如下: