Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Quaternion Rotation to Vector3 Position (Orbit)

Discussion in 'Scripting' started by patrick_murphy_, May 23, 2023.

  1. patrick_murphy_

    patrick_murphy_

    Joined:
    Apr 16, 2017
    Posts:
    24
    Is there perhaps an established pattern for the following?

    I have an anchor object (currently at 0,0,0, to reduce possible attack surface of the issue, but could be elsewhere in the future) that will rotate at arbitrary angles as supplied by a device's gyroscope.

    Surrounding that object, at various distances, are other objects. I would like for the objects to update their position so that they are always positioned relative to the rotation of the central object. You can think of it like a physical sphere, with sticks that radiate out to other smaller spheres. All relative positions of the smaller spheres will be the same over time, and they all orbit in world space around to match the central sphere.

    After digging and experimenting, I have the following 2 classes I've put together:

    This class handles applying gyroscopic rotation to an object.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class GyroscopicRotation : MonoBehaviour
    4. {
    5.     public bool GryoEnabled = false;
    6.  
    7.     public Quaternion rotationalDelta = Quaternion.identity;
    8.     private Quaternion previousAttitude = Quaternion.identity;
    9.     private Quaternion calculatedAttitude = Quaternion.identity;
    10.  
    11.     void Awake()
    12.     {
    13.         Input.gyro.enabled = true; // Android
    14.     }
    15.  
    16.     void Update()
    17.     {
    18.         if(GryoEnabled)
    19.         {
    20.             Debug.Log("Input.gyro.attitude: " + Input.gyro.attitude);
    21.  
    22.             // This if/else tracks delta of rotation and applies to object
    23.             if (previousAttitude == Quaternion.identity)
    24.             {
    25.                 previousAttitude = InterpretGyroscope(Input.gyro.attitude);
    26.             }
    27.             else
    28.             {
    29.                 rotationalDelta = Quaternion.Inverse(previousAttitude) * InterpretGyroscope(Input.gyro.attitude);
    30.                 calculatedAttitude = transform.rotation * rotationalDelta;
    31.                 transform.rotation = calculatedAttitude;
    32.  
    33.                 previousAttitude = InterpretGyroscope(Input.gyro.attitude);
    34.             }
    35.         }
    36.     }
    37.  
    38.     // Necessary for translating left-handed gyroscope to right-handedness in Unity
    39.     private static Quaternion InterpretGyroscope(Quaternion q)
    40.     {
    41.         return new Quaternion(q.x, q.y, -q.z, -q.w);
    42.     }
    43.  
    44.     public void EnableGyro()
    45.     {
    46.         GryoEnabled = true;
    47.     }
    48.  
    49.     public void DisableGyro()
    50.     {
    51.         previousAttitude = Quaternion.identity;
    52.         GryoEnabled = false;
    53.     }
    54. }
    This class handles orbiting any object about the anchor in the first class:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Orbit : MonoBehaviour
    4. {
    5.     public GyroscopicRotation LocusOfRotation;
    6.  
    7.     void Update()
    8.     {
    9.         OrbitPoint(LocusOfRotation.transform.position, LocusOfRotation.rotationalDelta);
    10.     }
    11.  
    12.     private void OrbitPoint(Vector3 pivotPoint, Quaternion rotation)
    13.     {
    14.         rotation.ToAngleAxis(out var angle, out var axis);
    15.  
    16.         //transform.RotateAround(pivotPoint, axis, angle * Time.deltaTime);
    17.         transform.RotateAround(pivotPoint, axis, angle);
    18.     }
    19. }
    20.  
    This works well... on first glance.

    For testing I made a quad as an anchor (which has the first script attached), and have 6 spheres (which all have the second script attached) at various positions. Both of the following images are outside of runtime, simply moving the camera to give an idea of initial testing locations:

    upload_2023-5-23_13-25-46.png

    upload_2023-5-23_13-26-7.png

    When starting, everything seems to orbit correctly, but as I rotate the device along multiple axes, the spheres begin updating strangely. Sometimes they appear to be moving in the opposite direction than intended, for instance if I turn the device 90 degrees on one axis, and 90 degrees on another. However they are keeping their correct distance, and the quad itself is always correctly updating. So I think I am inverting or not inverting angles somewhere?

    Does anyone have any ideas?
     
    Last edited: May 23, 2023
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Not sure what your ultimate solution will be here, but likely just clever parenting with perhaps a few extra parent objects between the root object and orbiting objects.

    I will observe that anything to do with euler angles is unlikely to be helpful to you. Here's why:

    All about Euler angles and rotations, by StarManta:

    https://starmanta.gitbooks.io/unitytipsredux/content/second-question.html

    How to instantly see gimbal lock for yourself:

    https://forum.unity.com/threads/confusing-bug.1165789/#post-7471441
     
  3. patrick_murphy_

    patrick_murphy_

    Joined:
    Apr 16, 2017
    Posts:
    24
    Thanks for the speedy reply. Unfortunately I cannot use the parenting strategy here because this is step one towards further functionality, including axis constraints, clamping, smoothing, etc. So I am going programmatically from the start.

    I don't think that I am using any Euler angles, but if I have a hidden oversight somewhere, I would very much appreciate being shown where.

    Unfortunately your first link is 401 for me so I guess I would need to sign up/in for that, but thank you for the resource.

    This doesn't visually look like gimbal lock when it's in motion, but of course that's a very tricky thing to see in real time in 3D space.

    I'll spawn/destroy debug spheres to track orbits over time, along with Debug.DrawRay() from the anchor, to get an idea of where they begin to diverge.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Parenting can work with code. Nothing says those two are exclusive.

    You can do the parenting offset work yourself of course, but I wouldn't bother unless you need it.

    Set up your scene like so;

    MainRootObject
    CentralSphereBlue
    GreenSphereRotator
    GreenSphere
    RedSphereRotator
    RedSphere
    ... etc


    ALL second-depth objects below MainRootObject would have .localPosition (0,0,0)

    All colored Sphere objects would be offset from their parents appropriately.

    Then spin the "Rotator" Transforms above however you like, around whatever axis.
     
    patrick_murphy_ likes this.
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    To show you parenting AND code, put these two scripts into correctly-named files, point a camera at it and press play and you should have a little solar system whizzling around in a very chaotic way.

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. // @kurtdekker - spinning in code
    6. // to use; drop this on an empty gameobject, point a camera at it
    7.  
    8. public class BusySolarSystem : MonoBehaviour
    9. {
    10.     [Header( "Pluto will always be a planet to me.")]
    11.     public int NumBodies = 10;
    12.  
    13.     void Start ()
    14.     {
    15.         GameObject root = new GameObject( "BusySolarSystem");
    16.  
    17.         for (int i = 0; i < NumBodies; i++)
    18.         {
    19.             string PlanetName = "Planet " + i;
    20.  
    21.             float orbitalDistance = i;
    22.  
    23.             GameObject rotor = new GameObject( PlanetName + " - rotor offset");
    24.             rotor.transform.SetParent( root.transform);
    25.  
    26.             GameObject spinner = new GameObject( PlanetName + " - spinner");
    27.             spinner.transform.SetParent( rotor.transform);
    28.  
    29.             GameObject planet = GameObject.CreatePrimitive( PrimitiveType.Sphere);
    30.             planet.transform.SetParent( spinner.transform);
    31.  
    32.             // axis is always UP, so we go RIGHT
    33.             planet.transform.localPosition = Vector3.right * orbitalDistance;
    34.  
    35.             float size = Random.Range( 0.1f, 0.5f);
    36.             planet.transform.localScale = Vector3.one * size;
    37.  
    38.             SpinOnAxis spinController = spinner.AddComponent<SpinOnAxis>();
    39.             spinController.angle = Random.Range( 0.0f, 360.0f);
    40.             spinController.axis = Vector3.up;
    41.             spinController.rate = Random.Range( 20, 200);
    42.  
    43.             rotor.transform.localRotation = Random.rotation;
    44.         }  
    45.     }
    46. }
    and...

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. // @kurtdekker - spin 'em
    6.  
    7. public class SpinOnAxis : MonoBehaviour
    8. {
    9.     [Header( "Inputs:")]
    10.     public Vector3 axis;
    11.     public float rate;
    12.  
    13.     [Header( "Input and State:")]
    14.     public float angle;
    15.  
    16.     void Update ()
    17.     {
    18.         float step = rate * Time.deltaTime;
    19.  
    20.         angle += step;
    21.  
    22.         angle %= 360;    // keep things near zero
    23.  
    24.         transform.localRotation = Quaternion.AngleAxis( angle, axis);
    25.     }
    26. }
    Screen Shot 2023-05-23 at 12.27.50 PM.png
     
    patrick_murphy_ likes this.
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    I think your error is in applying gyroscope to delta.

    What I would do instead is compute everything in absolute terms. Don't do delta.
    When you're done with the local angle-axis position, then apply the world rotation as specified by gyroscope.

    Simple as that. Also no need for parenting, it's just a compound rotation like you did, but the order of operations matters. I'll make a demo tomorrow if I find time.

    In a nutshell I believe you're hitting an accumulation error because of 32-bit floating point. You're getting the fringe values because the changes are very small, yet you do everything incrementally.
     
  7. patrick_murphy_

    patrick_murphy_

    Joined:
    Apr 16, 2017
    Posts:
    24
    This was neat :)

    I will see if I can apply some of this to my use case, thanks :D
     
  8. patrick_murphy_

    patrick_murphy_

    Joined:
    Apr 16, 2017
    Posts:
    24
    So that is a valid concern, and I can't rule that out, however to get more in depth with further findings...

    1. If I simply twist the phone 90 degrees along the global Z axis from portrait to landscape, all goes well.

    2. If I return to neutral position (phone facing me in portrait) or start a new test from neutral position... if I rotate an upright phone in portrait mode about the Y-axis -90 degrees (turning the phone so that someone to my right at a 4 person dining table can see it)... all goes well.

    3. Now, if when the phone is now facing my right, I then twist the phone 90 degree about the (global) X-axis, meaning it's still facing the same person but is now landscape ...the 4 test spheres on the perimeter of the quad rotate *opposite* of what would be expected.

    As a final step, I twist the phone back to portrait, and then do another -90 degree rotation like in the very first step, such that the phone is now -180 degrees away, standing in portrait mode, facing perfectly away from me. When twisting to landscape here, the perimeter spheres again suddenly rotate correctly.

    There is something about the translation of quaternions here, where when approaching the angle of the X-axis, the proportion of radial twist of another axis inverts to -1, and then slowly reverts again back to 1 when leaving the angle of the X-axis.

    I feel like I am so close to solving this, but will try your and Kurt's suggestions. But could you expand slight on this?

    "When you're done with the local angle-axis position, then apply the world rotation as specified by gyroscope."

    I'm not exactly sure how to directly apply the rotation of the anchor (or the gyroscope itself) to objects at distance.
    I initially tried something more direct and it resulted in the spheres spinning wildly, even controlling for time.
     
    Last edited: May 24, 2023
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    It sounds like you're constantly in the mindset of working with a delta.
    As far as I know gyroscopes, being compass-like, provide you with a current world space rotation of your device.
    And certainly you're grabbing that in your code by doing
    Code (csharp):
    1. Input.gyro.attitude
    But then you're applying your local rotations on top of that, and I think this is where you get lost. I also can't tell what "orbit" means in this context. Are your spheres on circular paths? It seems like you're incrementally building up your orbits from some rather noisy input based on gyroscope. The more I look at it, the more I'm confused.

    I'll go with this description.
    Sticks that radiate out to other smaller spheres sounds good to me. They seem to be in static relation to each other, hanging on this imaginary sphere which itself is orbiting.

    So why not think about this sphere like it's an object?
    First you sort out the local space. Bunch of small spheres hanging around at some distance from the pivot.
    Here's one of them
    Code (csharp):
    1. var spherePos = radius * (Quaternion.Euler(phi, theta, 0f) * Vector3.right);
    Then you apply some rotation
    Code (csharp):
    1. var spherePos = globalRotation * spherePos;
    Then you apply your global gyroscope
    Code (csharp):
    1. var spherePos = Quaternion.Inverse(getGyroAttitude()) * (globalRotation * spherePos);
    2.  
    3. Quaternion getGyroAttitude() {
    4.   var g = Input.gyro.attitude;
    5.   return new Quaternion(g.x, g.y, -g.z, -g.w);
    6. }
    (Edit: redacted the example a bit, first version wasn't entirely truthful.)

    But then you split this up completely to avoid having to compute each position in every frame.
    Instead you arrange your spheres in advance, and here we can avoid that Euler by doing Random.onUnitSphere.
    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public void MyGyroThing : MonoBehaviour {
    4.  
    5.   const int SPHERES = 16;
    6.   const float TAU = 2f * MathF.PI;
    7.  
    8.   [SerializeField] GameObject _prefab;
    9.  
    10.   [SerializeField] float _minDistance;
    11.   [SerializeField] float _maxDistance;
    12.  
    13.   [SerializeField] float _minSize;
    14.   [SerializeField] float _maxSize;
    15.  
    16.   [SerializeField] float _spinningSpeed;
    17.   [SerializeField] Vector2 _spinningAngles; // in degrees
    18.  
    19.   float _curAngleOfSpin;
    20.   Vector3 _spinningAxis;
    21.  
    22.   void Start() {
    23.  
    24.     _spinningAxis = Quaternion.Euler(_spinningAngles.x, _spinningAngles.y, 0f) * Vector3.right;
    25.  
    26.     for(int i = 0; i < SPHERES; i++) {
    27.  
    28.       // instantiate prefabs to match these properties
    29.       var obj = Instantiate(...)
    30.       // obj.transform.localPosition = Random.Range(_minDistance, _maxDistance) * Random.onUnitSphere;
    31.       // obj.transform.localRotation = Quaternion.identity;
    32.       // obj.transform.localScale = Random.Range(_minSize, _maxSize) * Vector3.one;
    33.  
    34.       // and assign them as children of this transform
    35.       obj.transform.SetParent(transform);
    36.  
    37.     }
    38.  
    39.   }
    40.  
    41.   void Update() {
    42.     _curAngleOfSpin = (_curAngleOfSpin + _spinningSpeed * Time.deltaTime) % TAU;
    43.     var spinning = Quaternion.AngleAxis(_curAngleOfSpin, _spinningAxis);
    44.     transform.localRotation = Quaternion.Inverse(gyro) * spinning;
    45.   }
    46.  
    47.   Quaternion _lastGyro = Quaternion.identity;
    48.  
    49.   Quaternion gyro {
    50.     get {
    51.       if(Input.gyro.enabled) {
    52.         var g = Input.gyro.attitude;
    53.         _lastGyro = new Quaternion(g.x, g.y, -g.z, -g.w);
    54.       }
    55.       return _lastGyro;
    56.     }
    57.   }
    58.  
    59. }

    Here I'm still parenting the objects, because it makes the whole thing more elegant in code, but as you can see in the solution above, it's not required.

    Edit2: fixed some typos and added changes explained in post #12.
    Edit3: _curAngleOfSpin is a modular value, which is important to prevent losing floating point precision over time. Also turned gyro into private property
     
    Last edited: May 24, 2023
    patrick_murphy_ likes this.
  10. patrick_murphy_

    patrick_murphy_

    Joined:
    Apr 16, 2017
    Posts:
    24
    Thanks for the thoughtful response! I will check this out today.

    I see how you could be scratching your head over why I went with a delta. I'm using a delta because it's important to poll for instantaneous rotation, with the ability to turn on and off. If I pull raw values then I instead have to save rotation values at turn on/turn off events and manage subtraction of values against the live gyroscope, which initially seems like a bigger headache, to me.

    What I mean by "orbit" is less like a solar system and more like a camera movement in 3D programs. Alt + Left Click + drag in Unity editor, for example.
     
  11. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Ok so my hunch was right. I even wrongly called the rotation of the sphere an "orbit". I think I got you subconsciously.
    I've just noticed an error in the first example
    Code (csharp):
    1. var spherePos = radius * Quaternion.Euler(phi, theta, 0f) * Vector3.right;
    should be
    Code (csharp):
    1. var spherePos = radius * (Quaternion.Euler(phi, theta, 0f) * Vector3.right);
    I apologize if some things don't work right off the bat, I write these from my head.

    The 2nd example in the previous post deals with the gyroscope being disabled, however, it could handle intermittent disabling better.

    For example, you can save the last known value like this
    Code (csharp):
    1. Quaternion _lastGyro = Quaternion.identity;
    2.  
    3. Quaternion getGyroAttitude() {
    4.   if(Input.gyro.enabled) {
    5.     var g = Input.gyro.attitude;
    6.     _lastGyro = new Quaternion(g.x, g.y, -g.z, -g.w);
    7.   }
    8.   return _lastGyro;
    9. }
    This solution isn't necessarily better, depends on your specs. The original would simply cancel out any gyro effect on disable.

    Edit: fixed the method
     
    Last edited: May 24, 2023
  12. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    I can't quite understand what you mean by this though. Are you sure you're not thinking about this backward?

    You want your scene working without any gyro, just imagine if your device was in a position where it would yield an identity rotation.

    After your scene is done and spinning properly, you just want to add the last touch, the feedback from the gyro. It's a global, universal value, with which you blanket the whole thing. You don't depend on it. Nothing depends on it.

    That way a scene works with or without it. The only thing that matters is what exactly you want to do with the transition, if it's somehow normal or expected that gyroscope can be turned on/off in the middle of animation. Or maybe its update is irregular idk.

    The change I introduced above will preserve the last value, and it's maybe wrong to handle it like that, but I figured it's better than resetting the whole scene back to where it was, so you have to decide this for yourself.