如何在循环的值(例如色相或旋转)之间保持平衡?


25

联合动画示例

观看演示

我试图使关节围绕画布的中心顺着鼠标指针的角度平滑旋转。我所拥有的作品有效,但我希望它能够动画化尽可能短的距离,以达到鼠标角度。当值在水平线(3.14-3.14)处循环时,会发生问题。将鼠标悬停在该区域上,即可查看方向的切换方式,并且需要很长的路程。

相关代码

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;  
joint.targetAngle = Math.atan2( dy, dx );

如何使它旋转最短的距离,甚至“跨越间隙”?



1
@VaughanHilts不知道我该如何使用它。您能否进一步阐述?
jackrugile 2014年

Answers:


11

它并不总是最好的方法,而且计算起来可能会更昂贵(尽管这最终取决于您存储数据的方式),但是我会提出这样一个论点,即在大多数情况下,使用2D值相当有效。可以使用所需的归一化方向矢量,而不用使用所需的角度。

与“选择最短角度角度方法”方法相比,此方法的优点之一是,当您需要在两个以上的值之间进行插值时,该方法将起作用。

输入色调值时,可以替换hue[cos(hue), sin(hue)]向量。

在您的情况下,请遵循标准化的关节方向:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
    diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);

如果可以使用2D向量类,则代码可以更短。例如:

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);

谢谢,这在这里非常有用codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f我了解您所做的大部分工作,但是您可以在规范化过程中在代码的第5行中详细说明吗?不知道那里到底发生了什么dx /= len...
jackrugile 2014年

1
将向量除以其长度称为归一化。它确保其长度为1。len ? len : 1.0在极少数情况下,鼠标恰好位于关节位置,因此该零件只是避免被零除。可以这样写:if (len != 0) dx /= len;
sam hocevar 2014年

-1。在大多数情况下,这个答案远非最佳。如果要在和之间插值180°怎么办?向量形式:[1, 0][-1, 0]。如果为,则内插向量将为您提供180°或除以0的误差t=0.5
Gustavo Maciel

@GustavoMaciel不是“大多数情况”,这是一个非常具体的极端情况,在实践中从来没有发生过。另外,没有除以零的情况,请检查代码。
sam hocevar

@GustavoMaciel再次检查了代码,它实际上非常安全,即使在您描述的特殊情况下也能正常工作。
sam hocevar

11

诀窍是要记住角度(至少在欧几里得空间中)是周期性的2 * pi。如果当前角度和目标角度之间的差太大(即光标已经越过边界),只需通过相应地加或减2 * pi来调整当前角度。

在这种情况下,您可以尝试以下操作:(我以前从未使用Javascript编程,因此请原谅我的编码风格。)

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
  joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;

编辑:在此实现中,围绕关节中心过快地移动光标会导致其跳动。这是预期的行为,因为关节的角速度始终与成正比dtheta。如果这种行为是不希望的,则可以通过在接头的角加速度上加一个盖来轻松解决问题。

为此,我们需要跟踪关节的速度并施加最大加速度:

  joint = {
    // snip
    velocity: 0,
    maxAccel: 0.01
  },

然后,为方便起见,我们将介绍一个裁剪功能:

function clip(x, min, max) {
  return x < min ? min : x > max ? max : x
}

现在,我们的移动代码如下所示。首先,我们dtheta像以前一样进行计算,并joint.angle根据需要进行调整:

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;

然后,我们无需立即移动关节,而是计算目标速度,并使用该速度将clip其强制在可接受的范围内。

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
  joint.velocity = clip(targetVel,
                        joint.velocity - joint.maxAccel,
                        joint.velocity + joint.maxAccel);
  joint.angle += joint.velocity;

即使仅在切换方向时,也能产生平滑的运动,而仅在一维中执行计算。此外,它允许独立调节关节的速度和加速度。在此处查看演示:http : //codepen.io/anon/pen/HGnDF/


这个方法真的很接近,但是如果我的鼠标太快,它会开始以错误的方式略微跳开。演示在这里,让我知道我是否没有正确实现:codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile 2014年

