You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在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);
        }
    }
}

代码说明

  1. Catmull-Rom转贝塞尔CatmullRomToBezier函数通过相邻控制点计算贝塞尔曲线的两个控制点,保证相邻贝塞尔段在衔接处的切线方向一致,实现平滑过渡。
  2. 资源管理:使用using语句自动释放PathGeometryGeometrySink,避免内存泄漏;添加了Dispose方法清理Direct2D资源。
  3. 绘制逻辑优化:修复了原代码中多次创建/释放几何对象的问题,优化了控制点的绘制逻辑。

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

火山引擎 最新活动