[FIX INSIDE] Quaternion.FromToRotation discontinuity

Ok, this is just a heads up that I’ve found a weird discontinuity in FromToRotation. (edit: oh 2019.1.0f2)

My test case is a bit convoluted and I have no time for straightening it up atm, but here’s a little mockup so you can get a feel for what’s going on:

void test() {
  Vector3[] pts = new Vector3[] {
    new Vector3(0f, 0f, 0f),
    new Vector3(.5f, 0f, 0f),
    new Vector3(0f, .25f, 0f)
  };

  for(float v = -.5f; v < .5f; v += .05f) {
    pts[1].z = v;
    Debug.Log(string.Concat($"when v is {v.ToString("F2")} -- point: {calcTestPoint(pts)}",
                    (Mathf.Abs(v) < .01f)? " <<< watch this" : ""));
  }
}

Vector3 calcTestPoint(Vector3[] pts) {
  var normal = Vector3.Cross(pts[1] - pts[0], pts[2] - pts[0]).normalized;
  var delta = pts[1] - pts[0];
  var q = Quaternion.FromToRotation(normal, Vector3.back);
  return q * delta.normalized;
}

This will violently flip the X component when the second point reaches (0.5, 0, 0), a completely arbitrary behavior, and I’m guessing it has to do with picking a wrong perp because of the points being too zeroed out.

To solve this I had to implement a different FromToRotation function, and I ended up using the one presented here (<< click), although slightly reconfigured to provide for the Unity’s axis-angle notation, instead of scalar-vector.

static public Quaternion FromToRotation(Vector3 dir1, Vector3 dir2) {
  float r = 1f + Vector3.Dot(dir1, dir2);
  Vector3 w;

  if(r < 1E-6f) {
    r = 0f;
    w = Mathf.Abs(dir1.x) > Mathf.Abs(dir1.z)? new Vector3(-dir1.y, dir1.x, 0f) :
                                               new Vector3(0f, -dir1.z, dir1.y);
  } else {
    w = Vector3.Cross(dir1, dir2);
  }

  return new Quaternion(w.x, w.y, w.z, r).normalized;
}

No discontinuities.

I haven’t tested this thoroughly and I don’t know if it’s performing better or worse, or if there are other considerations and/or corner cases – for now it appears to be a more accurate implementation in my case, which is a rich numerical test bed with UV projections and whatnot and nothing broke so far.

Note that I have deliberately changed the function to assume unit vectors, so its behavior is undefined with the vectors of arbitrary lengths. Consult the original site for details.

You can also change or expose the epsilon to suit your needs.

I haven’t reported this as a bug.

Whoever watched this thread, or is reading this in the future, here’s an update.
After following through with more consideration of what’s going on, this is what the solution should look like, when the arguments are valid unit vectors (basically dir1 and dir2 must be normalized beforehand):

// default value is optional
static public Quaternion FromToRotation(Vector3 dir1, Vector3 dir2,
              Quaternion whenOppositeVectors = default(Quaternion)) {

  float r = 1f + Vector3.Dot(dir1, dir2);

  if(r < 1E-6f) {
    if(whenOppositeVectors == default(Quaternion)) {
      // simply get the default behavior
      return Quaternion.FromToRotation(dir1, dir2);
    }
    return whenOppositeVectors;
  }

  Vector3 w = Vector3.Cross(dir1, dir2);
  return new Quaternion(w.x, w.y, w.z, r).normalized;
}

No discontinuities. Supports custom handling of the special case.
Borrows from the original implementation. If it works, it works. If it doesn’t, you can override it.

I have tested this in the meanwhile and I’m 99.9% sure that it works as expected from anectodal evidence and some sort of brute-force check against what the original FromToRotation already does. I can’t be 100% sure atm, because it’s extremely difficult to write a proper test for this, as I have nothing to compare against.

Even that discontinuity I’ve encountered is a hardcore rare issue, that turns out to be rather specific for my use case. In general purpose testing, I can’t seem to encounter it. But it’s there.

In any case, here’s an explanation for those who aren’t versed with the math involved.

What this does is basic (in quaternion rotation terms, but I cannot go into much depth here about that), and isn’t that different from the previous function (except the “special” part). The thing is, the whole thing becomes numerically unstable when two vectors go in opposite directions (i.e. exactly or nearly 180 degrees apart).

