分段三阶贝塞尔曲线的光滑连续性条件及在WPF应用问题的一些解决方法

分段三阶贝塞尔曲线的光滑连续性条件

1. 贝塞尔曲线简介 [1]

计算机图形学中有一类很常用的曲线,俗称贝塞尔曲线。1962年,法国数学家Pierre Bézier 第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名是为贝塞尔曲线。 很多程序语言都有实现贝塞尔曲线的API,而该曲线本身也拥有强大的近似其它曲线的能力,即使一条不能够胜任,那么分段的多条贝塞尔曲线也足够用来近似我们想绘制的曲线。

这里给出贝塞尔曲线的一般公式:
P(t)=\sum_{i=0}^n{B_{i,n}(t)\mathbf{P_i}}
其中:
B_{i,n}(t)=\frac{n!}{i!(n-i)!}t^i(1-t)^{n-i},t\in[0,1]
对于以P_{i-1}P_{i}作为起点和终点的三阶贝塞尔曲线段,具有两个控制点C_{i,1}C_{i,2},方程为:
P_{i-1,i}(t)= P_{i-1} +(- 3P_{i-1} + 3C_{i,1} )t +( 3P_{i-1} - 6C_{i,1} + 3C_{i,2} )t^2 +(- P_{i-1} + 3C_{i,1} - 3C_{i,2} + P_{i})t^3
对应的方程一阶导数即为:
\frac{dP_{i-1,i}(t)}{dt} = (- 3P_{i-1} + 3C_{i,1} ) +( 6P_{i-1} -12C_{i,1} + 6C_{i,2} )t +(- 3P_{i-1} + 9C_{i,1} - 9C_{i,2} + 3P_{i})t^2

2. 光滑连续性条件

要求曲线段P_{i-1,i}(t)P_{i,i+1}(t)光滑连续,就是要求两曲线段在点P_{i}处可导,则一充分非必要条件为:
\frac{dP_{i-1,i}(t)}{dt}|_{t=1}=\frac{dP_{i,i+1}(t)}{dt}|_{t=0}
化简为:
\mathbf{P_{i}}-\mathbf{C_{i,2}}=\mathbf{C_{i+1,1}}-\mathbf{P_{i}}
即是矢量\mathbf{C_{i,2}}\mathbf{P_{i}} 与矢量 \mathbf{P_{i}}\mathbf{C_{i+1,1}}相等。

3. 通过点的分段三阶贝塞尔曲线绘制方案

3.1 基本绘制方法

由于得到的光滑连续性条件为矢量\mathbf{C_{i,2}}\mathbf{P_{i}} 与矢量 \mathbf{P_{i}}\mathbf{C_{i+1,1}}相等,但其大小和方向未知,可以采取以下方案绘制光滑连续的分段三阶贝塞尔曲线:

  1. 计算相邻三点P_{i-1}P_{i}P_{i+1}的中点P_{i-1,i}^{avg}P_{i,i+1}^{avg}
  2. 计算中点的中点P_{i-1,i,i+1}^{avg}
  3. 将点P_{i-1,i}^{avg}P_{i,i+1}^{avg}沿矢量\mathbf{P_{i-1,i,i+1}^{avg} P_{i}}进行平移
  4. 得到的两点就作为前一段曲线的第二控制点C_{i,2}和后一段曲线的第一控制点C_{i+1,1}

通过上述方式可以得到:
C_{i,2}=P_{i}-\frac{1}{4}(P_{i+1}-P_{i-1})
C_{i+1,1}=P_{i}+\frac{1}{4}(P_{i+1}-P_{i-1})

3.2 控制点伸缩方法

P_{i}点周围的控制点C_{i,2}C_{i+1,1}进行内缩或外扩,可以通过将点沿矢量\mathbf{ P_{i-1,i}^{avg} P_{i,i+1}^{avg}}左右平移得到。
要使控制点线段C_{i,2}C_{i+1,1}为原来的k倍,若正方向指向点P_{i},有左侧控制点平移矢量:
V_{left}=\frac{1-k}{4}(P_{i+1}-P_{i-1})
右侧控制点平移矢量:
V_{right}=-1\cdot\frac{1-k}{4}(P_{i+1}-P_{i-1})
则:
C_{i,2}=C_{i,2}+V_{left}=P_{i}-\frac{k}{4}(P_{i+1}-P_{i-1})
C_{i+1,1}=C_{i+1,1}+V_{right}=P_{i}+\frac{k}{4}(P_{i+1}-P_{i-1})
其中
k\in(0,1]时为内缩,
k\in[1,+\infty)时为外扩。
一般k[0.4,0.6]较为合适。

3.3 非封闭的分段三阶贝塞尔曲线

