Unity messes up X-axis rotation

I have a very simple script that I wish to rotate something on the X-axis, and I'd expect the other two axes to stay put(they flip between 0 and 180), and only X to change.

Below you can see the code intended to do just that.

    public class TestScript : MonoBehaviour
    {
        private void Start()
        {
            transform.eulerAngles = Vector3.zero;
            Debug.Log($"start rotation: {transform.eulerAngles}");
        }

        private void Update()
        {
            float time = (Time.time - 1f) * 10f;
            if (time < 0f || time > 1f)
            {
                return;
            }
            Vector3 rotation = transform.eulerAngles;
            float x = Mathf.Lerp(170f, 0, time);
            rotation.x = x;
            transform.eulerAngles = rotation;
            Debug.Log($"rotation: {transform.eulerAngles}, x: {x}");
        }
    }

The output from the console. You can clearly see that the rotation does not go from 170 to 0, but from 0 to 90 and then back to 0.

result

Now, I'm pretty sure this has something to do with quaternions and their identity, but not sure how can this be avoided

PS: The same idea but for Y and Z works just fine.


Solution 1:

OP:

I have a very simple script that I wish to rotate something on the X-axis, and I'd expect the other two axes to stay put(they flip between 0 and 180), and only X to change.

You can clearly see that the rotation does not go from 170 to 0, but from 0 to 90 and then back to 0. Now, I'm pretty sure this has something to do with quaternions

Well the real culprit is Euler angles.

If we take a look at your code:

Vector3 rotation = transform.eulerAngles;
float x = Mathf.Lerp(170f, 0, time);
rotation.x = x;
transform.eulerAngles = rotation;
Debug.Log($"rotation: {transform.eulerAngles}, x: {x}");

...we can see you are performing rotations via transform.eulerAngles. The thing about 3D rotations is that you should avoid using Euler due to their limitations and problems (gimbal lock anyone) and use quaternions instead. The latter is the source of truth.

Unity (my emphasis):

When you read the .eulerAngles property, Unity converts the Quaternion's internal representation of the rotation to Euler angles. Because, there is more than one way to represent any given rotation using Euler angles, the values you read back out may be quite different from the values you assigned. This can cause confusion if you are trying to gradually increment the values to produce animation.

...which is exactly what is happening with your code.

Consider this:

enter image description here

Notice anything about the 23.5 and the 156.5?

23.5 + 156.5 = 180

In other words both will lead to the same rotation as per "there is more than one way to represent any given rotation".

An arguable simpler approach is:

public class RotateWithTime : MonoBehaviour
{
    [SerializeField,Tooltip("Rotation rate in degrees/second")] 
    private Vector3 rotationSpeed; // e.g. (30,0,0) for 30 deg/sec X-only

    private void Reset()
    {
        rotationSpeed = Vector3.zero;
    }

    // Update is called once per frame
    void Update()
    {
        var amount = rotationSpeed * Time.deltaTime;
        transform.Rotate(amount);
    }
}

And a version without Vector3s:

public class RotateWithTimeNoV3 : MonoBehaviour
{
    [SerializeField,Tooltip("Rotation rate in degrees/second")] 
    private float rotationSpeedX; // e.g. 30 for 30 deg/sec X-only

    private void Reset()
    {
        rotationSpeedX = 0f;
    }

    // Update is called once per frame
    void Update()
    {
        var amount = rotationSpeedX * Time.deltaTime;
        transform.Rotate(amount, 0f, 0f);
    }
}