[SOLVED] Why is Transform.lossyScale readonly?

Sometimes you want to adjust the lossy scale of a game object. Setting the transform’s localScale equal to the desired lossyScale only works for certain situations.

If works for this example:
GameObject A. Local scale = 1,1,1
------>GameObject B. Local scale = 1,1,1

But you may have this:
GameObject A. Local scale = 2,2,2
------>GameObect B. Local scale = 1,1,1

If you want GameObject B to be lossy scale 2,2,2, you cannot set the GameObject B to have a local scale of 2,2,2, because then it would have a lossy scale of 4,4,4.

So how can you get around this without reading the entire hierarchy upwards?

It would be nice to have an API function to set a lossy scale, which in turn, automatically sets the local scale such that the gameOjbect ends up with the lossy scale you requested.

If you want to set the global scale of an object that is parented you can unparent it, set the scale and then parent back.

In your example:

gameObjectB.transform.parent = null;
gameObjectB.transform.localScale = new Vector3(2f, 2f, 2f);
gameObjectB.transform.parent = gameObjectA.transform;
4 Likes

Ok, thanks for this.
It’s surprising the Unity API doesn’t already provide a way to do this.

The reasoning behind this is probably that it does not represent anything useful as soon as you apply some rotations and non uniform scale. Child objects deform in a way that gives a scale value in world space little meaning. Positions and rotation can be pretty accurately represented, but scale not so much.

edit: It’s hard to type on a phone that doesn’t let you see what you type while typing and doesn’t show you the caret until after you have typed.

2 Likes

That might be true for most scenarios, but in my case I’m taking a game object and bringing it a certain distance in front of the camera, and I know exactly what the global scale needs to be, it’s hard-coded in the software. The parenting in the hierarchy can change during gameplay, so I can’t hard-code a local scale. The above code works fine for setting the global scale.

Note, ThermalFusion is actually right.

The problem with setting some global scale when its the child of something is that… what axes are the scales representing? Is it the global x, or the local x? The lossyScale that spits out from unity is the scale as it would be on each ‘local’ axis. But a ‘globalScale’ isn’t very intuitive at conveying that idea. If you flip something 90 degrees around the z, and than globally scale it in the y by 2, would you expect it to grow up/down or left/right. scaling up/down would be ‘global’ in description, but lossyScale represents the ‘left/right’. Because ‘lossyScale’ represents the local scale with all its parents scales applied. Not its scale in global space!

It’s all a bit wishy washy.

It’s why Unity calls it lossyScale. You lose information. So to set it back you have to invent information.

BUT, yes, I agree there can be uses for this. Such as the situation you describe.

Note though, changing of the transform parent is actually an expensive call. You’re restructuring the hierarchy.

You can do this arithmetically instead.

Here you can see I do it in my ‘Trans’ struct:

        public void SetToGlobal(Transform trans, bool bSetScale)
        {
            if (bSetScale)
            {
                trans.position = Position;
                trans.rotation = Rotation;
                trans.localScale = Vector3.one;
                //this simulates what it would be like to set lossyScale considering the way unity treats it
                var m = trans.worldToLocalMatrix;
                m.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
                trans.localScale = m.MultiplyPoint(Scale);
            }
            else
            {
                trans.position = Position;
                trans.rotation = Rotation;
            }
        }

        public void SetToGlobal(Transform trans, bool bSetScale, bool bSetScaleOnGlobalAxes)
        {
            if (bSetScale)
            {
                trans.position = Position;
                trans.rotation = Rotation;
                trans.localScale = Vector3.one;
                var m = trans.worldToLocalMatrix;
                if(bSetScaleOnGlobalAxes)
                {
                    m.SetColumn(0, new Vector4(m.GetColumn(0).magnitude, 0f));
                    m.SetColumn(1, new Vector4(0f, m.GetColumn(1).magnitude));
                    m.SetColumn(2, new Vector4(0f, 0f, m.GetColumn(2).magnitude));
                }
                m.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
                trans.localScale = m.MultiplyPoint(Scale);
            }
            else
            {
                trans.position = Position;
                trans.rotation = Rotation;
            }
        }

See though that I have 2 parameters controlling it. This conveys the ambiguity of setting a global scale.

1 parameter is for if we’ll set it at all (usually you wouldn’t, cause its not directly supported, and the idea is rather odd).
Another parameter defines if we are going to set it on the global axes or the local axes.

The local axes version is the cheapest (no magnitude calls). And since you really shouldn’t be setting global scales unless you all of its parents have NO rotation… it works as it assumes no rotation.

So go with that mathematical algorithm.

6 Likes

Knowing all of the parent rotations will be (0,0,0) I created the following function.
The function determines the global-to-local scale factor

public static Vector3 GetGlobalToLocalScaleFactor(Transform t)
   {
     Vector3 factor = Vector3.one;

     while(true)
     {
       Transform tParent = t.parent;

       if (tParent != null)
       {
         factor.x *= tParent.localScale.x;
         factor.y *= tParent.localScale.y;
         factor.z *= tParent.localScale.z;

         t = tParent;
       }
       else
       {
         return factor;
       }
     }
   }

The code is used with:

