以前有人发表过类似的文章,是用Texture2D模拟贝赛尔曲线 ,而本文是基于顶点绘制的。
在XNA中,使用DrawUserPrimitives方法可以绘制直线,但是没有直接绘制贝塞尔曲线的方法。其实绘制贝塞尔曲线就是绘制一组与贝赛尔曲线近似的折线段。
面向对象编程,首先定义贝赛尔曲线类。三次贝赛尔曲线由两端的锚点和中间两个控制点组成,没什么好说的,上代码:
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;
}
}
}
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)。
代码如下:
/// <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;
}
/// 用指定参数创建贝赛尔曲线的新实例。
/// </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;
}