Search Unity

Smooth Slerp Rotation

Discussion in 'Scripting' started by lovemoebius, Feb 27, 2017.

  1. lovemoebius

    lovemoebius

    Joined:
    Dec 12, 2016
    Posts:
    88
    I've been reading support threads for an hour and I just don't understand how this is supposed to work.
    This is my current script.

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class randomPrize : MonoBehaviour {
    7.    
    8.     public GameObject movingHandle;
    9.     public float speed = 0.1f;
    10.     public Quaternion rotation1 = Quaternion.Euler(0, 0, 90);
    11.     public Quaternion rotation2 = Quaternion.Euler(90, 0, 90);
    12.    
    13.     // Update is called once per frame
    14.     void Update () {
    15.         if (Input.GetKeyDown (KeyCode.Return)) {
    16.             print (movingHandle.transform.rotation.eulerAngles.x);
    17.             movingHandle.transform.rotation = Quaternion.Slerp (rotation1, rotation2, speed * Time.deltaTime);
    18.         }
    19.     }
    20. }
    21.  
    22.  
    I just want this thing to rotate from its current rotation to another smoothly, but I can't seem to do it.
    When I press enter the object just instantly snaps into place. I tried removing the if statement. I tried changing the speed.

    How do I get this to work so that once I press enter it makes the thing rotate a certain amount of degrees and then it stops?
     
  2. AndyGainey

    AndyGainey

    Joined:
    Dec 2, 2015
    Posts:
    216
    The (S)Lerp family of functions take a "progress" value as their third parameter. This value is typically between 0 and 1, and determines how closely the output of the function biases towards the first parameter (when progress is 0) or towards the second (when progress is 1). In your code, you're always passing a small value that does not increase from 0 to 1 over time, but is almost always around 0.001667, assuming a frame rate of 60.

    To get a smooth transitioning effect, you need to call (S)Lerp once per frame, passing the exact same values as the first two parameters each time, and smoothly updating the third to start at 0 and gradually move toward 1 a bit each frame. This can be a bit awkward in conventional procedural code, but is made easy with Unity's coroutines, which can spread code over frames with the yield keyword.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class randomPrize : MonoBehaviour {
    6.  
    7.     public GameObject movingHandle;
    8.     public float speed = 0.1f;
    9.     public Quaternion rotation1 = Quaternion.Euler(0, 0, 90);
    10.     public Quaternion rotation2 = Quaternion.Euler(90, 0, 90);
    11.  
    12.     // Update is called once per frame
    13.     void Update () {
    14.         if (Input.GetKeyDown (KeyCode.Return)) {
    15.             print (movingHandle.transform.rotation.eulerAngles.x);
    16.             StartCoroutine(RotateOverTime(rotation1, rotation2, 1f / speed));
    17.         }
    18.     }
    19.  
    20.     IEnumerator RotateOverTime (Quaternion originalRotation, Quaternion finalRotation, float duration) {
    21.         if (duration > 0f) {
    22.             float startTime = Time.time;
    23.             float endTime = startTime + duration;
    24.             movingHandle.transform.rotation = originalRotation;
    25.             yield return null;
    26.             while (Time.time < endTime) {
    27.                 float progress = (Time.time - startTime) / duration;
    28.                 // progress will equal 0 at startTime, 1 at endTime.
    29.                 movingHandle.transform.rotation = Quaternion.Slerp (originalRotation, finalRotation, progress);
    30.                 yield return null;
    31.             }
    32.         }
    33.         movingHandle.transform.rotation = finalRotation;
    34.     }
    35. }
     
    Last edited: Feb 27, 2017
    takatok likes this.
  3. lovemoebius

    lovemoebius

    Joined:
    Dec 12, 2016
    Posts:
    88
    Hard to believe something so simple actually needs all that to work, I would've never gotten it by myself.
    Thank you so much :)

    Had to change Time.time to Time.deltaTime to make it work though, not sure if it's just my version of Unity or something else.
     
    Vaupell likes this.
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    We can shorten that up quite a bit.

    Also... not sure what 'speed' is supposed to represent. In AndyGainey he just inverted it to get an arbitrary time.

    Usually speed is something like 'unitsOfDistance per unitsOfTime'... so I went with 'degrees per second'. That'd be a usual rotational speed unit.

    Anyways:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class randomPrize : MonoBehaviour {
    7.  
    8.     public GameObject movingHandle;
    9.     public float speed = 0.1f; //speed in degrees per second?
    10.     public Quaternion rotation1 = Quaternion.Euler(0, 0, 90);
    11.     public Quaternion rotation2 = Quaternion.Euler(90, 0, 90);
    12.  
    13.     // Update is called once per frame
    14.     void Update () {
    15.         if (Input.GetKeyDown (KeyCode.Return)) {
    16.             var a = Quaternion.Angle(rotation1, rotation2); //degrees we must travel
    17.             StartCoroutine(RotateOverTime(rotation1, rotation2, a / speed)); //a/speed = time it'd take to travel a at such speed
    18.         }
    19.     }
    20.  
    21.     IEnumerator RotateOverTime(Quaterion start, Quaterion end, float dur)
    22.     {
    23.         float t = 0f;
    24.         while(t < dur)
    25.         {
    26.             movingHandle.transform.rotation = Quaternion.Slerp(start, end, t / dur);
    27.             yield return null;
    28.             t += Time.deltaTime;
    29.         }
    30.         movingHandle.transform.rotation = end;
    31.     }
    32. }
    33.  
     
    AndyGainey likes this.
  5. AndyGainey

    AndyGainey

    Joined:
    Dec 2, 2015
    Posts:
    216
    There may be a nicer way to implement that would simply work "out-of-the-box" with little effort for most purposes, but I feel it's a tricky problem to architect such that it works even for people who don't understand the internal details. Really, I find Lerp() to be plenty sufficient, but it does require that users have a strong grasp of frame/time calculations so that you know the reasoning behind the code. If you're doing working in games coding, I'd consider that one of the central skills to pick up.

    As for the time/deltaTime swap, I'm certain that my code as listed ought to be using time, not deltaTime. Not sure what behavior you were seeing with it, and how it started working when you used deltaTime.

    Stylistic differences, I guess. I prefer to work with absolute times rather than accumulating delta times, given the imprecision that results, even if the imprecision is so small as to be irrelevant in most cases. And a few other steps were just over-engineering to which I tend to always succumb. :) I have my reasons for them, but yeah, including them doesn't help pedagogically.

    As for speed, I interpreted it as proportion of total movement per second, but that is indeed a subjective interpretation, and would lead to variable rates of movement depending on how far apart the original and final orientations were. Your addition of the calculation to get degrees per second is spot on for most people's expectations, for sure. Though there have been times when I explicitly wanted an animation to last a specific duration regardless of the magnitude of change, in which case proportion per second would be ideal.
     
  6. lovemoebius

    lovemoebius

    Joined:
    Dec 12, 2016
    Posts:
    88
    For some reason time wasn't working.
    But I tried again and now it does. My bad.

    Also I cannot seem to get the code posted by lordofduct to work.
     
  7. mowax74

    mowax74

    Joined:
    Mar 3, 2015
    Posts:
    97
    I also don't get the "time" instead of "deltaTime" approach. "deltaTime" is the time a frame needs to render. When using this value you are able to have the same rotation speed on computers with more or less framerate.

    When you want to play this rotation in 1 second and the computer can do 60fps, the animation duration will be over 60 frames. When a computer is only able to reach 30fps, then the progress step per frame needs to be larger, since you don't want the animation
    playing over 2 seconds. And this is what you achieve with deltaTime, not Time.

    So my approach would be:

    Code (CSharp):
    1.  
    2. IEnumerator PerformRotation (Quaternion targetRotation) {
    3.          
    4.     float progress = 0f;
    5.     float speed = 0.5f;
    6.          
    7.     while (progress < 1f) {
    8.  
    9.         tr.rotation = Quaternion.Slerp (tr.rotation, targetRotation, progress);
    10.         progress += Time.deltaTime * speed;
    11.  
    12.         if (progress <= 1f) {
    13.             yield return null;
    14.         }
    15.     }
    16. }
    17.  
    This gives you a smooth transition with slow down at the end, good for animated doors and stuff like that. If you want a more linear animation, set a fixed start rotation as Slerp rotation start value like in the other examples above.
     
  8. Chris9465

    Chris9465

    Joined:
    Nov 3, 2020
    Posts:
    24
    Code (CSharp):
    1. // The object whose rotation we want to match.
    2.     public Transform target;
    3.     public AnimationCurve curve;
    4.     public float lerpDuration = 3;
    5.     float startValue = 0;
    6.     float endValue = 315;
    7.     float valueToLerp;
    8.     void Start()
    9.     {
    10.         lerpDuration = curve[curve.length - 1].time;
    11.         StartCoroutine(Lerp());
    12.     }
    13.     IEnumerator Lerp()
    14.     {
    15.         float animationTime = 0;
    16.         while (animationTime < lerpDuration)
    17.         {
    18.             valueToLerp = Mathf.Lerp(startValue, endValue, curve.Evaluate(animationTime));
    19.             transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(new Vector3(0, valueToLerp, 0)), 179);
    20.             animationTime += Time.deltaTime;
    21.             yield return null;
    22.         }
    23.         valueToLerp = endValue;
    24.     }
    25.  
    Had an issue with Slerp taking the shortest path instead of the direction I wanted it to move, so I used RotateTowards and manually calculated the frame by frame degrees. As long as the per frame rotation doesn't exceed 180, (clamped at 179) it should always rotate in the direction desired. It also uses an animation curve to quickly adjust rotation speed and apply a dampening effect Lerp would otherwise not have.