When that happens, Vector3.Dot(dir1, dir2) becomes -1, and thus r becomes 0.
This is what if(r < 1E-6f) tries to capture, because we don’t want to compare a floating point with an exact 0, but we’re checking for a near-zero essentially.

Anyway, if and when that happens, there are situations in which we can’t actually tell the proper axis of rotation, we just know that the vectors are opposite, so Unity in its FromToRotation assumes the X axis in several ambiguous cases, thus Quaternion.Euler(180f, 0f, 0f) becomes the Unity’s preferred solution.

But, let’s imagine that you are comparing two vectors that lie on the XY plane. If they happen to be directly opposite to each other (for example, exactly up and down), it means that one should be rotated 180 degrees around the Z axis, simply because of the context.

But, if you Debug.Log this, this is what you get:

Quaternion.FromToRotation(Vector3.up, Vector3.down) == Quaternion.Euler(180f, 0f, 0f);
// True
Quaternion.FromToRotation(Vector3.up, Vector3.down) == Quaternion.Euler(0f, 0f, 180f);
// False

Which makes perfect sense because you could technically rotate up 180 degrees on the X axis and get down, but it’s heavily biased. Please bear in mind that this has nothing to do with the strange discontinuity I described earlier, but it is perhaps important to pay special attention to this special case with the opposing vectors, so that’s why I’ve added the optional default value.

2 Likes

It’s there and affecting me in game. I also think it may be a discontinuity.

I have a top-down shooter and in turning an object to face another, my sprites keep disappearing. I’ve been using transform.up = direction. Trying to find a solution, people say to use Quaternion.FromToRotation, but this returns (0,180,180) when vectors are in opposition. And not even exact opposition; an x delta of 0.01f is enough to get the y axis to flip.

Confusingly, a test case in a new project has no visual error using transform.up = direction because the rotation produced is (180,0,0) instead of (0,180,180) in my game.That’s a discontinuity, no?

Indeed, in my test case, assigning transform.rotation = Quaternion.Euler(0,180,180) has different results in different projects.

I’m sorry that I haven’t seen your comment earlier, but try searching for my other comments on Quaternions (especially FromToRotation) as I’ve posted a complete solution that kind of fixes the issue and made a full Euler recreation with it that matches and extends Unity’s (and compares the two), and also visually showcases the Unity’s error if you tweak it a little (explained in the post).

The issue arises from computing a quaternion on one of the base axis-aligned planes, however the Unity’s method is heavily biased, and for some reason this is something we have to live with (just like with having implicit casts from Vector2 to Vector3 and having a Mathf library that casts 64-bit System.Math results into 32-bit and back, god…).

Well I’ve basically extended FromToRotation to allow for this ambiguity to occur in a controlled manner, by letting you supply a specific quaternion rotation when this happens (it’s always a single case scenario, and its potential outcome is quite predictable). The blind spot of the math behind it is basically a rotation that turns the vector opposite to self (antiparallel), and it is this case where Unity’s FromToRotation will simply assume a plane of rotation, which is practically invalid 50% of the time. If you think about the general case, if you have a forward vector, there are two ways to get to the back vector, one is by rotating 180 degrees over X, and the other is by rotating 180 degrees over Y. With my function you can specify your intentions upfront. Sometimes it really does matter, as was the case behind this original post above, because I was doing UVs, and the actual output would unexpectedly flip when everything was lined up orthogonally.

Oh, here’s the link , I’ve managed to find the post. Scroll down for the rest of it (and click on the spoiler buttons).
The Euler rotation code is entirely self-sufficient (just tack the script on a gameobject), but I’ve copy/pasted a version that uses my internal helpers and stuff (i.e. remove ‘using CommonExtensions’). It is very easy to modify/make it work, as I’ve explained everything in that post. Ask there if you have any questions. This is probably the best breakdown of Euler and Quaternions, in code, you’ll find on the internet. Seriously, I know it sounds pretentious, but it is what it is, I’ve spent a decade trying to understand them.

From the example you can also observe the Unity’s handedness (fyi) as well as the bug this thread is all about. It also lets you sequence Euler rotations differently (and teaches you how to code such permutations without having to branch in code for each). Unity’s standard is to apply the three axis rotations in the order ZXY (it is stated in the docs and this confirms it).

Hope this helps.

1 Like

