将双曲线转换为贝塞尔曲线以绘制轨道路径 1:曲线展平2:贝塞尔近似3:使用 Catmull-Rom 样条的 Poly-Bezier4:结合1和25:结合1和3

问题描述

我正在使用涉及轨道力学的 HTML 画布编写 2D 模拟器和游戏。该程序的一个特点是在一个点上取一颗卫星的位置和速度矢量,并返回围绕一颗行星的二维轨道的半长轴、偏心率、近点角等。当偏心率小于 1 时,我可以使用 ctx.ellipse() 轻松地将轨道绘制为椭圆。然而,对于大于 1 的偏心率,轨道的正确形状是双曲线。目前,如果偏心率大于 1,我的程序将不绘制任何内容,但我希望它绘制正确的双曲线轨道。由于没有内置的“双曲线”函数,我需要将我的轨道转换为贝塞尔曲线。我对如何做到这一点感到有些茫然。输入将是一个焦点的位置、半长轴、偏心率和近点自变量(基本上是轨道旋转了多远),它应该返回正确的控制点来绘制双曲线的 Bézier 曲线近似值。它不必非常完美,只要它足够贴合即可。我该如何解决这个问题?

解决方法

就圆锥截面而言,不幸的是,双曲线是 Canvas 无法 本地渲染的一类曲线,因此您只能逼近您需要的曲线。这里有一些选项:

  1. 通过在距离上的一个或两个点和极值附近的很多点采样双曲线来展平曲线,以便您可以绘制一个看起来的简单多边形就像一条曲线。
  2. 使用单个“最佳近似”二次或三次曲线对双曲线建模。
  3. 正如@fang 提到的:在几个点处对曲线进行采样,然后通过这些点将 Catmull-Rom 样条曲线转换为 Bezier 形式。
  4. 结合方法 1 和 2。使用单个贝塞尔曲线来近似双曲线中实际看起来弯曲的部分,并使用直线表示没有弯曲的部分。
  5. 组合方法 1 和 3,对弯曲钻头使用 Catmull-Rom 样条,对直钻头使用直线。

1:曲线展平

曲线平坦化基本上是微不足道的。旋转曲线直到它与轴对齐,然后使用标准双曲函数计算给定 yx,其中 a 是极值之间距离的一半,b是短半轴:

x²/a² - y²/b² = 1
x²/a² = 1 + y²/b² 
x²/a² - 1 = y²/b² 
b²(x²/a² - 1) = y²
b²(x²/a² - 1) = y²
± sqrt(b²(x²/a² - 1)) = y

插入您的值,迭代 x 以获得一系列 (x,y) 坐标(记住在极值附近生成更多坐标),然后将它们转换为 moveTo()第一个坐标,然后是其余的 lineTo() 调用。只要您的点密度对于您呈现的比例来说足够高,这看起来应该没问题:

function flattenHyperbola(a,b,inf=1000) {
  const points = [],a2 = a**2,b2 = b**2;

  let x,y,x2;

  for (x=inf; x>0.1; x/=2) {
    x2 = (a+x)**2;
    y = -Math.sqrt(b2*x2/a2 - b2);
    points.push({x: a+x,y});
  }

  points.push({x:a,y:0});

  for (x=0.1; x<inf; x*=2) {
    x2 = (a+x)*(a+x);
    y = Math.sqrt(b2*x2/a2 - b2);
    points.push({x:  a+x,y});
  }

  return points;
}

让我们用红色绘制双曲线,用蓝色绘制近似值:

curve flattening using logarithmic intervals

当然,这种方法的缺点是您需要为用户可能查看图形的每个比例创建单独的扁平曲线。或者,您需要生成带有很多点的平坦曲线,然后根据放大/缩小的程度跳过坐标来绘制它。

2:贝塞尔近似

双曲线的参数表示是 f(t)=(a*sec(t),b*tan(t))(或者更确切地说,这是 y 轴对齐双曲线的表示 - 我们可以通过应用标准旋转变换来获得任何其他变体)。我们可以快速浏览一下这些函数的泰勒级数,看看我们可以使用哪种贝塞尔曲线:

sec(t) = 1 + t²/2 + 5t⁴/15 + ...
tan(t) = x + t³/3 + 2t⁵/15 + ...

因此,我们可能可以只使用每个维度的前两项,在这种情况下,我们可以使用三次贝塞尔(因为最高阶是 t³):

Second/Third order Taylor approximation of the hyperbola

事实证明,这行不通:它太不准确了,所以我们必须更好地近似:我们创建了一条贝塞尔曲线,起点和终点“很远”,控制点集 such that the Bezier midpoint coincides with the hyperbola's extrema。如果我们尝试这样做,我们可能会误以为这会奏效:

midpoint-aligned Bezier approximation over a small interval

但如果我们选择距离足够远的 x,我们会发现这个近似值很快就失效了:

function touchingParabolicHyperbola(a,inf=1000) {
  const beziers = [],x2,A,CA;

  for(x=50; x<inf; x+=50) {
    x2 = x**2;
    y = sqrt(b2*x2/a2 - b2);

    // Hit up https://pomax.github.io/bezierinfo/#abc
    // and model the hyperbola in the cubic graphic to
    // understand why the next,very simple-looking,// line actually works:
    A = a - (x-a)/3;

    // We want the control points for this A to lie on
    // the asymptote,but for small x we want it to be 0,// otherwise the curve won't run parallel to the
    // hyperbola at the start and end points.
    CA = lerp(0,A*b/a,x/inf);

    beziers.push([
      {x,y: -y},{x: A,y:-CA},y: CA},{x,y},]);
  }

  return beziers;
}

