Quaternion.LookRotation not giving correct result (2020.3.15f1)

Hi,

I was doing some vector maths to calculate rotation angles and I stumbled upon a problem with LookRotation.

The documentation states:

Returns identity if forward and upwards are colinear.

Unfortunately, this is not true. I did this test:

Quaternion test = Quaternion.LookRotation(Vector3.up, Vector3.up);

The result is (-0.7, 0.0, 0.0, 0.7) and not (0.0, 0.0, 0.0, 1.0) as expected.

Has anyone seen this or should I file a bug report?

If they say forward and upwards that means (Vector3.forward, Vector3.up)
not (Vector3.up, Vector3.up)

edit: ah no, scratch that, I see what you meant. I’m still infusing coffee.

Hmmm
Implementation wise, this function only stabilizes the basis vectors, so that the rolling rotation is well-defined.
And so it takes your vectors and gets a cross product which is the “arrow of stabilization”.
That said, a cross product of two identical vectors gets you a degenerate case, a vector with a magnitude of 0 (no parallelogram can be made). And this is why it is said it will behave the same as if your vectors were of magnitude of 0.

I think you also got the correct rotation.
I’m guessing it did work only because it was an orthogonal case.
In other words, try with Vector3.one.normalized, see what you’ll get.

Here’s an implementation that claims to be 100% convertible with UnityEngine.Quaternion

private static MyQuaternion LookRotation(ref Vector3 forward, ref Vector3 up)
{
  forward = Vector3.Normalize(forward);
  Vector3 right = Vector3.Normalize(Vector3.Cross(up, forward));
  up = Vector3.Cross(forward, right);

  var m00 = right.x;
  var m01 = right.y;
  var m02 = right.z;
  var m10 = up.x;
  var m11 = up.y;
  var m12 = up.z;
  var m20 = forward.x;
  var m21 = forward.y;
  var m22 = forward.z;
  float num8 = (m00 + m11) + m22;
  var quaternion = new MyQuaternion();
  if (num8 > 0f)
  {
    var num = (float)System.Math.Sqrt(num8 + 1f);
    quaternion.w = num * 0.5f;
    num = 0.5f / num;
    quaternion.x = (m12 - m21) * num;
    quaternion.y = (m20 - m02) * num;
    quaternion.z = (m01 - m10) * num;
    return quaternion;
  }
  if ((m00 >= m11) && (m00 >= m22))
  {
    var num7 = (float)System.Math.Sqrt(((1f + m00) - m11) - m22);
    var num4 = 0.5f / num7;
    quaternion.x = 0.5f * num7;
    quaternion.y = (m01 + m10) * num4;
    quaternion.z = (m02 + m20) * num4;
    quaternion.w = (m12 - m21) * num4;
    return quaternion;
  }
  if (m11 > m22)
  {
    var num6 = (float)System.Math.Sqrt(((1f + m11) - m00) - m22);
    var num3 = 0.5f / num6;
    quaternion.x = (m10 + m01) * num3;
    quaternion.y = 0.5f * num6;
    quaternion.z = (m21 + m12) * num3;
    quaternion.w = (m20 - m02) * num3;
    return quaternion;
  }
  var num5 = (float)System.Math.Sqrt(((1f + m22) - m00) - m11);
  var num2 = 0.5f / num5; // 0.5
  quaternion.x = (m20 + m02) * num2; // 0
  quaternion.y = (m21 + m12) * num2; // 0.5
  quaternion.z = 0.5f * num5;
  quaternion.w = (m01 - m10) * num2;
  return quaternion;
}

Unfortunately, turns out it’s not convertible, at least not in your case.
And it also doesn’t return identity, but (0.5, 0, 0, -0.5)

I need to investigate more.

Hi,

thanks for your insight on this :slight_smile: I hope someone from Unity might jump in to confirm a regression.

I managed to implement something that behaves identical to LookRotation, and from this it is possible to infer that Unity’s implementation does something differently. It seems to be testing for a scenario that is mathematically undefined and corrects it. I think it has to do with singularities (they assume that only singularities aren’t well defined) but without the original source code it’s hard to tell.

In any case, what docs state is true, UNLESS you also introduce a condition for this test, then it simply snaps to one of the two possible cases. You can compare this if you make a test environment.

