Mono vs DOTS quaternion issues

Hey all. I am having an issue rotating my camera in ECS. I’ve created a pretty simple piece of code to demonstrate the issue. The code that executes in MonoBehaviour form works as expected. However, the code that executes in ECS seems to produce a gimbal lock. I have tried all variations of quaternion.Euler, but the results remain the same.
Code

using Unity.Entities;
using UnityEngine;
using Unity.Burst;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Transforms;

namespace Assets.Scripts.Test
{
    public class CameraRotation : MonoBehaviour
    {
        public float Speed = 1f;

        public Vector3 TargetEuler = new Vector3(30f, 180f, 0f);

        public void Update()
        {
            var cameraEuler = this.transform.rotation.eulerAngles;
            var interpolatedEuler = Vector3.Lerp(cameraEuler, TargetEuler, Time.deltaTime * Speed);
            var offsetEuler = interpolatedEuler - cameraEuler;
            var rotationInfluence = Quaternion.Euler(offsetEuler);
            this.transform.rotation = Quaternion.LookRotation(this.transform.rotation * rotationInfluence * Vector3.forward, Vector3.up);
        }
    }
  
    public class CameraRotationBaker : Baker<CameraRotation>
    {
        public override void Bake(CameraRotation authoring)
        {
            this.AddComponent(GetEntity(authoring, TransformUsageFlags.Dynamic), new RotateCameraData
            {
                Speed = authoring.Speed,
                TargetEuler = math.radians(authoring.TargetEuler)
            });
        }
    }

    public struct RotateCameraData : IComponentData
    {
        public float Speed;
        public float3 TargetEuler;
    }
  
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    [BurstCompile]
    public partial struct CameraBehaviorSystem : ISystem
    {
        private EntityQuery _query;

        public void OnCreate(ref SystemState state)
        {
            using (var builder = new EntityQueryBuilder(Allocator.Temp)
                       .WithAllRW<RotateCameraData>()
                       .WithAllRW<LocalTransform>())
            {
                _query = builder.Build(ref state);
            }
        }
      
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {

            CameraUpdateJob cameraUpdateJob = new CameraUpdateJob
            {
                DeltaTime = state.WorldUnmanaged.Time.DeltaTime,
            };
            state.Dependency = cameraUpdateJob.Schedule(_query, state.Dependency);
        }
    }

    [BurstCompile]
    public partial struct CameraUpdateJob : IJobEntity
    {
        public float DeltaTime;
      
        public void Execute(ref LocalTransform camera, ref RotateCameraData rotateCameraData)
        {
            // With eulers
            // camera.Rotation.eulerAngles(out float3 cameraEuler);
            // var interpolatedEuler = math.lerp(cameraEuler, rotateCameraData.TargetEuler, DeltaTime * rotateCameraData.Speed);
            // var offsetEuler = interpolatedEuler - cameraEuler;
            // var rotationInfluence = quaternion.Euler(offsetEuler);
          
            //With mostly quaternions
            var interpolatedRotation = math.slerp(camera.Rotation, quaternion.Euler(rotateCameraData.TargetEuler), DeltaTime * rotateCameraData.Speed);
            var offsetRotation = math.mul(interpolatedRotation, math.inverse(camera.Rotation));
            var rotationInfluence = offsetRotation;
          
          
            camera.Rotation = quaternion.LookRotationSafe(math.mul(math.mul(camera.Rotation, rotationInfluence), math.forward()), math.up());
        }
    }
}

The commented-out code was my initial attempt. The uncommented code below that is me trying it while minimizing the use of eulers. Anyone run into this issue or know how I can go about fixing it?

The easiest solution is avoid conversion of Quaternions into quaternions and vice versa.
If you need Quaternions - use Mathf. If you need quaternions - use math.
Both Quaternions and quaternion can be used in jobs and even burst compiled since they’re structs.
For something like camera (which is usually low quantity entity), it shouldn’t really matter performance wise.

Proper solution is to avoid eulers after authoring phase.
Use directions to figure out what and where should point. Its easier than figuring out angles.
Or alternatively, “add” quaternions by storing initial quaternion / rotation, and a separate angle per axis.

1 Like

That’s definitely not what’s going on in the code. However, you did inspire me to convert the euler to a quaternion at bake time instead of at runtime. Still getting a gimbal lock. Must be related to LookRotation somehow.

Gimbal locks are caused by using angles instead of quaternions.
Its just how mathematics works. LookRotation works a bit differently for the math lib as well.

For example, if passed forward direction matches passed up axis - you’ll get a completely messed up rotation.
Where as non math-lib variant would work just fine (as it is caught instead of being silently processed).

Sorry to revisit this, but it’s becoming a bit of an issue.

Here’s my updated code. I’ve tried to make this as one-to-one as possible and in a fresh project to eliminate any potential contamination.
Code

using System.Runtime.CompilerServices;
using Unity.Entities;
using UnityEngine;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Transforms;

namespace Assets.Scripts.Test
{
    public class CameraRotation : MonoBehaviour
    {
        public float Speed = 1f;