这向我们展示了一系列曲线,这些曲线开始看起来不错,但很快就变得完全没用了:

midpoint-aligned Bezier approximation over a large interval

一个明显的问题是曲线最终会超过渐近线。我们可以通过将控制点强制为 (0,0) 来解决这个问题,这样贝塞尔包是一个三角形,而曲线将始终位于其内部。

function tangentialParabolicHyperbola(a,y;

  for(x=50; x<inf; x+=50) {
    x2 = x**2;
    y = sqrt(b2*x2/a2 - b2);  
    beziers.push([
      {x,y:-y},{x: 0,y:0},]);
  }

  return beziers;
}

这会导致一系列曲线,从一侧无用,到另一侧无用:

tangent-aligned Bezier approximations

所以单曲线近似并不是那么好。如果我们使用更多曲线会怎样?

3:使用 Catmull-Rom 样条的 Poly-Bezier

我们可以通过沿着双曲线使用多条贝塞尔曲线来克服上述问题,我们可以(几乎很简单)通过在双曲线上选取几个坐标来计算,然后通过这些点构造一个 Catmull-Rom spline。由于通过 N 个点的 Catmull-Rom 样条等效于由 N-3 个线段组成的 poly-Bezier,因此这可能是获胜策略。

function hyperbolaToPolyBezier(a,b2 = b**2,step = inf/10;

  let x,for (x=a+inf; x>a; x-=step) {
    x2 = x**2;
    y = -Math.sqrt(b2*x2/a2 - b2);
    points.push({x,y});
  }

  for (x=a; x<a+inf; x+=step) {
    x2 = x**2;
    y = Math.sqrt(b2*x2/a2 - b2);
    points.push({x,y});
  }

  return crToBezier(points);
}

the conversion function 是:

function crToBezier(points) {
  const beziers = [];

  for(let i=0; i<points.length-3; i++) {
    //  NOTE THE i++ HERE! We're performing a sliding window conversion.
    let [p1,p2,p3,p4] = points.slice(i);
    beziers.push({
      start: p2,end: p3,c1: { x: p2.x + (p3.x-p1.x)/6,y: p2.y + (p3.y-p1.y)/6 },c2: { x: p3.x - (p4.x-p2.x)/6,y: p3.y - (p4.y-p2.y)/6 }
    })
  }

  return beziers;
}

让我们绘制:

catmull-Rom approximation

与展平相比,我们必须在前期做更多的工作,但好处是我们现在有一条曲线,在任何比例下实际上都“看起来像一条曲线”。

4:结合(1)和(2)

现在,大部分双曲线实际上“看起来是直的”,所以对这些部分使用大量贝塞尔曲线确实感觉有点傻:为什么不仅用曲线建模弯曲的部分,而且用直线建模直的部分?

我们已经看到,如果我们将控制点固定为 (0,0),那么可能会有一条至少足够体面的曲线,所以让我们结合方法 1 和方法 2,在那里我们可以创建一条带有 start 和端点“足够接近”曲线,并将两条线段附加到将贝塞尔曲线连接到渐近线上的两个远点(位于 y=±b/a * x,因此 {{1} 的任何大值} 将产生足够可用的 x)

当然,诀窍是找到单条曲线仍然捕获曲率的距离,同时还使我们的无穷大线看起来像它们平滑地连接到我们的单条曲线上。 Bezier projection identity 再次派上用场:我们希望 y 位于 A,我们希望 Bezier 中点位于 (0,0),这意味着我们的起点和终点应该具有 (a,0)x 坐标:

4a

这给了我们以下结果(蓝色贝塞尔曲线,黑色线段):

Combining one Bezier and straight lines

所以这不是很好,但也不是很糟糕。如果观众不仔细检查渲染,它当然足够好,而且它绝对便宜,但是我们可以通过多做一点工作做得更好,所以:让我们也看看我们可能在这里想出的最佳近似值:

5:结合(1)和(3)

如果单个 Bezier 不起作用,并且我们已经看到使用 Catmull-Rom 样条而不是单个曲线效果更好,那么我们当然也可以结合方法 1 和方法 3。我们可以形成一个多通过生成以极值为中心的五个点并将通过这些点生成的 Catmull-Rom 样条曲线转换为 Bezier 形式,可以构建两条贝塞尔曲线而不是一条贝塞尔曲线,从而更好地拟合极值:

function hyperbolicallyFitParabolica(a,inf=1000) {
  const a2 = a**2,x = 4*a,x2 = x**2,y = sqrt(b2*x2/a2 - b2)
        bezier = [
          {x: x,y: 0},{x: x,y: y},],start = { x1:x,y1:-y,x2:inf,y2: -inf * b/a},end   = { x1:x,y1: y,y2:  inf * b/a};

  return [start,bezier,end];
}

这给了我们以下结果(蓝色为 CR,黑色为线段):

Combining Catmull-Rom and straight lines

这可能是我们在“计算成本低”、“易于扩展”和“外观正确”之间进行权衡的最佳选择。