Quaternion LR(Vector3 forward, Vector3 up) {
  forward = forward.normalized;
  var right = Vector3.Cross(up, forward).normalized;
  up = Vector3.Cross(forward, right).normalized;

  var m00 = right.x;
  var m01 = right.y;
  var m02 = right.z;
  var m10 = up.x;
  var m11 = up.y;
  var m12 = up.z;
  var m20 = forward.x;
  var m21 = forward.y;
  var m22 = forward.z;

  var tr = m00 + m11 + m22;
  var q = new Quaternion();

  if(tr > 0f) {
    var num = Mathf.Sqrt(tr + 1f);
    q.w = .5f * num;
    num = .5f / num;
    q.x = -(m21 - m12) * num;
    q.y = -(m02 - m20) * num;
    q.z = -(m10 - m01) * num;
  } else if((m00 >= m11) && (m00 >= m22)) {
    var num7 = Mathf.Sqrt(1f + m00 - m11 - m22);
    var num4 = .5f / num7;
    q.x = .5f * num7;
    q.y = (m01 + m10) * num4;
    q.z = (m02 + m20) * num4;
    q.w = -(m21 - m12) * num4;
  } else if(m11 > m22) {
    var num6 = Mathf.Sqrt(1f + m11 - m00 - m22);
    var num3 = .5f / num6;
    q.x = (m01 + m10) * num3;
    q.y = .5f * num6;
    q.z = (m12 + m21) * num3;
    q.w = -(m02 - m20) * num3;
  } else {
    var num5 = Mathf.Sqrt(1f + m22 - m00 - m11);
    var num2 = .5f / num5;
    q.x = (m02 + m20) * num2;
    q.y = (m12 + m21) * num2;
    q.z = .5f * num5;
    q.w = -(m10 - m01) * num2;
  }

  return q;
}

(Excuse the negative signs, I modified a solution that had the “wrong” hand rule.)

Therefore the result should really be (0.5, 0, 0, -0.5), however it is also invalid (and behaves like a discontinuity), while Unity prevents this but definitely fails to detect that the vectors were collinear to begin with.

The solution (that would marry this code’s results with Unity’s) is to introduce just one check after line 3

if(right.sqrMagnitude == 0f) right = Vector3.right;

I wasn’t able to reproduce any errors or differences between two algorithms (my tests were naive, but thorough, still I wouldn’t call this claim 100% error-proof). It also behaves the same when vectors are (0,0,0). I specifically tried all orthogonal configurations.

Though it’s probably better to do

if(right.x == 0f && right.y == 0f && right.z == 0f) right = Vector3.right;

Btw the original code is basically the same, the reason why I dismissed it was because I didn’t have the proper environment and it gave a different answer.

Here’s a full code with a solution for the behavior you wanted

Quaternion LookRotation(Vector3 forward, Vector3 up) {
  forward = forward.normalized;
  var right = Vector3.Cross(up, forward).normalized;
  if(right.sqrMagnitude <= 1E-7f) return Quaternion.identity; //right = Vector3.right;
  up = Vector3.Cross(forward, right).normalized;

  var m00 = right.x;
  var m01 = right.y;
  var m02 = right.z;
  var m10 = up.x;
  var m11 = up.y;
  var m12 = up.z;
  var m20 = forward.x;
  var m21 = forward.y;
  var m22 = forward.z;

  var tr = m00 + m11 + m22;
  var q = new Quaternion();

  if(tr > 0f) {
    var num = Mathf.Sqrt(tr + 1f);
    q.w = .5f * num;
    num = .5f / num;
    q.x = (m12 - m21) * num;
    q.y = (m20 - m02) * num;
    q.z = (m01 - m10) * num;
  } else if((m00 >= m11) && (m00 >= m22)) {
    var num7 = Mathf.Sqrt(1f + m00 - m11 - m22);
    var num4 = .5f / num7;
    q.x = .5f * num7;
    q.y = (m01 + m10) * num4;
    q.z = (m02 + m20) * num4;
    q.w = (m12 - m21) * num4;
  } else if(m11 > m22) {
    var num6 = Mathf.Sqrt(1f + m11 - m00 - m22);
    var num3 = .5f / num6;
    q.x = (m01 + m10) * num3;
    q.y = .5f * num6;
    q.z = (m12 + m21) * num3;
    q.w = (m20 - m02) * num3;
  } else {
    var num5 = Mathf.Sqrt(1f + m22 - m00 - m11);
    var num2 = .5f / num5;
    q.x = (m02 + m20) * num2;
    q.y = (m12 + m21) * num2;
    q.z = .5f * num5;
    q.w = (m01 - m10) * num2;
  }

  return q;
}

And nothing stops you from implementing this without reimplementing LookRotation.

Quaternion LookRotation(Vector3 forward, Vector3 up) {
  if(Vector3.Cross(up, forward.normalized).normalized.sqrMagnitude <= 1E-7f) return Quaternion.identity;
  return Quaternion.LookRotation(forward, up);
}

tl;dr conclusion: Unity docs are not correct for this case, though the method is fairly robust (unlike FromToRotation).