diff --git a/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs b/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs
new file mode 100644
index 0000000..2598f60
--- /dev/null
+++ b/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs
@@ -0,0 +1,114 @@
+using UnityEngine;
+
+namespace UnityGyroscope.Parallax
+{
+ ///
+ /// 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.
+ ///
+ 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() ?? cube2D.AddComponent();
+ 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 { target2D };
+ targets2DField.SetValue(rotator2D, targetsList2D);
+ }
+
+ // Setup 3D component (Gimbal Lock free)
+ var rotator3D = cube3D.GetComponent() ?? cube3D.AddComponent();
+ 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 { 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!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs.meta b/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs.meta
new file mode 100644
index 0000000..515b5b9
--- /dev/null
+++ b/Unity-Package/Assets/Demo/GimbalLockComparisonDemo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8d8811e245074a9987be4acd9c8fb478
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D.meta b/Unity-Package/Assets/root/Scripts/Mover3D.meta
new file mode 100644
index 0000000..789b117
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8fc587ffaf8640fc82b88fd00f84c60b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs
new file mode 100644
index 0000000..0c45bad
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs
@@ -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 targets = new List();
+
+ 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; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs.meta b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs.meta
new file mode 100644
index 0000000..507409c
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c8ff68d95adb444588bdabe9975a320e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs
new file mode 100644
index 0000000..ea66fc2
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs
@@ -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
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs.meta b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs.meta
new file mode 100644
index 0000000..7514654
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DAttitude.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f96ba47be2ba4efba9f59e380e1773cc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs
new file mode 100644
index 0000000..fb6fb73
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs
@@ -0,0 +1,67 @@
+using UnityEngine;
+using Gyroscope = UnityGyroscope.Manager.Gyroscope;
+
+namespace UnityGyroscope.Parallax
+{
+ public class GyroMover3DGravity : GyroMover3D
+ {
+ Vector3 gravity;
+ Vector3 originGravity;
+
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+ originGravity = Gyroscope.Instance.Gravity.Value;
+ }
+
+ protected override void OnDisable()
+ {
+ if (!Gyroscope.Instance.HasGyroscope)
+ return;
+
+ base.OnDisable();
+ }
+
+ protected override void Subscribe()
+ {
+ Gyroscope.Instance.SubscribeGravity();
+ }
+
+ protected override void Unsubscribe()
+ {
+ Gyroscope.Instance.UnsubscribeGravity();
+ }
+
+ protected override void OnUpdatePrepare()
+ {
+ gravity = (Gyroscope.Instance.Gravity.Value - originGravity).normalized;
+ }
+
+ protected override void ApplyTransform(GyroTarget target, Vector2 offsetMultiplier)
+ {
+ Vector3 targetPosition = new Vector3(
+ target.OriginalLocalPosition.x + Mathf.Lerp(
+ -target.maxOffset.x * offsetMultiplier.x,
+ target.maxOffset.x * offsetMultiplier.x,
+ (target.inverseX ? -gravity.x : gravity.x) + 0.5f
+ ),
+ target.OriginalLocalPosition.y + Mathf.Lerp(
+ -target.maxOffset.y * offsetMultiplier.y,
+ target.maxOffset.y * offsetMultiplier.y,
+ (target.inverseY ? -gravity.y : gravity.y) + 0.5f
+ ),
+ target.OriginalLocalPosition.z + Mathf.Lerp(
+ -target.maxOffset.z,
+ target.maxOffset.z,
+ (target.inverseZ ? -gravity.z : gravity.z) + 0.5f
+ )
+ );
+
+ target.target.localPosition = Vector3.Lerp(
+ target.target.localPosition,
+ targetPosition,
+ Time.deltaTime * target.speed * speedMultiplier
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs.meta b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs.meta
new file mode 100644
index 0000000..ca413bd
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Mover3D/GyroMover3DGravity.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b286297ddb5c4e989eebb9ae22e08304
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/QUICK_START_3D.cs b/Unity-Package/Assets/root/Scripts/QUICK_START_3D.cs
new file mode 100644
index 0000000..a4c7bde
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/QUICK_START_3D.cs
@@ -0,0 +1,49 @@
+/*
+=== UNITY GYROSCOPE PARALLAX 3D COMPONENTS ===
+Solution to Gimbal Lock Issue
+
+PROBLEM SOLVED:
+The original 2D components suffered from "Gimbal Lock" causing:
+- Incorrect starting positions when holding phone vertically vs horizontally
+- Jerky rotation at certain angles
+- Loss of one degree of freedom during rotation
+
+SOLUTION:
+New 3D components use Quaternion arithmetic throughout to avoid Gimbal Lock
+
+QUICK START GUIDE:
+1. Replace your existing 2D components:
+ - GyroRotator2DAttitude → GyroRotator3DAttitude
+ - GyroRotator2DGravity → GyroRotator3DGravity
+ - GyroMover2DAttitude → GyroMover3DAttitude
+ - GyroMover2DGravity → GyroMover3DGravity
+
+2. Update your maxOffset settings:
+ - Old: Vector2(x, y)
+ - New: Vector3(x, y, z) - now supports Z-axis!
+
+3. Configure the new inverseZ property if needed
+
+4. Enjoy smooth, consistent gyroscope behavior!
+
+AVAILABLE COMPONENTS:
+▶ GyroRotator3DAttitude - Smooth attitude-based rotation using Quaternions
+▶ GyroRotator3DGravity - Gravity-based rotation with 3D support
+▶ GyroMover3DAttitude - Position movement based on device attitude
+▶ GyroMover3DGravity - Position movement based on gravity vector
+
+TECHNICAL BENEFITS:
+✅ No more Gimbal Lock issues
+✅ Smooth rotation at all angles
+✅ Consistent behavior regardless of phone orientation
+✅ Full 3D support including Z-axis
+✅ Better interpolation using Quaternion.Slerp
+✅ Same familiar API as 2D components
+
+For detailed documentation, see:
+- README_3D_Components.md
+- GimbalLockComparisonDemo.cs for side-by-side comparison
+- GimbalLockMathValidation.cs for mathematical proof
+
+Happy parallaxing! 🎮📱
+*/
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/README_3D_Components.md b/Unity-Package/Assets/root/Scripts/README_3D_Components.md
new file mode 100644
index 0000000..5469df2
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/README_3D_Components.md
@@ -0,0 +1,75 @@
+# Gyroscope Parallax 3D Components
+
+This directory contains the new 3D gyroscope components that resolve the **Gimbal Lock** problem found in the 2D components by using Quaternion-based calculations instead of Euler angles.
+
+## The Problem: Gimbal Lock
+
+The original 2D components suffer from Gimbal Lock, which occurs when using Euler angles for rotations. This causes:
+- Loss of one degree of freedom when certain angles align
+- Unpredictable rotation behavior when the phone orientation matches specific angles
+- Incorrect starting positions when holding the phone vertically vs. horizontally
+
+## The Solution: Quaternion-Based 3D Components
+
+The new 3D components resolve this by:
+- **Using Quaternion arithmetic** throughout the calculation process
+- **Storing original rotations as Quaternions** instead of Euler angles
+- **Only converting to Euler** when necessary for constraint application
+- **Using Quaternion.Slerp** for smooth interpolation instead of Quaternion.Lerp
+- **Supporting full 3D transformations** including Z-axis rotation/movement
+
+## Available Components
+
+### Rotator Components
+- **`GyroRotator3DAttitude`** - Rotates objects based on device attitude using Quaternions
+- **`GyroRotator3DGravity`** - Rotates objects based on gravity vector using Quaternions
+
+### Mover Components
+- **`GyroMover3DAttitude`** - Moves objects based on device attitude
+- **`GyroMover3DGravity`** - Moves objects based on gravity vector
+
+## Usage
+
+1. Add any of the 3D components to a GameObject instead of the 2D versions
+2. Configure the **targets** list with the objects you want to affect
+3. Adjust **maxOffset** values for X, Y, and Z axes (3D components support Z-axis)
+4. Set **inverseX**, **inverseY**, **inverseZ** to control direction
+5. Adjust **speed** and global **speedMultiplier** for responsiveness
+
+## Key Improvements Over 2D Components
+
+| Feature | 2D Components | 3D Components |
+|---------|---------------|---------------|
+| Gimbal Lock | ❌ Suffers from it | ✅ Resolved |
+| Rotation Method | Euler angles | Quaternions |
+| Interpolation | Quaternion.Lerp | Quaternion.Slerp |
+| Z-axis Support | ❌ Limited | ✅ Full support |
+| Phone Orientation | ❌ Inconsistent | ✅ Consistent |
+| Smooth Rotation | ⚠️ Can be jerky | ✅ Always smooth |
+
+## Migration from 2D to 3D
+
+To migrate from existing 2D components:
+
+1. **Replace the component**: Change `GyroRotator2DAttitude` → `GyroRotator3DAttitude`
+2. **Update maxOffset**: Change from `Vector2` to `Vector3` (add Z value)
+3. **Add inverseZ**: New boolean property for Z-axis control
+4. **Test and adjust**: The behavior should be smoother and more consistent
+
+## Technical Details
+
+The 3D components avoid Gimbal Lock by:
+
+```csharp
+// OLD 2D approach (Gimbal Lock prone):
+var euler = attitude.eulerAngles;
+// Math operations on euler angles...
+target.localRotation = Quaternion.Euler(toX, toY, toZ);
+
+// NEW 3D approach (Gimbal Lock free):
+gyroRotation = Quaternion.Inverse(originGyroRotation) * attitude;
+// Quaternion operations...
+target.localRotation = Quaternion.Slerp(current, target, time);
+```
+
+This ensures smooth, predictable rotation regardless of device orientation.
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/README_3D_Components.md.meta b/Unity-Package/Assets/root/Scripts/README_3D_Components.md.meta
new file mode 100644
index 0000000..464219f
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/README_3D_Components.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 60605f1a082945b393d7cc8a2cf375a4
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D.meta b/Unity-Package/Assets/root/Scripts/Rotator3D.meta
new file mode 100644
index 0000000..12ba13c
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 80daeaaabe0f40bfb35e42fde7399b98
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs
new file mode 100644
index 0000000..2125287
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs
@@ -0,0 +1,77 @@
+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 GyroRotator3D : MonoBehaviour
+ {
+ public float speedMultiplier = 1;
+ public Vector2 offsetMultiplier = Vector2.one;
+
+#if ODIN_INSPECTOR
+ [Required]
+#endif
+ [SerializeField] List targets = new List();
+
+ protected virtual void OnEnable()
+ {
+ if (!Gyroscope.Instance.HasGyroscope)
+ return;
+
+ foreach (var target in targets)
+ target.OriginalLocalRotation = target.target.localRotation;
+
+ Subscribe();
+ }
+
+ protected virtual void OnDisable()
+ {
+ if (!Gyroscope.Instance.HasGyroscope)
+ return;
+
+ Unsubscribe();
+
+ foreach (var target in targets)
+ target.target.localRotation = target.OriginalLocalRotation;
+ }
+
+ 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(30, 30, 30);
+ public Axes axes = Axes.XY;
+
+ public Quaternion OriginalLocalRotation { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs.meta b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs.meta
new file mode 100644
index 0000000..1100bfb
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0e868f7a7e9745bcafa80a766bfa09a9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs
new file mode 100644
index 0000000..27dad97
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs
@@ -0,0 +1,95 @@
+using System.Collections;
+using UnityEngine;
+using Gyroscope = UnityGyroscope.Manager.Gyroscope;
+
+namespace UnityGyroscope.Parallax
+{
+ public class GyroRotator3DAttitude : GyroRotator3D
+ {
+ 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();
+ }
+
+ protected override void OnUpdatePrepare()
+ {
+ // Calculate the delta rotation from the origin quaternion
+ // This gives us the relative rotation change since initialization
+ gyroRotation = Quaternion.Inverse(originGyroRotation) * Gyroscope.Instance.Attitude.Value;
+ }
+
+ protected override void ApplyTransform(GyroTarget target, Vector2 offsetMultiplier)
+ {
+ // Extract euler angles only for applying constraints
+ 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;
+
+ // Apply offset constraints and inversion
+ float constrainedX = Mathf.Clamp(
+ (target.inverseX ? -gyroEuler.x : gyroEuler.x),
+ -target.maxOffset.x * offsetMultiplier.x,
+ target.maxOffset.x * offsetMultiplier.x
+ );
+
+ float constrainedY = Mathf.Clamp(
+ (target.inverseY ? -gyroEuler.y : gyroEuler.y),
+ -target.maxOffset.y * offsetMultiplier.y,
+ target.maxOffset.y * offsetMultiplier.y
+ );
+
+ float constrainedZ = Mathf.Clamp(
+ (target.inverseZ ? -gyroEuler.z : gyroEuler.z),
+ -target.maxOffset.z,
+ target.maxOffset.z
+ );
+
+ // Create the constrained rotation as a quaternion
+ Quaternion constrainedRotation = Quaternion.Euler(constrainedX, constrainedY, constrainedZ);
+
+ // Apply the rotation to the original rotation
+ Quaternion targetRotation = target.OriginalLocalRotation * constrainedRotation;
+
+ // Smoothly interpolate to the target rotation
+ target.target.localRotation = Quaternion.Slerp(
+ target.target.localRotation,
+ targetRotation,
+ Time.deltaTime * target.speed * speedMultiplier
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs.meta b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs.meta
new file mode 100644
index 0000000..f6a1e3a
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DAttitude.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 32e9077874354c89893f6058c456076d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs
new file mode 100644
index 0000000..4d66889
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs
@@ -0,0 +1,73 @@
+using UnityEngine;
+using Gyroscope = UnityGyroscope.Manager.Gyroscope;
+
+namespace UnityGyroscope.Parallax
+{
+ public class GyroRotator3DGravity : GyroRotator3D
+ {
+ Vector3 gravity;
+ Vector3 originGravity;
+
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+ originGravity = Gyroscope.Instance.Gravity.Value;
+ }
+
+ protected override void OnDisable()
+ {
+ if (!Gyroscope.Instance.HasGyroscope)
+ return;
+
+ base.OnDisable();
+ }
+
+ protected override void Subscribe()
+ {
+ Gyroscope.Instance.SubscribeGravity();
+ }
+
+ protected override void Unsubscribe()
+ {
+ Gyroscope.Instance.UnsubscribeGravity();
+ }
+
+ protected override void OnUpdatePrepare()
+ {
+ gravity = (Gyroscope.Instance.Gravity.Value - originGravity).normalized;
+ }
+
+ protected override void ApplyTransform(GyroTarget target, Vector2 offsetMultiplier)
+ {
+ // Convert gravity vector to rotation angles with constraints
+ float rotationX = Mathf.Lerp(
+ -target.maxOffset.x * offsetMultiplier.x,
+ target.maxOffset.x * offsetMultiplier.x,
+ (target.inverseX ? -gravity.x : gravity.x) + 0.5f
+ );
+
+ float rotationY = Mathf.Lerp(
+ -target.maxOffset.y * offsetMultiplier.y,
+ target.maxOffset.y * offsetMultiplier.y,
+ (target.inverseY ? -gravity.y : gravity.y) + 0.5f
+ );
+
+ float rotationZ = Mathf.Lerp(
+ -target.maxOffset.z,
+ target.maxOffset.z,
+ (target.inverseZ ? -gravity.z : gravity.z) + 0.5f
+ );
+
+ // Create target rotation as quaternion
+ Quaternion gravityRotation = Quaternion.Euler(rotationX, rotationY, rotationZ);
+ Quaternion targetRotation = target.OriginalLocalRotation * gravityRotation;
+
+ // Apply smooth rotation using Slerp
+ target.target.localRotation = Quaternion.Slerp(
+ target.target.localRotation,
+ targetRotation,
+ Time.deltaTime * target.speed * speedMultiplier
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs.meta b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs.meta
new file mode 100644
index 0000000..71fd448
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Rotator3D/GyroRotator3DGravity.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5f7b2469daf04451b6f8d693b7ce0053
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs b/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs
new file mode 100644
index 0000000..b01292a
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs
@@ -0,0 +1,110 @@
+using UnityEngine;
+
+namespace UnityGyroscope.Parallax
+{
+ ///
+ /// Mathematical validation of the 3D component approach.
+ /// This script demonstrates why Quaternions resolve Gimbal Lock.
+ ///
+ public class GimbalLockMathValidation : MonoBehaviour
+ {
+ [Header("Gimbal Lock Demonstration")]
+ [SerializeField] bool runValidation = false;
+
+ [ContextMenu("Run Gimbal Lock Validation")]
+ void RunValidation()
+ {
+ Debug.Log("=== Gimbal Lock Mathematical Validation ===");
+
+ // Demonstrate problematic Euler angle scenario
+ TestEulerAngleGimbalLock();
+
+ // Demonstrate Quaternion solution
+ TestQuaternionSolution();
+
+ Debug.Log("=== Validation Complete ===");
+ }
+
+ void TestEulerAngleGimbalLock()
+ {
+ Debug.Log("\n--- Testing Euler Angle Approach (2D Components) ---");
+
+ // Simulate a problematic rotation where Gimbal Lock occurs
+ // This happens when Y rotation is ±90 degrees
+ Quaternion attitude1 = Quaternion.Euler(45, 89, 30);
+ Quaternion attitude2 = Quaternion.Euler(45, 91, 30);
+
+ // OLD 2D approach - convert to Euler and do math
+ Vector3 euler1 = attitude1.eulerAngles;
+ Vector3 euler2 = attitude2.eulerAngles;
+
+ // Normalize to -180 to 180 (as 2D components do)
+ euler1.x = (euler1.x + 180f) % 360 - 180;
+ euler1.y = (euler1.y + 180f) % 360 - 180;
+ euler2.x = (euler2.x + 180f) % 360 - 180;
+ euler2.y = (euler2.y + 180f) % 360 - 180;
+
+ Vector3 deltaEuler = euler2 - euler1;
+
+ Debug.Log($"Attitude 1 Euler: {euler1}");
+ Debug.Log($"Attitude 2 Euler: {euler2}");
+ Debug.Log($"Delta Euler: {deltaEuler}");
+ Debug.Log($"Delta magnitude: {deltaEuler.magnitude}");
+
+ // Show the problem: small attitude change can cause large Euler delta
+ if (deltaEuler.magnitude > 10f)
+ {
+ Debug.LogWarning("⚠️ Gimbal Lock detected! Small attitude change caused large Euler delta");
+ }
+ }
+
+ void TestQuaternionSolution()
+ {
+ Debug.Log("\n--- Testing Quaternion Approach (3D Components) ---");
+
+ // Same problematic rotations
+ Quaternion attitude1 = Quaternion.Euler(45, 89, 30);
+ Quaternion attitude2 = Quaternion.Euler(45, 91, 30);
+
+ // NEW 3D approach - work directly with Quaternions
+ Quaternion deltaQuat = Quaternion.Inverse(attitude1) * attitude2;
+
+ // Extract angle of rotation
+ float angle;
+ Vector3 axis;
+ deltaQuat.ToAngleAxis(out angle, out axis);
+
+ Debug.Log($"Delta Quaternion: {deltaQuat}");
+ Debug.Log($"Rotation angle: {angle} degrees");
+ Debug.Log($"Rotation axis: {axis}");
+
+ // Quaternions handle this smoothly
+ Debug.Log("✅ Quaternion approach handles this rotation smoothly!");
+
+ // Demonstrate smooth interpolation
+ Quaternion lerped = Quaternion.Slerp(attitude1, attitude2, 0.5f);
+ Debug.Log($"Smooth interpolation result: {lerped.eulerAngles}");
+ }
+
+ void Update()
+ {
+ if (runValidation)
+ {
+ runValidation = false;
+ RunValidation();
+ }
+ }
+
+ [ContextMenu("Explain Solution")]
+ void ExplainSolution()
+ {
+ Debug.Log("=== How 3D Components Solve Gimbal Lock ===");
+ Debug.Log("1. Store original rotation as Quaternion (not Euler)");
+ Debug.Log("2. Calculate gyro delta as Quaternion multiplication");
+ Debug.Log("3. Apply constraints only when converting to final rotation");
+ Debug.Log("4. Use Quaternion.Slerp for smooth interpolation");
+ Debug.Log("5. Avoid Euler angle arithmetic in the critical path");
+ Debug.Log("\nResult: Smooth rotation regardless of device orientation!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs.meta b/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs.meta
new file mode 100644
index 0000000..37f7fb4
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Utils/GimbalLockMathValidation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1c9aea956cd04d0688e9817db4e874e4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs b/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs
new file mode 100644
index 0000000..a69458b
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs
@@ -0,0 +1,57 @@
+using UnityEngine;
+
+namespace UnityGyroscope.Parallax
+{
+ ///
+ /// Demonstration script that shows the key differences between 2D and 3D gyroscope components.
+ ///
+ /// The main improvement in 3D components:
+ /// - Uses Quaternion operations throughout to avoid Gimbal Lock
+ /// - Stores and works with original rotation as Quaternion
+ /// - Only converts to Euler when necessary for constraint application
+ /// - Uses Quaternion.Slerp for smooth interpolation
+ ///
+ /// This resolves the Gimbal Lock issue that occurs in 2D components when using Euler angles.
+ ///
+ public class Gyro3DQuaternionDemo : MonoBehaviour
+ {
+ [Header("Demonstration of Quaternion vs Euler Approach")]
+ [SerializeField] bool showDifference = true;
+
+ void Start()
+ {
+ if (showDifference)
+ {
+ Debug.Log("=== Gyroscope Parallax 3D Components ===");
+ Debug.Log("Key improvements over 2D components:");
+ Debug.Log("1. Uses Quaternion arithmetic to avoid Gimbal Lock");
+ Debug.Log("2. Stores original rotation as Quaternion");
+ Debug.Log("3. Only converts to Euler for constraint application");
+ Debug.Log("4. Uses Quaternion.Slerp for smooth interpolation");
+ Debug.Log("5. Supports full 3D rotation including Z-axis");
+ }
+ }
+
+ ///
+ /// Example of how the 3D components avoid Gimbal Lock by using Quaternions
+ ///
+ public void ExplainQuaternionApproach()
+ {
+ // Old 2D approach (Gimbal Lock prone):
+ // 1. Convert Quaternion to Euler angles
+ // 2. Do math with Euler angles
+ // 3. Apply constraints to Euler angles
+ // 4. Convert back to Quaternion with Quaternion.Euler()
+ // 5. This can cause Gimbal Lock when certain angles align
+
+ // New 3D approach (Gimbal Lock free):
+ // 1. Work directly with Quaternions when possible
+ // 2. Store original rotation as Quaternion
+ // 3. Calculate delta rotation as Quaternion
+ // 4. Only convert to Euler for constraint application
+ // 5. Apply final rotation using Quaternion.Slerp
+
+ Debug.Log("3D Components use Quaternion operations to avoid Gimbal Lock!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs.meta b/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs.meta
new file mode 100644
index 0000000..317cf9b
--- /dev/null
+++ b/Unity-Package/Assets/root/Scripts/Utils/Gyro3DQuaternionDemo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 99714e602516419aa075dc4260c529d6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: f3c512bd213abde419ed19595756ec2d, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs b/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs
new file mode 100644
index 0000000..5136105
--- /dev/null
+++ b/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs
@@ -0,0 +1,97 @@
+using System.Collections;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+using UnityGyroscope.Parallax;
+
+namespace Extensions.Unity.Gyroscope.Parallax.Tests
+{
+ public class Gyro3DComponentsTest
+ {
+ private GameObject testObject;
+
+ [SetUp]
+ public void SetUp()
+ {
+ testObject = new GameObject("TestObject");
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (testObject != null)
+ Object.DestroyImmediate(testObject);
+ }
+
+ [Test]
+ public void GyroRotator3DAttitude_CanBeAddedToGameObject()
+ {
+ // Test that the component can be added without errors
+ var component = testObject.AddComponent();
+ Assert.IsNotNull(component);
+ }
+
+ [Test]
+ public void GyroRotator3DGravity_CanBeAddedToGameObject()
+ {
+ var component = testObject.AddComponent();
+ Assert.IsNotNull(component);
+ }
+
+ [Test]
+ public void GyroMover3DAttitude_CanBeAddedToGameObject()
+ {
+ var component = testObject.AddComponent();
+ Assert.IsNotNull(component);
+ }
+
+ [Test]
+ public void GyroMover3DGravity_CanBeAddedToGameObject()
+ {
+ var component = testObject.AddComponent();
+ Assert.IsNotNull(component);
+ }
+
+ [Test]
+ public void GyroRotator3D_TargetStoresOriginalRotation()
+ {
+ var targetObj = new GameObject("Target");
+ var originalRotation = Quaternion.Euler(45, 30, 15);
+ targetObj.transform.localRotation = originalRotation;
+
+ var component = testObject.AddComponent();
+ var target = new GyroRotator3D.GyroTarget
+ {
+ target = targetObj.transform
+ };
+
+ // Manually set the original rotation (simulating OnEnable)
+ target.OriginalLocalRotation = targetObj.transform.localRotation;
+
+ Assert.AreEqual(originalRotation, target.OriginalLocalRotation);
+
+ Object.DestroyImmediate(targetObj);
+ }
+
+ [Test]
+ public void GyroMover3D_TargetStoresOriginalPosition()
+ {
+ var targetObj = new GameObject("Target");
+ var originalPosition = new Vector3(10, 20, 30);
+ targetObj.transform.localPosition = originalPosition;
+
+ var component = testObject.AddComponent();
+ var target = new GyroMover3D.GyroTarget
+ {
+ target = targetObj.transform
+ };
+
+ // Manually set the original position (simulating OnEnable)
+ target.OriginalLocalPosition = targetObj.transform.localPosition;
+
+ Assert.AreEqual(originalPosition, target.OriginalLocalPosition);
+
+ Object.DestroyImmediate(targetObj);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs.meta b/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs.meta
new file mode 100644
index 0000000..faeaa4a
--- /dev/null
+++ b/Unity-Package/Assets/root/Tests/Editor/Gyro3DComponentsTest.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ae718905ce3a4f9585ab95e90403139b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: