LocalToWorld.Rotation returns invalid rotation when it is scaled (uniformly).
Test Code
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class L2W_Quaternion_Bug : MonoBehaviour
{
public float3 EulerDegree = math.float3(90, 90, 0);
[Range(0, 10)] public float scale = 0.1f;
public float3 Translate = 0;
public quaternion rotationIn;
public quaternion rotationOut;
[NaughtyAttributes.Button(nameof(TestL2WRotation))]
public void TestL2WRotation()
{
var l2w = new LocalToWorld();
rotationIn = quaternion.EulerZXY(math.radians(EulerDegree));
l2w.Value = float4x4.TRS(Translate, rotationIn, scale);
rotationOut = l2w.Rotation;
if (math.dot(rotationOut.value, rotationIn.value) < 0) rotationOut.value = -rotationOut.value;
}
}
due to
/// <summary>Constructs a unit quaternion from an orthonormal float4x4 matrix.</summary>
public quaternion(float4x4 m)
{
float4 u = m.c0;
float4 v = m.c1;
float4 w = m.c2;
uint u_sign = (asuint(u.x) & 0x80000000);
float t = v.y + asfloat(asuint(w.z) ^ u_sign);
uint4 u_mask = uint4((int)u_sign >> 31);
uint4 t_mask = uint4(asint(t) >> 31);
float tr = 1.0f + abs(u.x);
uint4 sign_flips = uint4(0x00000000, 0x80000000, 0x80000000, 0x80000000) ^ (u_mask & uint4(0x00000000, 0x80000000, 0x00000000, 0x80000000)) ^ (t_mask & uint4(0x80000000, 0x80000000, 0x80000000, 0x00000000));
value = float4(tr, u.y, w.x, v.z) + asfloat(asuint(float4(t, v.x, u.z, w.y)) ^ sign_flips); // +---, +++-, ++-+, +-++
value = asfloat((asuint(value) & ~u_mask) | (asuint(value.zwxy) & u_mask));
value = asfloat((asuint(value.wzyx) & ~t_mask) | (asuint(value) & t_mask));
value = normalize(value);
}
summary said orthonormal, but uniform scaled Matrix is orthonormal.
uniform scale should be properly decomposed ahead.
Some thing like:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool DecomposeRotationScale(this float3x3 rotScSh, out float3 scale, out float3x3 rotation, out float3x3 shearRotation, MainAxisID mainAxis = MainAxisID.ZAxis)
{
float3 c0 = rotScSh.c0, c1 = rotScSh.c1, c2 = rotScSh.c2;
var lengthSq = math.float3(math.dot(c0, c0), math.dot(c1, c1), math.dot(c2, c2));
bool hasValidScale =
math.cmin(lengthSq) > Epsilon &&
math.cmax(lengthSq) < EpsilonRcp &&
math.isfinite(lengthSq.x) &&
math.isfinite(lengthSq.y) &&
math.isfinite(lengthSq.z);
if (hasValidScale)
{
var mainAxisID = (int)mainAxis;
var secondAxisID = (mainAxisID + 1) % 3;
var thirdAxisID = (secondAxisID + 1) % 3;
scale = math.sqrt(lengthSq);
scale[thirdAxisID] = (math.determinant(rotScSh) < 0f) ? -scale[thirdAxisID] : scale[thirdAxisID];
var scaleRcp = math.rcp(scale);
shearRotation = math.float3x3(c0 * scaleRcp.x, c1 * scaleRcp.y, c2 * scaleRcp.z);
rotation = RotationFromAxisAssumeNormalizedValid(shearRotation[mainAxisID], shearRotation[secondAxisID], mainAxisID);
}
else
{
scale = 1;
rotation = float3x3.identity;
shearRotation = rotScSh;
}
return hasValidScale;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float3x3 RotationFromAxisAssumeNormalizedValid(float3 mainAxis, float3 secondAxis, int mainAxisID = 2)
{
var secondAxisID = (mainAxisID + 1) % 3;
var thirdAxisID = (mainAxisID + 2) % 3;
var thirdAxis = cross(mainAxis, secondAxis);
float3x3 rot = default;
rot[mainAxisID] = mainAxis;
rot[secondAxisID] = cross(thirdAxis, mainAxis);
rot[thirdAxisID] = thirdAxis;
return rot;
}
these code is to decompose shear and none-uniform scale.
uniform scale should be much simpler.
Also LocalToWorld.Right/Up/Forward are all not normalized. but that’s fine.
Users still need to know that these values are not ensured to be Unit length.