35

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

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

给定度数的单位圆:

单位圆

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

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

4

10 回答 10

42

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

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

就是这样

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

于 2013-01-24T10:14:52.490 回答
13

对不起,这有点令人费解,这里有一个更简洁的版本:

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

谁有更优化的版本?

于 2010-04-25T15:29:49.493 回答
9

我认为更好的方法是插入 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。

于 2015-05-08T17:07:38.380 回答
3

注意:使用 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);

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

于 2010-04-25T15:16:42.773 回答
2

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

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

第 1 部分 - 走哪条路?

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

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

上面的公式检查逆时针从Bto A(与顺时针从Ato相同B)是否小于或等于 180 度。如果不是,则往另一个方向走会更短。

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

第 2 部分 - 顺时针公式

现在您想在和N之间插入时间,无论是顺时针还是逆时针。顺时针公式比较简单。AB

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 度角。我能想到的最简单的方法是将 360 添加到A,然后使用该MOD(FORMULA,VALUE)函数将答案调制 360。

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

(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 度吗?这就是将您的 Antiitarian_formula 包装在一个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 部分的解释)

于 2014-09-06T08:38:08.343 回答
0

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

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

于 2010-04-25T14:23:26.927 回答
0

我对度数的解决方案。在我的 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
于 2018-12-03T19:06:09.497 回答
0

修改 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
于 2019-11-28T01:02:44.787 回答
0

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

于 2021-03-17T00:20:30.950 回答
0

My personal recommendation?: Don't! Similar to 3d rotation with Euler angles , I find that using a higher dimension abstraction far less error prone and much easier to implement. In this case, instead of Quaternions, just use a simple 2 dimensional vector, perform the linear interpolation on the vector ( a trivial and unambiguous operation), and then use atan2 to get the angle! Something like this:

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

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

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

于 2021-05-16T07:27:13.897 回答