0
\$\begingroup\$

According to the Unity docs for Transform.lossyScale, this attribute is "The global scale of the object". From what I can tell, this isn't actually an estimate of the scaling aligned with the World's coordinate system, but instead it is aligned to something else - possibly the top-most ancestor of the object in the Scene hierarchy, but it's not very clear.

To see what I mean, create a Grand Parent, Parent and Child object hierarchy in a new scene, and have them all print out their transform.lossyScale vectors in Update(). Set the scaling on the Grant Parent to something like x=10, y=1, z=1. Now run the project and rotate the Grant Parent object within the scene and you'll note that none of the lossy scale vectors actually change when the Grand Parent object rotates. But rotate just the Parent and you'll see that both it and the Child lossy scale vectors do change. It's as if they are both expressed in the Grand Parent's local frame of reference. But why both? Why isn't the Child's lossy scale expressed in the Parent's local frame of reference instead? Perhaps it's the highest object in the hierarchy that is used as the final frame of reference?

None of this seems to suggest that the lossy scale vector is in any way aligned with the World coordinate system.

In terms of what I'm actually trying to achieve - I have a script that creates a CapsuleCollider along the local X axis of a GameObject. I want the collider to be sized such that its overall height ("height" + 2 * "radius") is determined by some parameter of the GameObject ("width"):

public class MyCollider : MonoBehaviour
{
    [SerializeField] private float radius = 0.5f;
    [SerializeField] private float width = 1.0f;

    private CapsuleCollider _collider;

    private void Start()
    {
        _collider = gameObject.AddComponent<CapsuleCollider>();
        _collider.radius = radius;

        //_collider.height = width + 2 * radius * transform.lossyScale.x;  // works when aligned with parent's X axis
        //_collider.height = width + 2 * radius * transform.lossyScale.z;  // works when aligned with parent's Z axis

        _collider.direction = 0;  // X-axis
    }

I want the final CapsuleCollider size to be determined by any scaling on the parent (and perhaps its parents, although a single level will do for now). I've managed to get to the point where my scaling works correctly when the object with this script is in the same frame of reference as the parent object (no rotation!) - see first commented line, using the x component. If I rotate the object 90 degrees to align with the parent's Z axis, I can get away with using the z component. But I can't find a general solution for an arbitrary rotation of my MyCollider object.

I've tried converting lossyScale to local coordinates with transform.InverseTransformDirection(transform.lossyScale) but because lossyScale isn't really in World coordinates, this doesn't work.

So how can I get an estimate or otherwise of final scaling in my object's local reference frame? Any suggestions for a fix or alternate approach?

I am aware that Transform.lossyScale is really only a guess at the final scaling, due to skew caused by rotation. However in my case I'm constrained to a single Parent and therefore only its rotation and the object's own rotation.

I'm also aware that the CapsuleCollider's actual radius can be different to the value specified, and that non-uniform scaling of the collider on an axis will actually do some kind of max(abs()) of the other two axes,

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I think you are using the wrong tool for this job, and you should consider using a Bounds structure instead of trying to infer the bounds from the lossyScale. Also bear in mind that any object or parent scaling applied to the object is also applied to the capsule collider on that object (imperfectly, if you're using non-uniform scales anywhere), so you do not need to multiply the collider's dimensions by those scale factors - they already get applied through the normal transformation hierarchy. \$\endgroup\$
    – DMGregory
    Jul 6, 2021 at 2:30
  • \$\begingroup\$ @DMGregory I will look into a Bounds structure - thank you. I was able to get something working but I had to reverse-engineer the hemispherical radius scaling based on the direct parent's scaling and local rotation. It works in my application where I can guarantee the parent relationship and no scaling above that. \$\endgroup\$
    – davidA
    Jul 7, 2021 at 4:49
  • \$\begingroup\$ If you've solved your problem, consider posting your solution as an Answer below. \$\endgroup\$
    – DMGregory
    Jul 7, 2021 at 4:54
  • \$\begingroup\$ I've considered doing this however it's not really a fix, and it's also not answering my original question about why the lossyScale isn't really global but relative to the top-most parent. Once I get a definitive answer on this I'll write an answer, if nobody else has already. \$\endgroup\$
    – davidA
    Jul 9, 2021 at 23:11

1 Answer 1

1
\$\begingroup\$

I've run into similar issue and this is what I've found:

I am sure you are aware of this (since I am answering), but this thread provides code snippet for how the capsule collider size is actually computed and it indeed uses lossyScale.

So what is lossy scale actually? It's related to the physics engine used in Unity - PhysX. For rigid body simulation it uses colliders with transformation that are represented only by rotation and translation. See the following links for docs:

  1. Geometry
  2. PxTransform Class Reference

If the object has only uniform scale, you don't care about the missing scale, you just take the scales along the hierarchy and apply it to the radius/height. But if there is both a rotation and scale the matrices (and sizes) will never match because of the shear.

To resolve this you take the localToWorldMatrix on your transform and then pure rotation matrix (computed by quaternion multiplication - Matrix4x4.Rotate(transform.parent.localRotation * transform.localRotation)).

If you take matrices and extract the first three columns, you get the vectors representing your X,Y,Z axis in world space. The second matrix is orthonormal (the axes are unit vectors and perpendicular to each other). If you take the length of columns in localToWorldMatrix, it will tell you how the original basis (normal basis (1,0,0), (0,1,0), (0,0,1)) is scaled. If there is shear however, you need these projected onto the orthonormal basis (so that it can be used by PhysX). And that's the lossyScale - world space size of the object, if we use orthonormal basis.

Now on how to add the collider. If you add collider to an object with a mesh, it will automatically be created for you with correct scales applied. If you plan on using your own script with some values, these values should be done in the local coordinate space so you should directly use your radius/height.

But if you want to create a collider, that has height 5 meters in world space on axis X, then you would take the lossyScale (for example (2,1,3)) and set the height of capsule collider to 2.5. I can imagine this could be very useful for assigning colliders to hierarchy objects without renderable since Unity does not handle these cases.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .