如何用贝塞尔曲线最佳逼近几何弧?

45

当使用贝塞尔曲线逼近方法绘制2D圆弧时,如果已知圆心点、起始和结束角度以及半径,如何计算两个控制点?


1
你是在尝试近似一个圆弧吗? - Beska
1
http://math.stackexchange.com/questions/11698/how-elliptic-arc-can-be-represented-by-cubic-bézier-curve - Ciro Santilli OurBigBook.com
1
最好的方法是使用有理贝塞尔曲线和伯恩斯坦加权值,以便完全匹配曲线部分。 - Tatarize
9个回答

36

这是一个8年前的问题,但最近我遇到了同样的问题,所以我想分享一下我的解决方案。我花了很多时间尝试使用Aleksas Riškus的文章中的解决方案(9),但在没有进行谷歌搜索之前,我无法从中获得任何有意义的数字。后来我得知,方程式中似乎存在一些打字错误。根据这篇博客文章中列出的更正,给定弧线的起始点和结束点(分别为[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

希望这能帮到除了我之外的其他人!

对于弧度小于等于 PI / 2 (90º) 的情况,运行完美。 - vicegax
Java和Processing用户注意:k2计算必须使用浮点数,因此请写成4./3或者4f/3以获得正确的结果。 - H.Scheidl
这个很好用。你知道有没有相反的解决方案吗? - Bret
这个公式看起来非常像来自Aleksas Riškus的文章(https://www.researchgate.net/publication/265893293_Approximation_of_a_cubic_bezier_curve_by_circular_arcs_and_vice_versa),也被@Nic的回答引用。 - ceztko

20

在StackOverflow帖子中很难解释清楚这个问题,特别是因为向您证明它涉及许多详细步骤。然而,您所描述的是一个常见问题,有许多详尽的解释。请参见这里这里;我非常喜欢#2并且以前也用过。


13
#2 结尾处有错别字:"x3=x1" 应为 "x3=x0"。 - xan
3
我在http://pomax.github.io/bezierinfo/#circles_cubic上提供了一个稍微通用一些的解释,涵盖了第二个链接的结论,同时解释了任何弧长的坐标,而不仅仅是四分之一圆的角度。 - Mike 'Pomax' Kamermans
@Mike'Pomax'Kamermans,我喜欢这篇文章中提供的公式,但源代码完全不同,似乎只适用于半径为1的圆。 - FMaz008
圆的半径并不重要,您只需按自己圆的半径比例缩放该值即可。有一个半径为100的圆?使用55.1785而不是0.551785。 - Mike 'Pomax' Kamermans

12

在Aleksas Riškus的文章《Approximation of a cubic bezier curve by circular arcs and vice versa》1中提供了一个很好的解释。

简而言之,使用Bezier曲线可以达到最小误差为1.96×10^-4,对于大多数应用程序来说这相当不错。

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

p0 = [0, radius]

p1 = [radius * K, radius]  
 
p2 = [radius, radius * K]

p3 = [radius, 0]

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

K = 0.5522847498

1
K是弧的一个函数。当弧度为PI/2时,K = 0.5522847498才有效。 - dodev
如果您要将其用于四分之一的tau部分,最好使用Mortensen在0.55191502449351057处的值,以最小化正负误差。而如果您想要最小化整体误差,奇怪的是0.552比这两个值都更好。 - Tatarize
Naive几何值的总误差为((4/3)*(sqrt(2) - 1),在10000000个样本中为1401。Mortensen的误差为1180,.552的误差为1160。 - Tatarize
2
为了最小化总平方误差,最佳值为0.5519703814011128603134107。为了最小化最大误差,最佳值是Mortensen的,更精确地说是0.5519150244935105707435627。 - Deadcode

7
我将回答这个古老的问题(它应该属于数学,因此编写公式将是可怕的),并进行一些演示。
假设P0和P3是您的弧的起始点和终点,P1和P2是Bézier曲线的控制点,x是角度的度量值除以二。 假设x小于pi / 2。
让PM成为线段P0P3的中点,PH成为弧的中点。为了近似弧,我们希望Bézier曲线从P0开始,通过PH,结束于P3,并在P0和P3处切线与弧相切。
(单击“运行代码段”以显示图形。诅咒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>

PE成为切线于P0P3的弧线的交点。为了使曲线切于弧线,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) sec(x) / 2

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

为了简化计算,我将所有向量点都视为基于中心,但最终这并不重要。

通用的四点贝塞尔曲线由以下公式给出:

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 sec(x)^2) / 8

这是我们用来找到k的方程式(乘以8):

8 C(1/2) = 8 PH

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

我们可以消除向量(P0 + P3),然后我们得到:

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

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

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

现在你知道应该把控制点放在哪里了。太好了!

如果你有x = pi/4,那么你会得到k = 0.552... 你可能在其他地方看到过这个值。

当处理椭圆弧时,你只需相应地缩放点的坐标即可。

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


当公式没有明确说明控制点的x或y值时,实际掌握起来很困难。你能否提供实际的公式,如c1x = (...),c1y = (...),c2x = (...),c2y = (...)? - FMaz008
@FMaz008 你可以从*C(t)*的公式中推导出它们,例如 Cx(t) = t^3 P3x + 3(1 - t)t^2 P2x + 3(1 - t)^2 t P1x + (1 - t)^3 P0x - MaxArt

5

2
一个有效的链接来解释同样的事情是http://pomax.github.io/bezierinfo/#circles_cubic - Mike 'Pomax' Kamermans

4

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

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

测试一下,如果有某些路径失败了,请告诉我。

编辑:刚刚意识到这是三年前的帖子...


2
我使用这个通用解决方案作为椭圆弧的三次贝塞尔曲线,取得了成功。它甚至在公式中包含了起始和结束角度,因此不需要额外的旋转(对于非圆形椭圆会有问题)。这里是相关文档。请注意保留HTML标签。

1
最近我遇到了这个问题。我从这里提到的文章中编译了一个模块形式的解决方案。
它接受起始角度、终止角度、中心和半径作为输入。
它相当准确地近似小弧(≤ PI/2)。如果您需要近似从 PI/2 到 2*PI 的弧,您可以将它们分成小于 PI/2 的部分,计算相应的曲线,然后将它们连接起来。
这个解决方案的起始和结束角度顺序是不确定的 - 它总是选择较小的弧。
结果,您可以得到四个点,用于在绝对坐标中定义一个三次贝塞尔曲线。
我认为这最好通过代码和注释来解释:
'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);
}

0

Swift解决方案基于@k88lawrence 答案

适用于弧度 <= PI / 2

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接