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

Spin transform to a target rotation?

Discussion in 'Scripting' started by bryantdrewjones, Jan 3, 2015.

  1. bryantdrewjones

    bryantdrewjones

    Joined:
    Aug 29, 2011
    Posts:
    147
    Hello everyone :)

    I'm trying to write a coroutine that will spin a game object around the specified axis for n rotations before landing at the final target rotation. The math is giving me a hard time, and I feel like I'm overcomplicating it. But I know next to nothing about quaternions, so I'm at a loss :(

    This is what I have currently:

    Code (CSharp):
    1. yield return StartCoroutine( transform.SpinTo( 170, 3, Vector3.forward, 1.5f ) );
    2.  
    3. // ...
    4.  
    5. static public IEnumerator SpinTo( this Transform transform, float target, int rotations, Vector3 axis, float duration ) {
    6.     float elapsed = 0;
    7.     Vector3 start = transform.localRotation.eulerAngles;
    8.  
    9.     // Zero out the dimensions not being rotated
    10.     Vector3 scaledStart = Vector3.Scale( start, axis );
    11.  
    12.     // Find shortest distance between starting angle and zero
    13.     float deltaZero = Quaternion.Angle( Quaternion.Euler( scaledStart ), Quaternion.identity );
    14.  
    15.     // Feels like the axis parameter should dictate the direction... (Vector3.forward vs. Vector3.back)
    16.     float direction = Mathf.Sign( target );
    17.     target += ( 360f * rotations * direction );
    18.     Vector3 targetEulerAngles = ( target + ( deltaZero * direction ) ) * axis;
    19.  
    20.     while( elapsed < duration ) {
    21.         elapsed = Mathf.MoveTowards( elapsed, duration, Time.deltaTime );
    22.         transform.localRotation = Quaternion.Euler( start + ( targetEulerAngles * ( elapsed / duration ) ) );
    23.         yield return 0;
    24.     }
    25.    
    26.     transform.localRotation = Quaternion.Euler( start + targetEulerAngles );
    27. }
    What's frustrating is that this code works for most cases (I think...). But if, say, the game object starts with a rotation of (0, 0, 270) and I call transform.SpinTo( -20, 1, Vector3.forward, 5.0f ), the game object ends up with a final rotation of 160 :(

    There must be an easier way to go about this... Any hints? :)

    Cheers! :D
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    You should look into a tweening engine like HOTween or iTween... it'll take care of a lot of the heavy lifting for you.

    If you don't want to, I really suggest just doing it all with quaternions, and not with eulers converted to quats. Otherwise you'll have to normalize all those angles so that they are relative to one another, which can be a pain.
     
  3. bryantdrewjones

    bryantdrewjones

    Joined:
    Aug 29, 2011
    Posts:
    147
    Hi lord :)

    Thanks for the super quick response! I generally would use a tweening engine (I usually use GoKit), but in this specific case I'd prefer to keep the code contained to a one-off coroutine.

    Yes, quaternions feel like the answer :) But they're not very intuitive to use.... If you're aware of any "Quaternions for Dummies" articles, please send them my way :D
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    Honestly, a quats for dummies is a hard thing to come across.

    So, what you're going to want to do is ignore any numbers inside the quat. They're each the coefficient of a complex number that requires some pretty advanced mathematical knowledge. Thing is... you don't need to know all that stuff to use them.

    In the end a quaternion represents a rotation. Not a position of rotation, but the change in rotation. It's like how a vector3 represents a 3d translation, a quaternion represents a rotation. This means that like with Vector3, you can say it's a posiiton, or a direction with magnitude, it's a change... you can add a vector to a vector to get a combined translation. Well so to can you with a quaternion.

    You append quaternions by multiplying them (as opposed to adding them, like you do with a vector3).

    That's about as dummy of a tutorial I could give for them.

    I'm going to show you how to do this, and comment the code so you can follow along.

    Code (csharp):
    1.  
    2.     public static IEnumerator SpinTo(Transform trans, float target, int rotations, Vector3 axis, float duration )
    3.     {
    4.         //note the order of operations, if we have the rotation around some axis first in the order, it's treated as the global.
    5.         //if we flip it around, than it's relative to the rotation you're multiplying it to, meaning, the local space of the thing being rotated
    6.  
    7.         //first we're going to get our target rotation, what we're saying is rotate our target around axis by target degrees...
    8.         //it has to be that, going to target degrees around axis doesn't make sense... to 70 degrees around < 1,1,1>? Where is that?
    9.         //That's not enough info. If you want that you need the axis you're measuring off. Just like 90 deg around y-axis off x-axis is really what the euler.y is
    10.         var targetRot = Quaternion.AngleAxis(target, axis) * trans.rotation;
    11.  
    12.         //now we're going to get the full amount of degrees we're going to rotate... this is because we can't store more than 360 degrees
    13.         //of rotation into a single quat... it'll just normalize. 370 degrees will become 10 degrees
    14.         var shortAngle = Quaternion.Angle (targetRot, trans.rotation);
    15.         float longAngle = shortAngle + rotations * 360f;
    16.  
    17.         //lets calculate the amount of change per second. This will be used to calculate the per frame change in rotation
    18.         float anglePerSecond = longAngle / duration;
    19.  
    20.         //timer
    21.         float t = 0f;
    22.         //for the duration of the animation, rotate the amount of degrees we should rotate for that frame
    23.         while (t < duration) {
    24.             yield return null;
    25.             trans.rotation = Quaternion.AngleAxis (anglePerSecond * Time.deltaTime, axis) * trans.rotation;
    26.             t += Time.deltaTime;
    27.         }
    28.  
    29.         //at the end, set the rotation to the target, so it's not off by a small amount
    30.         trans.rotation = targetRot;
    31.     }
    32.  
     
    Last edited: Jan 3, 2015
  5. bryantdrewjones

    bryantdrewjones

    Joined:
    Aug 29, 2011
    Posts:
    147
    Oh my goodness! Thank you so much for that explanation! I'm starting to fear quaternions less :)

    I tried running the code you posted, and it works perfectly when the starting rotation of the transform is 0. When that's not the case, the transform would rotate to starting angle + shortAngle before snapping to the target.

    I think I've worked through most of the edge cases now. Here's an updated code snippet based off of the implementation you posted:

    Code (CSharp):
    1. public static IEnumerator SpinTo(this Transform trans, float target, int rotations, Vector3 axis, float duration )
    2. {
    3.     // Don't multiply by trans.rotation here, otherwise the target rotation will be incorrect when the starting rotation is something other than 0.
    4.     var targetRot = Quaternion.AngleAxis(target, axis); // * trans.localRotation;
    5.    
    6.     var shortAngle = Quaternion.Angle (targetRot, trans.localRotation);
    7.  
    8.     // The long angle wouldn't be calculated correctly if the target is in between 180 and 360 degrees. In that case, we don't want the
    9.     // shortest distance from the starting angle to the target; we want to keep rotating in the initial direction, even if that's the longer route.
    10.     if( target > 180 && target < 360 ) {
    11.         shortAngle = 360 - shortAngle;
    12.     }
    13.  
    14.     // Prevent snapping if the target angle is behind the starting angle
    15.     var startingAngle = Quaternion.Angle( trans.localRotation, Quaternion.identity );
    16.     if( startingAngle > target ) {
    17.         shortAngle = 360 - shortAngle;
    18.     }
    19.  
    20.     float longAngle = shortAngle + rotations * 360f;
    21.    
    22.     float anglePerSecond = longAngle / duration;
    23.  
    24.     float t = 0f;
    25.     while (t < duration) {
    26.         yield return null;
    27.         trans.localRotation = Quaternion.AngleAxis (anglePerSecond * Time.deltaTime, axis) * trans.localRotation;
    28.         t += Time.deltaTime;
    29.     }
    30.    
    31.     trans.localRotation = targetRot;
    32. }
    Do these changes make sense, or did I butcher your implementation? :D It seems to be working correctly for me when rotating around Vector3.forward. There are still some snapping issues when I rotate around Vector3.back, so I think I have to manipulate the target angle when rotating in a negative direction....

    I really appreciate the help, lord! :) Thanks so much!
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    Sorry, I was a little drunk last night, I don't know why I did this line:

    Code (csharp):
    1.  
    2. var shortAngle =Quaternion.Angle(targetRot, trans.rotation);
    3.  
    You should be able to just do this:

    Code (csharp):
    1.  
    2.     public static IEnumerator SpinTo(Transform trans, float target, int rotations, Vector3 axis, float duration)
    3.     {
    4.         var targetRot = Quaternion.AngleAxis(target, axis) * trans.rotation;
    5.  
    6.         float longAngle = target + rotations * 360f * Mathf.Sign(target);
    7.  
    8.         float anglePerSecond = longAngle / duration;
    9.  
    10.         //timer
    11.         float t = 0f;
    12.         //for the duration of the animation, rotate the amount of degrees we should rotate for that frame
    13.         while (t < duration)
    14.         {
    15.             yield return null;
    16.             trans.rotation = Quaternion.AngleAxis(anglePerSecond * Time.deltaTime, axis) * trans.rotation;
    17.             t += Time.deltaTime;
    18.         }
    19.  
    20.         //at the end, set the rotation to the target, so it's not off by a small amount
    21.         trans.rotation = targetRot;
    22.     }
    23.  
    I just ran it on a cube that was rotated around in an odd direction, and I tested around Vector3.up, forward, back, and left. With values 70, 170, 190, 260 and -260. They all worked fine.

    I included a way to deal with negative values, where a negative target just changes the direction of rotation.
     
  7. bryantdrewjones

    bryantdrewjones

    Joined:
    Aug 29, 2011
    Posts:
    147
    Hmm... I gave that a try, and it still seems to have some issues when the starting rotation of the transform is something other than zero :(

    For example, I created a 2D sprite with an initial Euler rotation of (0, 0, 20). I then ran the coroutine with these parameters:

    Code (CSharp):
    1. yield return StartCoroutine( SpinTo( transform, 40, 1, Vector3.forward, 2.0f ) );
    The sprite correctly performed 1 full rotation, but instead of stopping at 40 degrees, it overshot to 60 degrees.

    Running the coroutine with these parameters is also problematic:

    Code (CSharp):
    1. yield return StartCoroutine( SpinTo( transform, 0, 1, Vector3.forward, 2.0f ) );
    Again, the sprite correctly performed 1 full rotation, but instead of continuing to rotate another 340 degrees to the 0 degree target, it stopped at 20 degrees (the sprite's initial rotation).

    I'm curious if you're seeing the same behaviour?

    Something funny is going on in those first two lines:

    Code (CSharp):
    1. var targetRot = Quaternion.AngleAxis(target, axis) * trans.rotation;
    2.  
    3. float longAngle = target + rotations * 360f * Mathf.Sign(target);
    When starting at 20 degrees and spinning to 40 degrees, targetRot has a Euler rotation of (0, 0, 60) and longAngle has a value of 400 instead of 380 (360 for the first rotation and another 20 to hit the target).

    When starting at 20 degrees and spinning to 0 degrees, targetRot has a Euler rotation of (0, 0, 20) and longAngle has a value of 360 instead of 700 (360 for the first rotation and another 340 to hit the target).

    Maybe the shortAngle value really is necessary?
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    Awww, I see what you're saying.

    Note this part of the notes in my initial post:

    Going TO 40 degrees around an arbitrary axis doesn't actually make sense. 40 degrees from what? It's technically not enough information to work with.

    If that axis is the x, y, or z axis... you have assumptions as to what 40 degrees around the x axis there. Which disguises that extra information from you. For instance the euler x rotation technically are not some angle around the x-axis... it's some angle around the x-axis measured off the z-axis (or rather some initial rotation). The extra information is which axis are you measuring off of.

    So what if I pass in the value <0.7071, 0.7071, 0> as the axis to rotate around. What is 40 degrees around that axis? What position is that? We need more information. If I were to just append that rotation onto some existing rotation... the starting rotation IS the extra information. It's 40 degrees around that axis off of this starting rotation. And that's what I do in my code.

    This is that whole thing that I was talking about with quaternions, they are not a value of rotation... they are a direction and magnitude of rotation. Just like vector3's are.


    If you only ever want to go to some rotation around the standard euler angles. Don't have a vector for the axis, have an enum for which axis you want to rotate around. Then you can make this extra data assumption.

    OR, you can pass in a quaternion or a vector that represents what the target is measured off of.

    OR, we can just assume all rotations are off the ident rotation.

    Here's the big problem though... around which axis should we rotate? You can't just rotate around the axis that the rotation is defined around. If you have an object rotated euler(45, 80, 30), there's no path around Vector3.up to take that goes to euler(0, 40, 0). The around which to rotated needs to be determined from the start and target rotations... mathematically we can determine this as the shortest rotation from a to b. But again... that's making MORE assumptions. Which again is why I went with the idea of target being the amount to rotate by.

    Personally if I was going to write this, I'd take the target as a Quaterion, not as an angle and axis. This way when you call it you can just decide whatever you want the rotation to be defined (by axis, copy some other gameobject, from eulers, etc).

    Give me a minute, and I can show you how to do this. But of course, the big issue is we're going to have to get that shortest angle and axis from the rotation between 2 quats. This is some complicated math (which is why your code is having issues), don't worry though, I have the formula and complex crap to muddle through. (unfortunately Unity doesn't have this function built in)
     
    Last edited: Jan 4, 2015
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    Sorry, I wasn't in my office when I posted that last bit. In my office now.

    OK, so this example is going to use a static util class of mine called QuaternionUtil, that can be found here:
    https://code.google.com/p/spacepupp.../trunk/SpacepuppyBase/Utils/QuaternionUtil.cs

    Code (csharp):
    1.  
    2.     public static IEnumerator SpinTo(Transform trans, Quaternion target, int rotations, float duration)
    3.     {
    4.         //first we get the axis angle from current rotation to target
    5.         Vector3 axis;
    6.         float angle;
    7.         QuaternionUtil.GetShortestAngleAxisBetween(trans.rotation, target, out axis, out angle);
    8.  
    9.         //again we get the longAngle with all the extra spins
    10.         float longAngle = angle + rotations * 360f;
    11.         float anglePerSecond = longAngle / duration;
    12.  
    13.         float t = 0f;
    14.         while (t < duration)
    15.         {
    16.             yield return null;
    17.             //note the new order of operations... this is critical, because the angle axis from above
    18.             //is relative to the first quaternion passed in, which is the the trans local rotation
    19.             trans.rotation *= Quaternion.AngleAxis(anglePerSecond * Time.deltaTime, axis);
    20.             t += Time.deltaTime;
    21.         }
    22.  
    23.         //at the end, set the rotation to the target, so it's not off by a small amount
    24.         trans.rotation = target;
    25.     }
    26.  
     
  10. bryantdrewjones

    bryantdrewjones

    Joined:
    Aug 29, 2011
    Posts:
    147
    Ahh! That works!! Wow, thank you *SO* much -- not just for fixing my broken code, but for taking the time to explain the logic behind it. I can't say I fully understand all the fancy quaternion math in the GetShortestAngleAxisBetween() function :) But this post of yours really helped clarify things for me. There's never a shortage of new things to learn :)

    Enjoy the rest of your weekend! :) Thanks again!! :D
     
    Nils-Koster likes this.