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

Resolved Discrepency between standard/Mathematics quaternion LookRotation functions

Discussion in 'Scripting' started by lukereeves, Jul 13, 2022.

  1. lukereeves

    lukereeves

    Joined:
    Nov 12, 2017
    Posts:
    3
    So I ran into this strange bug (at least, I think it's a bug) while doing some pretty vanilla rotation code. For certain rotations where the destination vector was directly up from an object's current position things would completely break in the Unity.Mathematics versions of quaternion rotations. Switching back to the UnityEngine versions fixed things up.

    After some digging I think I found the root issue; the math version of quaternion.LookRotation says "Returns a quaternion view rotation given a unit length forward vector and a unit length up vector" so I'm passing normalized vectors in but it seems like under the hood it's doing a second normalization which blows up if the resultant cross product is (0,0,0). Here's some sample code:

    Code (CSharp):
    1. // This is trying to calculate the quaternion for looking directly up with vectors normalized
    2. float3 direction = new float3(0, 1f, 0);
    3. float3 up = new float3(0, 1f, 0);
    4.  
    5. Quaternion q = Quaternion.LookRotation(direction, up);
    6. quaternion newRotation = quaternion.LookRotation(direction, up);
    7. quaternion safeRotation = quaternion.LookRotationSafe(direction, up);
    8.  
    9. Debug.Log($"Engine - {q}, math - {newRotation}, safe - {safeRotation}");
    10.  
    11. // Engine - (-0.70711, 0.00000, 0.00000, 0.70711), math - quaternion(NaNf, NaNf, NaNf, NaNf), safe - quaternion(0f, 0f, 0f, 1f)
    12.  
    13. // is it the normalization of the resultant 0,0,0 cross product that's failing?
    14. Debug.Log(math.normalize(new float3(0, 0, 0)));
    15. // float3(NaNf, NaNf, NaNf)
    16.  
    17. // How does Vector3 normalize handle this?
    18. Debug.Log(Vector3.Normalize(new Vector3(0, 0, 0)));
    19.  
    20. // What if we run the same code without a second normalize call?
    21. float3 straightCross = (math.cross(up, direction));
    22. quaternion nonNormalizedQuat = math.quaternion(math.float3x3(straightCross, math.cross(direction, straightCross), direction));
    23. Debug.Log(nonNormalizedQuat);
    24. // quaternion(-0.7071068f, 0f, 0f, 0.7071068f) - that's a bingo!
    25.  
    That seems like a bug that should be filed, right? This is in 2021.3.6f1 LTS with the 1.2.6 Mathematics package.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,893
    I think this is expected. Not a bug.

    The docs for the old LookRotation have a contingency for colinear vectors. The Unity.Mathematics versions have:
    - for non safe there's an assumption they are not colinear, so garbage in garbage out
    - for the safe one it says it returns the identity rotation if they are colinear.
     
    Last edited: Dec 1, 2022
    Bunny83 and lukereeves like this.
  3. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    595
    Documentation for Unity.Mathematics.quaternion.LookRotation it clearly states:
    So this seems to be functioning as designed and documented. This is no differ from a squareroot function doing whatever it does when you pass in a negative number. Different math libraries might have different strategies for handling edge cases or invalid inputs. So you always need to carefully read the docs and make sure the edge case behavior interacts well with rest of your code or check for the edge cases yourself and use a value suitable in your uses case.
     
    Bunny83, Kurt-Dekker and lukereeves like this.
  4. lukereeves

    lukereeves

    Joined:
    Nov 12, 2017
    Posts:
    3
    An expected 90 degree rotation is colinear? If the parameters were resulting in the identity quaternion (e.g. result == forward) I would expect that but as you can see in the example the resulting quaternion has a 90 degrees offset.

    Also why would the new function need to normalize the cross at all? Shouldn't that be the realm of the LookRotationSafe function?
     
    Last edited: Jul 13, 2022
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,893
    No your inputs are colinear:
    Code (CSharp):
    1. float3 direction = new float3(0, 1f, 0);
    2. float3 up = new float3(0, 1f, 0);
    The docs for LookRotation specifically say that the inputs should not be colinear:
    https://docs.unity3d.com/Packages/c...nity.Mathematics.quaternion.LookRotation.html
    And the docs for safe specifically say that colinear inputs will result in the identity quaternion which is exactly the results you're seeing:
    https://docs.unity3d.com/Packages/c....Mathematics.quaternion.LookRotationSafe.html
    The 90 degree offset is happening just because your input happens to be 90 degrees off from the identity. It will always return the identity for colinear vectors, as documented.
     
    Bunny83 and lukereeves like this.
  6. lukereeves

    lukereeves

    Joined:
    Nov 12, 2017
    Posts:
    3
    Ohhh I see, I was thinking about the result and the forward being colinear. My bad, thanks for clarifying! It is interesting that the function still seems to work in all my tests without the cross product normalization; sadly I am not versed enough in the math to understand that one :)
     
    PraetorBlue likes this.
  7. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Ok, so its really nasty to track down and should be fixed.

    E.g.
    - If Quaternion.LookRotation is used in MonoBehaviour prior which sets up to world up; + stores rotation as is.
    - And then you'd want to set direction as that up inside system via quaternion.LookRotationSafe (which is not safe at all in this case!)
    - Invalid result due to up == up.

    So expected -> forward direction as up quaternion rotation equivalent;
    Actual result -> quaternion.identity;
     
    Last edited: Dec 1, 2022
  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    I'm really not sure what you're saying here. I'm also not sure if you actually understand the point of the two vectors you have to provide to LookRotation. The first vector dictates the actual look direction. So the forward vector of the resulting orientation would be pointing in that exact direction. The up vector you provide is just a hint how to rotate around that first look vector. If you do not pass a second vector, you implicitly pass Vector3.up (0,1,0). Of course this would not work when you want to look straight up because it's impossible to determine how you want the orientation to be rotated around the look vector.

    Ideally the up vector should be perpendicular to your look vector. Though for most cases it's enough that it's not the same as your look vector and just roughly points in the desired up direction.

    Just as an example, if you want to look straight up, you can use

    Code (CSharp):
    1. Quaternion.LookRotation(Vector3.up, -Vector3.forward)
    This would look up and orient the up vector of your orientation to point into the negative world z direction. However you could also use

    Code (CSharp):
    1. Quaternion.LookRotation(Vector3.up, Vector3.right)
    This would also look straight up, but the local up vector would point towards the world right. So the view is rotated 90° clockwise around the view direction (world up). Of course if you pass:

    Code (CSharp):
    1. Quaternion.LookRotation(Vector3.up, Vector3.up)
    There's no way to calcuate a proper orientation as it's not clear how the orientation should be rotated around the look direction since both vectors are colinear.

    I read your post again, but I still can't make much sense of what you wrote there. As long as you pass valid input (so two vectors which are not colinear) the method produces a correct orientation.
     
  9. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Unless it doesn't.

    So here's an example:
    Decal is generated at surface. Its up vector matches world up vector.

    In a system, if you read that value, and attempt to align some entities forward to that decals' up vector, you'd get quaternion.identity.

    I get it, that its less ops. But insanely hard to track down later on if this happens.

    That's the problem. When there's no data for the where perpendicular vector should be pointing towards.
    And its only forward vector (as up direction).

    So it makes sense to just leave quaternion.LookRotation(forward, math.up()), right?
    Yeah, that's a ticking time bomb.

    Because forward is up. And now you've got quaternion.identity instead of forward aligned to up axis.
     
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    No, it does not make sense. You expect the method to magically choose a proper up vector for you which it can not do and does not do You have to decide what to do in this degenerate case when you have two colinear vectors. You can not pass two colinear vectors. Two colinear vectors are not valid input. So your first rebuttal makes no sense as it seems you still didn't get the point.

    You said
    So how should Unity decide what to do? It's a decision you have to make. The documentation is pretty clear about that. The wording when to use float3x3.LookRotationSafe may be a bit misleading as it mainly applies for the case when the vectors are not normalized. The documentation on float3x3.LookRotationSafe also clearly states:
    "safe" doesn't mean it can turn sh** into chocolate :)

    So you have to handle the cases in a reasonable manner when the input is invalid. A similar case would be when you want to calculate "a/b" where a and b are both 0 you want it to somehow return a meaningful value. It can not. 0/0 can literally be anything. Likewise not having a linear independent up vector gives you infinite possible orientations. Unity won't just pick a random one for you.
     
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    ps: How you should handle your specific case depends on your specific usecase. You seem to want to place a decal on a ceiling or floor. How did you decide where to place it? a raycast? If that's the case, who shoot the raycast? If it's a player, it has an up vector that is perpendicular to the ray direction. This would (almost) always work as it would never be world up when you aim at the floor or the ceiling.

    As I said, it's a problem you have to solve. Find a valid up vector. If you don't care about the orientation around forward, using a random vector would probably work in most cases, though I would not recommend it.
     
  12. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    I get it. But most of the users won't even care what second param is for, and default LookRotation falls back to world up.

    Plus there are cases where there's only one direction vector (forward) without any hints available.

    You're missing whole point.

    Decal is already placed by MonoBehaviour, or by a random system.
    Its rotation is read by a different system which receives up direction. (equivalent of transform.up);

    That up direction gets passed as forward.
    There's no extra matrices / data to figure out what local up for that direction is.

    So basically, this is the exact op case:
    Code (CSharp):
    1. float3 direction = new float3(0, 1f, 0);
    2. float3 up = new float3(0, 1f, 0);
    3. Quaternion q = Quaternion.LookRotation(direction, up);
    4. quaternion newRotation = quaternion.LookRotation(direction, up);
    5. quaternion safeRotation = quaternion.LookRotationSafe(direction, up);
    6. Debug.Log($"Engine - {q}, math - {newRotation}, safe - {safeRotation}");
    7. // Engine - (-0.70711, 0.00000, 0.00000, 0.70711), math - quaternion(NaNf, NaNf, NaNf, NaNf), safe - quaternion(0f, 0f, 0f, 1f)
    Note the difference. It should return a valid quaternion (forward equal to up).
    Instead it returns identity.

    Its solvable by writing a custom method, but I'd rather have a sustained implementation inside Mathematics package.

    Edit: Oh, its scripting subforum. No point in asking for FR here.
     
    Last edited: Dec 2, 2022