【问题标题】:Rotation Interpolation旋转插值
【发布时间】:2011-02-12 02:27:15
【问题描述】:

注意:为了简单起见,我将用度数提出这个问题,弧度,度数,不同的零位角,问题本质上是相同的。

有人对旋转插值背后的代码有任何想法吗?给定一个线性插值函数:Lerp(from, to, amount),其中 amount 为 0...1,它按数量返回 from 和 to 之间的值。如何将相同的函数应用于 0 到 360 度之间的旋转插值?鉴于度数不应返回 0 和 360 度之外。

给定度数的单位圆:

其中从 = 45 到 = 315,算法应该采用最短路径到角度,即它应该经过零,到 360,然后到 315 - 而不是一直绕到 90、180、270 到 315 .

有没有很好的方法来实现这一点?还是只是 if() 块的可怕混乱?我错过了一些很好理解的标准方法吗? 任何帮助将不胜感激。

【问题讨论】:

    标签: math rotation interpolation


    【解决方案1】:

    我知道这已经有 2 年历史了,但我最近一直在寻找同样的问题,如果没有在此处发布 ifs,我看不到一个优雅的解决方案,所以这里是:

        shortest_angle=((((end - start) % 360) + 540) % 360) - 180;
        return shortest_angle * amount;
    

    就是这样

    ps:当然,% 表示取模,shortest_angle 是保存整个插值角度的变量

    【讨论】:

    • Not all modulos behave the same,但在 Python 中,这可以简化为 shortest_angle = ((end-start) + 180) % 360 - 180
    • 你能解释为什么这些值吗?所以它可以转换为弧度而不是度数。
    • 为我猜的每个数字做N/180*Pi :)
    • @JonathanMee 感谢您的回复!有一阵子了!我已经在 J​​avaScript 中做了一个解决方案,如果你愿意,可以使用它:pastebin.com/wp15rK3v 该函数接受一个布尔值,确定它是一个角度还是一个整数值。例如:var tankRotation = new AnimatedValue(1,true); tankRotation.set(6.2,100);//6.2 = radians, 100 = 100ms time interpolation tankRotation.get(); // returns a value that is going to 0, and after reaching 0 it continues at PI*2
    • 以前的编辑打破了这个答案。如果您查看此帖子的先前版本,当amount = 0 时,给定的答案将始终返回0,而不是start 角度。我已将其改回工作版本。
    【解决方案2】:

    抱歉,这有点复杂,这里有一个更简洁的版本:

        public static float LerpDegrees(float start, float end, float amount)
        {
            float difference = Math.Abs(end - start);
            if (difference > 180)
            {
                // We need to add on to one of the values.
                if (end > start)
                {
                    // We'll add it on to start...
                    start += 360;
                }
                else
                {
                    // Add it on to end.
                    end += 360;
                }
            }
    
            // Interpolate it.
            float value = (start + ((end - start) * amount));
    
            // Wrap it..
            float rangeZero = 360;
    
            if (value >= 0 && value <= 360)
                return value;
    
            return (value % rangeZero);
        }
    

    谁有更优化的版本?

    【讨论】:

      【解决方案3】:

      我认为更好的方法是插入 sin 和 cos,因为它们不会受到多重定义的影响。令 w = "amount" 使得 w = 0 为角度 A, w = 1 为角度 B。然后

      CS = (1-w)*cos(A) + w*cos(B);
      SN = (1-w)*sin(A) + w*sin(B);
      C = atan2(SN,CS);
      

      必须根据需要转换为弧度和度数。还必须调整分支。对于 atan2,C 回到 -pi 到 pi 的范围内。如果你想要 0 到 2pi,那么只需将 pi 添加到 C。

      【讨论】:

      • 鉴于这个问题很老而且有很多答案,你能详细说明为什么这样更好吗?
      • 更好(至少对我而言)是我更有可能第一次正确编码。给出的大多数答案的问题是他们对相关分支进行了多次算术测试。这些测试的边缘条件也增加了我不止一次搞砸的复杂性。最初的问题是“我该走哪条路?”插值或我所在的角度分支从一开始就得到唯一的回答。
      • 这是唯一有效完美的答案。在偶尔的奇怪情况下,其他答案似乎有些口吃。
      • 仅供参考,当 a 和 b 相距很远(几乎 180 度)并且不是真正的线性插值时,这不起作用。
      • 要考虑的两个问题是 1) -180 的情况是否有一个独特的答案,它具有任何有用的物理意义? 2)为什么纯线性插值优于上面建议的非线性插值?
      【解决方案4】:

      注意:使用 C# 代码

      在我的大脑中疯狂翻找之后,这就是我想出的。 基本上前提是在最后一分钟执行 0-360 换行。在内部处理 0-360 之外的值,然后在函数请求值时将它们包装在 0-360 内。

      在您选择起点和终点时,执行以下操作:

      float difference = Math.Abs(end - start);
      if (difference > 180)
      {
          // We need to add on to one of the values.
          if (end > start)
          {
              // We'll add it on to start...
              start += 360;
          }
          else
          {
              // Add it on to end.
              end += 360;
          }
      }
      

      这为您提供了实际的开始和结束值,可能在 0-360 之外...

      我们有一个 wrap 函数来确保一个值在 0 到 360 之间...

      public static float Wrap(float value, float lower, float upper)
      {
          float rangeZero = upper - lower;
      
          if (value >= lower && value <= upper)
              return value;
      
          return (value % rangeZero) + lower;
      }
      

      然后在你从函数中请求当前值的时候:

      return Wrap(Lerp(start, end, amount), 0, 360);
      

      这几乎肯定不是问题的最佳解决方案,但它似乎确实可以始终如一地工作。如果有人有更优化的方法来做到这一点,那就太好了。

      【讨论】:

        【解决方案5】:

        我想重写我的答案以更好地解释问题的答案。我的公式使用 EXCEL,单位使用度数。

        为简单起见,B 是两个值中的较大者,A 是两个值中的较小者。您可以稍后在您的解决方案中分别使用MAX()MIN()

        第 1 部分 - 走哪条路?

        我们首先要做的是确定我们想要执行计算的方向,顺时针或逆时针。为此,我们使用IF() 声明:

        IF( (B-A)<=180, (Clockwise_Formula), (AntiClockwise_Formula) )
        

        上述公式检查从BA 的逆时针方向(与从AB 的顺时针方向相同)是否小于或等于180 度。如果没有,往另一个方向走会更短。

        要检查这个是否有效:90 - 45 = 45(小于或等于 180)使 IF 语句为 TRUE,因此顺时针方向更短,但 315 - 45 = 270(大于 180)使if 语句为 FALSE,所以逆时针公式会更短。

        第 2 部分 - 顺时针公式

        现在您想在AB 之间插入N 次,顺时针或逆时针。顺时针公式比较简单。

        Clockwise_Formula: ((B-A)/N*S)+A
        

        其中S 是插值次数的计数,从 1 开始到 N-1 结束(如果是 S = N,您的答案将是 B

        示例:A = 90、B = 270、N = 4

        S=1:     ((270-90)/4*1)+90 = 135
        S=2:     ((270-90)/4*2)+90 = 180
        S=3:     ((270-90)/4*3)+90 = 225
        

        第 3 部分 - 逆时针公式

        逆时针公式会稍微复杂一些,因为我们需要逆时针穿越 360 度角。我能想到的最简单的方法是在A 上加上360,然后使用MOD(FORMULA,VALUE) 函数将答案调制360。

        您还必须在公式中交换AB,因为B 现在是最小的数字。 (这听起来可能有点令人困惑,但它确实有效!)

        (Unmodulated) AntiClockwise_Formula: (((A+360)-B)/N*S)+B
        

        示例:A = 60、B = 300、N = 4

        S=1:     (((60+360)-300)/4*1)+300 = 330
        S=2:     (((60+360)-300)/4*2)+300 = 360
        S=3:     (((60+360)-300)/4*3)+300 = 390
        

        第 4 部分 - 将答案限制在 0 和 360 之间

        看到有时(但并非总是)答案会大于 360 度吗?这就是在 MOD() 函数中包装您的逆时针公式的地方:

        AntiClockwise_Formula: MOD((((A+360)-B)/N*S)+B,360)
        

        修改第 3 部分中使用的示例将为您提供:

        S=1:     330
        S=2:     0
        S=3:     30
        

        第 5 部分 - 将所有内容放在一起

        将第 1-4 部分中的所有元素组合在一起,答案是:

        IF((B-A)<=180,((B-A)/N*S)+A,MOD((((A+360)-B)/N*S)+B,360))
        

        地点:

        A = 两个值中较小的一个(可以将 A 替换为 MIN())

        B = 两个值中较大的一个(可以将 B 替换为 MAX())

        N = 您要进行的插值次数(例如,2 是一半,3 是三分之一等)

        S = 最多为 N-1 的增量计数(请参阅第 2 部分的解释)

        【讨论】:

          【解决方案6】:

          我首选的处理角度的方法是使用每转 2 次方的单位。例如,如果您使用 16 位有符号整数来表示 -180 到 +180 度,您可以简单地采用 (from-to)/num_steps 来进行插值。添加和减去角度总是有效的,因为二进制值会在您从 360 到 0 的位置溢出。

          在您的情况下,您可能想要做的是数学模 360。因此角度差计算为 (from-to)%360。其他 SO 问题中已经解决了一些符号问题。

          【讨论】:

          • 这个答案没有解决主要问题。使用from 10 度和to 350 度时,您的插值仍然有 180 度作为中点,而通过最短路径的插值将有 0 度作为中点。
          【解决方案7】:

          我对度数的解决方案。在我的 VarTracker 课程中

              @classmethod
          def shortest_angle(cls, start: float, end: float, amount: float):
              """ Find shortest angle change around circle from start to end, the return
                  fractional part by amount.
              VarTracker.shortest_angle(10, 30, 0.1) --> 2.0
              VarTracker.shortest_angle(30, 10, 0.1) --> -2.0
              VarTracker.shortest_angle(350, 30, 0.1) --> 4.0
              VarTracker.shortest_angle(350, 30, 0.8) --> 32.0
              VarTracker.shortest_angle(30, 350, 0.5) --> -20.0
              VarTracker.shortest_angle(170, 190, 0.1) --> 2.0
              VarTracker.shortest_angle(10, 310, 0.5) --> -30.0
              """
              sa = ((((end - start) % 360) + 540) % 360) - 180;
              return sa * amount;
          
          @classmethod
          def slerp(cls, current: float, target: float, amount: float):
              """ Return the new value if spherical linear interpolation from current toward target, by amount, all in degrees.
              This method uses abs(amount) so sign of amount is ignored.
              current and target determine the direction of the lerp.
              Wraps around 360 to 0 correctly.
          
              Lerp from 10 degrees toward 30 degrees by 3 degrees
              VarTracker.slerp(10, 30, 3.0) --> 13.0
              Ignores sign of amount
              VarTracker.slerp(10, 30, -3.0) --> 13.0
              VarTracker.slerp(30, 10, 3.0) --> 27.0
              Wraps around 360 correctly
              VarTracker.slerp(350, 30, 6) --> 356.0
              VarTracker.slerp(350, 30, 12) --> 2.0
              VarTracker.slerp(30, 350, -35) --> 355.0
              a = VarTracker.slerp(30, 3140, -35) --> 355.0
              VarTracker.slerp(170, 190, 2) --> 172.0
              VarTracker.slerp(10, 310, 12) --> 358.0
              Wraps over 0 degrees correctly
              VarTracker.slerp(-10, 10, 3) --> 353.0
              VarTracker.slerp(10, -10, 12) --> 358
              """
              a = VarTracker.shortest_angle(current, target, 1.0)
              diff = target - current
              if np.abs(amount) > np.abs(diff):
                  amount = diff
              if a < 0:
                  amount = -np.abs(amount)
              else:
                  amount = np.abs(amount)
              ret = current + amount
              while ret < 0:
                  ret = ret + 360
              ret = ret % 360
              return ret
          

          【讨论】:

            【解决方案8】:

            修改 user151496 的答案(原来是度数,也给了我错误的输出):

             def interp_angle(theta_1, theta_2, ratio):
                shortest_angle = ((((theta_2 - theta_1) % (np.pi*2)) + np.pi) % (np.pi*2)) - np.pi
                return (theta_1 + shortest_angle * ratio) % (np.pi*2)
            

            测试:使用

            运行
            theta1, theta2 = 0, 0.5
            print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
            theta1, theta2 = 0, 0.99
            print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
            theta1, theta2 = 0, 1.01
            print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
            theta1, theta2 = 0.1, -0.1
            print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
            theta1, theta2 = 0.1, 2-0.1
            print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
            

            给我:

            Average of 0pi rad and 0.5pi rad = 0.25pi rad
            Average of 0pi rad and 0.99pi rad = 0.495pi rad
            Average of 0pi rad and 1.01pi rad = 1.505pi rad
            Average of 0.1pi rad and -0.1pi rad = 0pi rad
            Average of 0.1pi rad and 1.9pi rad = 0pi rad
            

            【讨论】:

              【解决方案9】:

              对于这个问题,如果你的角度在 +-pi 范围内,请使用: ((end - start + pi)%tau + tau)%tau - pi

              【讨论】:

                【解决方案10】:

                我的个人建议?:不要!与欧拉角的 3d 旋转类似,我发现使用更高维度的抽象更不容易出错并且更容易实现。在这种情况下,不用四元数,只需使用一个简单的二维向量,对向量进行线性插值(一个简单且明确的操作),然后使用 atan2 来获得角度!像这样的:

                     Vector2 interop=lerp(v1,v2);
                     float angle=atan2(interop.x,interop.y);
                

                其中v1, v2 是指向单位圆上不同点的两个向量,lerp() 只是您的平均线性插值函数。根据您的环境,您可能有权访问向量类,也可能无法访问,但假设您甚至具有基本的数学背景,那么实现这些基础知识非常简单(如果您不介意的话,还有大量的库!) .作为额外的奖励,您可以简单地更改插值类型,而不会弄乱任何额外的条件等......

                附: 我对回答有关 SO 的问题相当陌生,所以我不确定通过将某人引导到完全不同的方法来回答问题是否可以接受。我已经看到它完成了,但它有时会受到反对......

                【讨论】:

                • 如果由于某种原因输入必须是角度测量,您可以轻松地将其转换为单位向量,进行 lerping,然后将其转换回来(再次类似于四元数)。
                猜你喜欢
                • 1970-01-01
                • 2011-02-22
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2017-01-19
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多