Vector3 desiredGlobalScale = new Vector3(0.5f, 1.0f, 10.0f);//What you want the global scale to be
Vector3 scaleFactor = U.GetGlobalToLocalScaleFactor(this.gameObject.transform); //Determine the factor

    //Determine what the new scale local scale should be
     Vector3 newLocalScale = new Vector3
       (desiredGlobalScale.x / scaleFactor.x,
        desiredGlobalScale.y / scaleFactor.y,
        desiredGlobalScale.z / scaleFactor.z);

     this.gameObject.transform.localScale = newLocalScale; //Set the new local scale
    //Now the gameObject has the requested global scale

I executed this 100,000 times and it took: 0.06 seconds.

The other method above (with setting the transform to null, then re-assigning the parent) took 0.42 seconds, which is 7 times longer than my code.

I haven’t yet tried your code.
99% of the time when I want to set a scale, it is done by setting the local scale. Very rare that I care about the global scale

I tried your code. I could not get it to work.
This is what I have:

public void SetToGlobal(Transform trans, Vector3 desiredScale)
   {   
     var m = trans.worldToLocalMatrix;
     m.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
     trans.localScale = m.MultiplyPoint(desiredScale);   
   }
   
   public void SetToGlobal(Transform trans, bool bSetScaleOnGlobalAxes, Vector3 desiredScale)
   {
     var m = trans.worldToLocalMatrix;
     if(bSetScaleOnGlobalAxes)
     {
       m.SetColumn(0, new Vector4(m.GetColumn(0).magnitude, 0f));
       m.SetColumn(1, new Vector4(0f, m.GetColumn(1).magnitude));
       m.SetColumn(2, new Vector4(0f, 0f, m.GetColumn(2).magnitude));
     }
     m.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
     trans.localScale = m.MultiplyPoint(desiredScale);
   }

void test()
{
Vector3 desiredGlobalScale = new Vector3(3.0f, 3.0f, 3.0f);

     Debug.Log ("1: Global Scale Before: "+ this.gameObject.transform.lossyScale.ToString());
     SetToGlobal(this.gameObject.transform, desiredGlobalScale);
     Debug.Log ("2: Global Scale After: "+ this.gameObject.transform.lossyScale.ToString());
}

The console outputs for both functions are:
1: Global Scale Before: (20.0, 54.0, 112.0)
2: Global Scale After: (1.5, 1.0, 0.8)

Using both functions, the result global scale is not the desiredGlobalScale. Maybe I’m doing something wrong.

Here is my hierarchy:
ObjectA: local scale = 2,3,4
ObjectB: local scale = 5,6,7
ObjectC: local scale = 2,3,4
GLOBAL SCALE of C: 20, 54, 112

(All positions and rotations are 0)
The code executes on ObectC transform

The global scale of the object before executing the code is a multiplication of local scales up the hierarchy.
I’m not sure how it arrived at the final global scale of (1.5, 1.0, 0.8) after executing your code.
Maybe i am doing something wrong?
j

You know, I forgot to include that use case of the object having a none one scale when using this.

But that’s easy enough to deal with. Just set its scale to ‘one’ before getting its matrix.

        public void SetToGlobal(Transform trans, bool bSetScale)
        {
            if (bSetScale)
            {
                trans.position = Position;
                trans.rotation = Rotation;
                trans.localScale = Vector3.one;
                var m = trans.worldToLocalMatrix;
                m.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
                trans.localScale = m.MultiplyPoint(Scale);
            }
            else
            {
                trans.position = Position;
                trans.rotation = Rotation;
            }
        }
2 Likes

Ok, I tried it again after adding trans.localScale = Vector3.one;, it works now.
It is 5 times faster than my solution, which means it 35x faster than using the first code solution posted here.
I have to read up more on what exactly a Matrix4x4 is and how to use them. Up until now I’ve never needed to use them.
This is interesting.
Thanks.

1 Like

Matrices are used to transformations in linear algebra.

It’s a set of rows and columns in a grid that defines the alterations to some value to transform it between 2 different spaces.

In the case of 3d, you get a 4x4 matrix (4 rows by 4 columns) which can represent scale, position, rotation, and skew.

Column 1 is the x axis
Column 2 is the y axis
Column 3 is the z axis
Column 4 is the translation value

The direction of the various axes is the rotation. If those axes are not adjacent to one another then there is skew. And the magnitudes of the axes are the scale.

And the arithmetic operations of them allow you to conjoin matrices. You can multiply 1 matrix to another to create a new matrix that is the both combined. So if you take the matrices of every parent and multiply them together, you get the total transformation all the way down to the child. And you can invert it to get the opposite, like division.

And this is all localToWorldMatrix and worldToLocalMatrix is. It’s the combined matrix of all parents with its self.

6 Likes

For any searching who land here. A simpler solution is to get the global scale up to the parent using the lossy scale call. Then scale your desired scale by that. This is basically what ArachnidAnimal was doing but just 3 or so lines.

// Get lossy scale up to parent.
Vector3 scaleFactor = transform.parent.lossyScale;
//Determine what the new scale local scale should be
Vector3 newLocalScale = new Vector3 (
_minimumLossyScale.x / scaleFactor.x,
_minimumLossyScale.y / scaleFactor.y,
 _minimumLossyScale.z / scaleFactor.z
);
 transform.localScale = newLocalScale;
4 Likes