Cheers mate! Strange that so few people are affected by this, or at least aware of it. I guess they are using Unity aligned with it’s default 3D andit’s only top-down 2D that experiences the issue.

@SoftwareGeezers
Well, it’s a weird thing, but overall, I think in the low end people don’t know math and are scared by any of this, and on the high end, well people are eligible to Unity’s technical support :wink: Or idk, they are greedy, or can’t / don’t want to share their solution, big studios and that kind of jazz. Which is bad for Unity in the long run, imho.

It’s a bit odd there are like 3 of 4 people so far who found this useful. It’s a bug ffs, and nobody wants to at least acknowledge it, it’s like over everybody’s head, and yet I also made a simple test to prove it. I’m really glad if it helps you or anyone.

1 Like

Why should anyone be affected by this? Using two opposite vectors do mathematically not result in a valid unique rotation axis. In fact there are infinite possible rotations and Unity just picks one of them. So this is more about the user not paying attention to the input. This is just an exceptional case as dividing by 0. Unity quaternions are used to describe 3d rotations and they are not based on or related to euler angles in any way. The described rotations are always relative rotations.

So I don’t really get the fuss about Unity’s behaviour. He ran into the exceptional case and complains about Unity’s behaviour. Mathematically the only valid result would be: “no unique solution possible”, So would you prefer the method to throw an exception?

How is that a bug? In your test case you ran into the exact edge case that has no unique / correct solution. So your claim is that because the way they handle the edge case doesn’t fit your specific case it’s a bug? Again you should be aware of the fact that two opposing vectors do not define a valid rotation axis, so don’t pass those into that method. Just like you should not normalize (0, 0, 0), pass a value greater than 1 or smaller than -1 into Math.Acos or divide 0 by 0.

All your solution did is providing an additional special handling for this edge case. However that’s not the responsibility of the FromToRotation method. This method should construct a rotation based on the two provided vectors which simply isn’t possible for your input. Instead of throwing an exception Unity simply uses a fallback solution which might even work for most people.

For this to be a “bug” there has to be a clear “correct” behaviour which you can not provide.

@Bunny83
Before I say anything else, let’s keep the words “undefined” and “unexpected” as having separate meanings to clear out any misunderstandings.

a) I have a little bit of a history with this, and you can explore it a bit more if you’re interested. This is an old thread that got necro’d, I’ve got newer threads, and one of them is linked here.

b) I have a proof that this is an oversight, and not just a problem with mathematics. There is unexpected behavior down the line, I’m not sure why are you suggesting otherwise if there are comments about the unexpected behaviors. No it’s not similar to dividing by zero, until you delve a little deeper and see what’s going on. Dividing by zero would be undefined behavior, not unexpected.

c) I’ve managed to make a visual retestable proof of how exactly Unity does the wrong thing, if you only follow the instructions in that linked thread, and I’ve managed to introduce a workaround that prevents this behavior to occur in a controlled manner, so that it’s no longer unexpected, which was the issue with the Unity’s implementation.

d) As a sidenote, I’ve spent serious time investigating both Quaternions and Unity’s implementation of them in the last eight years or so. I know why this matters, and have more than one project where I’ve stumbled on this issue and know what I’m talking about. I think there is a snippet of code in this thread as well that showcases a bias and a clear discontinuity, for no apparent reason. Yes the mathematics is convoluted, and ends up hard to digest, but is very simple at its core, and should not lead to signs being flipped. This is why I resorted to making the proper environment to test this out.

This statement isn’t true because I’ve provided a correct behavior.

Any behavior that always acts in ONE way when presented with TWO or more choices is heavily biased, or at the very least heavily undocumented. Maybe you disagree with me, but in my book, such a bias should be considered a bug.

If a vendor is not at the counter at the moment, you do not fallback to a conclusion that the shop has no owner. That kind of thing. From a theoretical perspective, one could go even further and argue that ANY unexpected behavior should be declared a bug.

And FromToRotation is kind of a big deal when you work professionally in 3D. When you look at its implementation (you can’t really because it’s hidden from view, but I mean generally) it becomes painfully clear it’s one of the most fundamental things about working with Quaternions in general.

That said, notice how all of the people here, including me, have noticed a discontinuity (edit: there aren’t that many in this thread, admittedly, but I’ve had them elsewhere on the forum over the years, it’s hard to keep up). A quaternion function that simply yields undefined results because of undefined math, should not exhibit discontinuities (flipping particular signs), and in the worst case it should outright produce NaNs, which is a standard undefined behavior for numeric functions.

