Revisiting a Quaternion Rotation Problem

I saw a thread very similar to the problem I describe here, but it’s nearly 5 years old, and I’ve been advised to begin a new thread to explain my problem in some detail. I agree with that advice, so here it is:

I have an array of cubes arranged in a larger square which I call a “cube”. I need to allow prospective users to rotate the cube, such that it will always rest in a flat, square presentation.
My problem is that I can perform Quaternion Rotations around the Y-Axis just fine, but when I attempt to use the same style of manipulation on the Z-Axis, I have trouble with gimbal lock.

I have my Camera attached to a GameObject in the very center of my cube. When rotate the cube, I’m rotating the GameObject so the Camera swings around my cube–all the while looking directly at my cube:

I’ve posted a video on YouTube, which must be the easiest way to demonstrate the problem I’m having:

In the video, I try to demonstrate that I can rotate the cube to the RIGHT or the LEFT with no difficulties, but when I try to rotate it UP or DOWN, the cube gets disoriented and twisted up. I’ve been trying different approaches to rotating my cube, since before Jan 4, 2023, as revealed by a GitHub commit I made with that date. (a bit over a year ago.) I’m eager to put this problem to bed, as they say.

First off, I’m using the MonoBehavior.Update() method to realize the user’s intent to rotate the cube:

  private void Update()
  {
      // test to see if user wishes to hide any layers in the cube
      if (!GetLayerKeyCode())
      {
          // rotate the cube, if the user wants....
          if (!rotateKube())
          {
              // adjust camera distance to cube, if needed.
              if (Mathf.Abs(_moveDistance) > 0F)
              {
                  _newPosition = transform.position;
                  AdjustPositionTakingAxisintoAccount(ref _newPosition, _moveDistance);
              }
          }

      }
  }

my rotateKube() function checks for user arrow-key presses, and initiates the rotation code:

 private bool rotateKube()
{
     bool rotate = false;
     if (Input.GetKeyUp(KeyCode.LeftArrow))
     {
         rotate = true;
         _center.Rotate(eRotate.LEFT);
     }
     else
     {
         if (Input.GetKeyUp(KeyCode.RightArrow))
         {
             rotate = true;
             _center.Rotate(eRotate.RIGHT);
         }
         else
         {
             if (Input.GetKeyUp(KeyCode.UpArrow))
             {
                 rotate = true;
                 _center.Rotate(eRotate.UP);
             }
             else
             {
                 if (Input.GetKeyUp(KeyCode.DownArrow))
                 {
                     rotate = true;
                     _center.Rotate(eRotate.DOWN);
                 }
             }
         }
     }
     return rotate;
}

Then, my Rotate() method calculates the rotation required to move the cube to the desired rotation:

   public void Rotate(eRotate dir)
   {
       // no need to waste time calculating Quaternion.Lerp() rotations if we are done rotating.
       _doneRotating = true;
       switch (dir) // eRotate enum
       {
           case LEFT:                            // const float ROTATE_ANGLE = 99F;
               _newRotation = Quaternion.AngleAxis(-ROTATE_ANGLE, Vector3.down) * transform.localRotation;
               _doneRotating = false;
               break;
           case RIGHT:
               _newRotation = Quaternion.AngleAxis(ROTATE_ANGLE, Vector3.down) * transform.localRotation;
               _doneRotating = false;
               break;
           case UP:
               _newRotation = Quaternion.AngleAxis(ROTATE_ANGLE, Vector3.forward) * transform.localRotation;
               _doneRotating = false;
               break;
           case DOWN:
               _newRotation = Quaternion.AngleAxis(-ROTATE_ANGLE, Vector3.forward) * transform.localRotation;
               _doneRotating = false;
               break;
       }
   }

finally, in my FixedUpdate(), I use Quaterion.Lerp() to gradually rotate my cube:

    private void FixedUpdate()
    {
        if (!_doneRotating)
        {
            _doneRotating = doneRotating();
            if (!_doneRotating)
            {
                transform.rotation = Quaternion.Lerp(transform.rotation, _newRotation, Time.deltaTime * ROTATESPEED);
            }
 
        }
    }

Which seems to wrap up the code fragments significant to the problem I’m having. I’m hoping I’ve described my problem in enough detail that someone knowledgeable with Quaternions can point at the problem: Alright, I know that I’m the problem, but I’m hoping someone is kind enough to say “Quaternions are hard. Here’s what you need to do:"

Thanks in advance for your time and helpfulness!

Rosco-y

I am going to skip all the lerping, which I don’t have too much of a problem with.

You are effectively doing desiredFinalRotation = Quaternion.AngleAxis(degrees, worldAxis) * transform.localRotation. Why?

Quaternions are not commutative; that is, A * B is not the same as B * A.

If you want to calculate the effect of the current rotation followed by an additional rotation, then desiredFinalRotation = originalRotation * desiredAdditionalRotation, in that order. Skip the transform.localRotation part, especially since you are applying it after the AngleAxis quaternion, it’s probably what is getting you in trouble.

Now, if your desired additional rotation is relative to the world, Quaternion.AngleAxis(degrees, Vector3.up) is fine; if your desired additional rotation is relative to the camera, Quaternion.AngleAxis(degrees, myCamera.transform.up) makes more sense.

:slight_smile: Thank you so very much for your prompt reply!!

You ask why am I doing: desiredFinalRotation = Quaternion.AngleAxis(degrees, worldAxis) * transform.localRotation ?

The answer is that I don’t know what I’m doing, and I must have seen it done that way somewhere, and I’m trying every permutation I can think of, hoping that something will work.

