Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using UnityEngine;

namespace UnityGyroscope.Parallax
{
/// <summary>
/// Comparison demo script showing the difference between 2D and 3D components.
/// This script can be used to set up a side-by-side comparison in a demo scene.
/// </summary>
public class GimbalLockComparisonDemo : MonoBehaviour
{
[Header("Comparison Setup")]
[SerializeField] GameObject cube2D;
[SerializeField] GameObject cube3D;
[SerializeField] bool autoSetupComponents = true;

[Header("Test Settings")]
[SerializeField] Vector3 maxOffset = new Vector3(30, 30, 30);
[SerializeField] float speed = 5f;

void Start()
{
if (autoSetupComponents)
SetupComparison();
}

void SetupComparison()
{
if (cube2D == null || cube3D == null)
{
Debug.LogWarning("Please assign cube2D and cube3D GameObjects to see the comparison.");
return;
}

// Setup 2D component (prone to Gimbal Lock)
var rotator2D = cube2D.GetComponent<GyroRotator2DAttitude>() ?? cube2D.AddComponent<GyroRotator2DAttitude>();
var target2D = new GyroRotator2D.GyroTarget
{
target = cube2D.transform,
maxOffset = new Vector2(maxOffset.x, maxOffset.y),
speed = speed,
inverseX = true,
inverseY = true
};

// Use reflection to set the targets list since it's private
var targets2DField = typeof(GyroRotator2D).GetField("targets",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (targets2DField != null)
{
var targetsList2D = new System.Collections.Generic.List<GyroRotator2D.GyroTarget> { target2D };
targets2DField.SetValue(rotator2D, targetsList2D);
}

// Setup 3D component (Gimbal Lock free)
var rotator3D = cube3D.GetComponent<GyroRotator3DAttitude>() ?? cube3D.AddComponent<GyroRotator3DAttitude>();
var target3D = new GyroRotator3D.GyroTarget
{
target = cube3D.transform,
maxOffset = maxOffset,
speed = speed,
inverseX = true,
inverseY = true,
inverseZ = false
};

var targets3DField = typeof(GyroRotator3D).GetField("targets",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (targets3DField != null)
{
var targetsList3D = new System.Collections.Generic.List<GyroRotator3D.GyroTarget> { target3D };
targets3DField.SetValue(rotator3D, targetsList3D);
}

// Position cubes for comparison
cube2D.transform.position = new Vector3(-2, 0, 0);
cube3D.transform.position = new Vector3(2, 0, 0);

// Add labels
CreateLabel(cube2D.transform, "2D Component\n(Gimbal Lock)", new Vector3(0, 2, 0));
CreateLabel(cube3D.transform, "3D Component\n(Gimbal Lock Free)", new Vector3(0, 2, 0));

Debug.Log("=== Gimbal Lock Comparison Demo ===");
Debug.Log("Left Cube: 2D Component (may experience Gimbal Lock)");
Debug.Log("Right Cube: 3D Component (Gimbal Lock resistant)");
Debug.Log("Test by rotating your device to extreme angles - the 3D component should remain smooth.");
}

void CreateLabel(Transform parent, string text, Vector3 offset)
{
GameObject labelObject = new GameObject("Label");
labelObject.transform.SetParent(parent);
labelObject.transform.localPosition = offset;

// In a real Unity project, you'd use TextMesh or TextMeshPro here
// For this demo, we'll just log the setup
labelObject.name = $"Label_{text.Replace("\n", "_").Replace(" ", "_")}";
}

[ContextMenu("Explain Gimbal Lock")]
void ExplainGimbalLock()
{
Debug.Log("=== What is Gimbal Lock? ===");
Debug.Log("Gimbal Lock occurs when using Euler angles and two rotation axes align,");
Debug.Log("causing loss of one degree of freedom and unpredictable rotation behavior.");
Debug.Log("");
Debug.Log("In gyroscope applications, this manifests as:");
Debug.Log("- Incorrect starting positions based on phone orientation");
Debug.Log("- Jerky or unpredictable rotation at certain angles");
Debug.Log("- Objects 'snapping' to unexpected orientations");
Debug.Log("");
Debug.Log("The 3D components solve this by using Quaternions instead of Euler angles!");
}
}
}
11 changes: 11 additions & 0 deletions Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Unity-Package/Assets/root/Scripts/Mover3D.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Gyroscope = UnityGyroscope.Manager.Gyroscope;

#if ODIN_INSPECTOR
using Sirenix.OdinInspector;
#endif

namespace UnityGyroscope.Parallax
{
public abstract class GyroMover3D : MonoBehaviour
{
public float speedMultiplier = 1;
public Vector2 offsetMultiplier = Vector2.one;

#if ODIN_INSPECTOR
[Required]
#endif
[SerializeField] List<GyroTarget> targets = new List<GyroTarget>();

protected virtual void OnEnable()
{
if (!Gyroscope.Instance.HasGyroscope)
return;

foreach (var target in targets)
target.OriginalLocalPosition = target.target.localPosition;

Subscribe();
}

protected virtual void OnDisable()
{
if (!Gyroscope.Instance.HasGyroscope)
return;

Unsubscribe();

foreach (var target in targets)
target.target.localPosition = target.OriginalLocalPosition;
}

protected abstract void Subscribe();
protected abstract void Unsubscribe();
protected abstract void OnUpdatePrepare();
protected abstract void ApplyTransform(GyroTarget target, Vector2 offsetMultiplier);

protected virtual void Update()
{
if (!Gyroscope.Instance.HasGyroscope)
return;

OnUpdatePrepare();

foreach (var target in targets)
{
if (target != null)
ApplyTransform(target, offsetMultiplier);
}
}

[Serializable]
public class GyroTarget
{
public Transform target;
public bool inverseX = true;
public bool inverseY = true;
public bool inverseZ = false;
public float speed = 1;
public Vector3 maxOffset = new Vector3(100, 100, 100);

public Vector3 OriginalLocalPosition { get; set; }
}
}
}
11 changes: 11 additions & 0 deletions Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Collections;
using UnityEngine;
using Gyroscope = UnityGyroscope.Manager.Gyroscope;

namespace UnityGyroscope.Parallax
{
public class GyroMover3DAttitude : GyroMover3D
{
Quaternion gyroRotation;
Quaternion originGyroRotation;

protected override void OnEnable()
{
base.OnEnable();

StartCoroutine(InitializeAfterFrame());
}

IEnumerator InitializeAfterFrame()
{
yield return null;
originGyroRotation = Gyroscope.Instance.Attitude.Value;
}

protected override void OnDisable()
{
if (!Gyroscope.Instance.HasGyroscope)
return;

originGyroRotation = Gyroscope.Instance.Attitude.Value;

base.OnDisable();
}

protected override void Subscribe()
{
Gyroscope.Instance.SubscribeAttitude();
}

protected override void Unsubscribe()
{
Gyroscope.Instance.UnsubscribeAttitude();
}

private float RoundInRange(float min, float max, float value)
=> Mathf.Max(min, Mathf.Min(max, value));

protected override void OnUpdatePrepare()
{
// Calculate the delta rotation from the origin quaternion
gyroRotation = Quaternion.Inverse(originGyroRotation) * Gyroscope.Instance.Attitude.Value;
}

protected override void ApplyTransform(GyroTarget target, Vector2 offsetMultiplier)
{
// Extract euler angles for position calculation
Vector3 gyroEuler = gyroRotation.eulerAngles;

// Normalize angles to -180 to 180 range
gyroEuler.x = (gyroEuler.x + 180f) % 360 - 180;
gyroEuler.y = (gyroEuler.y + 180f) % 360 - 180;
gyroEuler.z = (gyroEuler.z + 180f) % 360 - 180;

var maxOffsetX = Mathf.Abs(target.maxOffset.x);
var maxOffsetY = Mathf.Abs(target.maxOffset.y);
var maxOffsetZ = Mathf.Abs(target.maxOffset.z);

Vector3 targetPosition = new Vector3(
target.OriginalLocalPosition.x + RoundInRange(
-maxOffsetX * offsetMultiplier.x,
maxOffsetX * offsetMultiplier.x,
target.inverseX ? -gyroEuler.x : gyroEuler.x
),
target.OriginalLocalPosition.y + RoundInRange(
-maxOffsetY * offsetMultiplier.y,
maxOffsetY * offsetMultiplier.y,
target.inverseY ? -gyroEuler.y : gyroEuler.y
),
target.OriginalLocalPosition.z + RoundInRange(
-maxOffsetZ,
maxOffsetZ,
target.inverseZ ? -gyroEuler.z : gyroEuler.z
)
);

target.target.localPosition = Vector3.Lerp(
target.target.localPosition,
targetPosition,
Time.deltaTime * target.speed * speedMultiplier
);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading