Issues with simple rotational conversions

I am having some problems with rotation conversions between local/world in a system. I am curious if anyone is able to identify what mistakes I am making.

I have a simple system that I am iterating through a few points at a very basic level. I can’t seem to get the rotation to convert properly back into a local space and create the intended result even though I have a few conversion equations that seem to be working. So I must assume it is an inconsistency in my knowledge of quaternion notation and/or operation.

I’ve tested the sequence of calculations in non-ecs and it is working fine. At a high level it is:

Vector3 pos = worldPoints[j].transform.position;
                    Vector3 toLastPoint = worldPoints[worldPoints.Length - 1].transform.position - pos;
                    Vector3 toTarget = target- pos;

                    Quaternion rotation= worldPoints[j].transform.rotation;

                    Quaternion targetRotation = Quaternion.FromToRotation(toLastPoint, toTarget) * pointRotation;

                    if (w >= 1) worldPoints[j].transform.rotation = targetRotation;
                    else worldPoints[j].transform.rotation = Quaternion.Lerp(pointRotation, targetRotation, w);

Of course it is worth noting that these positions and rotations are in world space.

When doing this with entities, the quaternion from the Rotation component and the float3 from the Translation are given in localSpace. I have tried both converting them to world (preferred solution) as well as converting everything to local. Neither seem to have the same result and again, I assume it is something wrong with the way I am handling the quaternions.

with ECS I am doing the following, with the translation and rotation taken from the input for the job they are in.

float3 ePos= EntityManager.GetComponentData<Translation>(e).Value;
LocalToWorld e1LTW = EntityManager.GetComponentData<LocalToWorld>(e);
float3 ePosWorld = GetEntityWorldPosition(e1LTW, ePos);
float3 toLastPoint = ePos2 - ePos;
float3 toTarget = localTarget - ePosWorld;

quaternion eRot = EntityManager.GetComponentData<Rotation>(e).Value;
                                    quaternion eRotWorld = GetWorldRotation(e1LTW, eRot);

quaternion targetRotation = math.mul(FromToRotation(toLastPoint, toTarget), eRotWorld);
quaternion targetRotationLocal = GetLocalRotation(e1LTW, eRotations[j]);
                                    entityManager.SetComponentData<Rotation>(e, new Rotation { Value = targetRotationLocal });

with the conversion as:

private static float3 GetEntityWorldPosition(LocalToWorld ltw, float3 t)
        {
            float4 pos = new float4(t, 1);
            float3 worldPos= math.mul(ltw.Value, pos).xyz;
            return worldPos;
        }

        private static float3 GetEntityLocalPosition(LocalToWorld ltw, float3 worldPosition)
        {
            float4 pos = new float4(worldPosition, 1);
            float4x4 worldToLocal = math.inverse(ltw.Value);
            float3 localPos = math.mul(worldToLocal, pos).xyz;
            return localPos;
        }

        public static quaternion GetWorldRotation(LocalToWorld ltw, quaternion q)
        {
            return math.mul(quaternion.LookRotationSafe(ltw.Forward, ltw.Up), q.value);
        }

        public static quaternion GetLocalRotation(LocalToWorld ltw, quaternion q)
        {
            var invWorld = math.inverse(quaternion.LookRotationSafe(ltw.Forward, ltw.Up));
            var result = math.mul(invWorld, q.value);

            return result;
        }

For the non-ecs portion the points line up as intended, and for the ecs, it keeps moving every frame and it is recalculating every frame.

Is there any issues with the quaternion conversions between world and local? Is there something wrong with my process of taking local position/rotation, converting to world, calculating and adding a new rotation, converting back to local, and assigning the new quaternion to the rotation?

Any help is greatly appreciated. Thank you.

Simplest way to debug this would be to draw rays from both expected and result quaternions step by step.
Same comes for points.

On the first glance, don’t use mul for ltw, use math.transform / math.rotate.
That should prevent invalid .w, casts, and simplify things a bit.

If you’re doing similar in ECS:

