How is localScale adjusted when re-parenting transforms?

I am trying to mimic Unity’s behaviour where it concerns transform parenting, and I’m stumbling on something I can’t explain.

Say I have 2 GameObjects. They both are without a parent. One is named “Root”, the other “Child”, because that is what the hierarchy will end up like. Both are already translated, rotated and Root is non-uniformly scaled.

Now, at run-time, I parent Child to Root. When this happens, Unity will preserve the global position and orientation of the Child, and in the documentation it also states it preserves scale. It does so by modifying the local transform values/matrix in such a way that its global transform is preserved. Or, that is the assumption.

What this new “local” matrix would need to be is

localTransformMatrix = parentTransform.worldToLocalMatrix * childTransform.localToWorldMatrix;

localRotation and localPosition would be easy enough to derive. And the localScale would seem to be found in the length of the colums of the localTransformMatrix (or its 3x3 rotation part).

But … that doesn’t stroke with what Unity actually does. The new localScale I actually get is (0.6140807, 0.8078736, 0.4113787)

And that’s where my confusion starts. If you parent objects, and inspect the localToWorldMatrix of the Child object, you’ll notice that in fact it is modified. Global position and orientation are preserved, but there is an effect on the scale (and subsequently the localScale) that I can’t seem to match when computing by hand.

So what is going on here? What is Unity actually doing when parenting, and how can I compute the correct values (matching what Unity gives) if I were to compute it by hand?

Well, the problem is that “scale” is always local to the local coordinate system along each local axis. As you might have discovered already, there is nothing like a world-space “scale” property like there is for position / rotation. Unity just provides the lossyScale property which tries to return “something” that comes closest to a worldspace scale. The scale is correctly displayed / calculated as long as all objects are only scaled uniformly (the same on all axis).

If you non-uniformly scale the parent and rotate the child, the child’s local space will be deformed. That means the 3 local axis of the child are no longer be perpendicular to each other since the object will be skewed. In such cases it’s impossible to calculate a worldspace scale for the child. Since parenting / unparenting a child should “perserve” it’s worldspace properties, that’s not possible for the scale.

In order to get the same worldspace properties, Unity has to adjust the local position / rotation / scale of the child object.

Keep in mind that an object actually only has it’s local properties (localPosition, localRotation, localScale). The worldspace properties (position, rotation, lossyScale) are always calculated by taking the parent space(s) into account.

I’m not sure how Unity actually approximates the lossyScale since i never have thought about it ^^.

The basic rules you should keep in mind is:

  • If the parent is non-uniformly scaled, never rotate a child object. The only rotations that would kind of work are 90° steps from the identity rotation. That way the child axis are still aligned with the parent axis.
  • If the child should be rotated your parent(s) should only be scaled uniformly.

See this image as reference:

As you can see the axis of a rotated child are no longer perpendicular when you scale the parent non-uniformly. So you can’t determine a world space scale based on that setup. Keep in mind that a worldspace scale would use the world axis. When scaled uniformly all proportions are preserved.

The answer of @Bunny83 set me on the right track. The problem is indeed that any non-uniform scale will leave you with axes that are no longer perpendicular. So you need to correct for this.

If you do so by hand, you can still get a valid rotation matrix with perpendicular axes from the quaternion rotations.

localRotation = (Quaternion.Inverse(parent.rotation) * rotation).normalized;

If you project the axes you get from

localTransformMatrix = parentTransform.worldToLocalMatrix * childTransform.localToWorldMatrix;

onto the axes you get from the quaternion (by making it a matrix, let’s say localRotationMatrix), you get the appropriate localScale values.

float x = Vector3.Dot(new Vector3(localRotationMatrix.m00, localRotationMatrix.m10, localRotationMatrix.m20), new Vector3(localTransformMatrix.m00, localTransformMatrix.m10, localTransformMatrix.m20));
//And similar for the y and z vectors. 

Plug those back into your local matrix transform, and when you update your localToWorldMatrix you’ll see it will have the same values as Unity gives you.

Mystery solved.