Euler Angles

When you started developing games in Unity, you were introduced to a term, most likely for the first time: "Quaternion". Putting in rotations in the Inspector makes sense: You type in three numbers for how you wanted to rotate this thing, and that was that.

And then you start programming. When trying to write code to see if your object is facing a certain direction, you found transform.rotation.x, y, and z, which match up with the three components of the rotation you saw in the inspector. And happily, you plug this into your code:

if (transform.rotation.x > 45f && transform.rotation.x < 60f)

But alas, these had weird values that seemed to have no relation whatsoever to the numbers in the inspector (unless they were all zero) - it seems like transform.rotation.x never gets above 1. Well, that's weird. So you dig through the documentation, and make a discovery: transform.rotation.eulerAngles gives you the values you expected to see. Armed with this new information, you happily plug this into your code:

if (transform.rotation.eulerAngles.x > 45f && transform.rotation.eulerAngles < 60f)

And, depending on how you've been rotating your objects, this might even work! At least, for a little while. But alas, you've fallen into the trap of Euler angles. Even if this works now, at some point in the future, almost invariably, you'll change one seemingly unrelated thing elsewhere, and this will suddenly break.

So what is the trap of Euler angles, exactly? It's essentially this: Any one component of Euler angles (x, y, or z) is 100% dependent on the other two components in order to make any sense whatsoever, which makes it unreliable to check against one of these components.

That knowledge will, ultimately, lead to the conclusion, which I'm going to lay out for you ahead of time so you know where we're going with this: The only situation in which it's appropriate to use Euler angles is so that a human can input a rotation value. That's it.

The Fundamental Problem with Euler angles

Let's begin with an experiment. Open the Unity editor, and create two identical objects in the scene. For the sake of visualizing what's going on, it's probably best if you use a non-symmetrical/non-mirrored 3D object. Your main character's mesh will probably work well. We're going to use these two objects to demonstrate that any one rotation has any number of ways to be represented by Euler angles - and therefore, that you can't predict which of these will be used.

Let's start with an easy one. Keep object A's rotation at (0, 0, 0), and set object B's rotation to (360, 0, 0). It should be obvious that these both look the same, and indeed, they do both convert internally to the exact same quaternion values - in this case, the identity (or no-rotation) quaternion. Therein lies the problem: Multiple sets of Euler angles can be boiled down to the same four values for the quaternion.

It's important to understand at this point that Unity's Transform class doesn't store rotations as Euler angles*. With one of your objects selected, switch the inspector to Debug mode, which shows you the raw data that composes your objects. Check out the rotation value on this object - there are your four numbers representing the quaternion. Whichever of your objects you're looking at, you'll see (0, 0, 0, 1) as its values for X, Y, Z, and W. You've discovered the identity rotation! Well done.

Armed with the knowledge that this is how Unity stores rotations internally, put yourself in the shoes of being the Transform class's quaternion-to-Euler conversion function, which is called upon anytime you type in transform.rotation.eulerAngles into your script. You come across this quaternion, (0, 0, 0, 1), and as we've learned, there are at least two possible ways to return this value: (0, 0, 0) and (360, 0, 0). In fact, there are an infinite number of ways to do this: (360, 360, -360), (720, -360, 1080), and on and on - any multiple of 360, positive or negative, can be added to any of the three components, and produce a Euler value that represents the exact same rotation.

What's a conversion function to do? Well, in this case, that's easy - you return the one where all three values are between 0 and 359.999 - for any given rotation, there should be only one of these, right? And in this case, that's a fair solution. But you can hopefully already see the beginnings of the problem: when you call objectB.transform.rotation.eulerAngles, it's going to return a different value (0, 0, 0) than the one you typed into the Inspector (360, 0, 0).

It gets worse, though. In the inspector, set object A's rotation to (90, 270, 180), and object B's rotation to (90, 90, 0). Now, compare their rotations - they look identical. And, as you may expect, the Quaternion values as seen in the debug inspector will be the same for both objects - in this case, (0.5, 0.5, -0.5, 0.5).

