Search Unity

LocalToWorld.Rotation affected by entity Scale

Discussion in 'Entity Component System' started by ElliotB, May 3, 2020.

  1. ElliotB

    ElliotB

    Joined:
    Aug 11, 2013
    Posts:
    289
    Hi,

    I've noticed some behavior I did not expect with `LocalToWorld` - it seems that scale affects the `LocalToWorld.Rotation`. For example, consider the following setup:

    Two entities as below:
    • Neither has a parent.
    • I set the Rotation in the Unity inspector to (90, 0, 0) for both.
    • I set the scale of the first to (0.1,0.1,0.1) and the second to (1,1,1):
    • Both have position (0,0,0)

    upload_2020-5-3_15-16-31.png

    Now, add the following simple system to output the result of `LocalToWorld.Rotation` to the console:

    Code (CSharp):
    1.  
    2. using Unity.Entities;
    3. using Unity.Transforms;
    4. using UnityEngine;
    5.  
    6. public class DebugRotationSystem : SystemBase
    7. {
    8.     protected override void OnUpdate()
    9.     {
    10.         Entities.ForEach(
    11.             (Entity e,
    12.             ref Rotation rotation,
    13.             ref LocalToWorld localToWorld) =>
    14.             {
    15.                 Debug.Log(string.Format("Rotation={0}, localToWorld.Rotation={1}", rotation.Value, localToWorld.Rotation));
    16.             }).WithoutBurst().Schedule();
    17.     }
    18. }
    19.  
    Running this, I get the following in the console:
    Code (csharp):
    1. Rotation=quaternion(0.7071068f, 0f, 0f, 0.7071068f), localToWorld.Rotation=quaternion(0.9838699f, 0f, 0f, 0.1788855f)
    2. Rotation=quaternion(0.7071068f, 0f, 0f, 0.7071068f), localToWorld.Rotation=quaternion(0.7071067f, 0f, 0f, 0.7071067f)

    So you can see that:
    • Both entities have the same value of the Rotation component
    • Both entities return different quaternions for localToWorld.Rotation
    This seems counter-intuitive to me, is this a bug?

    For the record I'm using Entities 0.9.1 preview.15
     

    Attached Files:

  2. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    110
    I believe that it is due to the fact that conversion system does not add Scale component to the Entities with scale of 1, 1, 1. That way LocalToWorld gets created differently:

    Code (CSharp):
    1.  
    2.                     else if (hasTranslation && !hasAnyScale)
    3.                     {
    4.                         for (int i = 0; i < count; i++)
    5.                         {
    6.                             var rotation = chunkRotations[i].Value;
    7.                             var translation = chunkTranslations[i].Value;
    8.                      
    9.                             chunkLocalToWorld[i] = new LocalToWorld
    10.                             {
    11.                                 Value = new float4x4(rotation,translation)
    12.                             };
    13.                         }
    14.                     }
    15.                     // 11
    16.                     else if (hasTranslation && hasAnyScale)
    17.                     {
    18.                         for (int i = 0; i < count; i++)
    19.                         {
    20.                             var rotation = chunkRotations[i].Value;
    21.                             var translation = chunkTranslations[i].Value;
    22.                             var scale = hasNonUniformScale ? float4x4.Scale(chunkNonUniformScales[i].Value) : ( hasScale ? float4x4.Scale(new float3(chunkScales[i].Value)) : chunkCompositeScales[i].Value );
    23.                          
    24.                             chunkLocalToWorld[i] = new LocalToWorld
    25.                             {
    26.                                 Value = math.mul(new float4x4(rotation, translation),scale)
    27.                             };
    28.                         }
    29.                     }  
    This then results in different quaternion generation when you call
    localToWorld.Rotation
    . It's been a while since I studied quaternions so take this with the grain of salt, but practically being a 3 dimensional complex number, you can get single rotation by more then one compositions of queaternion elements.
     
  3. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Bumping this because I kept scratching my head over a rotation problem I was having and I ended up here too.

    It really looks like LocalToWorld.Rotation returns an incorrect value if the scale of the object is not uniform, and I don't know how to get the correct rotation. Multiply it by inverse scale?

    Either way, this feels like either a bug, or a misleading API

    (Entities 0.16)
     
    Last edited: Jan 1, 2021
  4. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Have you tried adding NonUniformScale? Also look at the results of conversion and see what it does in your case.
     
  5. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Since I encountered this issue I have always just used
    math.LookRotationSafe(ltw.Forward, ltw.Up)
    .
    I believe it is overkill most of the time, but it hasn't been the main bottleneck in any of my projects that I am aware of, so I've kept using it.
     
    rivFox, LandonF, milos85miki and 2 others like this.
  6. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    @KwahuNashoba is right, scale is multiplied with rotation matrix in LocalToWorld. You can use Unity.Physics.Math.DecomposeRigidBodyOrientation(localToWorld.Value) or quaternion.LookRotationSafe(fwd, up) to get actual rotation.
     
    LandonF likes this.
  7. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    163
    I spent an embarrassingly long time debugging this issue. I really hope this issue gets fixed before 1.0.

    I believe the problem is that the LocalToWorld.Rotation property is calling the quaternion constructor that takes a float4x4. This constructor requires the passed in 4x4 matrix to be an orthonormal matrix. When the LocalToWorld is scaled, the basis vectors of the transform are no longer unit-length which means it is not orthonormal.

    As others have mentioned, one fix is to call quaternion.LookRotationSafe. However, as
    @PhilSA suggested, another option is to first multiply the LocalToWorld by the inverse scale before calling that quaternion constructor that takes a float4x4. I haven't profiled which one is faster, but a quick look through the code makes it look like it would be faster.

    Here are a few extensions methods that might be helpful to others. This allows you to replace LocalToWorld.Rotation with LocalToWorld.Rotation(). This also adds LocalToWorld.Scale() and LocalToWorld.NonUniformScale().

    Code (CSharp):
    1. public static class LocalToWorldExtensions {
    2.  
    3.     public static float3 NonUniformScale(this LocalToWorld ltw)
    4.     {
    5.         float sx = math.length(ltw.Right);
    6.         float sy = math.length(ltw.Up);
    7.         float sz = math.length(ltw.Forward);
    8.         return new float3(sx, sy, sz);
    9.     }
    10.  
    11.     // Note: This method assumes that the LocalToWorld has a uniform scale being applied
    12.     // to it. If this is not true, use NonUniformScale above instead.
    13.     public static float Scale(this LocalToWorld ltw) => math.length(ltw.Right);
    14.  
    15.     // The Rotation property in LocalToWorld is wrong when the LTW is scaled.
    16.     // See: https://forum.unity.com/threads/localtoworld-rotation-affected-by-entity-scale.882133/
    17.     public static quaternion Rotation(this LocalToWorld ltw) =>
    18.         quaternion.LookRotationSafe(ltw.Forward, ltw.Up);
    19.  
    20.     // Alternative version
    21.     public static quaternion Rotation2(this LocalToWorld ltw)
    22.     {
    23.         float3 scale = ltw.NonUniformScale();
    24.         float4x4 unscaled = math.mul(ltw.Value, float4x4.Scale(1 / scale));
    25.         return new quaternion(unscaled);
    26.     }
    27.  
    28. }
    I also noticed that this bug is present in the CopyTransformToGameObjectSystem. Once again, that quaternion constructor is being used to retrieve the rotation of a LocalToWorld float4x4 matrix. This is easily fixed by using one of the other methods above. However, as others have pointed out on the forum, the CopyTransformToGameObject doesn't work with scale simply because it never writes to the transform's scale in the CopyTransformToGameObjectSystem. Unfortunately, this can't easily be fixed as TransformAccess does not provide a way to set the world scale (lossyScale). I think the only way to fix this to support all use cases would be to write a non-bursted system that writes directly to the transform component on the game object instead of going through TransformAccess.

    However, I assume that most people that are using CopyTransformToGameObject are doing so on top-level game objects since this is meant to track the world position of an entity. Here is a forked version of the CopyTransformToGameObject functionality that should work correctly with scale, but only when syncing a top-level (has no parent) game object:

    Code (CSharp):
    1. using System;
    2. using Unity.Entities;
    3.  
    4. [GenerateAuthoringComponent]
    5. [Serializable]
    6. public struct CopyScaledTransformToGameObject : IComponentData { }
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Transforms;
    5. using UnityEngine;
    6. using UnityEngine.Jobs;
    7.  
    8. [ExecuteAlways]
    9. [UpdateInGroup(typeof(TransformSystemGroup))]
    10. [UpdateAfter(typeof(LocalToParentSystem))]
    11. public partial class CopyScaledTransformToGameObjectSystem : SystemBase
    12. {
    13.     [BurstCompile]
    14.     struct CopyTransforms : IJobParallelForTransform
    15.     {
    16.         [DeallocateOnJobCompletion]
    17.         [ReadOnly] public NativeArray<LocalToWorld> LocalToWorlds;
    18.  
    19.         public void Execute(int index, TransformAccess transform)
    20.         {
    21.             var value = LocalToWorlds[index];
    22.             transform.localPosition = value.Position;
    23.             transform.localRotation = value.Rotation();
    24.             transform.localScale = value.NonUniformScale();
    25.         }
    26.     }
    27.  
    28.     EntityQuery m_TransformGroup;
    29.  
    30.     protected override void OnCreate()
    31.     {
    32.         m_TransformGroup = GetEntityQuery(ComponentType.ReadOnly(typeof(CopyTransformToGameObject)), ComponentType.ReadOnly<LocalToWorld>(), typeof(UnityEngine.Transform));
    33.     }
    34.  
    35.     protected override void OnUpdate()
    36.     {
    37.         var transforms = m_TransformGroup.GetTransformAccessArray();
    38.         var copyTransformsJob = new CopyTransforms
    39.         {
    40.             LocalToWorlds = m_TransformGroup.ToComponentDataArrayAsync<LocalToWorld>(Allocator.TempJob, out var dependency),
    41.         };
    42.  
    43.         Dependency = copyTransformsJob.Schedule(transforms, dependency);
    44.     }
    45. }
    I haven't tested this myself as I don't currently need it, but I was helping someone else who ran into the same problem. Let me know if there are problems and I can edit the post.
     
    Last edited: Aug 1, 2022
    nath1339, Krooq and Occuros like this.
  8. Krooq

    Krooq

    Joined:
    Jan 30, 2013
    Posts:
    196
    I tried this and it does indeed work! Thanks Scott!
     
  9. PeppeJ2

    PeppeJ2

    Joined:
    May 13, 2014
    Posts:
    43
    Bump, did this get resolved for 1.0? Still on 0.51 and am using the workaround.