I’ll make the necessary adjustments and let you know how it goes!

Thank you again,
Rosco-y

I must apologize, I think I neglected to explain what might be an important detail: My GameManager is situated in the very center of my cube, and my camera is a child object of the GameManager and is positioned such that it is held away from the cube and “looking” at the cube. My rotations are achieved not by rotating the cube, or the camera: my rotations are achieved by rotating the GameManager in the center of the cube, effectively moving the camera around the exterior of the stationary cube, making it appear as if the cube is rotating when it is the camera revolving around the cube like the moon revolves around the earth.

So when I tried

"desiredRotation = transform.rotation * Quaternion.AngleAxis(degrees, axis);

My Y-axis rotations still work fine, but my z-axis rotations just wobble in a circular motion:

https://www.youtube.com/watch?v=x0kA1xEJXRg

Thank you sincerely, and I hope I haven’t wasted your time.

When you want to apply a rotation to another, you have to put it before the rotation you want to modify. Quaternion multiplication is not commutative. So it matters if you do A*B or B*A. With quaternions you just have to think about “function notation”. So like g(p) or g*p. So “g” is applied to “p”. It’s similar to matrix multiplications. A * B * C * v where A,B and C are matrices, you essentially apply first C, then B then A to v. You can also group them (A*B*C)*v into M*v where M = A*B*C. So matrix and quaternion multiplication is associative but not commutative.

Note that any Quaternion is actually a single rotation (±180°) around a single axis. This is what a quaternion actually represents. Every quaternion is essentially a relative rotation. When used as an absolute rotation / orientation you essentially rotate the coordinate space from the identity orientation which is simply the alignment with the parent space.

Thank you again for all of your wonderful help! I don’t know if this makes a difference, so I just made a video to help explain that I’m not rotating the cube, but I’m rotating a GameObject with my Camera tethered to it:

https://www.youtube.com/watch?v=qvbNqWRYcJk

I’ll try again with

desiredRotation = transform.rotation * Quaternion.AngleAxis(degrees, axis);

And report what effect that has.

I’m running out of ways to say “Thank You.”

so THANK YOU!

Rosco-y

I don’t know what I’m doing wrong, because these are the modifications I made to my Rotate() method (which calculates the desiredRotation):

public void Rotate(eRotate dir)
{
 
    _doneRotating = true;
    switch (dir) // eRotate enum
    {
        case LEFT:                            // const float ROTATE_ANGLE = 99F;
            _newRotation = transform.rotation * Quaternion.AngleAxis(-ROTATE_ANGLE, Vector3.down);
            _doneRotating = false;
            break;
        case RIGHT:
            _newRotation = transform.rotation * Quaternion.AngleAxis(ROTATE_ANGLE, Vector3.down);
            _doneRotating = false;
            break;
        case UP:
            _newRotation = transform.rotation * Quaternion.AngleAxis(ROTATE_ANGLE, Vector3.forward);
            _doneRotating = false;
            break;
        case DOWN:
            _newRotation = transform.rotation * Quaternion.AngleAxis(-ROTATE_ANGLE, Vector3.forward);
            _doneRotating = false;
            break;
    }
}

And my rotation results are the same as those seen in the video in post #4 .

Thank you,
Rosco-y

This is not related to your issue, but I suggest you try out OBS studio. It’s free, open-source software that many people use for streaming, and for recording videos. It doesn’t take very long to set up if you just watch some tutorials, and then your videos will not have that big Wondershare Filmora watermark banner across the middle.

Thanks for the tip. I’ll give OBS Studio a try!

i don’t understand all the extra noise on the implementation. but have you checked the second parameter to Quaternion.AngleAxis ? make sure that the “UP” vector is correct

Respectfully asking: what “extra noise” are you referring to? (is it in the code or the output, or somewhere else?)

I think Vector3.up (or Vector3.down) both work fine for my rotate-RIGHT and rotate-LEFT cases (they aren’t causing me grief as far as I can tell.)

Thank You–I appreciate any feedback I can get!

“extra noise” as in i didn’t read it all :smile: no harm intended. But since you told that it just doesn’t work on up and down movements (and looking just the code), the most likely to be causing the error is your second parameter in AngleAxis

Also why don’t you try using LookRotation instead of angle axis? but the implementation is different

instead of using angles, use vectors as direction
pseudocode:

a = vector3. up, down left or right
up = vector3.up
targetRotation = Quaternion.LookRotation (a,up);

Thank You!

Question: what does

Quaternion.Dot(transform.rotation,goal)>0.9999f)

do?

Rosco-y

basically the amount of rotation achieved for the target rotation? something like that.
= 1f means it exactly match. but lerp functions don’t really get to 1f so you check against a lower value. such as .99

lol – I didn’t read any harm into your comment :slight_smile: I just thought you had some valuable insight that I wasn’t quite understanding.

I’m not sure if I tried “LookRotation”–I’m going to make a special branch and see what happens with that.

Thanks Again!

Rosco-y

that last code what is it? the first code you posted was ok. use the Quaternion Lerp function!

   private void FixedUpdate()
    {
        if (!_doneRotating)
        {
            _doneRotating = doneRotating();
            if (!_doneRotating)
            {
                transform.rotation = Quaternion.Lerp(transform.rotation, _newRotation, Time.deltaTime * ROTATESPEED);
            }
        }
    }

this is fine just add the dot function to stop the lerp

I like this approach, giving it a try :slight_smile:

I don’t think the problem is in the lerp–I’ve validated that it’s accurate enough.