对于封闭的分段三阶贝塞尔曲线通过点集\mathbf{P_i}=\{i|i=0,1,2,...,n\}\,由于其首尾相连,可以看作在点P_0前有点P_n,在点P_n后有点P_0,从而确定第1个点的第一控制点C_{0,1}和第n个点的第二控制点C_{n,2}
而对于非封闭的分段三阶贝塞尔曲线通过点集\mathbf{P_i}=\{i|i=0,1,2,...,n\}\,无法确定第1个点的第一控制点C_{0,1}和第n个点的第二控制点C_{n,2},暂时的解决方案是:
C_{0,1}=C_{0,2},C_{n,1}=C_{n,2}

3.4 三阶贝塞尔曲线段的边界求解

求解三阶贝塞尔曲线段的边界,就是求解方程的一阶导数为0的点:
(- 3P_{i-1} + 3C_{i,1} ) +( 6P_{i-1} -12C_{i,1} + 6C_{i,2} )t +(- 3P_{i-1} + 9C_{i,1} - 9C_{i,2} + 3P_{i})t^2 =0
值得注意:上下边界用点P_{i}y值求解,左右边界用点P_{i}x值求解;方程存在二次项为0的情况,所以需要判断二次项是否为0
下面呈现具体计算代码:

//首先定义一个边界类,用以方便计算曲线边界
public class Boundary
{
    public Boundary(double xMax = Double.NaN, double xMin = Double.NaN, double yMax = Double.NaN, double yMin = Double.NaN)
    {
        XMax = xMax;
        XMin = xMin;
        YMax = yMax;
        YMin = yMin;
    }

    public Boundary(Point point1, Point point2)
    {
        if (point1.X >= point2.X)
        {
            XMax = point1.X;
            XMin = point2.X;
        }
        else
        {
            XMax = point2.X;
            XMin = point1.X;
        }
        if (point1.Y >= point2.Y)
        {
            YMax = point1.Y;
            YMin = point2.Y;
        }
        else
        {
            YMax = point2.Y;
            YMin = point1.Y;
        }
    }

    public double XMax { get; set; }
    public double XMin { get; set; }
    public double YMax { get; set; }
    public double YMin { get; set; }

    public void Resize(double xMax, double xMin, double yMax, double yMin)
    {
        XMax = xMax;
        XMin = xMin;
        YMax = yMax;
        YMin = yMin;
    }

    public void TryInclude(params Point[] points)
    {
        var length = points.Length;
        if (length <= 0 || points == null)
        {
            return;
        }

        double xmax;
        double xmin;
        double ymax;
        double ymin;
        bool isValueInvalid = Boundary.IsValueInvalid(this);
        if (isValueInvalid)
        {
            xmax = xmin = points[0].X;
            ymax = ymin = points[0].Y;
        }
        else
        {
            xmax = this.XMax;
            xmin = this.XMin;
            ymax = this.YMax;
            ymin = this.YMin;
        }

        double x;
        double y;
        for (int i = 0; i < length; i++)
        {
            x = points[i].X;
            y = points[i].Y;
            if (x > xmax)
            {
                xmax = x;
            }
            if (x < xmin)
            {
                xmin = x;
            }
            if (y > ymax)
            {
                ymax = y;
            }
            if (y < ymin)
            {
                ymin = y;
            }
        }

        this.XMax = xmax;
        this.XMin = xmin;
        this.YMax = ymax;
        this.YMin = ymin;
    }

    public void Merge(Boundary boundary)
    {
        bool isValueInvalid = Boundary.IsValueInvalid(this);
        if (!isValueInvalid)
        {
            if (Boundary.IsValueInvalid(boundary))
            {
                return;
            }
            if (boundary.XMax > this.XMax)
            {
                this.XMax = boundary.XMax;
            }
            if (boundary.XMin < this.XMin)
            {
                this.XMin = boundary.XMin;
            }
            if (boundary.YMax > this.YMax)
            {
                this.YMax = boundary.YMax;
            }
            if (boundary.YMin < this.YMin)
            {
                this.YMin = boundary.YMin;
            }
        }
        else
        {
            this.XMax = boundary.XMax;
            this.XMin = boundary.XMin;
            this.YMax = boundary.YMax;
            this.YMin = boundary.YMin;
        }
    }
    