1
我不确定您略微跳错方向是什么意思;对我来说,关节总是朝着正确的方向运动。当然,如果过快地在中心位置移动鼠标,则会超出关节的范围,并且在从一个方向切换到另一个方向时会发生跳动。这是预期的行为,因为旋转速度始终与成正比dtheta。您是否打算使联合有一定的发展动力?
张大卫

请参阅我的最后编辑,以消除因超出关节而造成的剧烈运动。
David Zhang

嗨,尝试了您的演示链接,但看起来它只是指向我的原始链接。您保存了一个新的吗?如果还不行,我应该可以在今晚晚些时候实现此功能,并查看其性能。我认为您在编辑中描述的更多是我想要的。会尽快回复您,谢谢!
jackrugile 2014年

1
哦,是的,你是对的。抱歉,是我的错。我会解决的。
David Zhang

3

我喜欢给出的其他答案。很技术!

如果您愿意,我有一个非常简单的方法来完成此操作。我们将为这些示例假设角度。该概念可以外推到其他值类型,例如颜色。

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
    // Adjust for 360/0 degree wrap
    if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
    // Adjust for 360/0 degree wrap
    if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle. 

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.


double NormalizeAngle(double angle) {
    while (angle < 0) 
        angle += MAX_ANGLE;
    while (angle >= MAX_ANGLE) 
        angle -= MAX_ANGLE;
    return angle;
}

我只是在浏览器中创建的,从未经过测试。我希望我第一次尝试就正确了。

[编辑] 2017/06/02-阐明了一些逻辑。

首先计算distanceForward和distanceBackwards,并允许结果超出范围(0-360)。

归一化角度会将这些值返回到(0-360)范围内。为此,您要添加360直到该值大于零,然后减去360直到该值大于360。所得的开始/结束角度将是等效的(-285与75相同)。

接下来,您将找到distanceForward或distanceBackward的最小归一化角度。在示例中,distanceForward变为75,它小于distanceBackward的标准化值(300)。

如果distanceForward是最小的AND endAngle <startAngle,请通过添加360将endAngle扩展到360以上(在示例中为375)。

如果distanceBackward是最小的AND endAngle> startAngle,请通过减去360将endAngle扩展到0以下。

您现在可以从startAngle(300)到新的endAngle(375)。引擎应自动减去360,将值调整为360以上。否则,如果引擎未为您标准化值,则您必须将300调整为360,然后将0调整为15。


好吧,尽管atan2基于此的想法很简单,但是它可以节省一些空间和一点性能(不需要存储x和y,也不需要太频繁地计算sin,cos和atan2)。我认为有必要扩展一下此解决方案正确性的原因(例如,选择圆或球面上的最短路径,就像SLERP用于四元数一样)。这个问题和答案应该放在社区Wiki中,因为这是大多数游戏程序员面临的一个真正普遍的问题。
teodron

好答案。我也喜欢@David Zhang给出的其他答案。但是,当我快速转弯时,使用他编辑的解决方案总是会产生奇怪的抖动。但是您的答案非常适合我的游戏。我对您的答案背后的数学理论感兴趣。尽管看起来很简单,但是我们为什么要比较不同方向的标准化角度距离并不清楚。
newguy

我很高兴我的代码可以正常工作。如前所述,这只是在浏览器中输入的,未经实际项目测试。我什至不记得回答这个问题(三年前)!但是,回头看一下,似乎我只是在扩展范围(0-360),以允许测试值超出/超出此范围,以便比较总度数差异并采用最短的差异。规范化只是将这些值再次置于(0-360)的范围内。因此distanceForward变为75(-285 + 360),小于distanceBackward(285),因此它是最短的距离。
Doug.McFarlane

由于distanceForward是最短的距离,因此使用IF子句的第一部分。由于endAngle(15)小于startAngle(300),因此我们将360添加为375。因此,您将从startAngle(300)过渡到新的endAngle(375)。引擎应自动减去360,将值调整为360以上。
Doug.McFarlane

我编辑了答案,以增加清晰度。
Doug.McFarlane
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.