Search Unity

Help with Rotation and Quaternions.

Discussion in 'Scripting' started by Burgrokash, May 18, 2020.

  1. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9
    Hi everyone! Well, the thing goes like this.
    I'm currently working on a mobile game, it's a fairly simple core mechanic, you start with a single 3D cube and each turn you are given a new one that needs to be put or snaped to a face of a cube in the scene. Each face of the cube has a value and is represented by a "city zone type". Say Housing Area, Park, Dumps, etc. For the sake of my problem, I won't enter on game design details. But, your objective is to create a CubeCity with the highest value you can.
    As I said earlier each time you create and put a cube, a new one is given to you with random values (face types), that new cube is represented on a "previewCube" that is used to preview the end result you want on the city you are creating. You can select different faces of the cubes on the scene and the preview cube will spawn near that face so you can rotate it and find your desired placement. When you are happy with your choice just tap the "build cube button" the preview cube will disappear and the new one with the same values and rotation will appear on the selected face.
    That is when I run into trouble. I am having a lot of trouble just to rotate the preview cube on the 3D space, after a lot of mistakes I came to a poor solution, but it does not work has its should. Beneath its the code, I assume that a more reliable approach will be to lerp or slerp the rotation within 2 angles but after trying and trying I wasn't able to find the way. I search nearly everywhere but there's not a simple straight forward approach to this problem. this script rotates the object avoiding the Gimbal Lock error but it's not precise because of the deltaTime each time you make a new rotation gets a little bit more offset.
    I just need to rotate an object around a selected Axis 90 or -90 degrees. That its, but it is useless hehe, please if someone could help me it will be a lot, this is bothering me for days now. Thanks!

    Code (CSharp):
    1. public class CubeBehaviour : MonoBehaviour
    2. {
    3.     private Movement _movement;
    4.  
    5.     private void Awake()
    6.     {
    7.         if (!GetComponent<Movement>())
    8.             Debug.LogWarning("Please add a movement script to the gameobject.");
    9.  
    10.         _movement = GetComponent<Movement>();
    11.     }
    12.  
    13.     public void Move(Vector3[] positions, Action callBack)
    14.     {
    15.         _movement.StartMove(positions,callBack);
    16.     }
    17.  
    18.     [ContextMenu("Rotate X Positive")]
    19.     public void RotateAround_xAxisPositive()
    20.     {
    21.         Rotate(RotationAxis.X, true);
    22.     }
    23.  
    24.     [ContextMenu("Rotate X Negative")]
    25.     public void RotateAround_xAxisNegative()
    26.     {
    27.         Rotate(RotationAxis.X, false);
    28.     }
    29.  
    30.     [ContextMenu("Rotate Y Positive")]
    31.     public void RotateAround_yAxisPositive()
    32.     {
    33.         Rotate(RotationAxis.Y, true);
    34.     }
    35.  
    36.     [ContextMenu("Rotate Y Negative")]
    37.     public void RotateAround_yAxisNegative()
    38.     {
    39.         Rotate(RotationAxis.Y, false);
    40.     }
    41.  
    42.     [ContextMenu("Rotate Z Positive")]
    43.     public void RotateAround_zAxisPositive()
    44.     {
    45.         Rotate(RotationAxis.Z, true);
    46.     }
    47.  
    48.     [ContextMenu("Rotate Z Negative")]
    49.     public void RotateAround_zAxisNegative()
    50.     {
    51.         Rotate(RotationAxis.Z, false);
    52.     }
    53.  
    54.     public void Rotate(RotationAxis axis, bool positiveRotation)
    55.     {
    56.         StartCoroutine(DoRotation(axis,90, positiveRotation));
    57.     }
    58.  
    59.     IEnumerator DoRotation(RotationAxis axis,float angles, bool positiveRotation)
    60.     {
    61.         float time = 1;
    62.         float elapsedTime = 0;
    63.         Quaternion destinationRotation = Quaternion.identity;
    64.         Vector3 anglesToRotate = Vector3.zero;
    65.  
    66.         switch (axis)
    67.         {
    68.             case RotationAxis.X:
    69.                 if (positiveRotation)
    70.                     anglesToRotate = new Vector3(90, 0, 0);
    71.                 else
    72.                     anglesToRotate = new Vector3(-90, 0, 0);
    73.                 break;
    74.             case RotationAxis.Y:
    75.                 if (positiveRotation)
    76.                     anglesToRotate = new Vector3(0, 90, 0);
    77.                 else
    78.                     anglesToRotate = new Vector3(0, -90, 0);
    79.                 break;
    80.             case RotationAxis.Z:
    81.                 if (positiveRotation)
    82.                     anglesToRotate = new Vector3(0, 0, 90);
    83.                 else
    84.                     anglesToRotate = new Vector3(0, 0, -90);
    85.                 break;
    86.             default:
    87.                 Debug.LogError("You must assign a rotation axis!");
    88.                 break;
    89.         }
    90.         destinationRotation *= this.transform.rotation * Quaternion.Euler(anglesToRotate);
    91.  
    92.         Quaternion yRotation = Quaternion.identity;
    93.         Quaternion xRotation = Quaternion.identity;
    94.         Quaternion zRotation = Quaternion.identity;
    95.  
    96.  
    97.         while (elapsedTime < time)
    98.         {
    99.             yRotation = Quaternion.AngleAxis(anglesToRotate.y * Time.deltaTime, Vector3.up);
    100.             xRotation = Quaternion.AngleAxis(anglesToRotate.x * Time.deltaTime, Vector3.right);
    101.             zRotation = Quaternion.AngleAxis(anglesToRotate.z * Time.deltaTime, Vector3.forward);
    102.  
    103.             this.transform.rotation = yRotation * xRotation * zRotation * this.transform.rotation;
    104.  
    105.             elapsedTime += Time.deltaTime;
    106.             yield return null;
    107.         }
    108.         //this.transform.rotation = yRotation * xRotation * zRotation;
    109.         //this.transform.rotation = destinationRotation;
    110.     }
    111.  
    112. }
    113.  
    114. public enum RotationAxis
    115. {
    116.     X,
    117.     Y,
    118.     Z
    119. }
    120.  
     
  2. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    Yes, you should use Lerp/Slerp.
    Also Store the target rotation virtually:
    Code (CSharp):
    1.  
    2. class CubeBehaviour : MonoBehaviour
    3. {
    4.    [SerializeField] float duration = 1; // seconds, must be >0.0f
    5.    Quaternion targetRotation = Quaternion.identity;
    6.  
    7.    [ContextMenu("Rotate Z Positive")]
    8.    public void RotateAround_zAxisPositive()
    9.    {
    10.        targetRotation *= Quaternion.Euler(0, 0, 90);
    11.        StopAllCoroutines();
    12.        StartCoroutine(DoRotation());
    13.    }
    14.    // other rotate methods
    15.  
    16.    IEnumerator DoRotation()
    17.    {
    18.        float currentTime = 0;
    19.        Quaternion startRotation = transform.rotation;
    20.  
    21.        while (currentTime < 1)
    22.        {
    23.            currentTime += Time.deltaTime / duration;
    24.  
    25.            transform.rotation = Quaternion.Slerp(startRotation,
    26.                targetRotation, currentTime);
    27.  
    28.            yield return null;
    29.        }
    30.        transform.rotation = targetRotation;
    31.    }
    32. }
    33.  
     
  3. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9

    I have tried that approach before but it runs into gimbal lock after a couple of rotations.
     
  4. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    duplicate your script to save it, then try my code, I am 100% sure it will work without running into any gimbal locks. ^^
    It can't run into one because it is using quaternions. ^^
     
  5. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    To explain this a bit more:
    Thats the whole point of using quaternions: avoiding gimbal lock.
    A gimbal lock occurs, when the euler-axis-rings are aligned, Quaternions work completely different, they are basically one Vector3 + a rotation around this vector called "w", in code, they are just 4 float values, xyzw.
    "targetRotation" is saved as Quaternion, so it can never run into a gimbal lock. It could, if it was a Vector3 - but thats not the case.
    As quaternions are complicated things, you can't add them with a "+" (plus) but "*" (multiply) does pretty much the same, you just have to be careful what quaternion is on which side.
    Quaternion.Euler(0, 0, 90)
    generates a quaternion on the fly, you could also write this in two lines, like so:

    Code (CSharp):
    1. Quaternion addRotation = Quaternion.Euler(0, 0, 90);
    2. targetRotation = targetRotation * addRotation;
    As you can see, we are not using euler angles at all here, we generate one quaternion, then "add" it to another one.
     
  6. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9
    Yes, yes. I did just that. And as I presume it has the same gimbal lock problem. Try your code with the same rotation amount on a different axis. I might be missing something, but it doesn't work for me. :S
     
  7. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9
    I Just copy your script and added tow more axis to test. After a couple of rotation on a
    different axis, I get a Gimbal Lock. Am I missing something?

    Code (CSharp):
    1. public class TestROtate : MonoBehaviour
    2. {
    3.     [SerializeField] float duration = 1; // seconds, must be >0.0f
    4.     Quaternion targetRotation = Quaternion.identity;
    5.  
    6.     [ContextMenu("Rotate X Positive")]
    7.     public void RotateAround_xAxisPositive()
    8.     {
    9.         targetRotation *= Quaternion.Euler(90, 0, 0);
    10.         StopAllCoroutines();
    11.         StartCoroutine(DoRotation());
    12.     }
    13.  
    14.     [ContextMenu("Rotate Y Positive")]
    15.     public void RotateAround_yAxisPositive()
    16.     {
    17.         targetRotation *= Quaternion.Euler(0, 90, 0);
    18.         StopAllCoroutines();
    19.         StartCoroutine(DoRotation());
    20.     }
    21.  
    22.     [ContextMenu("Rotate Z Positive")]
    23.     public void RotateAround_zAxisPositive()
    24.     {
    25.         targetRotation *= Quaternion.Euler(0, 0, 90);
    26.         StopAllCoroutines();
    27.         StartCoroutine(DoRotation());
    28.     }
    29.  
    30.  
    31.     // other rotate methods
    32.  
    33.     IEnumerator DoRotation()
    34.     {
    35.         float currentTime = 0;
    36.         Quaternion startRotation = transform.rotation;
    37.  
    38.         while (currentTime < 1)
    39.         {
    40.             currentTime += Time.deltaTime / duration;
    41.  
    42.             transform.rotation = Quaternion.Slerp(startRotation,
    43.                 targetRotation, currentTime);
    44.  
    45.             yield return null;
    46.         }
    47.         transform.rotation = targetRotation;
    48.     }
    49. }
     
  8. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    Are you sure you are talking about a gimbal lock? Because that means you are not able to rotate around a specific axis, and with the provided script thats always possible.
    Only thing I saw, when I checked the script myself today is that the rotation is local, which may not be what you want.
    There the sentence I said earlier

    Comes into play - if you want global rotations, you have to flip the multiplaction - if thats what you are asking for?
    But it definately is not a gimbal lock in any way - I rotated the cube now with both (new one with global rotation below) scripts about 500 times and it is just impossible to do it in a way, so that gimbal lock occurs. You can always rotate in any direction with the given commands.


    Code (CSharp):
    1.     [ContextMenu("Rotate X Positive")]
    2.     public void RotateAround_xAxisPositive()
    3.     {
    4.         targetRotation = Quaternion.Euler(90, 0, 0) * targetRotation;
    5.         StopAllCoroutines();
    6.         StartCoroutine(DoRotation());
    7.     }
    8.  
    9.     [ContextMenu("Rotate Y Positive")]
    10.     public void RotateAround_yAxisPositive()
    11.     {
    12.         targetRotation = Quaternion.Euler(0, 90, 0) * targetRotation;
    13.         StopAllCoroutines();
    14.         StartCoroutine(DoRotation());
    15.     }
    16.  
    17.     [ContextMenu("Rotate Z Positive")]
    18.     public void RotateAround_zAxisPositive()
    19.     {
    20.         targetRotation = Quaternion.Euler(0, 0, 90) * targetRotation;
    21.         StopAllCoroutines();
    22.         StartCoroutine(DoRotation());
    23.     }
     
  9. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9
    OOOOOh, hahah damned. I definitly wasent a Gimbal Lock. I didn't pay close attention to the Behaviour i was Getting. You are absolutly right, its not a Gimbal Lock, is just a local rotation, and that is not what I needed so i kinda lost my mind XD.
    Can you give me an example of how to make this a global rotation with the multiplication thing u say above? Thanks.
    You are awesome! :D
     
  10. Burgrokash

    Burgrokash

    Joined:
    Sep 12, 2018
    Posts:
    9
    OOOOOh Thanks to you i just made it!!!!!!!! I'm so happy right now XD.
    This is the end resutl:

    Code (CSharp):
    1. public class TestROtate : MonoBehaviour
    2. {
    3.     [SerializeField] float duration = 1; // seconds, must be >0.0f
    4.     Quaternion targetRotation = Quaternion.identity;
    5.  
    6.     [ContextMenu("Rotate X Positive")]
    7.     public void RotateAround_xAxisPositive()
    8.     {
    9.         targetRotation = this.transform.rotation;
    10.         targetRotation = Quaternion.Euler(90, 0, 0) * targetRotation;
    11.         StopAllCoroutines();
    12.         StartCoroutine(DoRotation());
    13.     }
    14.  
    15.     [ContextMenu("Rotate Y Positive")]
    16.     public void RotateAround_yAxisPositive()
    17.     {
    18.         targetRotation = this.transform.rotation;
    19.         targetRotation = Quaternion.Euler(0, 90, 0) * targetRotation;
    20.         StopAllCoroutines();
    21.         StartCoroutine(DoRotation());
    22.     }
    23.  
    24.     [ContextMenu("Rotate Z Positive")]
    25.     public void RotateAround_zAxisPositive()
    26.     {
    27.         targetRotation = this.transform.rotation;
    28.         targetRotation = Quaternion.Euler(0, 0, 90) * targetRotation;
    29.         StopAllCoroutines();
    30.         StartCoroutine(DoRotation());
    31.     }
    32.  
    33.  
    34.     // other rotate methods
    35.  
    36.     IEnumerator DoRotation()
    37.     {
    38.         float currentTime = 0;
    39.         Quaternion startRotation = transform.rotation;
    40.  
    41.         while (currentTime < 1)
    42.         {
    43.             currentTime += Time.deltaTime / duration;
    44.  
    45.             transform.rotation = Quaternion.Slerp(startRotation, targetRotation, currentTime);
    46.  
    47.             yield return null;
    48.         }
    49.         transform.rotation = targetRotation;
    50.     }

    I'm not absolutely sure why do i have to post multiplicate on this section "targetRotation = Quaternion.Euler(0, 0, 90) * targetRotation;" but that way it works. Also, before I wasn't updating the target rotation with the current transform.rotation this way (argetRotation = this.transform.rotation;) ". I don't know if that is absolutely necessary but that way works fine.
    I can't thank you enough, best of luck!!! CYA :D
     
  11. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    You will run into problems with
    targetRotation = this.transform.rotation;
    because the target rotation is always at 90 degree angles, all the time, while the transform rotation can be inbetween - so when you press a button twice within the duration of 1 second (or whatever "duration" you have set) you will get rotations out of the 90 degree scope, like 43,579 degrees. You should only set the target roration to your transform.rotation in the Start() method, and then only manipulate it with your methods (
    RotateAround_yAxisPositive()
    , ..) .
    This way, you can hammer the buttons to rotate the cube as much as you want without ever getting any rotation that can't be archived with 90 degree turns. ^^
     
    Burgrokash likes this.
  12. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    650
    Thats because a Quaternion is always a rotation from 0. No matter what quaternion you have, it is on it's own always an absolute rotation. So you can take any object, assign the quaternion and get a total rotation.
    Quaternions represent a rotation along a vector, with this technique, you can have absolute values for every possible rotation (sometimes, two different quaternion values (xyzw) represent the same rotation, but it is always possible to create a quaternion out of a rotation).
    Knowing that quaternions are absolute "always a rotation from 0 rotation", it's quite simple to understand why the order matters.
    Because you take one absolute rotation and rotate it around the left hand side of the multiplication.
    Imagine an object that is tilted to the right and one that is tilted forwards - if you rotate it to the right first and then (local) forwards, this is a different result than rotating it forwards and then (local) right.
    By "adding" the rotations together, you basically apply the first rotation (left hand side), then the second rotation (right hand side of the multiplication). And the right hand side will be rotated around the left hand side first and then applied.

    That's why my first solution was local and the second is not. ^^
     
    Burgrokash likes this.