Search Unity

Question Can't modify Quaternions additively? What's going on?

Discussion in 'Scripting' started by Merivo, Feb 26, 2024.

  1. Merivo

    Merivo

    Joined:
    May 19, 2022
    Posts:
    9
    I'm trying to create a script that makes a rotation animation using an AnimationCurve, and then use multiple copies of the script to overlay them. But to do that I need to apply rotations additively, and herein lies the issue I'm having...

    When I do:
    Code (CSharp):
    1. var anim = curve.Evaluate(time);
    2. transform.rotation = Quaternion.Euler(toRot*anim);
    Everything works fine, but when I do:

    Code (CSharp):
    1. var animstep = curve.Evaluate(time) - curve.Evaluate(prevtime);
    2. transform.rotation *= Quaternion.Euler(toRot*animstep);
    The rotation goes wrong. It's not out by some rounding error either. (Also for reference, doing this same style code for position works fine)

    Is anyone savvy enough to explain why the second code doesn't work, and how to change it so that it does without losing any rotational adjustments by other scripts?
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Goes wrong in what way? What kind of rotation are you trying to achieve? What are the types of
    curve
    ,
    animstep
    and
    toRot
    ? When and where is this code running? Once? Each frame? In a coroutine?

    The only thing I can say for certain as that the * operator composes quaternions. You can think of the result of A * B as applying the rotation of A then applying the rotation of B.
     
  3. Merivo

    Merivo

    Joined:
    May 19, 2022
    Posts:
    9
    Sorry I didn't make this clear in the OP:
    As a specific example I'm trying to apply a rotation of the euler angle (-25, 180, 0) to a transform that starts at (-90, 180, 0). Essentially as if I were dragging the rotation values within the editor, except I'm doing it in a script.
    The code is running in Update, so every frame.
    curve
    is an AnimationCurve with an EaseInOut line from 0 to 1.
    animstep
    is a float that represents the difference between the curve output of this frame vs the previous frame.
    toRot
    is the target rotation which is a Vector3.
     
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    It's unclear what
    toRot
    indicates. Is this the target rotation you are trying to rotate towards? if so it doesn't make sense to include it in the right side of the *= operator here... that would be adding the target rotation every frame. Very strange.

    If your goal is to use an animation curve to go from a starting rotation to a target rotation you would use Quaternion.Slerp. Here's a quick and dirty sample script that goes from the start rotation to the end rotation over 5 seconds according to an animation curve.

    Code (CSharp):
    1. public float duration = 5;
    2. public AnimationCurve curve;
    3. public Quaternion targetRotation;
    4.  
    5. float timer;
    6. Quaternion startRotation;
    7.  
    8. void Start() {
    9.   startRotation = transform.localRotation;
    10. }
    11.  
    12. void Update() {
    13.   timer += Time.deltaTime;
    14.  
    15.   float t = curve.Evaluate(timer / duration);
    16.   Quaternion rotation = Quaternion.Slerp(startRotation, targetRotation, t);
    17.   transform.localRotation = rotation;
    18. }
     
    Last edited: Feb 26, 2024
    CodeSmile likes this.
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,942
    Euler angles are tricky and ripe with issues. I can imagine the above line may flip a sign or make the values go out of bounds (must be in -90 to +90 range). Depends on what the curve looks like and what its range is.


    Praetor points you in the right direction. Animating rotations should be done by slerping (or similar Quaternion methods) because that calculates the rotation entirely within the quaternion space.
     
  6. Merivo

    Merivo

    Joined:
    May 19, 2022
    Posts:
    9
    The problem is localRotation is being affected by multiple scripts. If script A rotates to targetRotation, and script B rotates by an additional specific amount, then in this case script A will overrule script B's rotation. How do I combine them?
     
  7. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    That's entirely up to you. There are many possible potentially valid behaviors here and you need to decide what kind of resolution you're looking for. What is your desired behavior here, and also why do you have multiple scripts manipulating the rotation at once?

    Perhaps it's time to step out of the world of abstract concepts and give us an explanation/screenshots/etc of what this code is really doing in terms of your game and what you're trying to accomplish.
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
    Simplest way is to just insert additional parented Transforms in your hierarchy and manipulate one single axis at each Transform level.

    This has the added benefit that you can trivially test your rig in the editor by rotating the intended axis of each Transform by mouse and seeing that it does what you want.

    Here's an example with battleship turrets: the traverse lives "above" the elevate. See hierarchy:

    https://forum.unity.com/threads/vector-lookat-unprecise.858511/#post-5658304
     
    PraetorBlue likes this.
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,942
    That is indeed a problem. You'll want to have a single script controlling the rotation.
     
  10. Merivo

    Merivo

    Joined:
    May 19, 2022
    Posts:
    9
    That's fair.

    animation.jpg

    So in this case I'm making a card game, and I want an animation where a card is drawn from the deck with multiple "stages". It lifts up, flips over, then moves into the floating hand (in a position and rotation dependant on how many cards you have). I also want the stages to overlap, make multiple variants, even some with a bit of flare. This is why I want to add the rotations together. The timing will vary, and I want to make it feel natural rather than robotic.

    I suppose as long as I have multiple quaternions, I don't need multiple transforms and I can still apply the rotations like a hierarchy. For the stage 1 I can just do
    transform.localRotation = Quaternion.Slerp(startRot, toRot, anim)

    I'm just not sure what code I need to apply for stage 2 onwards
    transform.localRotation *= Quaternion.Slerp(Quaternion.identity, Quaternion.Euler(0,180,0), anim)
    ?
     
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    No, if the 3 rotations represent absolute orientations in space, you can not simply treat them as relative rotations and apply them all at once. Quaternions are always relative rotations. When a quaternion is used as an absolute orientation, it simply means it's a relative rotation from the identity rotation, so from being aligned with the parent / worldspace. In order to have a "hierarchy" of quaternions, they would need to be relative to the end rotation of the previous one. Just like any kinematic chain.

    Since you also want to affect the position, it's probably way easier to use an actual gameobject hierarchy. It's best to only do one transformation per gameobject hierachy. So a rotation on one local axis or one translation in one axis. That kinematic chain could be initially placed in it's end position. To animate this chain all you would have to do is to lerp / slerp each of the changed attributed in the chain from 0 to the target orientation. The first "stages" of your animation would probably be constant. The only thing you would need to adjust (before starting the animation) are the last few chain segments ehivh would probably be a rotation, a translation and another rotation. The first rotation would specify the deviation from the default direction where the card should end up. The translation would indicate how far the card should fly towards the screen from the last fix point in the chain. The last rotation would make sure that the card is facing the camera.

    This chain can be easily constructed when you have the end position and orientation as a Transform / GameObject. You use Quaternion.LookRotation from the last fix point to make it look at the target position. Now you just need to parent the target object to that rotated object and the chain is complete.

    To animate this all you need to do is remember those initial (or final) local positions and rotation and lerp / slerp all the local positions and rotations from (0,0,0) / Quaternion.Identity towards the recorded positions. This could be simply done with a small script on each piece that does the lerping with its own local properties. When all those things are animated at the same time, you should get a smooth transition from the start position on the table to the end position on the screen.
     
    Merivo likes this.
  12. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    IMHO the easiest solution is just to create an Animation clip and call it a day. Also why are you calculating the difference between two Curves evaluations over time? this defeats the whole purpose of a timed Curve, which gives you directly a y output for a given time t. So basically you're turning a non cumulative function to a cumulative one, which is wrong in your use case.
     
    PraetorBlue and Bunny83 like this.
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    But the point was that the animation should end up at different end positions if I understood the question correctly. So you usually have either a line or a grid of cards which you hold. So this card drawing animation needs to end up at different locations.

    I know some card games get around this by having a generic drawing animation that lifts and rotates the card in the middle of the screen and once that's done the card is moved linearly to the target position. In that case you don't have a smooth transition from start to finish. In certain cases this may be even preferable as it kinda resembles how humans draw cards. You usually lift and rotate it in your hand, then you look at it and after that you sort it into your other hand at the right spot. So I agree that if the whold drawing animation is the same every time, just creating an animation clip in Unity would be the simplest solution. Especially since the AnimationWindow and the recording of animation is super simple in Unity.
     
    Nad_B likes this.
  14. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,789
    Just do a lerp from the final rotation of step 2 (you can either cache it the moment you start step 2, or just use the previous target value as a starting point), to whatever target rotation you want. No need for *=
     
  15. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    There's also the tweening solution. I think it would be pretty easy to do it with DOTween for eg., probably no more than 3-5 lines of code, with a nice callback at the end to notify you when the card has reached its final destination.
     
    Merivo likes this.
  16. Merivo

    Merivo

    Joined:
    May 19, 2022
    Posts:
    9
    Ok after reading all the replies and testing some things out, I've decided to use 2 transforms. A parent transform for controlling the movement & rotation directly to the hand, and a child transform for the flip, flare and any other variance that I want to add along the way. From my testing, it looks like I can blend simple euler rotations without needing a hierarchy, but not when attempting to combine that with a rotation to a specific point.

    The reason I don't want a full hierarchy of transforms is because these transitions are rapid, and it would mean a lot of creating & destroying objects for the sake of a few animations. I do believe using maths is more efficient, correct me if I'm wrong.

    Unity's Animations don't offer enough control over variety for one. I want to be able to randomize the animation values in subtle ways to create natural feeling variance in movement. Also having motions only in chain rather than blended looks too robotic.

    DOTween also looks promising, but it's more of an unknown to me than using multiple transforms, so I'm choosing transforms. Thanks everyone for the replies. Maybe I'll let you know how I get on.