4

在网上搜索后,我看到各种论坛中的各种人都在暗示用二次曲线近似三次曲线。但我找不到公式。

我想要的是这样的:

输入:startX、startY、control1X、control1Y、control2X、control2Y、endX、endY 输出:startX、startY、controlX、controlY、endX、endY

其实,既然起点和终点是一样的,我真正需要的只是……

输入:startX、startY、control1X、control1Y、control2X、control2Y、endX、endY 输出:controlX、controlY

4

8 回答 8

7

如前所述,从 4 个控制点到 3 个控制点通常是一个近似值。只有一种情况是准确的 - 当三次贝塞尔曲线实际上是度数升高的二次贝塞尔曲线时。

您可以使用度数高度方程得出一个近似值。这很简单,而且结果通常都很好。

我们称三次方 Q0..Q3 的控制点和二次方 P0..P2 的控制点。那么对于度数提升,方程为:

Q0 = P0
Q1 = 1/3 P0 + 2/3 P1
Q2 = 2/3 P1 + 1/3 P2
Q3 = P2

在您的情况下,您有 Q0..Q3 并且您正在求解 P0..P2。从上述等式计算 P1 有两种方法:

P1 = 3/2 Q1 - 1/2 Q0
P1 = 3/2 Q2 - 1/2 Q3

如果这是一个度数提升的三次方,那么两个方程将对 P1 给出相同的答案。因为它可能不是,你最好的选择是平均它们。所以,

P1 = -1/4 Q0 + 3/4 Q1 + 3/4 Q2 - 1/4 Q3

翻译成您的条款:

controlX = -0.25*startX + .75*control1X + .75*control2X -0.25*endX

Y 的计算方式类似 - 维度是独立的,因此这适用于 3d(或 nd)。

这将是一个近似值。如果您需要更好的近似值,获得它的一种方法是使用 deCastlejau 算法细分初始三次,然后对每个段进行度数减少。如果您需要更好的连续性,还有其他不那么快速和肮脏的近似方法。

于 2010-01-08T18:16:34.207 回答
4

三次方可以有环和尖点,而二次方不能有。这意味着几乎从来没有简单的解决方案。如果三次方已经是二次方,则存在简单解。通常,您必须将立方除以二次方的部分。你必须决定细分的关键点是什么。

http://fontforge.org/bezier.html#ps2ttf说:“我在网上阅读的其他资料建议检查三次样条的拐点(二次样条不能有)并在那里强制中断。在我看来,这实际上使结果更糟,它使用了更多点,并且近似值看起来不像忽略拐点时那样接近。所以我忽略了它们。

这是真的,拐点(三次的二阶导数)是不够的。但是,如果您还考虑作为三次函数的一阶导数的局部极值 (min, max),并在所有这些上施加力中断,则子曲线都是二次曲线,可以用二次曲线表示。

我测试了以下函数,它们按预期工作(找到立方的所有临界点并将立方划分为向下提升的立方)。绘制这些子曲线时,曲线与原始三次曲线完全相同,但由于某种原因,当子曲线绘制为二次曲线时,结果几乎是正确的,但并不完全正确。

所以这个答案并不是对问题的严格帮助,而是这些函数为三次到二次转换提供了一个起点。

为了找到局部极值和拐点,下面get_t_values_of_critical_points()应该提供它们。这

