【问题标题】:Find the tangent of a point on a cubic bezier curve求三次贝塞尔曲线上一点的切线
【发布时间】:2011-05-04 14:49:27
【问题描述】:

对于三次贝塞尔曲线,通常有四个点 a、b、c 和 d,

对于给定的值 t,

此时如何最优雅地找到 切线

【问题讨论】:

    标签: ios swift bezier


    【解决方案1】:

    曲线的切线就是它的导数。 Michal 使用的参数方程:

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

    应该是

    的导数
    dP(t) / dt =  -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3 
    

    顺便说一句,这在您之前的问题中似乎是错误的。我相信您在那里使用的是二次贝塞尔曲线的斜率,而不是三次。

    从那里,实现一个执行此计算的 C 函数应该是微不足道的,就像 Michal 已经为曲线本身提供的那样。

    【讨论】:

      【解决方案2】:

      这里是复制和粘贴经过全面测试的代码:

      它沿着曲线绘制近似点,并且它绘制切线。

      bezierInterpolation 找到积分

      bezierTangent 求切线

      下面提供了两个版本bezierInterpolation

      bezierInterpolation 完美运行。

      altBezierInterpolation 完全一样,但它是以扩展、清晰、解释性的方式编写的。它使算术更容易理解。

      使用这两个例程中的任何一个:结果是相同的。

      在这两种情况下,使用bezierTangent 来查找切线。 (注:Michal 的精彩代码库here。)

      还包括一个如何与drawRect: 一起使用的完整示例。

      // MBBezierView.m    original BY MICHAL stackoverflow #4058979
      
      #import "MBBezierView.h"
      
      
      
      CGFloat bezierInterpolation(
          CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
      // see also below for another way to do this, that follows the 'coefficients'
      // idea, and is a little clearer
          CGFloat t2 = t * t;
          CGFloat t3 = t2 * t;
          return a + (-a * 3 + t * (3 * a - a * t)) * t
          + (3 * b + t * (-6 * b + b * 3 * t)) * t
          + (c * 3 - c * 3 * t) * t2
          + d * t3;
      }
      
      CGFloat altBezierInterpolation(
         CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
          {
      // here's an alternative to Michal's bezierInterpolation above.
      // the result is absolutely identical.
      // of course, you could calculate the four 'coefficients' only once for
      // both this and the slope calculation, if desired.
          CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
          CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
          CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
          CGFloat C4 = ( a );
      
          // it's now easy to calculate the point, using those coefficients:
          return ( C1*t*t*t + C2*t*t + C3*t + C4  );
          }
      
      
      
      
      
      
      
      CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
       {
          // note that abcd are aka x0 x1 x2 x3
      
      /*  the four coefficients ..
          A = x3 - 3 * x2 + 3 * x1 - x0
          B = 3 * x2 - 6 * x1 + 3 * x0
          C = 3 * x1 - 3 * x0
          D = x0
      
          and then...
          Vx = 3At2 + 2Bt + C         */
      
          // first calcuate what are usually know as the coeffients,
          // they are trivial based on the four control points:
      
          CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
          CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
          CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
          CGFloat C4 = ( a );  // (not needed for this calculation)
      
          // finally it is easy to calculate the slope element,
          // using those coefficients:
      
          return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
      
          // note that this routine works for both the x and y side;
          // simply run this routine twice, once for x once for y
          // note that there are sometimes said to be 8 (not 4) coefficients,
          // these are simply the four for x and four for y,
          // calculated as above in each case.
       }
      
      
      
      
      
      
      
      @implementation MBBezierView
      
      - (void)drawRect:(CGRect)rect {
          CGPoint p1, p2, p3, p4;
      
          p1 = CGPointMake(30, rect.size.height * 0.33);
          p2 = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
          p3 = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
          p4 = CGPointMake(-30 + CGRectGetMaxX(rect), rect.size.height * 0.66);
      
          [[UIColor blackColor] set];
          [[UIBezierPath bezierPathWithRect:rect] fill];
          [[UIColor redColor] setStroke];
          UIBezierPath *bezierPath = [[[UIBezierPath alloc] init] autorelease];   
          [bezierPath moveToPoint:p1];
          [bezierPath addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
          [bezierPath stroke];
      
          [[UIColor brownColor] setStroke];
      
       // now mark in points along the bezier!
      
          for (CGFloat t = 0.0; t <= 1.00001; t += 0.05) {
        [[UIColor brownColor] setStroke];
      
              CGPoint point = CGPointMake(
                  bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x),
                  bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));
      
                  // there, use either bezierInterpolation or altBezierInterpolation,
                  // identical results for the position
      
              // just draw that point to indicate it...
              UIBezierPath *pointPath =
                 [UIBezierPath bezierPathWithArcCenter:point
                   radius:5 startAngle:0 endAngle:2*M_PI clockwise:YES];
              [pointPath stroke];
      
              // now find the tangent if someone on stackoverflow knows how
              CGPoint vel = CGPointMake(
                  bezierTangent(t, p1.x, p2.x, p3.x, p4.x),
                  bezierTangent(t, p1.y, p2.y, p3.y, p4.y));
      
              // the following code simply draws an indication of the tangent
              CGPoint demo = CGPointMake( point.x + (vel.x*0.3),
                                            point.y + (vel.y*0.33) );
              // (the only reason for the .3 is to make the pointers shorter)
              [[UIColor whiteColor] setStroke];
              UIBezierPath *vp = [UIBezierPath bezierPath];
              [vp moveToPoint:point];
              [vp addLineToPoint:demo];
              [vp stroke];
          }   
      }
      
      @end
      
      to draw that class...
      MBBezierView *mm = [[MBBezierView alloc]
                           initWithFrame:CGRectMake(400,20, 600,700)];
      [mm setNeedsDisplay];
      [self addSubview:mm];
      

      以下是沿贝塞尔三次曲线计算近似等距点及其切线的两个例程。

      为了清晰和可靠,这些例程以最简单、最具解释性的方式编写。

      CGFloat bezierPoint(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
          {
          CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
          CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
          CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
          CGFloat C4 = ( a );
      
          return ( C1*t*t*t + C2*t*t + C3*t + C4  );
          }
      
      CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
          {
          CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
          CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
          CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
          CGFloat C4 = ( a );
      
          return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
          }
      

      四个预先计算的值 C1 C2 C3 C4 有时称为贝塞尔曲线的系数。 (回想一下,a b c d 通常被称为四个控制点。)

      当然,t 从 0 到 1,例如每 0.05 次。

      只需为 X 调用一次,然后为 Y 单独调用一次。

      希望它对某人有所帮助!


      重要事实:

      (1) 绝对事实:不幸的是,Apple 提供的绝对没有方法可以从 UIBezierPath 中提取点。截至 2019 年为真。

      (2) 不要忘记,在 UIBezierPath 上沿 制作动画就像馅饼一样简单。谷歌many examples

      (3) 很多人问,“CGPathApply 不能用来从 UIBezierPath 中提取点吗?” 不,CGPathApply 完全不相关:它只是给你您的“制作任何路径的说明”列表(因此,“从这里开始”、“画一条直线到该点”等)名称令人困惑,但 CGPathApply 与贝塞尔路径完全无关。


      对于游戏程序员 - 正如@Engineer 指出的那样,您可能想要切线的法线,幸运的是 Apple 内置了向量数学:

      https://developer.apple.com/documentation/accelerate/simd/working_with_vectors
      https://developer.apple.com/documentation/simd/2896658-simd_normalize

      【讨论】:

      • 非常有用,谢谢。请注意,一旦计算出切线,就应该对其进行归一化,因为该函数提供了一个任意长度的向量 - 实际上它随着 t 的增长而增长。 This 应该帮助其他人做到这一点。
      • @Engineer 谢谢,确实在很多情况下你想要规范化切线 - 好消息,实际上有用于规范化和其他向量数学的内置函数! developer.apple.com/documentation/simd/2896658-simd_normalize
      • 哈,甚至没有将它用于Apple相关的开发,并且习惯于编写我自己的矢量代码 - 无论语言/平台如何,这都是一个很好的通用答案。
      【解决方案3】:

      我发现使用提供的方程式太容易出错。太容易错过一个微妙的 t 或放错位置的括号。

      相比之下,维基百科提供了一个更清晰、更清晰、更衍生的恕我直言:

      ...在代码中很容易实现:

      3f * oneMinusT * oneMinusT * (p1 - p0)
      + 6f * t * oneMinusT * (p2 - p1)
      + 3f * t * t * (p3 - p2)
      

      (假设您已用您选择的语言配置了 vector-minus;问题没有专门标记为 ObjC,iOS 现在有多种语言可用)

      【讨论】:

      • 完美,但不幸的是,这么长的时间在当前的 Swift 中会中断
      【解决方案4】:

      这是我的 Swift 实现。

      我尽我所能通过消除所有冗余数学运算来优化速度。即对数学运算进行最少的调用。并使用尽可能少的乘法(这比求和要昂贵得多)。

      有 0 次乘法来创建贝塞尔曲线。 然后 3 次乘法得到一个贝塞尔曲线。 和 2 次乘法得到贝塞尔曲线的切线。

      struct CubicBezier {
      
          private typealias Me = CubicBezier
          typealias Vector = CGVector
          typealias Point = CGPoint
          typealias Num = CGFloat
          typealias Coeficients = (C: Num, S: Num, M: Num, L: Num)
      
          let xCoeficients: Coeficients
          let yCoeficients: Coeficients
      
          static func coeficientsOfCurve(from c0: Num, through c1: Num, andThrough c2: Num, to c3: Num) -> Coeficients
          {
              let _3c0 = c0 + c0 + c0
              let _3c1 = c1 + c1 + c1
              let _3c2 = c2 + c2 + c2
              let _6c1 = _3c1 + _3c1
      
              let C = c3 - _3c2 + _3c1 - c0
              let S = _3c2 - _6c1 + _3c0
              let M = _3c1 - _3c0
              let L = c0
      
              return (C, S, M, L)
          }
      
          static func xOrYofCurveWith(coeficients coefs: Coeficients, at t: Num) -> Num
          {
              let (C, S, M, L) = coefs
              return ((C * t + S) * t + M) * t + L
          }
      
          static func xOrYofTangentToCurveWith(coeficients coefs: Coeficients, at t: Num) -> Num
          {
              let (C, S, M, _) = coefs
              return ((C + C + C) * t + S + S) * t + M
          }
      
          init(from start: Point, through c1: Point, andThrough c2: Point, to end: Point)
          {
              xCoeficients = Me.coeficientsOfCurve(from: start.x, through: c1.x, andThrough: c2.x, to: end.x)
              yCoeficients = Me.coeficientsOfCurve(from: start.y, through: c1.y, andThrough: c2.y, to: end.y)
          }
      
          func x(at t: Num) -> Num {
              return Me.xOrYofCurveWith(coeficients: xCoeficients, at: t)
          }
      
          func y(at t: Num) -> Num {
              return Me.xOrYofCurveWith(coeficients: yCoeficients, at: t)
          }
      
          func dx(at t: Num) -> Num {
              return Me.xOrYofTangentToCurveWith(coeficients: xCoeficients, at: t)
          }
      
          func dy(at t: Num) -> Num {
              return Me.xOrYofTangentToCurveWith(coeficients: yCoeficients, at: t)
          }
      
          func point(at t: Num) -> Point {
              return .init(x: x(at: t), y: y(at: t))
          }
      
          func tangent(at t: Num) -> Vector {
              return .init(dx: dx(at: t), dy: dy(at: t))
          }
      }
      

      像这样使用:

      let bezier = CubicBezier.init(from: .zero, through: .zero, andThrough: .zero, to: .zero)
      
      let point02 = bezier.point(at: 0.2)
      let point07 = bezier.point(at: 0.7)
      
      let tangent01 = bezier.tangent(at: 0.1)
      let tangent05 = bezier.tangent(at: 0.5)
      

      【讨论】:

      • 我同时进一步改进了算法,确保编译器不会在不需要它们存在的地方创建乘法。享受吧。
      【解决方案5】:

      直到我意识到对于参数方程,(dy/dt)/(dx/dt) = dy/dx

      【讨论】:

        猜你喜欢
        • 2011-03-10
        • 2012-07-27
        • 2017-12-12
        • 1970-01-01
        • 1970-01-01
        • 2016-06-24
        • 1970-01-01
        • 1970-01-01
        • 2016-10-05
        相关资源
        最近更新 更多