Search Unity

[SOLVED] Is it possible that Quaternion.RotateTowards() doesn't reach end rotation?

Discussion in 'Scripting' started by stefan_s_from_h, Jan 9, 2019.

  1. stefan_s_from_h

    stefan_s_from_h

    Joined:
    Nov 26, 2017
    Posts:
    72
    ANSWER: Yes. The last value (when the value doesn't change anymore) isn't always the rotation you supplied as end rotation. Even the angle between the last value and the requested end rotation can be greater than 0.

    ------------------

    Code (CSharp):
    1.         if (_turnToKillerRobot)
    2.         {
    3.             // Rotate
    4.             if (transform.rotation != _turnToRotation)
    5.             {
    6.                 var step = CaughtTurnToRobotRotateSpeed * Time.deltaTime;
    7.                 transform.rotation = Quaternion.RotateTowards(transform.rotation, _turnToRotation, step);
    8.             }
    9.             else // Turned
    10.             {
    11.                 _turnToKillerRobot = false;
    12.             }
    13.         }
    I have a bug that I can't really reproduce. It comes up very infrequently and I suspect it has something to do with the above code. Can it be that
    Quaternion.RotateTowards(transform.rotation, _turnToRotation, step)
    never reaches the end rotation _turnToRotation?

    Unity's source for RotateTowards:
    https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Quaternion.cs#L193

    It tests if the angle between the two rotations is 0.0f and then returns the second one (parameter to).

    It uses SlerpUnclamped but the documentation to Quaternion.RotateTowards states that it will not overshoot.

    UPDATE: I missed the call to
    Mathf.Min()
    in Unity's code. The
    SlerpUnclamped 
    doesn't overshoot the result of
    RotateTowards
    .
     
    Last edited: Jan 10, 2019
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,742
    Use a nonzero difference check... floating point numbers should never be tested for true equality. You can use Mathf.Abs() and check less than some amount, or use Mathf.Approximately.
     
  3. stefan_s_from_h

    stefan_s_from_h

    Joined:
    Nov 26, 2017
    Posts:
    72
    I'm not testing any floats. I compare Quaternions. And this isn't an exact test on equality (see Unity's code for operator==).

    It's similar to comparing Vector3s, which isn't comparing exact equality of x, y, z.
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    You see, the thing is quaternion is made of floats. And you try to test against values being equal. Which will lead to lack of consistency. The nature of floats is, they not guarantee to be equal.

    Rotate toward and lerp/slerp, is not guarantee that final rotation is exactly same as target rotation. There is a tolerance, of fraction of degree. Which is directly relevant to floats, their precision and float point error..

    Solution
    Use dot product for quaterions, to check, if rotation is same.
    The Quaternion.dot returns 0.0f to 1.0f range, where 1 means is EXACT same.
    But don't assume you will get EXACT 1.0f. You may get 0.999993
    And that is not one. So you can add simple rounding.
    Lets say
    Code (CSharp):
    1. if ( Mathf.Round ( Quaternion.dot ( q0, q1 ) * 10000 ) == 10000 )
    Also, you shouldn't check vector0 == vector1, equality result is not guaranteed.
     
  5. stefan_s_from_h

    stefan_s_from_h

    Joined:
    Nov 26, 2017
    Posts:
    72
    This is what happens when you compare Quaternions q0 and q1 with the == operator:

    Code (CSharp):
    1. if (q0 == q1)
    2. {
    3.     [...]
    4. }
    Gets computed as:

    Code (CSharp):
    1. if (Quaternion.Dot(q0, q1) > 1.0f - 0.000001F)
    2. {
    3.     [...]
    4. }
    Because the == operator is overloaded.

    As for Vector3s:

    Code (CSharp):
    1. if (Vector3.SqrMagnitude(vector0, vector1) < 0.00001F * 0.00001F)
    2. {
    3.     [...]
    4. }
     
  6. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    May I ask what is your point?
    You trying to find if rotations
    Code (CSharp):
    1. else // Turned
    are equal. Isn't it? But these simply are not guaranteed.
     
  7. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Alternatively, you can check the angle between the two quaternions (Quaternion.Angle()).
    If it's less than tolerance -> it's the rotation you need.
     
    Antypodish likes this.
  8. stefan_s_from_h

    stefan_s_from_h

    Joined:
    Nov 26, 2017
    Posts:
    72
    That's what I'm about to do now. After I realized that
    RotateTowards(Quaternion from, Quaternion to, float maxDegreesDelta)
    isn't always returning
    to
    .

    I included the angle and the last return from RotateTowards into the debug information:

    upload_2019-1-10_14-45-9.png

    Most of the time "Turn To Rotation" and "Last Rotation" (last value from RotateTowards) are the same and the angle is 0. But the screenshot shows what's happening when RotateTowards reached its end but hasn't returned the "to" parameter.

    The documentation for == says "Note that because quaternions can represent rotations that are up to two full revolutions (720 degrees), this comparison can return false even if resulting rotations look the same." but I never expected to find some wild rotation when I have a clear end rotation for RotateTowards (which internally uses SlerpUnclamped with maximum of 1.0f for parameter "t".)

    Hell, even the Angle isn't exactly 0 in the end, which means it isn't even 0 within the tolerance of Unity's Quaternion code. (They return exactly 0f when the dot of the 2 Quaterninons is greater than 1.0f - 0.000001F.)

    My next steps: Trying to reproduce the bug at least 5 times and see if the angle runs wild. Then I'll probably be satisfied with any angle < 1f. This is just some silly thing that turns the player to the robot that killed him. So that the killer is visible in the "Game Over" menu, eyeing its dead victim behind the buttons.
     
  9. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    As mentioned above, you're probably hitting float precision issue.
    This is where float == float may not be true whatever you do due to how it is represented. In these cases it's always recommended to use tolerance checks instead.

    Above applies to the vectors as well.
     
  10. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    I wouldn't trusted those anyway.
     
    Antypodish likes this.
  11. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    This is not a bug. This is normal computational behavior, based on an float points calculations imprecision.
    From hardware to hardware and compiler, and programming language, same float based calculation may give different errors.

    I gave you solution, which for some unknown reason you are ignore.With rounding of decimal points, you can define, how precise you want to find matching.

    Of course is up to you. But you will probably end up spending hours, trying fighting with a wind.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Not only is float error your enemy here.
    Also trig is your enemy.

    Note that RotateTowards takes an amount of degrees to max step by. But this will have to be converted to radians (multiplied by an imprecise estimate of pi), and then have trig applied to it.

    Triginometric methods are continuous functions that deal primarily with ratios of irrational values. You have a computer that has a discrete word size and discrete memory depth. You can't represent all of these irrational values correctly.

    So the float error just keeps piling up on itself. Float error from binary representation, float error from irrational numbers, float error from continuous trig functions.

    And there's FOUR of them (quats being made of 4 floats).

    Yeah... it's not going to be exact. It ain't even going to be within the Unity == operator's epsilon which is 1.0f - 0.000001F. That epsilon is on the edge of a single point float's sig value range... it's really only going to catch rounding errors.

    You need an epsilon more in the 0.01 range, but this gets into other issues about visual tolerances and what not, which is all personal and to your needs/tastes.

    But in the end... float error, especially this level of compounded float error, just sometimes can't be avoided. It's a fundamental flaw with floats in general. And is why in accounting and the sort you just don't use it... if precision is important you use a more precise number type.

    ...

    With all that said... what is OP trying to accomplish?
     
    Antypodish likes this.
  13. stefan_s_from_h

    stefan_s_from_h

    Joined:
    Nov 26, 2017
    Posts:
    72
    Contrast on the screenshot is a bit bad. Here are the numbers in text: Quaternion.RotateTowards gets told to turn to (0.0f, 0.7071067f, 0.0f, 0.7071068f) and returns (0.0f, -0.7071067f, 0.0f, -0.7071068f) at the end. Angle between the two is 0.03956468f.
     
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Both of those quats technically represent the same value. They're both effectively 90 degrees around the y-axis.

    The amount of error is 0.03 degrees, but mind you degrees are a pretty conversion. Really it's a difference of 0.00069 radians (note I suggested an epsilon of 0.01, much more appropriate for this). I mean honestly, 0.03 degrees is tiny, it's 1/12,000th of a full rotation.

    The dot product of the quats is -0.99999991179113, or an error of 0.000000089. A difference less than 1/10 millionth.

    You can see that this tiny float error in the quat of the 4 floats compounds from the trig/conversion to degrees to be 0.03, despite the quats themselves being only minutely different. We went from a 1/10millionth difference to a 1/12000th difference

    ...

    Thing is, they're off by such a tiny amount... 7 degrees of magnitude, less than the 6 degrees of magnitude that == operator checks for.

    BUT, you'll notice that both quats are actually negatives of one another. That dot product is -0.999, not +0.999, the == operator does not account for this.

    Because the == operator isn't testing if they represent the same rotation... the == operator is testing if they are equal (within some reason to account for float error). And technically they aren't equal... they just happen to represent the same orientation.

    Cause there's the thing about rotation. Any given orientation can be reached via different paths. Because at the end of the day a Quaternion isn't a "orientation", it's a "direction and magnitude of rotation". It's the path to take to get to that orientation. And as a result both are not actually equal.

    You can kind of look at it as 1 quat is saying rotate clockwise to an orientation, and the other is rotating counter-clockwise. It's like how -90 and 270 are technically the same orientation, just reached by different paths.

    So like I said, float error isn't your only problem. It's the trig. It's the fact they're rotations.

    ...

    Checking equality of quats, just like eulers, and vectors, and floats... just should NOT be trusted.

    Usually how I go about a situation where I want to know if I've completed rotating/moving to a rotation/position is not to constantly check if I'm close to it. But rather calculate how long it will take to get there up front, and assume I'm in the act for the duration of that time.

    Coroutines/tweens are good for this since you know when it's done. It's when the coroutine/tween ends.
     
    Last edited: Jan 10, 2019
    Antypodish likes this.
  15. abandon_games

    abandon_games

    Joined:
    Sep 1, 2017
    Posts:
    38
    ^^ this is the correct answer - there are many ways to compare rotations, but I've found that `Quaternion.Dot` is the most accurate
     
  16. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,002
    Actually, no. The issue here was not related to RotateTowards at all. RotateTowards literally returns the target rotation when the angle between the source and the target is 0. The "Angle" method used does already take an epsilon into account. So when you get really close to the target, The Angle will be 0 and RotateTowards will return the target rotation.

    The issue here is that assigning transform.rotation and reading it back does not necessarily yield the same value. The rotation may be broken down into a local rotation (in case the object has a parent) and re-assembled when you read it back. Also Unity will generally normalize any quaternion you assign to the rotation property. That property is not a variable, it's a property with logic behind it.

    It is true that there are several ways how to check for equality between two quaternions and using a different approach may solve it. However the source of the problem is not RotateTowards but the fact that assigning and reading back the worldspace rotation of an object in most cases does not roundtrip without rounding errors or conversion errors.

    If you would actually use a Quaternion variable, say "currentRotation" and you actually rotate that towards the target rotation and just assign it to the rotation and never read the rotation back from the Transform, it should work fine.