以前有人发表过类似的文章,是用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;
}
}
}
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;
}
/// 用指定参数创建贝赛尔曲线的新实例。
/// </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;
}
}
}
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();
}
}
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);
}
}
}
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);
}
}
}
演示效果图如下: