Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Scale from a point other than pivot (almost working code...)

Discussion in 'Scripting' started by HiddenMonk, Aug 8, 2018.

  1. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I am trying to basically have a ScaleAround method, similar to the transform.RotateAround method, where you can scale from a point other than the transforms pivot.
    I know you can use parent gameobjects and what not, but for reasons I would like for it to be done without them.

    The code I have right now seems to work except its a little inaccurate when the object is a child of a parent that has rotation and non uniform scale. I think this is due to transform.lossyScale not being accurate in those cases, which I think means I would need to get into Matrix4x4 and what not, which I have tried and failed.

    Code (CSharp):
    1.  
    2.     public void ScaleAround(Transform target, Vector3 worldPivot, Vector3 newScale)
    3.     {
    4.         //Seemed to work, except when under a parent that has a non uniform scale and rotation it was a bit off.
    5.         //This might be due to transform.lossyScale not being accurate under those conditions, or possibly something else is wrong...
    6.         //Maybe things can work if we can find a way to convert the "newPosition = ..." line to use Matrix4x4 for possibly more scale accuracy.
    7.         //However, I have tried and tried and have no idea how to do that kind of math =/
    8.  
    9.         Vector3 localOffset = target.InverseTransformPoint(worldPivot);
    10.  
    11.         Vector3 localScale = target.localScale;
    12.         Vector3 scaleRatio = new Vector3(SafeDivide(newScale.x, localScale.x), SafeDivide(newScale.y, localScale.y), SafeDivide(newScale.z, localScale.z));
    13.         Vector3 scaledLocalOffset = localOffset;
    14.         scaledLocalOffset.Scale(scaleRatio);
    15.         Vector3 newPosition = target.rotation * Vector3.Scale(localOffset - scaledLocalOffset, target.lossyScale) + target.position;
    16.  
    17.         target.localScale = newScale;
    18.         target.position = newPosition;
    19.     }
    20.  
    21.     float SafeDivide(float value, float divider)
    22.     {
    23.         if(divider == 0) return 0;
    24.         return value / divider;
    25.     }
     
  2. Scabbage

    Scabbage

    Joined:
    Dec 11, 2014
    Posts:
    268
    You can eliminate lossyScale from it by just working in local space the whole time:
    Code (csharp):
    1.  
    2. public void ScaleAround(Transform target, Vector3 worldPivot, Vector3 newScale)
    3. {
    4.      Vector3 localOffset = target.InverseTransformPoint(worldPivot);
    5.      Vector3 localScale = target.localScale;
    6.      Vector3 scaleRatio = new Vector3(SafeDivide(newScale.x, localScale.x), SafeDivide(newScale.y, localScale.y), SafeDivide(newScale.z, localScale.z));
    7.      Vector3 scaledLocalOffset = localOffset;
    8.      scaledLocalOffset.Scale(scaleRatio);
    9.  
    10.      target.localScale = newScale;
    11.      target.localPosition = scaledLocalOffset;
    12. }
    13.  
    I'm not at my pc though so I haven't tested it.
     
  3. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    If only it were that easy =(
    It doesnt work, not even for basic non skewed objects.
     
  4. Scabbage

    Scabbage

    Joined:
    Dec 11, 2014
    Posts:
    268
    I just wrote this script in the editor, it appears to work as far as I can tell:
    Code (csharp):
    1.  public class Resizer : MonoBehaviour {
    2.  
    3.     public Transform scalePoint;
    4.     public Vector3 rescale;
    5.     public bool scale = false;
    6.     public bool smoothScale = false;
    7.  
    8.     // Use this for initialization
    9.     void Start () {
    10.        
    11.     }
    12.    
    13.     // Update is called once per frame
    14.     void Update () {
    15.         if (scale) {
    16.             if(!smoothScale)
    17.                 scale = false;
    18.             ScaleAround (transform, scalePoint.position, rescale);
    19.         }
    20.     }
    21.  
    22.     static void ScaleAround(Transform transform, Vector3 worldPos, Vector3 newScale)
    23.     {
    24.         Vector3 localScalePos = transform.InverseTransformPoint (worldPos);
    25.         Vector3 scaleVector = transform.localPosition - localScalePos;
    26.         Vector3 oldScale = transform.localScale;
    27.         Vector3 scaleRatio = Div (newScale, oldScale);
    28.         print (scaleRatio);
    29.         transform.localScale = newScale;
    30.         transform.localPosition = Scale (scaleVector, scaleRatio) + localScalePos;
    31.     }
    32.  
    33.     static Vector3 Scale(Vector3 a, Vector3 b)
    34.     {
    35.         return new Vector3 (a.x * b.x, a.y * b.y, a.z * b.z);
    36.     }
    37.  
    38.     static Vector3 Div(Vector3 a, Vector3 b)
    39.     {
    40.         return new Vector3 (b.x == 0f ? 0 : a.x / b.x, b.y == 0f ? 0 : a.y / b.y, b.z == 0f ? 0 : a.z / b.z);
    41.     }
    42. }
    I stuck it on a cube and set the cube as a child of an empty gameobject. The scalePoint global var is another empty object. It looks like it works even with offsets and parent rotation/scale. If smoothScale and scale are true then it will constantly rescale the object. If smoothScale is false then it will only rescale when you click "scale" in the editor.
     
  5. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    Doesnt seem to work.
    This is what I am seeing.
    In the video I have a gameobject set at the origin (0,0,0) and a cube parented under it set to Y position -0.5 so that the gameobject parent can act as its pivot point right on top of it.
    I than have a cube with no parent just set to Y -0.5 and a gameobject at the origin that will be the pivot that we place in your script.
    Scaling both by 5 in the Y axis shows different results.
    The gameobject that was a parent of the cube showed normal results, which is the cube looks as if its being scaled downward while its top stays where it is touching the gameobject pivot.
    The cube using your script shows it is moving away from the pivot as it scales.

    Using my code gave the correct expected results, but that seems to rely on lossyScale, which I think is the issue =(
     
    Last edited: Aug 8, 2018
  6. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    What is weird is it seems like unitys own scaling might also be inaccurate.
    It seems in the unity editor, scaling a skewed object in one go compared to scaling it little by little with multiple clicks causes different results.
    Here is a video showing what I mean

    Here is the accuracy the code in my original post gave for the skewed object. Its a little off.
    ScaleAround.png

    So maybe a little inaccuracy for skewed objects wont matter too much =/
    (might be a lot inaccurate depending on skew, though usually seems not too much).
    I would still like to at least have the same accuracy/results as unity as the scale in one go though, so things are consistent.

    Edit-
    It seems there might actually be 2 types of scaling unity does.

    1 - Scale from a point (Such as creating a transform to be your pivot point, making your object a child to that pivot transform, and then only scaling the pivot transform)
    2 - Scale offset from a point (Such as the editors Center mode instead of Pivot mode. The Center is the offset from the pivot point)

    I would think that scaling offset from a point would just be the same as moving your point to the offset and then scaling, but that doesnt seem to be the case, so I guess they handle scaling from the Center differently than scaling from a point (usually the pivot point).

    So that might be why my attempts keep failing, since I keep getting results similar to the #1 scaling type (Scale from a point), when I was trying to get the #2 type (Scale offset from point).

    It seems to do scale type #1 you would only need this code.
    Code (CSharp):
    1.  
    2.     public void ScaleAround3(Transform target, Vector3 worldPivot, Vector3 newScale)
    3.     {
    4.         Vector3 localOffset = target.InverseTransformPoint(worldPivot);
    5.  
    6.         Vector3 localScale = target.localScale;
    7.         Vector3 scaleRatio = new Vector3(SafeDivide(newScale.x, localScale.x), SafeDivide(newScale.y, localScale.y), SafeDivide(newScale.z, localScale.z));
    8.         Vector3 scaledLocalOffset = Vector3.Scale(localOffset, scaleRatio);
    9.  
    10.         Vector3 newPosition = target.TransformPoint(localOffset - scaledLocalOffset);
    11.  
    12.         target.localScale = newScale;
    13.         target.position = newPosition;
    14.     }
    To do it manually with matrices you can do this
    Code (CSharp):
    1.  
    2.     public void ScaleAround(Transform target, Vector3 worldPivot, Vector3 newScale)
    3.     {
    4.         Matrix4x4 childMatrix = Matrix4x4.TRS(-target.InverseTransformPoint(worldPivot), Quaternion.identity, Vector3.one);
    5.  
    6.         Matrix4x4 parentMatrix = Matrix4x4.identity;
    7.         if(target.parent != null)
    8.         {
    9.             parentMatrix = (Matrix4x4.TRS(target.parent.InverseTransformPoint(worldPivot), target.localRotation, newScale));
    10.         }else{
    11.             parentMatrix = (Matrix4x4.TRS(worldPivot, target.localRotation, newScale));
    12.         }
    13.  
    14.         Matrix4x4 totalMatrix = Matrix4x4.identity;
    15.         if(target.parent != null)
    16.         {
    17.             //totalMatrix = GetAbsoluteTransformMatrix(target.parent); //This seems to return the same as target.parent.localToWorldMatrix
    18.             totalMatrix = target.parent.localToWorldMatrix;
    19.         }
    20.  
    21.         totalMatrix = totalMatrix * parentMatrix;
    22.         totalMatrix = totalMatrix * childMatrix;
    23.  
    24.         Vector3 newPosition = MatrixToPosition(totalMatrix);
    25.  
    26.         //Might get NaNs if scales are 0 or something...
    27.         if(float.IsNaN(newPosition.x)) newPosition.x = target.position.x;
    28.         if(float.IsNaN(newPosition.y)) newPosition.y = target.position.y;
    29.         if(float.IsNaN(newPosition.z)) newPosition.z = target.position.z;
    30.  
    31.         target.localScale = newScale;
    32.         target.position = newPosition;
    33.     }
    34.  
    35.     Matrix4x4 GetAbsoluteTransformMatrix(Transform trans)
    36.     {
    37.         Matrix4x4 localMatrix = Matrix4x4.TRS(trans.localPosition, trans.localRotation, trans.localScale);
    38.  
    39.         if(trans.parent != null)
    40.         {
    41.             return GetAbsoluteTransformMatrix(trans.parent) * localMatrix;
    42.         }
    43.  
    44.         return localMatrix;
    45.     }
    46.  
    47.  

    To do scale type #2 you would need my original post code, which my code can get inaccurate due to lossyScale being inaccurate at times.
     
    Last edited: Aug 10, 2018
  7. Scabbage

    Scabbage

    Joined:
    Dec 11, 2014
    Posts:
    268
    The script assumes the cube is the child of some other object. The correct behaviour happens when it has a root object at (0, -0.5, 0) and the cube is the child of that object.