【问题标题】:How to best approximate a geometrical arc with a Bezier curve?如何用贝塞尔曲线最好地逼近几何弧?
【发布时间】:2009-04-09 12:51:58
【问题描述】:

在使用贝塞尔曲线近似绘制 2D 圆弧时,如果您有圆的中心点、起点和终点角度以及半径,如何计算两个控制点?

【问题讨论】:

标签: math graphics geometry


【解决方案1】:

这是一个有 8 年历史的问题,但我最近一直在努力解决这个问题,所以我想我会分享我的想法。我花了很多时间尝试使用来自this text 的解决方案 (9) 并且无法从中获得任何合理的数字,直到我进行了一些谷歌搜索并了解到,显然,方程式中有一些拼写错误。根据this blog post 中列出的更正,给定弧的起点和终点(分别为 [x1, y1] 和 [x4, y4])和圆的中心([xc, yc]),可以导出三次贝塞尔曲线([x2, y2] 和 [x3, y3])的控制点如下:

ax = x1 - xc
ay = y1 - yc
bx = x4 - xc
by = y4 - yc
q1 = ax * ax + ay * ay
q2 = q1 + ax * bx + ay * by
k2 = (4/3) * (sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx)

x2 = xc + ax - k2 * ay
y2 = yc + ay + k2 * ax
x3 = xc + bx + k2 * by                                 
y3 = yc + by - k2 * bx

希望这对我以外的人有所帮助!

【讨论】:

  • 非常适合弧形
【解决方案2】:

这在 StackOverflow 帖子中不容易解释,特别是因为向您证明它会涉及许多详细步骤。但是,您所描述的是一个常见问题,并且有许多详尽的解释。见herehere;我很喜欢#2,之前也用过。

【讨论】:

  • #2 结尾附近的错字:“x3=x1”应该是“x3=x0”。
  • 我在pomax.github.io/bezierinfo/#circles_cubic 有一个更通用的解释,它涵盖了第二个链接的结论,同时解释了任何弧长的坐标是什么,而不是角度高达四分之一圆的弧.
【解决方案3】:

Cubic Bezier Curve by Circular Arcs 的近似”中提供了一个很好的解释

长话短说:使用贝塞尔曲线,您可以实现 1.96×10^-4 的最小误差,这对于大多数应用程序来说都还不错。

对于正象限弧,请使用以下几点:

p0 = [0, radius]

p1 = [radius * K, radius]  

p2 = [radius, radius * K]

p3 = [radius, 0]

其中K是所谓的“幻数”,是一个无理数。可以近似如下:

K = 0.5522847498