Now put yourself in the shoes of the conversion function. When you see (0.5, 0.5, -0.5, 0.5), which of those two combinations of numbers should you return? In this case, it turns out it's even worse: it happens to return another value entirely, (90, 360, 270). Not only is this a third set of congruent values, it doesn't even follow the zero-to-359 rule we thought would be totally sensible for this conversion process!

So is .eulerAngles broken? No, of course not - it's doing the best it can with the information it has, and more importantly, all of these sets are a completely valid interpretation of that particular rotation. It's not broken - but it is fantastically unreliable.

* The Transform class doesn't store the Euler angles you typed in, but for the sake of not driving its users insane, Unity's Transform inspector does. That's how, in the above example, you're able to type in (360, 0, 0) and have it stay that way. However, the instant you change transform.rotation via any other means than the Inspector, all bets are off - it no longer knows what you, the user, expected that rotation to be, and now has to convert from quaternion to Euler, with all the unpredictability that that entails. Importantly, this stored value is also 100% inaccessible to your runtime code, and just as well, given that it can't be relied on once any sort of rotation affects it.

What's a quaternion, anyway?

I have a confession: I don't fully understand quaternions myself. I know that it represents a rotation, and is composed of four numbers, each between -1 and 1 - but beyond that, I got nothing. If you showed me those four numbers, the only way I'd have any hope of knowing what it means is if they're (0, 0, 0, 1) (which represents the default non-rotated rotation, which I've come to learn is called the "identity" quaternion). There's a Wikipedia article on the subject, but I've honestly never bothered to read it, nor have I made any attempt to watch any explanations of the concept on Youtube, which I'm sure are abundant.

So why is a guy who doesn't understand quaternions explaining how to use them? Because you don't need to understand them to use them, thanks to Unity's fantastic Quaternion class. It's got a whole pile of helper functions that completely abstract away any need to understand what's going on inside the class.

The best way to use this class is divided into two sections: input (assigning values in or otherwise changing the rotation), and output (reading back values or checking what direction an object is facing). We'll get to those, but first, I feel like we need to answer a question that's no doubt been burning in your mind since you first discovered that X isn't X.

Why does Unity use this weird four-value thing to represent rotations?

I could write up a long explanation about this, but in truth it comes down to two words: Gimbal lock. And those two words are better explained by this video than I could ever hope to do in text. But to summarize: Using Euler angles as your native language for rotations can lead to bizarre rotational artifacts when you attempt to transition from one rotation to another. Quaternions don't have this problem.

The Solution: Input and Output

Input

The good news is that setting rotations is fairly easy. In fact, you can probably still use Euler angles for setting rotations if you want to - most of the issues with them are in the conversion from quaternions back to Euler angles. There are a few caveats, though, mostly related to gimbal lock as described above. One special consideration is that Euler angles don't give you full control over which order the rotations are applied in. Therefore, it's almost always preferable to use Quaternion.AngleAxis instead.

[TODO: example where euler is weird]

Instead, multiple Quaternion.AngleAxis calls can do the trick, with more control:

Note that the * operator is the rotation combination operator for multiple quaternions. You can also use *= to apply a new rotation on top of a transform's existing rotation.

In addition to .AngleAxis, there are a whole host of helper functions to create new rotations from scratch - check the static functions in the Quaternion class.

Output

This is the tricky part, and the part where Euler angles fail horribly. Let's go back to our original example.

if (transform.rotation.eulerAngles.x > 45f && transform.rotation.eulerAngles < 60f)

In this case, what we thought we were asking is, is the object facing downwards at an angle between 45 and 60 degrees? But as we've learned, we can't count on that - other values in the Y and Z components might make the X component behave unpredictably, while still being a valid Euler value for that rotation.

Direction Vectors

