在Direct2D中绘制支持任意数量控制点的贝塞尔曲线技术问询
关于Direct2D绘制多控制点平滑曲线的问题
你说得没错,Direct2D原生只支持二次(3个控制点)和三次(4个控制点)贝塞尔曲线,没办法直接传入N个控制点绘制一条完整的贝塞尔曲线。但要实现多控制点的平滑曲线,我们可以通过拼接三次贝塞尔曲线段的方式达成——比如用Catmull-Rom样条(一种平滑插值样条)将多个控制点转换为连续的三次贝塞尔段,这样就能保证曲线在衔接处平滑过渡(一阶导数连续,G1平滑)。
你的现有代码问题分析
你当前的代码是每4个点绘制一段独立的三次贝塞尔曲线,但这些线段之间没有做平滑处理:相邻段的端点处切线方向不一致,所以曲线会出现明显的折点,自然达不到平滑的效果。
C#示例代码(基于你的项目修改)
下面是修改后的代码,加入了Catmull-Rom样条转三次贝塞尔的逻辑,用你的10个控制点生成平滑曲线:
using System; using System.Windows.Forms; using JeremyAnsel.DirectX.D2D1; namespace ShakeAlgorithm { public class Form1 : Form { public Form1() { SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true); points = new D2D1Point2F[count]; for (int i = 0; i < count; i++) { points[i] = new D2D1Point2F(0, 0); } } [STAThread] static void Main() { Application.Run(new Form1()); } private D2D1Factory factory; private D2D1HwndRenderTarget hwndRenderTarget; private D2D1SolidColorBrush d2D1Brush; private const int count = 10; private D2D1Point2F[] points = new D2D1Point2F[count]; private int index = 0; protected override void OnHandleCreated(EventArgs e) { factory = D2D1Factory.Create(D2D1FactoryType.SingleThreaded); D2D1RenderTargetProperties renderTargetProperties = new D2D1RenderTargetProperties { RenderTargetType = D2D1RenderTargetType.Hardware, Usage = D2D1RenderTargetUsages.None, PixelFormat = new D2D1PixelFormat() { AlphaMode = D2D1AlphaMode.Premultiplied, Format = JeremyAnsel.DirectX.Dxgi.DxgiFormat.B8G8R8A8UNorm } }; D2D1HwndRenderTargetProperties hwndRenderTargetProperties = new D2D1HwndRenderTargetProperties { Hwnd = Handle, PixelSize = new D2D1SizeU((uint)ClientSize.Width, (uint)ClientSize.Height), PresentOptions = D2D1PresentOptions.Immediately }; hwndRenderTarget = factory.CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties); hwndRenderTarget.AntialiasMode = D2D1AntialiasMode.PerPrimitive; d2D1Brush = hwndRenderTarget.CreateSolidColorBrush(new D2D1ColorF(D2D1KnownColor.Yellow)); base.OnHandleCreated(e); } protected override void OnPaintBackground(PaintEventArgs e) { // 不需要绘制GDI背景,交给Direct2D处理 } protected override void OnPaint(PaintEventArgs e) { hwndRenderTarget.BeginDraw(); hwndRenderTarget.Clear(new D2D1ColorF(D2D1KnownColor.White)); if (points != null) { if (index < count) { // 绘制已选控制点的连线 d2D1Brush.Color = new D2D1ColorF(D2D1KnownColor.Blue); for (int i = 0; i < index - 1; i++) { hwndRenderTarget.DrawLine(points[i], points[i + 1], d2D1Brush); } // 绘制已选控制点 d2D1Brush.Color = new D2D1ColorF(D2D1KnownColor.Black); for (int i = 0; i < index; i++) { hwndRenderTarget.FillEllipse(new D2D1Ellipse(points[i], 2, 2), d2D1Brush); } } else { // 用Catmull-Rom样条生成平滑曲线(转成贝塞尔段) d2D1Brush.Color = new D2D1ColorF(D2D1KnownColor.Red); using (var pathGeometry = factory.CreatePathGeometry()) { using (var geometrySink = pathGeometry.Open()) { geometrySink.BeginFigure(points[0], D2D1FigureBegin.Hollow); // 生成Catmull-Rom样条对应的贝塞尔段 for (int i = 0; i < count - 1; i++) { // 获取当前段的四个控制点(Catmull-Rom需要前后各一个点计算切线) D2D1Point2F p0 = i == 0 ? points[0] : points[i - 1]; D2D1Point2F p1 = points[i]; D2D1Point2F p2 = points[i + 1]; D2D1Point2F p3 = i == count - 2 ? points[count - 1] : points[i + 2]; // 将Catmull-Rom段转换为三次贝塞尔曲线 var bezier = CatmullRomToBezier(p0, p1, p2, p3); geometrySink.AddBezier(bezier); } geometrySink.EndFigure(D2D1FigureEnd.Open); geometrySink.Close(); } hwndRenderTarget.DrawGeometry(pathGeometry, d2D1Brush, 3f); } // 绘制所有控制点 d2D1Brush.Color = new D2D1ColorF(D2D1KnownColor.Black); for (int i = 0; i < count; i++) { hwndRenderTarget.FillEllipse(new D2D1Ellipse(points[i], 2, 2), d2D1Brush); } } } var result = hwndRenderTarget.EndDraw(); if (result.Failed) { // 处理绘制错误(可选) } } // 将Catmull-Rom样条段转换为三次贝塞尔曲线 private D2D1BezierSegment CatmullRomToBezier(D2D1Point2F p0, D2D1Point2F p1, D2D1Point2F p2, D2D1Point2F p3) { // Catmull-Rom转贝塞尔的系数:控制点p1到p2的贝塞尔控制点由相邻点计算 float tension = 0.5f; // 张力,0.5是默认值,越小曲线越贴近控制点,越大越平滑 D2D1Point2F cp1 = new D2D1Point2F( p1.X + (p2.X - p0.X) * tension / 3, p1.Y + (p2.Y - p0.Y) * tension / 3 ); D2D1Point2F cp2 = new D2D1Point2F( p2.X - (p3.X - p1.X) * tension / 3, p2.Y - (p3.Y - p1.Y) * tension / 3 ); return new D2D1BezierSegment(cp1, cp2, p2); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (e.Button == MouseButtons.Left) { if (index < count) { points[index++] = new D2D1Point2F(e.X, e.Y); } else { index = 0; points[index++] = new D2D1Point2F(e.X, e.Y); } Invalidate(); } } protected override void OnResize(EventArgs e) { base.OnResize(e); if (IsHandleCreated) { hwndRenderTarget.Resize(new D2D1SizeU((uint)ClientSize.Width, (uint)ClientSize.Height)); Invalidate(); } } protected override void Dispose(bool disposing) { if (disposing) { d2D1Brush?.Dispose(); hwndRenderTarget?.Dispose(); factory?.Dispose(); } base.Dispose(disposing); } } }
代码说明
- Catmull-Rom转贝塞尔:
CatmullRomToBezier函数通过相邻控制点计算贝塞尔曲线的两个控制点,保证相邻贝塞尔段在衔接处的切线方向一致,实现平滑过渡。 - 资源管理:使用
using语句自动释放PathGeometry和GeometrySink,避免内存泄漏;添加了Dispose方法清理Direct2D资源。 - 绘制逻辑优化:修复了原代码中多次创建/释放几何对象的问题,优化了控制点的绘制逻辑。
C++版本核心逻辑
如果需要C++版本,核心逻辑完全一致:用Catmull-Rom样条生成贝塞尔段,然后调用ID2D1GeometrySink::AddBezier绘制。关键函数示例:
// C++中Catmull-Rom转贝塞尔的函数 D2D1_BEZIER_SEGMENT CatmullRomToBezier(D2D1_POINT_2F p0, D2D1_POINT_2F p1, D2D1_POINT_2F p2, D2D1_POINT_2F p3) { float tension = 0.5f; D2D1_POINT_2F cp1 = D2D1::Point2F( p1.x + (p2.x - p0.x) * tension / 3.0f, p1.y + (p2.y - p0.y) * tension / 3.0f ); D2D1_POINT_2F cp2 = D2D1::Point2F( p2.x - (p3.x - p1.x) * tension / 3.0f, p2.y - (p3.y - p1.y) * tension / 3.0f ); return D2D1::BezierSegment(cp1, cp2, p2); }
然后在绘制时遍历控制点,生成贝塞尔段并添加到路径几何中即可。
内容的提问来源于stack exchange,提问作者jtxkopt - STOP GENOCIDE