if (w >= 1) worldPoints[j].transform.rotation = targetRotation;
                    else worldPoints[j].transform.rotation = Quaternion.Lerp(pointRotation, targetRotation, w);

Note percentages do not clamp in 0…1 ranges.

1 Like

Thanks. While going through and checking / comparing results between two identically placed systems, the part that they become different in the calculations are at:

Quaternion.FromToRotation(toLastBone, toTarget);

I revisited the code I had for this and checked some posts for other implementations. I found a post with some help from @andrew-lukasik and @Bunny83 : https://answers.unity.com/questions/1750909/unitymathematics-equivalent-to-quaternionfromtorot.html
so thank you for that.

and it seems to work the same as the Quaternion.FromToRotation so it seems I am on the right track.

1 Like

In ecs there doesn’t seem to be a quaternion.lerp, so I am using .slerp. I will make sure to clamp as I didn’t know it didn’t clamp, so thanks I appreciate that comment.
Slerp is fine in my use case but is there any reason they don’t have lerp for quaternions?

There’s nlerp in math:

/// <summary>Returns the result of a normalized linear interpolation between two quaternions q1 and a2 using an interpolation parameter t.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static quaternion nlerp(quaternion q1, quaternion q2, float t)
        {
            float dt = dot(q1, q2);
            if(dt < 0.0f)
            {
                q2.value = -q2.value;
            }

            return normalize(quaternion(lerp(q1.value, q2.value, t)));
        }

        /// <summary>Returns the result of a spherical interpolation between two quaternions q1 and a2 using an interpolation parameter t.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static quaternion slerp(quaternion q1, quaternion q2, float t)
        {
            float dt = dot(q1, q2);
            if (dt < 0.0f)
            {
                dt = -dt;
                q2.value = -q2.value;
            }

            if (dt < 0.9995f)
            {
                float angle = acos(dt);
                float s = rsqrt(1.0f - dt * dt);    // 1.0f / sin(angle)
                float w1 = sin(angle * (1.0f - t)) * s;
                float w2 = sin(angle * t) * s;
                return quaternion(q1.value * w1 + q2.value * w2);
            }
            else
            {
                // if the angle is small, use linear interpolation
                return nlerp(q1, q2, t);
            }
        }

Probably because quaternions values when interpolated directly could produce invalid values, and as a result - has to be normalized. Plus its vectorized this way.

1 Like

Awesome, thank you so much! Wasn’t aware of nLerp.

Hi. Looking at your ecs variable naming choices I sense some possible confusion there, please let me clear some things out:

float3 localPosition = EntityManager.GetComponentData<Translation>(entity).Value;
quaternion localRotation = EntityManager.GetComponentData<Rotation>(entity).Value;
LocalToWorld transform = EntityManager.GetComponentData<LocalToWorld>(entity);
float3 worldPosition = transform.Position;
quaternion worldRotation = transform.Rotation;

LocalToWorld is your final transformation matrix for given entity. Both Translation (local translation) and Rotation (local rotation) are inputs for TransformSystemGroup’s systems to calculate LocalToWorld.

2 Likes

My first iteration when translating this code into dots would probably be something like this (not tested at all, just an initial sketch):

struct JobData : IComponentData
{
    public float3 lastPoint;
    public float3 target;
    public float w;
    public quaternion pointRotation;
}

Entities
    .WithName("some_kind_of_rotation_job")
    .WithChangeFilter<JobData,Rotation>()
    .ForEach( ( ref Rotation localRotation , in JobData data , in LocalToWorld transform ) =>
    {
        float3 worldPosition = transform.Position;
        float3 toLastPoint = data.lastPoint - worldPosition;
        float3 toTarget = data.target - worldPosition;
        quaternion targetRotation = math.mul( FromToRotation(toLastPoint,toTarget) , data.pointRotation );
        quaternion newWorldRotation = math.slerp( data.pointRotation , targetRotation , math.saturate(data.w) );

        localRotation.Value = math.mul( newWorldRotation , math.inverse(transform.Rotation) );// idk
    })
    .WithBurst().ScheduleParallel();
1 Like