Again if you check out the fully featured Euler reconstruction I’ve made, you can test out exactly where Unity’s implementation gets this wrong. You even have step by step instructions of how to reproduce it with 100% certainty, and a side by side comparison!

Again, the link is here . If you have any trouble with the code, I’ll be glad to help.
Check out the next post for the standalone reproduction of this example.
I’ll also make sure to attach a GIF here, showcasing a clear case of biased code that yields unexpected behavior.

Appendix:

As a matter of fact, there is always a simple unique / correct solution, which is exactly why the result is unexpected, and btw Quaternion.Euler handles this correctly.

Furthermore, dividing by zero in this context always leads to a correct solution, and if you don’t care much about the actual path, it’s also always unique. It’s just that there are two paths to take, so it is ambiguous: rotation by axis 1 or rotation by axis 2 (edit: at least two, but sure, you could argue that there infinitely many, mathematically-speaking, but that doesn’t add anything of value implementation-wise). Quaternion.Euler handles this 100% correctly, while Quaternion.FromToRotation handles this 50% correctly. In certain contexts, because of the XZ plane bias, there is a 100% chance you’ll end up with a solution that was unexpected, for no reason…

Now this is obviously because Euler has a little bit of user context to gather data from, but the remedy is exactly that. Let the user add a little context for the added robustness of the function. You know, like LookAt has the worldUp and similar, or maybe there is a better analogy, but you get the point. Let the user resolve potential ambiguities by optionally supplying context.

I am well aware that FromToRotation is supposed to be plain mathematical function that can backfire when used in an ambiguous context (antiparallel or null vectors), however it should just as well produce values that are consistent with said ambiguity and that are indicative to an out of bounds scenario, not just flip the signs and be like ‘done’ with both hands in the air.

It should either:
a) produce NaN values (standard behavior)
b) throw an error (one might argue it would unnecessarily lower the performance)
c) let the user provide additional parameters that might prevent any discontinuities (likely the best trade off)

On top of this, whatever the solution, it should be thoroughly documented.

This is why I’m seeing the current implementation as buggy, or at least as an example of a bad/rushed design of the fundamentals, just like having Vector3s implicitly cast to Vector2s violates any sane programming specification, silently dropping data like it’s nothing.

Now we can argue about this, but at least refrain from doing so until you’ve checked my example.

edit:
Oh, I forgot to include that the code in the original post fixed any apparent discontinuities even though it also had to work with antiparallel vectors. At this point I can’t really tell whether I just added a different bias, or if it did in fact improved the behavior across the table, but it was clear from the get go that having an unexpected discontinuity is not the same as having undefined result / ambiguous rotation, and that there are ways to handle this better.

Quaternions are 4-dimensional spheres – how on Earth do you even introduce discontinuities with them? Without an opportunity to actually see the code, I can only guess, but my hunch is that there is a badly formed IF block in there, throwing a wrench in an otherwise beautiful mathematics.

1 Like

Here I will reproduce the code from the other thread in its entirety for ease of reference
(I’ve made it copy/pasteable, tell me if it doesn’t work because I didn’t test the changes made)

using UnityEngine;

[ExecuteInEditMode]
public class QuaternionEulerTest : MonoBehaviour {

  [SerializeField] [Range(0f, 360f)] public float x;
  [SerializeField] [Range(0f, 360f)] public float y;
  [SerializeField] [Range(0f, 360f)] public float z;
  [SerializeField] public EulerOrder order = EulerOrder.ZXY;
  [SerializeField] public bool sameOrigin;
  [SerializeField] public VectorEnum pickVector = VectorEnum.Basis;

  public Quaternion myEuler(Vector3 euler, EulerOrder order = EulerOrder.ZXY) {
    euler *= Mathf.Deg2Rad;

    var qx = fromToRotation(Vector3.forward, to_zy(polar(-euler.x)), new Quaternion(1f, 0f, 0f, 0f));
    var qy = fromToRotation(Vector3.right,   to_xz(polar(-euler.y)), new Quaternion(0f, 1f, 0f, 0f));
    var qz = fromToRotation(Vector3.right,   to_xy(polar(+euler.z)), new Quaternion(0f, 0f, 1f, 0f));

    Quaternion q = Quaternion.identity;

    int o = (int)order;
    for(int axis = 0; axis < 3; axis++) {
      switch((o >> (axis << 1)) & 0x3) {
        case 1:  q *= qy; break;
        case 2:  q *= qz; break;
        default: q *= qx; break;
      }
    }

    return q;
  }

