Matrix4x4 extracting the incorrect scale

Hello everyone.

I’m working with importing an external 3D format into objects in Unity, and have hit a stumbling block on the matrix conversion.
I’m reading in a matrix from this format: LDraw.org - LDraw File Format Specification
e.g. 1 0 0 0 1 0 0 0 1 is the identity matrix.

The problem is that some matrix values, mainly with negative values are given a very wrong scale value.

        // Convert the matrix into Unity objects
        Matrix4x4 rotationMatrix = new Matrix4x4 (
            new Vector4 (matrixTokens[0], matrixTokens[3], matrixTokens[6], 0),
            new Vector4 (matrixTokens[1], matrixTokens[4], matrixTokens[7], 0),
            new Vector4 (matrixTokens[2], matrixTokens[5], matrixTokens[8], 0),
            new Vector4 (positionTokens[0], positionTokens[1], positionTokens[2], 1)
        );

        Vector3 position = new Vector3 (rotationMatrix.m03, -rotationMatrix.m13, rotationMatrix.m23); // -Y is up
        Quaternion rotation = rotationMatrix.rotation;
        Vector3 scale = rotationMatrix.lossyScale;

A simple scaling matrix of 6 0 0 0 -4 0 0 0 6 is generating a scale value of (-6.0, 4.0, 6.0) instead of (6.0, -4.0, 6.0) (Note the X scale is negative, not the Y), with no rotation((0, 0, 0)).
Full matrix:

6.00000    0.00000    0.00000    0.00000
0.00000   -4.00000    0.00000    0.00000
0.00000    0.00000    6.00000    0.00000
0.00000    0.00000    0.00000    1.00000

I understand the scale is not guaranteed to be a perfect mapping (lossy), but this seems way too simple for it to be getting wrong.

Checking another object that has a matrix of 1 0 0 0 -5 0 0 0 1 ends up with a scale of (-1.0, 5.0, 1.0), but a Euler rotation of (0, -90, 180).

Can anyone see if I’m missing anything obvious here, or can recommend a way to get the appropriate scaling value from the matrix?

(Full code in guthub: VLD/Assets/Scripts/LDrawPart.cs at master · DeeHants/VLD · GitHub)

Many thanks.

Well, the combination of rotation and scale is correct again, i.e. yields the same result.

Edit: I was wrong.

But they’re not. The first example has a rotation of (0, 0, 0), and scaled on the X axis, not the Y.
Rotation can also not always do the same as scaling, especially when used to mirror an object.
While the second example “yields the same result”, it happens to be symmetrical in this case. If it wasn’t, it wouldn’t.

Apologies, you had not given the rotation for the first example and I assumed it would be compensated by the rotation, as I thought was the case in the second example. I think I somehow dropped the rotation around y when trying to visualize this in my head. I don’t know what’s going wrong there.

If you knew that all your input had no rotational component, I guess you could compute the scale with something like

matrix.MultiplyPoint(Vector3.one) - matrix.MultiplyPoint(Vector3.zero)

But that does not seem to be an assumption you can make.

I think, in general, if one of the scales is negative you cannot tell from the matrix which one it is, as you can always swap the sign of two axes’ scales by adding a 180 degree rotation around the third one.

Sorry, I’d only found the rotation was changed when I extracted my second example and didn’t think to mention it for the first.
I have a very hacky workaround which works for simple scaling matrices (like those mentioned) but break when there is any rotation:

// FIXME Hack for negative scaling. Look for simple scaling and create the vector and reset the rotation
        if (
            rotationMatrix.m01 == 0 && rotationMatrix.m02 == 0 &&
            rotationMatrix.m10 == 0 && rotationMatrix.m12 == 0 &&
            rotationMatrix.m20 == 0 && rotationMatrix.m21 == 0
        ) {
            Vector3 correctScale = new Vector3 (rotationMatrix.m00, rotationMatrix.m11, rotationMatrix.m22);
            if (scale != correctScale) {
                Debug.LogFormat ("Incorrectly interpreted matrix: Should be {0} but got {1}.\n{2}", correctScale, scale, rotationMatrix.ToString ());
                rotation = Quaternion.identity;
                scale = correctScale;
            }
        }

It’s far from a fix, or permanent though!

Thanks for the comments so far though.

I’ve now parred this down to the simplest of code which removes any outside influence:

Vector3 startScale = new Vector3 (1, -5, 1);
// {(1.0, -5.0, 1.0)}
Matrix4x4 matrix = Matrix4x4.Scale (startScale);
// {1.00000  0.00000 0.00000 0.00000
//  0.00000 -5.00000 0.00000 0.00000
//  0.00000  0.00000 1.00000 0.00000
//  0.00000  0.00000 0.00000 1.00000}
Vector3 finalScale = matrix.lossyScale;
// {(-1.0, 5.0, 1.0)}
Quaternion finalRotation = matrix.rotation;
// {(0.0, 0.0, 0.0, 0.0)}

Still the X/Y scale has been corrupted with no rotation.
Can anyone else reproduce this?

I tested similar last time, with the same result as you. However, I cannot reproduce it anymore. Now, I do get a rotation on your example.

matrix 
1.00000   0.00000   0.00000   0.00000
0.00000   -5.00000   0.00000   0.00000
0.00000   0.00000   1.00000   0.00000
0.00000   0.00000   0.00000   1.00000
 scale (-1.0, 5.0, 1.0) 
quaternion (0.7, 0.0, -0.7, 0.0) euler (0.0, 270.0, 180.0)

I also tested with System.Numerics.Matrix4x4.Decompose. Last time I got the same weird numbers as with the Unity class, but now I do get the correct scale from that. I wonder if perhaps the .NET framework got an update…

It seems it depends on the magnitude of the scale Y value.

Debug.LogFormat ("{0} converted to {1} ({2}/{3})", startScale, finalScale, finalRotation, finalRotation.eulerAngles);
(1.0, -5.0, 1.0) converted to (-1.0, 5.0, 1.0) ((0.7, 0.0, -0.7, 0.0)/(0.0, 270.0, 180.0))
(6.0, -10.0, 6.0) converted to (-6.0, 10.0, 6.0) ((0.7, 0.0, -0.7, 0.0)/(0.0, 270.0, 180.0))
(6.0, -6.0, 6.0) converted to (-6.0, 6.0, 6.0) ((1.0, 0.0, 0.0, 0.0)/(0.0, 180.0, 180.0))
(6.0, -5.0, 6.0) converted to (-6.0, 5.0, 6.0) ((0.0, 0.0, 0.0, 1.0)/(0.0, 0.0, 0.0))
(6.0, -4.0, 6.0) converted to (-6.0, 4.0, 6.0) ((0.0, 0.0, 0.0, 1.0)/(0.0, 0.0, 0.0))

shrugs and gives up