Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question How to rotate a game object based on x and y values?

Discussion in 'Scripting' started by markuskir, Mar 22, 2023.

  1. markuskir

    markuskir

    Joined:
    Mar 2, 2018
    Posts:
    11
    Hey, I'm trying to do something I thought would be simple but can't manage to get working.

    I want to rotate a transform based on two angle values, x and y. x should determine the yaw, while y should determine the pitch. Pic attached.

    The following worked but is continuous movement rather than a single rotation:
    Code (CSharp):
    1. transform.RotateAround(transform.position, Vector3.up, input.x);
    2. transform.RotateAround(transform.position, transform.right, input.y);
    Trying to use Quaterion.AngleAxis, I run into gimbal lock and unexpected rotations. Any help? Thank you!
     

    Attached Files:

  2. markuskir

    markuskir

    Joined:
    Mar 2, 2018
    Posts:
    11
    Oh, well, this worked:
    Code (CSharp):
    1. transform.eulerAngles = new Vector3(input.y, input.x, 0);
    If someone comes across this and wants to shed light on the rotation / quaternion-based approach, please do, but at least this is a working solution.
     
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    You can do the same thing if you use Quaternion.AngleAxis then combine two such rotations into one.

    For example you want to rotate 45 degrees on the Y axis
    Code (csharp):
    1. var r1 = Quaternion.AngleAxis(45f, Vector.up);
    Or you want to rotate 30 degrees on the X axis
    Code (csharp):
    1. var r2 = Quaternion.AngleAxis(30f, Vector.right);
    Now you can combine the two by multiplying them
    Code (csharp):
    1. var r = r1 * r2;
    (this reads as "apply r1 to r2".)

    With this, you get a rotation that is rotated by 45 degrees on the Y and 30 degrees on the X. However, the order matters, and this operation is anti-commutative. I.e. if you change the order you will get a different result, something that is rotated 30 degrees on the X, then 45 degrees on the Y.

    In a nutshell, this is exactly what Quaternion.Euler does internally, and there is a prescribed order in which these rotations take place: Z, X, then Y. And in fact, you should be using Quaternion.Euler method, and not eulerAngles like you did.
     
    Last edited: Mar 22, 2023
    Bunny83 and markuskir like this.
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    If you're interested to learn more, here's how Euler works when you dissolve it completely.
    This would also allow you to recreate the methods by using radian angles, if you wanted for some reason. I do.

    The core methods to formulating quaternions are AngleAxis and FromToRotation (imo).
    AngleAxis representation is already pretty close to what quaternion is. Quaternions describe a rotation on a unit 4D sphere where each full revolution depicts a twofold rotation of an object in 3D (double cover). The actual mathematical definition is more complex than that because the numbers involved are actually complex (or, better put, quaternions themselves are an 4-dimensional extension of the complex plane), but all you need to know is that quaternions are pretty much stored in this angle axis format numerically.

    With all that, the 4 components of a quaternion can be simplified down to the following formulas:
    a = cos(half_angle)
    b = axis.x * sin(half_angle)
    c = axis.y * sin(half_angle)
    d = axis.z * sin(half_angle)

    However, Unity used a slightly shifted notation called 'axis angle' instead of 'angle axis' and this is why there are obsolete methods with this name. And so it's the 'w' component that actually gets a cosine.

    This is what the method looks like
    Code (csharp):
    1. Quaternion AngleAxis(float radians, Vector3 axis) {
    2.   var halfAngle = radians / 2f;
    3.   axis *= sin(halfAngle);
    4.   return new Quaternion(axis.x, axis.y, axis.z, cos(halfAngle));
    5. }
    Looks benign enough?
    However, we also need to do some sanitation, namely the axis must be of unit length and we need to normalize the result as well. Also we can't produce a vector of unit length if it's a zero vector to begin with, so
    Code (csharp):
    1. Quaternion AngleAxis(float degrees, Vector3 axis) {
    2.   const float k = pi / 360f ; // division by 2 is baked in this constant
    3.   var result = Quaternion.identity; // (0, 0, 0, 1)
    4.   if(axis.sqrMagnitude < 3E-8f) return result;
    5.   var halfAngle = degrees * k;
    6.   axis.Normalize();
    7.   axis *= sin(halfAngle);
    8.   result = new Quaternion(axis.x, axis.y, axis.z, cos(halfAngle));
    9.   result.Normalize();
    10.   return result;
    11. }
    The other thing is FromToRotation which gives you a quaternion representing a 3D rotation between two arbitrary 3D vectors in space, which is a neat thing. And well, if you have both of these, you can easily produce Euler rotation whether by using AngleAxis or FromToRotation.

    FromToRotation however introduces a slight problem because there are cases in which it is not defined well. For example, if you take an upright pencil and rotate it to suddenly point downwards, there is no way to tell which axis of rotation was used, whether it was X or Z, or anything in between really. When that happens, Unity's FromToRotation is prone to unexpected discontinuities (if you use this for continuous motion) because of how their method is implemented, so we can also address this.

    The other thing that FromToRotation can't do is to properly define a local rotation (around the core of the pencil), and this is because this information is lacking when you just supply two 3D vectors, and why we need AngleAxis in the first place.
    Code (csharp):
    1. Quaternion FromToRotation(Vector3 fromDir, Vector3 toDir) {
    2.   var w = 1f + dot(fromDir, toDir);
    3.   var axis = cross(fromDir, toDir);
    4.   return new Quaternion(axis.x, axis.y, axis.z, w);
    5. }
    That's it. Obviously both vectors need to be of unit length. Here's the improved version.
    Code (csharp):
    1. Quaternion FromToRotation(Vector3 fromDir, Vector3 toDir, Quaternion opposite = default) {
    2.   var result = Quaternion.identity; // (0, 0, 0, 1)
    3.   if(fromDir.sqrMagnitude < 3E-8f || toDir.sqrMagnitude < 3E-8f) return result;
    4.   fromDir.Normalize();
    5.   toDir.Normalize();
    6.   var w = 1f + dot(fromDir, toDir);
    7.   if(w < 1E-6f) { // happens when dot approximates -1, meaning the vectors are antiparallel
    8.     if(opposite == default)
    9.       return Quaternion.FromToRotation(fromDir, toDir);
    10.     return opposite;
    11.   }
    12.   var axis = cross(fromDir, toDir);
    13.   result = new Quaternion(axis.x, axis.y, axis.z, w);
    14.   result.Normalize();
    15.   return result;
    16. }
    Here 'default' or (0, 0, 0, 0) is an invalid quaternion, and we can use this to detect whether the argument was actually supplied. If the rotation was detected as not well defined, we either use the
    opposite
    argument as is, or (if it's not available) we simply gather the usual result from the Unity's Quaternion.FromToRotation, to make things nice and compatible.

    To speed up all of these methods, you can avoid having to normalize the input vectors inside the methods, which is actually the most expensive operation about them. This slight speed improvement, the discontinuity I mentioned, and the opportunity to make AngleAxis work with radians are the best (and only) reasons to have this around, aside from learning.

    Knowing all this, here is one possible implementation of Euler where you can change the order of the axes.

    If you want to learn what makes quaternion special, here are the juicy entrails. Yummy.
    If you can't understand a word of what I'm saying, that's also fine. Don't feel pressured.

    Oh btw, the functions I took for granted (for simplicity) are
    Code (csharp):
    1. pi => System.MathF.PI
    2. sin => System.MathF.Sin(...)
    3. cos => System.MathF.Cos(...)
    4. dot => UnityEngine.Vector3.Dot(...)
    5. cross => UnityEngine.Vector3.Cross(...)
     
    Last edited: Mar 27, 2023
    Ryiah likes this.
  5. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,952
    Isn't Transform.eulerAngles just calling Quaternion.Euler()? Or is there a difference I'm missing?
    Code (csharp):
    1. public Vector3 eulerAngles { get { return rotation.eulerAngles; } set { rotation = Quaternion.Euler(value); } }
    https://github.com/Unity-Technologi...ransform/ScriptBindings/Transform.bindings.cs
     
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    Yes, it's the same thing, but then you end up having two different ways to do the exact same thing in your codebase. I don't find that very useful especially given that one is a property and the other is a static method. Yet they pretend to be somehow different.

    Frankly, eulerAngles should've been read-only. These kinds of things in Unity are usually a thing of legacy or a matter of indecisiveness when it comes to API design. This is why I'd strongly suggest against the setter, there is nothing to gain except confusion. Especially if you later want to switch to your own version of Euler (like I do).
     
    Kurt-Dekker and Ryiah like this.
  7. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209

    It gets the Euler and sets the quaternion .

    // The rotation as Euler angles in degrees.
    public Vector3 eulerAngles { get { return rotation.eulerAngles; } set { rotation = Quaternion.Euler(value); } }



    Euler angles work up to 360* on any of the three axis. If you go over 360* on any axis and don’t wrap it yourself back to 0 on the very same frame ! Then you will get a garbage result.
    Generally the Euler is the identity of the quaternion. The quaternion cant formulate an identity without Euler. The notion of an Euler angle being read only would ruin the 99% of triple A games that use them.
     
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    There are many issues I'm having with this comment but in a nutshell I am very confused by your confusion.

    As stated by Ryiah, the setter is literally just
    Code (csharp):
    1. rotation = Quaternion.Euler(value);
    And of course, any change to the existing API would break any title that relies on it, not just triple A. I really don't get much of what you're saying, especially the part with garbage results, how's that supposed to have anything to do with this topic?? In fact, what garbage results?

    If you mean that because eulerAngles do not always give useful results, this is because they're an active conversion of the internally stored quaternions, there are no garbage results, just unhinged conversion that can be rather unusable if you don't know what you're doing (and most people don't).

    These sentences are also mighty crazy. You have two ways of formulating a quaternion literally three posts above, and btw saying that Euler is the identity of the quaternion means nothing, you clearly don't know what identity means in this context. There is Euler's identity which is something else compared to quaternion identity, and I think you're confusing yourself big time.
     
  9. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209
    HellCraft 2023-03-29 14-41-27.gif
    If euler angles are read-only this is not possible without a large chunk of quaternion code.

    From what I took from it, Quaternion is used because Euler is Vector3 and Position is vector3, quaternion helps another piece of code tell if its a Position or Rotation the vector discusses. So Quaternion helps identify itself as a rotation using V4. Because otherwise Euler and Position need to be entirely separated when for example using a Void( "that takes any order of optional inputs" Quaternion, Vector, Vector) in any order. If you used two vectors in there you cant tell the difference between the two if they were optional.
    For example, on instantiate there is an optional position and v4 rotation. But no optional euler, because you can only have one optional of each type in order for it to be identified, the position is the only vector3 available to be optionally identified. Otherwise it cannot be identified, so the clever trick is to discuss rotation using a V4. As it is prudent to sometimes have both optional position and optional facing. And this is achieved through employment of quaternion to deliver that info that could have been optional. Its pretty complex. And a smart solution.
     
    Last edited: Mar 29, 2023
  10. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    I will write this once more
    Code (csharp):
    1. transform.rotation = Quaternion.Euler(...)
    The rest of your message shows that you're not entirely sure what quaternions are, but feel weirdly compelled to engage in a heated discussion.

    Here, I'll waste some time trying to explain what quaternions are, for the thousandth time.

    First of all: when it comes to rotations in Unity nothing but quaternions exists. Yes you read that right, it all boils down to quaternions. So pay attention to what I'm writing in my posts, it's not an opinion. There are NO Euler rotations, there are only conversions to Euler angles, to and from quaternions.

    Why the whole world uses quaternions? Because they are superior to any other solution, both computationally and because they have almost zero drawbacks, such as gimbal lock as a classical example. Are they intuitive? Most people would immediately say 'No', but I would reply with 'it depends'. Why would I say that, aren't the values obviously nonsensical? Well, they are not nonsensical if you try to understand what they represent, and then work around them to figure out a new way to intuit about them.

    Let me get this straight, are colors intuitive? What exactly is intuitive about them? Here I claim that they are not intuitive, they are completely crazy, they only become slightly more easier to grasp once you adopt the RGB model and separate them into their constituent colors. You see, every common sense regarding colors you'll ever find is always something about the constituent base colors which you can mix together and so on, otherwise there is nothing intuitive about arriving from red to blue, and even this continuity changes its trajectory significantly with each and every color model.

    So it's the RGB color space that makes some sense for most people, and they also like Euler's angles for a similar reason. You just add a little bit of rotation X with rotation Y and voila you get some compound rotation.

    Now get this: a unit quaternion (a type of a quaternion which is used for rotations) also known as versor, which simply carries what is known as Euler vector to angle-axis representation (shown in post #4) which allows for a more robust computation (because it effortlessly translates into matrix computation), has a simple rule: it must be of norm 1. Long story short, it's normalized similarly to how we normalize vectors. In this sense, it literally behaves like Vector4 would.
    Code (csharp):
    1. Vector3 Normalize(Vector3 v) {
    2.   var mag = MathF.Sqrt(Vector3.Dot(v, v));
    3.   return new Vector3(v.x / mag, v.y / mag, v.z / mag);
    4. }
    5.  
    6. Quaternion Normalize(Quaternion q) {
    7.   var mag = MathF.Sqrt(Quaternion.Dot(v, v));
    8.   return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag);
    9. }
    However, unlike with vectors, zero quaternion is illegal, because (being versors) they must be unit. So the actual normalization does this instead.
    Code (csharp):
    1. Quaternion Normalize(Quaternion q) {
    2.   var mag = MathF.Sqrt(Quaternion.Dot(v, v));
    3.   if(mag < Mathf.Epsilon) return Quaternion.identity;
    4.   return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag);
    5. }
    What is this identity? Well for unit quaternions that's the basic assumed nominal state of (0, 0, 0, 1). Why is this combination special? Because mathematical identities, by definition, when applied to other states, do nothing. Similarly, a value of 1 in real number multiplication behaves as an identity. Because multiplying by 1 does not change the resulting value.

    If you observe what x, y, z do from the AngleAxis implementation above, it becomes apparent that they somehow correlate with the axes in 3D space, steering the axis component around the origin.

    And with all this in mind now we can safely arrive to a newfound place from which we can develop a new intuition regarding quaternions. First of all, their components are always of such combined magnitude to never go below or above the length of 1. Second, that 'w' component seems to be special. As if the identity is trying to say "no power, no power, no power, full power". Full power of doing what? Doing nothing of course.

    I will make 4 cardinal quaternions by setting one component to 1 to help you observe what each value does.
    Remember, the formulas go like this
    x = axis.x * sin(angle/2)
    y = axis.y * sin(angle/2)
    z = axis.z * sin(angle/2)
    w = cos(angle/2)

    So we can reverse them if we know the final values and assume the axis to be of length 1.
    For x,y,z it's angle = 2 * arcsin(x,y,z)
    for w it's angle = 2 * arccos(w) but I will omit that because it literally serves as a lingering accumulator to satisfy the norm 1 requirement

    (1, 0, 0, 0) => (180°, 0°, 0°) => rotate by 180° on X
    (0, 1, 0, 0) => (0°, 180°, 0°) => rotate by 180° on Y
    (0, 0, 1, 0) => (0°, 0°, 180°) => rotate by 180° on Z
    (0, 0, 0, 1) => (0°, 0°, 0°) => do nothing

    These values can be negative as well, so let's check that too

    (-1, 0, 0, 0) => (-180°, 0°, 0°) => rotate by -180° on X
    (0, -1, 0, 0) => (0°, -180°, 0°) => rotate by -180° on Y
    (0, 0, -1, 0) => (0°, 0°, -180°) => rotate by -180° on Z
    (0, 0, 0, -1) => (0°, 0°, 0°) => do nothing

    Now this doesn't mean that you can naively do (0.5, 0, 0, 0) and get a rotation of 90° on X.
    This is exactly why Unity docs say "do not attempt to tweak the quaternions directly".

    This also doesn't mean that you can naively do (0.5, 0, 0, 0.5) and get a rotation of 90° on X.

    No, this means that you can naively do (√2/2, 0, 0, √2/2) and actually get a rotation of 90° on X.
    Why? Because we have to satisfy x² + y² + z² + w² = 1 at all times.
    (√2/2)² + 0 + 0 + (√2/2)² = 2(√2/2)² = 2*(2/2²) = 4/4 = 1

    √2/2 also happens to be sin(90°/2) so that's an easy way to do this properly

    You can do this to make this much easier (I don't recommend it, this is for learning purposes)
    Code (csharp):
    1. var q = new Quaternion(0.5, 0f, 0f, 0.5).normalized
    Try it:
    Code (csharp):
    1. Debug.Log(q);
    2. Debug.Log(Quaternion.Euler(90f, 0f, 0f));
    Now let's return to colors. Can you appreciate this newfound intuition? We've literally taken half power from "nothing" and added it to the "X rotation" to obtain a half rotation on X. The values reflect this even though they're not "intuitive" if you're unfamiliar with the length of a vector in a four-dimensional space.

    ---
    Again, there are no Euler's angles in Unity, it's all just a convenient translation for those who don't or won't understand quaternions. And yet, because this conversion math lacks any context, any naive conversion from a quaternion back to Euler's angles will seem like "garbage" to you. That's because there are no Euler's angles to begin with, but the result will be mathematically correct, sadly without any guarantee of continuity or awareness of your specific use case.

    To untangle this confusion in your mind, I strongly advise you to work with the quaternions, not against them, to learn them, understand them, and come up with incredibly robust and powerful ways of isolating your rotations to simple planes instead of fighting with the Euler angles, which is like trying to repair a wrist watch with a circular saw.
     
  11. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209
    Code (CSharp):
    1.  
    2.        if (CHAR_FACING > 359)
    3.             CHAR_FACING = 0 + (CHAR_FACING - 360);
    4.         if (CHAR_FACING < 0)
    5.             CHAR_FACING = 359 + -CHAR_FACING;
    6.         transform.eulerAngles = new Vector3(0, CHAR_FACING, 0);
    7.  
    I think as Orion Syndrome says, if using the euler angle correctly, then you do not need to modify the quaternions behind the scenes, or re-perform any of those calculations. Euler angles don't like to be greater than 359 or less than 0 and If any of them are it can break the motion simulation. 0-359 encompasses the whole 360* rotation, and all decimals between each degree. The above code is rough, and wouldn't account for a rotation applied greater than 360*, So I couldn't add 540 to my euler because I had not written the wrap for the case. Values are usually applied small to a rotation, maybe 1 or 2, 4 or 6, or 0.25f and so the code handles the case when a value is only a few digits over 359 or under 0. But an improved handle could be written.

    The code that works for you now will break if the X or Y value breaks outside of 0 and 359 degrees. Aside that if it remains in those bounds it will work, and that's why it works for you.