function compare_num(a,b) {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

function find_inflection_points(p1x,p1y,p2x,p2y,p3x,p3y,p4x,p4y)
{
  var ax = -p1x + 3*p2x - 3*p3x + p4x;
  var bx = 3*p1x - 6*p2x + 3*p3x;
  var cx = -3*p1x + 3*p2x;

  var ay = -p1y + 3*p2y - 3*p3y + p4y;
  var by = 3*p1y - 6*p2y + 3*p3y;
  var cy = -3*p1y + 3*p2y;
  var a = 3*(ay*bx-ax*by);
  var b = 3*(ay*cx-ax*cy);
  var c = by*cx-bx*cy;
  var r2 = b*b - 4*a*c;
  var firstIfp = 0;
  var secondIfp = 0;
  if (r2>=0 && a!==0)
  {
    var r = Math.sqrt(r2);
    firstIfp = (-b + r) / (2*a);
    secondIfp = (-b - r) / (2*a);
    if ((firstIfp>0 && firstIfp<1) && (secondIfp>0 && secondIfp<1))
    {
      if (firstIfp>secondIfp)
      {
        var tmp = firstIfp;
        firstIfp = secondIfp;
        secondIfp = tmp;
      }
      if (secondIfp-firstIfp >0.00001)
        return [firstIfp, secondIfp];
      else return [firstIfp];
    }
    else if (firstIfp>0 && firstIfp<1)
      return [firstIfp];
    else if (secondIfp>0 && secondIfp<1)
    {
      firstIfp = secondIfp;
      return [firstIfp];
    }
    return [];
  }
  else return [];
}

function get_t_values_of_critical_points(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
    var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
    b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
    c = p1x - c1x,
    t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a,
    t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a,
    tvalues=[];
    Math.abs(t1) > "1e12" && (t1 = 0.5);
    Math.abs(t2) > "1e12" && (t2 = 0.5);
    if (t1 >= 0 && t1 <= 1 && tvalues.indexOf(t1)==-1) tvalues.push(t1)
    if (t2 >= 0 && t2 <= 1 && tvalues.indexOf(t2)==-1) tvalues.push(t2);

    a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
    b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
    c = p1y - c1y;
    t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
    t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
    Math.abs(t1) > "1e12" && (t1 = 0.5);
    Math.abs(t2) > "1e12" && (t2 = 0.5);
    if (t1 >= 0 && t1 <= 1 && tvalues.indexOf(t1)==-1) tvalues.push(t1);
    if (t2 >= 0 && t2 <= 1 && tvalues.indexOf(t2)==-1) tvalues.push(t2);

    var inflectionpoints = find_inflection_points(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
    if (inflectionpoints[0]) tvalues.push(inflectionpoints[0]);
    if (inflectionpoints[1]) tvalues.push(inflectionpoints[1]);

    tvalues.sort(compare_num);
    return tvalues;
};

当你有那些关键的 t 值(从 0-1 范围内)时,你可以将立方分成几部分:

function CPoint()
{
  var arg = arguments;
  if (arg.length==1)
  {
    this.X = arg[0].X;
    this.Y = arg[0].Y;
  }
  else if (arg.length==2)
  {
    this.X = arg[0];
    this.Y = arg[1];
  }
}

function subdivide_cubic_to_cubics()
{
    var arg = arguments;
    if (arg.length!=9) return [];
    var m_p1 = {X:arg[0], Y:arg[1]};
  var m_p2 = {X:arg[2], Y:arg[3]};
  var m_p3 = {X:arg[4], Y:arg[5]};
  var m_p4 = {X:arg[6], Y:arg[7]};
  var t = arg[8];
  var p1p = new CPoint(m_p1.X + (m_p2.X - m_p1.X) * t,
                       m_p1.Y + (m_p2.Y - m_p1.Y) * t);
  var p2p = new CPoint(m_p2.X + (m_p3.X - m_p2.X) * t,
                       m_p2.Y + (m_p3.Y - m_p2.Y) * t);
  var p3p = new CPoint(m_p3.X + (m_p4.X - m_p3.X) * t,
                       m_p3.Y + (m_p4.Y - m_p3.Y) * t);
  var p1d = new CPoint(p1p.X + (p2p.X - p1p.X) * t,
                       p1p.Y + (p2p.Y - p1p.Y) * t);
  var p2d = new CPoint(p2p.X + (p3p.X - p2p.X) * t,
                       p2p.Y + (p3p.Y - p2p.Y) * t);
  var p1t = new CPoint(p1d.X + (p2d.X - p1d.X) * t,
                       p1d.Y + (p2d.Y - p1d.Y) * t);
  return [[m_p1.X, m_p1.Y, p1p.X, p1p.Y, p1d.X, p1d.Y, p1t.X, p1t.Y],
          [p1t.X, p1t.Y, p2d.X, p2d.Y, p3p.X, p3p.Y, m_p4.X, m_p4.Y]];
}

subdivide_cubic_to_cubics()在上面的代码中,将原始三次曲线除以值 t 为两部分。因为get_t_values_of_critical_points()将 t 值作为按 t 值排序的数组返回,所以可以轻松遍历所有 t 值,得到对应的子曲线。当您拥有这些分割曲线时,您必须将第二条子曲线除以下一个 t 值。

进行所有拆分后,您将拥有所有子曲线的控制点。现在只剩下三次控制点转换为二次。因为现在所有的子曲线都是向下提升的三次曲线,所以对应的二次控制点很容易计算。二次控制点的第一个和最后一个与三次(子曲线)的第一个和最后一个控制点相同,中间的控制点位于 P1-P2 和 P4-P3 线相交的点中。

于 2013-01-25T02:40:32.430 回答
3

约定/术语

  • 三次定义:P1/2 - 锚点,C1/C2 控制点
  • |x| 是 x 的欧几里得范数
  • 三次方的中点近似:与三次方共享相同锚点且控制点位于 C = (3·C2 - P2 + 3·C1 - P1)/4 的四边形

算法

  1. 选择一个绝对精度(prec
  2. 计算 Tdiv 作为(三次)方程的根 sqrt(3)/18 · |P2 - 3·C2 + 3·C1 - P1|/2 · Tdiv ^ 3 = prec
  3. 如果 Tdiv < 0.5 在 Tdiv 处划分立方。第一段 [0..Tdiv] 可以通过中点近似值通过二次方来近似,缺陷小于prec。从第 2 步开始重复第二个结果段(对应于 1-Tdiv)
  4. 0.5<=Tdiv<1 - 简单地将立方体一分为二。两半可以通过中点逼近来逼近
  5. Tdiv>=1 - 整个立方可以通过中点近似来近似

本页演示了第 2 步中的“魔术公式”(带有交互式示例)。

于 2010-07-26T12:57:29.090 回答
2

tfinniga 答案的另一个推导:
首先查看 Wikipedia Bezier curve 了解二次和三次 Bezier 曲线的公式(也是不错的动画):

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

要求这些在中间匹配,t = 1/2:

(P0 + 2 Q + P3) / 4 = (P0 + 3 P1 + 3 P2 + P3) / 8  
=> Q = P1 + P2 - (P0 + P1 + P2 + P3) / 4  

(这样写的 Q 有一个几何解释:

Pmid = middle of P0 P1 P2 P3  
P12mid = midway between P1 and P2  
draw a line from Pmid to P12mid, and that far again: you're at Q.  

希望这是有道理的——举几个例子。)

于 2010-01-11T11:25:34.297 回答
1

我应该注意到,Adrian 的解决方案非常适合单个三次,但是当三次是平滑三次样条的分段时,使用他的中点近似方法会导致分段节点处的斜率连续性丢失。因此,如果您正在使用字体字形或出于任何其他原因想要保持曲线的平滑度(很可能是这种情况),那么http://fontforge.org/bezier.html#ps2ttf中描述的方法要好得多.

尽管这是一个老问题,但很多像我这样的人会在搜索结果中看到它,所以我在这里发布。

于 2013-01-07T12:50:25.593 回答
1

通常,您将不得不使用多条二次曲线——许多三次曲线的情况甚至不能用一条二次曲线模糊地近似。

在http://www.timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm上有一篇很好的文章讨论了这个问题,以及解决它的一些方法(包括交互式演示)。

于 2010-01-06T19:54:15.543 回答
0

我可能会绘制一系列曲线,而不是尝试使用不同的算法绘制一条曲线。有点像画两个半圆来组成一个完整的圆。

于 2010-01-05T22:17:15.237 回答
0

尝试寻找开源 Postcript 字体到 Truetype 字体转换器。我确定他们有。Postscript 使用三次贝塞尔曲线,而 Truetype 使用二次贝塞尔曲线。祝你好运。

于 2010-06-19T23:04:56.407 回答