    public static bool IsValueInvalid(Boundary boundary)
    {
        if (Double.IsNaN(boundary.XMax))
        {
            return true;
        }
        if (Double.IsNaN(boundary.XMin))
        {
            return true;
        }
        if (Double.IsNaN(boundary.YMax))
        {
            return true;
        }
        if (Double.IsNaN(boundary.YMin))
        {
            return true;
        }
        return false;
    }
}
//接下来给出具体贝塞尔曲线边界的计算方法,该方法用以返回某段曲线的边界
private Boundary CalculateBezierSegmentBoundary(Point p0, Point c1, Point c2, Point p1)
{
    Boundary boundaryBS = new Boundary(p0, p1);
    double AY = 3 * c1.Y - 3 * c2.Y + p1.Y - p0.Y;
    double BY = 3 * (p0.Y + c2.Y - 2 * c1.Y);
    double CY = 3 * (c1.Y - p0.Y);
    double DY = p0.Y;

    double AX = 3 * c1.X - 3 * c2.X + p1.X - p0.X;
    double BX = 3 * (p0.X + c2.X - 2 * c1.X);
    double CX = 3 * (c1.X - p0.X);
    double DX = p0.X;

    double ay = 3 * AY;
    double by = 2 * BY;
    double cy = CY;
    double deltay = Math.Pow(by, 2) - 4 * ay * cy;

    double ax = 3 * AX;
    double bx = 2 * BX;
    double cx = CX;
    double deltax = Math.Pow(bx, 2) - 4 * ax * cx;

    bool IsAYApproximateZero = (ay > -1 * 1e-10) && (ay < 1e-10);
    bool IsAXApproximateZero = (ax > -1 * 1e-10) && (ax < 1e-10);
    if (IsAYApproximateZero || IsAXApproximateZero)
    {
        if (IsAYApproximateZero)
        {
            double ty = -1 * cy / by;
            if (ty > 0 && ty < 1)
            {
                double x = AX * ty * ty * ty + BX * ty * ty + CX * ty + DX;
                double y = AY * ty * ty * ty + BY * ty * ty + CY * ty + DY;
                boundaryBS.TryInclude(new Point(x, y));
            }
        }
        if (IsAXApproximateZero)
        {
            double tx = -1 * cx / bx;
            if (tx > 0 && tx < 1)
            {
                double x = AX * tx * tx * tx + BX * tx * tx + CX * tx + DX;
                double y = AY * tx * tx * tx + BY * tx * tx + CY * tx + DY;
                boundaryBS.TryInclude(new Point(x, y));
            }
        }
        return boundaryBS;
    }
    bool IsDeltaYGreaterThanZero = deltay > 0;
    bool IsDeltaXGreaterThanZero = deltax > 0;
    bool IsDeltaYApproximateZero = (deltay > -1 * 1e-10) && (deltay < 1e-10);
    bool IsDeltaXApproximateZero = (deltax > -1 * 1e-10) && (deltax < 1e-10);
    if (IsDeltaYGreaterThanZero || IsDeltaXGreaterThanZero)
    {
        if (!IsAYApproximateZero)
        {
            if (IsDeltaYGreaterThanZero)
            {
                double t1y = (-1 * by + Math.Pow(deltay, 0.5)) / (2 * ay);
                double t2y = (-1 * by - Math.Pow(deltay, 0.5)) / (2 * ay);
                if (t1y > 0 && t1y < 1)
                {
                    double x = AX * t1y * t1y * t1y + BX * t1y * t1y + CX * t1y + DX;
                    double y = AY * t1y * t1y * t1y + BY * t1y * t1y + CY * t1y + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
                if (t2y > 0 && t2y < 1)
                {
                    double x = AX * t2y * t2y * t2y + BX * t2y * t2y + CX * t2y + DX;
                    double y = AY * t2y * t2y * t2y + BY * t2y * t2y + CY * t2y + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
            }
        }
        if (!IsAXApproximateZero)
        {
            if (IsDeltaXGreaterThanZero)
            {
                double t1x = (-1 * bx + Math.Pow(deltax, 0.5)) / (2 * ax);
                double t2x = (-1 * bx - Math.Pow(deltax, 0.5)) / (2 * ax);
                if (t1x > 0 && t1x < 1)
                {
                    double x = AX * t1x * t1x * t1x + BX * t1x * t1x + CX * t1x + DX;
                    double y = AY * t1x * t1x * t1x + BY * t1x * t1x + CY * t1x + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
                if (t2x > 0 && t2x < 1)
                {
                    double x = AX * t2x * t2x * t2x + BX * t2x * t2x + CX * t2x + DX;
                    double y = AY * t2x * t2x * t2x + BY * t2x * t2x + CY * t2x + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
            }
        }
        return boundaryBS;
    }
    else if (IsDeltaYApproximateZero || IsDeltaXApproximateZero)
    {
        if (!IsAYApproximateZero)
        {
            if (IsDeltaYApproximateZero)
            {
                double ty = -1 * by / (2 * ay);
                if (ty > 0 && ty < 1)
                {
                    double x = AX * ty * ty * ty + BX * ty * ty + CX * ty + DX;
                    double y = AY * ty * ty * ty + BY * ty * ty + CY * ty + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
            }
        }
        if (!IsAXApproximateZero)
        {
            if (IsDeltaXApproximateZero)
            {
                double tx = -1 * bx / (2 * ax);
                if (tx > 0 && tx < 1)
                {
                    double x = AX * tx * tx * tx + BX * tx * tx + CX * tx + DX;
                    double y = AY * tx * tx * tx + BY * tx * tx + CY * tx + DY;
                    boundaryBS.TryInclude(new Point(x, y));
                }
            }
        }
        return boundaryBS;
    }
    else
    {
        return boundaryBS;
    }
}
//最后可以使用Boundary类中的Merge方法合并边界以计算所有分段三阶贝塞尔曲线边界

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352