        public Vector3 TargetEuler = new Vector3(30f, 180f, 0f);

        public void Update()
        {
            var rotateCameraData = new RotateCameraData
            {
                Speed = this.Speed,
                TargetEulerRadians = math.radians(this.TargetEuler),
            };
            var deltaTime = Time.deltaTime;

            var cameraRotation = this.transform.rotation;
            var cameraEuler = math.radians(cameraRotation.eulerAngles);
          
            #region identical code
            var interpolatedEuler = math.lerp(cameraEuler, rotateCameraData.TargetEulerRadians,
                deltaTime * rotateCameraData.Speed);
            var offsetEuler = interpolatedEuler - cameraEuler;
            var rotationInfluence = quaternion.Euler(offsetEuler);

            var newRotation =
                quaternion.LookRotation(math.mul(math.mul(cameraRotation, rotationInfluence), Vector3.forward),
                    Vector3.up);
            #endregion
          
            this.transform.rotation = newRotation;
        }
    }
  
    public class CameraRotationBaker : Baker<CameraRotation>
    {
        public override void Bake(CameraRotation authoring)
        {
            this.AddComponent(GetEntity(authoring, TransformUsageFlags.Dynamic), new RotateCameraData
            {
                Speed = authoring.Speed,
                TargetEulerRadians = math.radians(authoring.TargetEuler),
            });
        }
    }

    public struct RotateCameraData : IComponentData
    {
        public float Speed;
        public float3 TargetEulerRadians;
    }
  
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    public partial struct CameraBehaviorSystem : ISystem
    {
        private EntityQuery _query;

        public void OnCreate(ref SystemState state)
        {
            using (var builder = new EntityQueryBuilder(Allocator.Temp)
                       .WithAllRW<RotateCameraData>()
                       .WithAllRW<LocalToWorld>()
                        .WithAllRW<LocalTransform>())
            {
                _query = builder.Build(ref state);
            }
        }
      
        public void OnUpdate(ref SystemState state)
        {

            CameraUpdateJob cameraUpdateJob = new CameraUpdateJob
            {
                deltaTime = state.WorldUnmanaged.Time.DeltaTime,
            };
            state.Dependency = cameraUpdateJob.Schedule(_query, state.Dependency);
        }
    }

    public partial struct CameraUpdateJob : IJobEntity
    {
        public float deltaTime;

        public void Execute(ref LocalTransform camera, ref RotateCameraData rotateCameraData)
        {
            var cameraRotation = camera.Rotation;
            var cameraEuler = cameraRotation.eulerAngles();
          
            #region identical code
            var interpolatedEuler = math.lerp(cameraEuler, rotateCameraData.TargetEulerRadians,
                deltaTime * rotateCameraData.Speed);
            var offsetEuler = interpolatedEuler - cameraEuler;
            var rotationInfluence = quaternion.Euler(offsetEuler);

            var newRotation =
                quaternion.LookRotation(math.mul(math.mul(cameraRotation, rotationInfluence), Vector3.forward),
                    Vector3.up);
            #endregion

            camera.Rotation = newRotation;
        }
    }
  
  
}

public static class Extensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static float3 eulerAngles(in this quaternion q) {
        float3 angles;
        // roll (x-axis rotation)
        double sinr_cosp = 2 * (q.value.w * q.value.x + q.value.y * q.value.z);
        double cosr_cosp = 1 - 2 * (q.value.x * q.value.x + q.value.y * q.value.y);
        angles.x = (float)math.atan2(sinr_cosp, cosr_cosp);
        // pitch (y-axis rotation)
        double sinp = 2 * (q.value.w * q.value.y - q.value.z * q.value.x);
        if (math.abs(sinp) >= 1)
            angles.y = (float)(math.abs(math.PI / 2) * math.sign(sinp));
        else
            angles.y = (float)math.asin(sinp);
        // yaw (z-axis rotation)
        double siny_cosp = 2 * (q.value.w * q.value.z + q.value.x * q.value.y);
        double cosy_cosp = 1 - 2 * (q.value.y * q.value.y + q.value.z * q.value.z);
        angles.z = (float)math.atan2(siny_cosp, cosy_cosp);
        return angles;
    }

}

I attached animated gifs to observe the behavior in Mono vs ECS.

Does anyone have an idea of what’s going on and how to fix it while still using Eulers (they are important for specific kinds of interpolation I intend on using). My intent is to layer behaviors like this and weigh them, but I can’t get past applying the rotation.

9449438--1326377--CameraRotationECS.gif
9449438--1326380--CameraRotationMono.gif

Issue was the eulerAngles algorithm. Thanks goes to @DreamingImLatios for helping me figure out the issue.

A correct version of the eulerAngles algorithm is here:
https://stackoverflow.com/questions/12088610/conversion-between-euler-quaternion-like-in-unity3d-engine/12122899#12122899

As a side note, the LookRotation and LookRotationSafe extensions suffer from gimbal locks as well. Recommendation is to use quaternion.RotateX, etc.