Search Unity

Confusion with Quaternion.Slerp

Discussion in 'Scripting' started by kikendo, Mar 19, 2018.

  1. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Hi everyone,

    I'm trying to do something here and I quite don't understand what is going on.
    My script is working sometimes but sometimes it's not and it has to do with how Quaternion.Slerp is behaving or, rather, how I am using it (probably badly ;P) .

    The idea is that the object will rotate to a random Quaternion and when it gets there it will try to go to another random Quaternion and so on, indefinitely.
    Here's the code.
    Code (CSharp):
    1.     void Rotating () {
    2.         oldRotation = transform.rotation;
    3.         Quaternion lerRotation = new Quaternion();
    4.    
    5.         if (oldRotation==newRotation){
    6.             newRotation = Quaternion.Euler(Random.Range(0,360),Random.Range(0,360),Random.Range(0,360));
    7.             print("new rotation angle");
    8.         } else {
    9.             lerRotation = Quaternion.Slerp (oldRotation, newRotation, (Time.deltaTime+(1.0f-Time.timeScale))*speed);
    10.        
    11.             transform.rotation = lerRotation;
    12.             print("rotating to: "+newRotation+"/ current: "+lerRotation);
    13.         }
    14. }
    So what I notice happening, from the output of the print statement, is that this smoothly rotates from one rotation to another, and the end position is always of the same absolute value, but different sign. So when the Slerp reaches the absolute value of the destination, it stops, and then the initial IF statement that creates a new random value is never met, because oldRotation is never equal to newRotation (in signs)

    A typical output where teh rotation system gets stuck would be:
    "rotating to: (-0.2, -0.2, -0.8, 0.5)/ current: (-0.2, -0.2, -0.8, 0.5)"
    or
    "rotating to: (-0.3, 0.6, -0.7, -0.4)/ current: (0.3, -0.6, 0.7, 0.4)"

    Why are there even signs here? I thought Quaternion.Euler would yield out values between 0 and 1.
    Obviously I am doing something wrong here, I have to say it is the first time I deal with a Quaternion object.

    Any help appreciated!
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    You should never do an equality comparison of floats, or anything made up of floats. This includes Vectors and Quaternions.

    There is no guarantee that the floats will be exactly equal.

    Instead you should test if they're near each other by some small amount that is considered close. With vectors this can be by using the distance method, or sqrMagnitude of the difference. With Quaternions this can be the angle between the two quaternions.

    ...

    On an aside, you probably should move that 'lerRotation' into the else statement. No reason to call the Quaternion constructor every time if you're not always going to use it.

    Also, you seem to be wanting to rotate at some constant speed. You should probably use Quaternion.RotateTowards for that:
    https://docs.unity3d.com/ScriptReference/Quaternion.RotateTowards.html

    Slerp is just spherical interpolation (as opposed to lerp, or linear interpolation). It finds a point t between start and end and returns that. So basically if t = 0.5, it returns the rotation halfway between. As start and end get closer, that distance is smaller, so it appears to slow down as you get closer (and you also technically never reach your target, ala Zeno's paradox of Achilles and the Tortoise... although you do technically get there because floats technically can't hold infinitely small values and instead will just be 0).

    RotateTowards on the other hand moves the amount of degrees towards the target. When called repeatedly, it always moves the same amount of distance. So it's a perfect candidate for cosntant speed motion.
     
    Homicide likes this.
  3. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Worth noting that this already done by the == operator of both, Vectors and Quaternions. They compare against a threshold value.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Oh yeah, I forgot about that. I remember now seeing that in the decompiled code.
     
  5. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,834
    Don't even worry about trying to analyze the x,y,z,w values of a Quaternion. It is imaginary/complex numbers, and generally there is no need to be concerned about these values directly.

    All:
    I tried running this code and experienced major issues using the "==" operator, which might be what the OP was witnessing. Looking at the UnityEngine code, the Unity Quaternion "==" operator is doing the following:
    Code (csharp):
    1.  
    2. public static bool operator ==(Quaternion lhs, Quaternion rhs)
    3.         {
    4.             return Quaternion.Dot(lhs, rhs) > 0.999999f;
    5.         }
    6.  
    But the test I ran, the Dot product was -1, so the "==" operator was always returning false.

    here is the code:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. public class TestRotate : MonoBehaviour {
    5.  
    6.     public Quaternion target = Quaternion.Euler(332f, 116f, 99f);
    7.  
    8.     // Update is called once per frame
    9.     void Update ()
    10.     {
    11.  
    12.         var angleBetween = Quaternion.Angle (transform.rotation, target);
    13.         var dotProduct = Quaternion.Dot (transform.rotation, target);
    14.  
    15.         Debug.Log ("angle= " + angleBetween + ". Dot=" + dotProduct);
    16.  
    17.         Quaternion newRotation = Quaternion.Slerp (transform.rotation, target, Time.deltaTime);
    18.         transform.rotation = newRotation;
    19.         Debug.Log ("new rotation: " + transform.rotation.eulerAngles + ". target=" + target.eulerAngles);
    20.  
    21.  
    22.     }
    23.  
    24.     }
    25.  
    The console prints out:
    angle= 0. Dot=-1
    new rotation: (332.0, 116.0, 99.0). target=(332.0, 116.0, 99.0)

    The angle between is 0, but dot product is -1, not 1.
    Hence, the == operator alone is not reliable for this example.

    I was banging my head against the wall.
    Can anyone else try this out? Did I miss something?[/CODE]

    Something like this might work :
    Code (csharp):
    1.  
    2. if (angle < 0.1f)
    3.  
     
    Last edited: Mar 20, 2018
  6. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Wow lots of great input here, thanks guys.
    This might solve other sort of random issues I was encountering, maybe because I Am comparing floats with "==". Will have to take a look at that.

    About using RotateTowards, I actually wanted to have the decelerating effect on the rotation, I didn't want it to be constant. I understand better how Slerp works thanks to your explanation then. I might try with RotateTowards, but I still would have the issue of not knowing when the Quaternion reached the intended target angle so a new random target angle is generated to rotate towards.

    So to summarize, if I can't use the "==" operator, how should I be doing this?

    I feel like maybe I shouldn't be converting values from degrees with the Euler function, because if I can compare to integers, "==" becomes useful again. Perhaps this also happens because, yes, I wasn't that interested in calculating the angle between the Quaternions haha. My brain just blocked off math like this (I used to work with structures like this at school, but that was a LONG time ago).

    The intended behavior should be that this object should rotate to the desired randomly generated angle and when it reaches that, it generates a new random angle and moves to that, ad infinitum.

    Thanks so much for your help so far.
     
  7. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Since you're lerping, you need to use a timing variable anyways:

    Code (csharp):
    1.  
    2. const float rotationDuration = 0.5f;
    3. float rotationTime = 0f;
    4.  
    5. Quaternion rotationStart; // initialize in awake
    6. Quaternion rotationEnd;
    7.  
    8. void Rotating()
    9. {
    10.    rotationTime += Time.deltaTime;
    11.  
    12.    transform.rotation = Quaternion.Slerp(rotationStart, rotationEnd, rotationTime / rotationDuration);
    13.  
    14.    if(rotationTime >= rotationDuration)
    15.    {
    16.        rotationTime = 0f;
    17.  
    18.        rotationStart = rotationEnd;
    19.        rotationEnd = Random.rotation;
    20.    }
    21. }
    22.  
    If you don't want the rotation to be perfectly linear, I'd recommend you use an animation curve.
     
    eisenpony likes this.
  8. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,834
    All of your code worked, except for the "==". I tried running your equivalent code:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. public class TestRotate2 : MonoBehaviour {
    5.  
    6.     public float speed = 2.0f;
    7.     Quaternion oldRotation;
    8.     public Quaternion newRotation = Quaternion.identity;
    9.  
    10.     // Update is called once per frame
    11.     void Update () {
    12.         Rotating();
    13.     }
    14.  
    15.     void Rotating () {
    16.  
    17.         oldRotation = transform.rotation;
    18.         Quaternion lerRotation = new Quaternion();
    19.  
    20.         var angle = Quaternion.Angle(oldRotation, newRotation);
    21.  
    22.         if (angle < 1.0f)
    23.         {
    24.             newRotation = Quaternion.Euler(Random.Range(0,360),Random.Range(0,360),Random.Range(0,360));
    25.                        
    26.         } else {
    27.  
    28.             lerRotation = Quaternion.Slerp (oldRotation, newRotation, Time.deltaTime*speed);
    29.  
    30.             transform.rotation = lerRotation;
    31.             //print("rotating to: "+newRotation.eulerAngles+"/ current: "+lerRotation.eulerAngles); //swapped
    32.         }
    33.     }
    34. }
    35.  
    36.  
    But instead of "==", check the angle. An angle < 1 degree might be close enough to be considered equal.
    As I posted about above, the "==" was evaluating to false due to how the Quaternion API does this comparison.
    Note the object moves very quickly at first then gradually slows down. Is that really what you want?
     
    Last edited: Mar 21, 2018
  9. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Wow I am impressed that almost all of my code worked! Haha.

    I thought I had to do some mathematics on my own to check the angle between the Quaternions. Should have read the reference, I feel like a fool now >_< but since it's been a while since I calculated the angle between two magnitude whatever vectors, I was afraid to go that way and thought an equality comparison would suffice. Thanks for clearing all that up!

    I will try this and also move the creation of the variable into the else as suggested by lordofduct (good call mate).

    Yes! I want it to move quickly at first and slow down towards destination. Yes I could have done that with an animation curve, Grozzler, but this is more exciting and flexible for my case use :)

    OK, added it and it works great!! Amazing!

    Thanks again everyone! What would I do without your help?
     
  10. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I'm glad you got it working, but lerping in that way with a moving A target and using deltaTime for your T parameter creates wildly inconsistent interpolations. If your game isn't running at a perfectly steady FPS, even for a few frames, your rotations will be radically different because of the compounding math. You really should lerp properly.
     
  11. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Amazing article dealing with all the math I loathe, haha. I will have this into consideration, I didn't think the animation would change if the FPS drop, was not aware that the time object was dependent of this. The whole reason I was using it was that I thought it was a solid source.

    Thanks!
     
  12. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Thanks for that too, you helped me figure out something else I had in mind for some other exercise!
     
  13. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    That article on Lerp isn't too bad, but the first thing in it is a misunderstanding. Lerp(A, B, [small constant value]) is a perfectly good, simple "fast then slow" movement (the article claims it's a common mistake.)

    Then, a quibble, it claims Lerp(A, B, [0-1 percent]) is what most people want. I think most people really want MoveTowards. MoveTowards lets you pick the speed, and it take as long as it takes. Lerp(,,pct) lets you pick the time, and the speed is however fast it need to be.
     
  14. kikendo

    kikendo

    Joined:
    Jul 19, 2017
    Posts:
    61
    Thanks for this, it really helps. I don't think I ever tried MoveTowards. This separation you make should help me choose when I Should use Lerp or MoveTowards.