【讨论】:

  • K 是弧的函数。 K = 0.5522847498 仅在弧度为 PI/2 弧度时有效。
  • 如果你要将它用于四分之一 tau 部分,你最好使用 Mortensen 的值 0.55191502449351057,以最大限度地减少正负误差。如果你想最小化整体误差,奇怪的是 0.552 比这两者都要好。
  • Naive 几何值 ((4/3)*(sqrt(2) - 1) 的总误差为 1401 超过 10000000 个样本。1180 与 Mortensen 的和 1160 在 0.552。
  • @Tata​​rize 为了最小化总平方误差,最佳值为 0.5519703814011128603134107。为了最小化最大误差,最佳值是 Mortensen 的,更精确地说是 0.5519150244935105707435627。
【解决方案4】:

我正在通过一些演示来回答这个老问题(它应该属于数学,所以写公式会很糟糕)。

假设 P0P3 是你的弧的起点和终点,P1P2 是控制贝塞尔曲线的点,x 是角度除以 2 的度量。假设 x 小于 pi/2。

PM为线段P0P3的中点,PH为圆弧的中点。为了近似圆弧,我们希望贝塞尔曲线从 P0 开始,经过 PH,在 P3 结束,并与圆弧相切在 P0P3

(点击“运行代码sn-p”显示该图。诅咒imgur仍然不支持SVG。)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 20 80 80">
    <style>text{font-size:40%;font-style:italic;text-anchor:middle}tspan{font-size:50%;font-style:normal}</style>
    <rect x="10" y="20" width="80" height="80" fill="none" stroke="gray"></rect>
    <path stroke="gray" stroke-dasharray="3,2" fill="none" d="M25,30 62.6,31.62 80,65 22.19,95.13 25,30 80,65 M22.19,95.13 62.6,31.62"></path>
    <path stroke="black" fill="none" d="M25,30A65.19 65.19 0 0 1 80,65"></path>
    <circle r="1" fill="red" cx="25" cy="30"></circle>
    <circle r="1" fill="green" cx="80" cy="65"></circle>
    <circle r="1" fill="magenta" cx="22.19" cy="95.13"></circle>
    <circle r="1" fill="darkgreen" cx="52.5" cy="47.5"></circle>
    <circle r="1" fill="yellow" cx="57.19" cy="40.13"></circle>
    <circle r="1" fill="maroon" cx="62.6" cy="31.62"></circle>
    <circle r="1" fill="orange" cx="48.27" cy="31"></circle>
    <circle r="1" fill="teal" cx="69.24" cy="44.35"></circle>
    <text x="25" y="28">P<tspan>0</tspan></text>
    <text x="48.27" y="29">P<tspan>1</tspan></text>
    <text x="71.24" y="42.35">P<tspan>2</tspan></text>
    <text x="83" y="63">P<tspan>3</tspan></text>
    <text x="62.6" y="29.62">P<tspan>E</tspan></text>
    <text x="59.19" y="47.13">P<tspan>H</tspan></text>
    <text x="54.5" y="54.5">P<tspan>M</tspan></text>
</svg>

PEP0P3中圆弧相切的线相交。为了使曲线与圆弧相切,P1 必须位于线段 P0PE 上,而 P2 必须位于 P3PE 上。令 k 为比率 P0P1/P0PE(也等于 P3P2/P3PE ):

P1 = (1 - k)P0 + k PE

P2 = (1 - k)P3 + k PE

我们还有以下(做一些比例):

PM = (P0 + P3) / 2

PH = PM / cos(x) = PM sec(x) = (P0 + P3) 秒(x) / 2

PE = PH / cos(x) = PM sec(x)^2 = (P0 + P3) 秒(x)^2 / 2

为了简化我们的计算,我认为所有矢量点都是基于中心的,但最终它并不重要。

通用的 4 点贝塞尔曲线由公式给出

C(t) = t^3 P3 + 3(1 - t)t^2 P2 + 3(1 - t)^2 t P1 + (1 - t)^3 P0

我们必须有 C(1/2) = PH,所以

C(1/2) = (P0 + 3 P1 + 3 P2 + P3) / 8

= ((P0 + P3) + 3(1 - k)P0 + 3 k PE + 3(1 - k)P3 + 3 k PE) / 8

= ((P0 + P3) + 3(1 - k)(P0 + P3) + 6 k PE) / 8

= (P0 + P3)(1 + 3(1 - k) + 3 k 秒(x)^2) / 8

所以,这是我们的等式(乘以 8)来找到 k

8 C(1/2) = 8 PH

=> (P0 + P3)(4 - 3 k + 3 k sec(x)^2) = 4(P0 + P3) 秒(x)

让我们去掉向量 (P0 + P3),我们得到:

4 - 3 k + 3 k 秒(x)^2 = 4 秒(x)

=> 3 k (sec(x)^2 - 1) = 4(sec(x) - 1)

=> k = 4 / 3(sec(x) + 1)

现在您知道在哪里放置控制点了。万岁!

如果你有 x = pi/4,你会得到 k = 0.552... 你可能已经看到过这个值.

在处理椭圆弧时,您所要做的就是相应地缩放点的坐标。

如果你必须处理更大的角度,我建议将它们分成更多的曲线。这实际上是一些软件在绘制圆弧时所做的,因为计算贝塞尔曲线有时比使用正弦和余弦更快。

【讨论】:

    【解决方案5】:

    Wolfram MathWorld 上有 Mathematica 代码:Bézier Curve Approximation of an Arc,应该可以帮助您入门。

    另见:

    【讨论】:

    【解决方案6】:

    Raphael 2.1.0 支持 Arc->Cubic (path2curve-function),并且在修复了 S 和 T 路径规范化中的错误之后,它现在似乎可以工作了。我更新了*随机路径生成器*,使它只生成弧线,以便轻松测试所有可能的路径组合:

    http://jsbin.com/oqojan/53/

    测试,如果某些路径失败,我很乐意得到报告。

    编辑:刚刚意识到这是 3 岁的线程......

    【讨论】:

      【解决方案7】:

      我已经成功地将这个general solution for any elliptical arc 用作三次贝塞尔曲线。它甚至包括公式中的开始和结束角度,因此不需要额外的旋转(这对于非圆形椭圆来说是个问题)。

      【讨论】:

        【解决方案8】:

        我最近偶然发现了这个问题。我从这里提到的文章中以模块的形式编译了一个解决方案。

        它接受起始角度、结束角度、中心和半径作为输入。

        它非常接近小弧 (

        此解决方案与开始和结束角度顺序无关 - 它总是选择次弧。

        因此,您获得了在绝对坐标中定义三次贝塞尔曲线所需的所有四个点。

        我认为这在代码和 cmets 中得到了最好的解释:

        'use strict';
        
        module.exports = function (angleStart, angleEnd, center, radius) {
            // assuming angleStart and angleEnd are in degrees
            const angleStartRadians = angleStart * Math.PI / 180;
            const angleEndRadians = angleEnd * Math.PI / 180;
        
            // Finding the coordinates of the control points in a simplified case where the center of the circle is at [0,0]
            const relControlPoints = getRelativeControlPoints(angleStartRadians, angleEndRadians, radius);
        
            return {
                pointStart: getPointAtAngle(angleStartRadians, center, radius),
                pointEnd: getPointAtAngle(angleEndRadians, center, radius),
                // To get the absolute control point coordinates we just translate by the center coordinates
                controlPoint1: {
                    x: center.x + relControlPoints[0].x,
                    y: center.y + relControlPoints[0].y
                },
                controlPoint2: {
                    x: center.x + relControlPoints[1].x,
                    y: center.y + relControlPoints[1].y
                }
            };
        };
        
        function getRelativeControlPoints(angleStart, angleEnd, radius) {
            // factor is the commonly reffered parameter K in the articles about arc to cubic bezier approximation 
            const factor = getApproximationFactor(angleStart, angleEnd);
        
            // Distance from [0, 0] to each of the control points. Basically this is the hypotenuse of the triangle [0,0], a control point and the projection of the point on Ox
            const distToCtrPoint = Math.sqrt(radius * radius * (1 + factor * factor));
            // Angle between the hypotenuse and Ox for control point 1.
            const angle1 = angleStart + Math.atan(factor);
            // Angle between the hypotenuse and Ox for control point 2.
            const angle2 = angleEnd - Math.atan(factor);
        
            return [
                {
                    x: Math.cos(angle1) * distToCtrPoint,
                    y: Math.sin(angle1) * distToCtrPoint
                },
                {
                    x: Math.cos(angle2) * distToCtrPoint,
                    y: Math.sin(angle2) * distToCtrPoint
                }
            ];
        }
        
        function getPointAtAngle(angle, center, radius) {
            return {
                x: center.x + radius * Math.cos(angle),
                y: center.y + radius * Math.sin(angle)
            };
        }
        
        // Calculating K as done in https://pomax.github.io/bezierinfo/#circles_cubic
        function getApproximationFactor(angleStart, angleEnd) {
            let arc = angleEnd - angleStart;
        
            // Always choose the smaller arc
            if (Math.abs(arc) > Math.PI) {
                arc -= Math.PI * 2;
                arc %= Math.PI * 2;
            }
            return (4 / 3) * Math.tan(arc / 4);
        }
        

        【讨论】:

          【解决方案9】:

          Swift 解决方案基于@k88lawrence answer

          适用于弧

          func controls(center: CGPoint, start: CGPoint, end: CGPoint) -> (CGPoint, CGPoint) {
              let ax = start.x - center.x
              let ay = start.y - center.y
              let bx = end.x - center.x
              let by = end.y - center.y
              let q1 = (ax * ax) + (ay * ay)
              let q2 = q1 + (ax * bx) + (ay * by)
              let k2 = 4 / 3 * (sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx))
              let control1 = CGPoint(x: center.x + ax - (k2 * ay), y: center.y + ay + (k2 * ax))
              let control2 = CGPoint(x: center.x + bx + (k2 * by), y: center.y + by - (k2 * bx))
              return (control1, control2)
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-09-29
            • 2018-12-03
            • 2021-12-18
            • 2017-10-12
            • 1970-01-01
            • 2010-10-20
            相关资源
            最近更新 更多