  Quaternion fromToRotation(Vector3 dir1, Vector3 dir2, Quaternion ifOpposite = default) {
    float w = 1f + Vector3.Dot(dir1, dir2);

    if(w < 1E-6f) {
      if(ifOpposite == default) return Quaternion.FromToRotation(dir1, dir2); // auto-fallback
      return ifOpposite;
    }

    Vector3 xyz = Vector3.Cross(dir1, dir2);
    return new Quaternion(xyz.x, xyz.y, xyz.z, w).normalized;
  }

  Vector3 to_zy(Vector2 vec2) => new Vector3(0f, vec2.y, vec2.x);
  Vector3 to_xz(Vector2 vec2) => new Vector3(vec2.x, 0f, vec2.y);
  Vector3 to_xy(Vector2 vec2) => new Vector3(vec2.x, vec2.y, 0f);

  Vector2 polar(float theta) => new Vector2(Mathf.Cos(theta), Mathf.Sin(theta));

  void OnDrawGizmos() {
    if(pickVector != VectorEnum.Basis) {
      draw(pickVector, Color.white);
    } else {
      draw(VectorEnum.Right, Color.red);
      draw(VectorEnum.Up, Color.green);
      draw(VectorEnum.Forward, Color.blue);
    }
  }

  void draw(VectorEnum vecType, Color lineColor) {
    Vector3 eulerAngles = new Vector3(x, y, z);

    drawRotatedPointAround(
      Vector3.zero,
      myEuler(eulerAngles, order), // custom Euler
      vecType, lineColor, Color.white
    );

    drawRotatedPointAround(
      sameOrigin? Vector3.zero : 2f * Vector3.right,
      Quaternion.Euler(eulerAngles), // Unity's Euler
      vecType, lineColor, Color.grey
    );

  }

  void drawRotatedPointAround(Vector3 p, Quaternion q, VectorEnum vecType, Color lineColor, Color dotColor) {
    var v = Vector3.forward;
    if(vecType == VectorEnum.Right) v = Vector3.right;
    else if(vecType == VectorEnum.Up) v = Vector3.up;

    var k = p + q * v;

    Gizmos.color = lineColor;
    Gizmos.DrawLine(p, k);

    Gizmos.color = dotColor;
    Gizmos.DrawSphere(k, .1f);
  }

  public enum EulerOrder : int {
    XYZ = 0b00_01_10, // (6)
    XZY = 0b00_10_01, // (9)
    YXZ = 0b01_00_10, // (18)
    YZX = 0b01_10_00, // (24)
    ZXY = 0b10_00_01, // (33)
    ZYX = 0b10_01_00  // (36)
  }

  public enum VectorEnum {
    Right,
    Up,
    Forward,
    Basis
  }

}

Here are the instructions in order to reproduce the discontinuities

Even though there are merits to what you’re saying, I hope you can appreciate that in the end it all boils down to what is truly undefined vs what is unexpected. Currently it is very easy to obtain an invalid result without knowing that it is invalid. In my mind, this kind of implementation is dangerous.

On the other hand, if this is really the limitation of the underlying math and they don’t want to stress the existing function, performance-wise, historically we had a similar issue with atan function that would normally lose all context because of the necessary division (signs would cancel and the actual quadrant would become undefined). This is why we now use atan2, and why this is a staple function in all math libraries. So even if someone think I’m wrong and that the ‘bug’ label is debatable, there are still ways and ways to improve it, because for some of us it just ain’t useful.

It’s undocumented and unexplained and unexpected. If you had created a top-down 2D game that needs to rotate to vectors, you’d have encountered this too and be scratching your head.

If you create 3D games, or 2D games aligned on a different axis, there’s no problem because Unity handles these gracefully by assuming the correct plane. But Unity does not communicate this or provide a solution for games on the XY plane.

I guess the reason most don’t stumble across it is because most games are pretty basic in mechanics. Or people are having disappearing sprites and think it’s just some drawing glitch as I was for a while.

1 Like