By far the most common corrected technique here is to check against one of the resulting direction vectors, not the rotation itself. If you want to check whether the character is pointing downwards, then check transform.forward.y: If facing downward, it'll be < 0 and > -1. More precise rotation checks will give you predictable values somewhere in between those two. And importantly,. transform.forward won't be affected unpredictably by the other rotation values.

if (transform.forward.y < -0.5f && transform.forward.y > -0.7f)

Similarly, if your goal is to check that a character is upright, you can use transform.up to get their upward-facing vector. In this case, you can use

float angle = Vector3.Angle(transform.up, Vector3.up);
if (angle > 45f && angle < 60f)

This would be slightly different than the above example in that A) pitching up or down would trigger the if statement (which could be accounted for by a simple transform.forward.y < 0f), and B) it would take into account the object's roll rotation, if any exists. But, it'd allow you to keep using degrees as your logic, as opposed to figuring out the proper zero-to-one value of transform.forward.y.

If you find yourself in the situation of having a Quaternion but not the Transform reference, you can use the following example to do the same logic without needing transform.forward et al:

Vector3 forward = myQuaternion * Vector3.forward;

The Atan2 function

This cryptically named function from the Mathf class might well be the most useful function in that class, except perhaps Lerp. It's certainly the most important when dealing with rotations, anyway. This technique is valuable when you actually do need a particular angle value - if your player is required to turn a wheel from 140 to 180 degrees to solve a puzzle, it's not going to be easy to convert that to a vector value.

While a particular rotation can't be reliably converted to a specific set of three Euler angles, it is possible to get one particular angle on one particular plane - and that specificity is what allows this method to work. Atan2 will take a Y and an X coordinate (which you can retrieve from one of the direction vectors as described above), and give you a single number, which will be from -π to π (in radians). This is easily converted to a -180 to 180 range for degrees.

//Returns the angle of the object's forward vector, as seen from the top
Vector3 forward = transform.forward;
float angleInDegrees = Mathf.Atan2(forward.z, forward.x) * Mathf.Rad2Deg;

Which two components you feed into Atan2 depends on which plane you want to measure the angle by: (z, x) for top-down, (y, x) for forward-facing (e.g. most 2D operations), or (y, z) for left-facing. You can even do more complicated planes here. As an example, if you want to aim your turret (rotating base with a raising/lowering gun) at a target, you'll want to use Atan2 with your target's relative Z and X to rotate the turret's base, and then for the gun itself, set its rotation angle based on the target's relative Y position, and its lateral distance for the second parameter (where the lateral distance is the 2-dimensional distance using only its X and Z distances).

Side note: counting rotations

Often, the reason one needs to get a particular angle as described above is because the player may need to rotate a wheel 5 times to complete a task, or something to that effect. Once you have the angle from the above technique (I'll assume that you have wrapped that up into a float GetCurrentRotation() function), you should be able to count up the total amount of degrees that have been rotated over time thusly:

private float rotationDegreeCount = 0f;
private float lastRotation = -1f;

void Start() {
    lastRotation = GetCurrentRotation();
}

void Update() {
    float thisRotation = GetCurrentRotation();
    while (thisRotation < lastRotation - 180f) thisRotation += 360f;
    while (thisRotation > lastRotation + 180f) thisRotation -= 360f;
    rotationDegreeCount += thisRotation - lastRotation;
    lastRotation = thisRotation;
}

Avoiding output from rotations altogether

An even better idea than correcting the way you read in values from your transform's rotation, is to simply never need to do so in the first place: treat transform.rotation purely as output, never input. Then you can use the source values from which you set the rotation in the first place as your value to check against.

This is only an option if you have full control over your object's rotation; if any other component controls the rotation you'll have to use the above techniques. Physics comes to mind as a common cause of this limitation, and if you're in VR, the player's controllers and headset (and anything they directly affect) will be entirely out of your control.

results for ""

    No results matching ""