Search Unity

SOLVED - Confused by Slerp/Lerp

Discussion in 'Scripting' started by wilhelmscream, May 4, 2015.

  1. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    The usual fairly basic issue....

    I have an object that I want to rotate back and forth on one axis (y) over a 120-degree arc. A simple swivel, right? Like a hinged gate - it can open, and then close. So far, using Slerp or Lerp (I don't really care which) has worked fine for getting it to go one way..... but when it reaches the end of the arc it "snaps" back to the starting position instantaneously rather than swiveling back at the same speed.

    My code:

    Code (JavaScript):
    1. #pragma strict
    2.  
    3. var to : Transform;
    4. var from : Transform;
    5. var speed : float = 1.0;
    6. var startPosition = false;
    7. var endPosition = false;
    8.  
    9. function Start () {
    10. }
    11.  
    12. function BackRotation () {
    13.     if (startPosition) {
    14.         transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, Time.time * speed);
    15.     }
    16.     if (transform.rotation == to.transform.rotation) {
    17.         endPosition = true;
    18.         startPosition = false;
    19.         StopCoroutine ("BackRotation");
    20.     }
    21. }
    22.  
    23. function ForeRotation () {
    24.     if (endPosition) {
    25.         transform.rotation = Quaternion.Slerp (to.rotation, from.rotation, Time.time * speed);
    26.     }
    27.     if (transform.rotation == from.transform.rotation) {
    28.         endPosition = false;
    29.         startPosition = true;
    30.         StopCoroutine ("ForeRotation");
    31.     }
    32. }
    33.  
    34. function Update () {
    35.     if (startPosition) {
    36.         StartCoroutine ("BackRotation");
    37.     }
    38.     if (endPosition) {
    39.         StartCoroutine ("ForeRotation");
    40.     }
    41. }
    Any and all input is appreciated.
     
  2. der_r

    der_r

    Joined:
    Mar 30, 2014
    Posts:
    259
    I suspect the problem is this: transform.rotation== to.transform.rotation . Those will never truly be exact. What you probably want to do is check if their difference is below some epsilon:

    if(Mathf.Abs(distance) < 0.05) then close_enough

    [edit] Once it's close enough, to make it exact you can set the transform.rotation = to.transform.rotation. Otherwise small errors might accumulate over time.
     
  3. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    Also, are you sure "Time.time * speed" is really what you want to provide for the "t" parameter? I know the example in the document also has this, but it doesn't really make sense. Instead, the "t" parameter to all lerp functions can usually be thought of as a percentage value from 0 to 1.
     
    JoeStrout likes this.
  4. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    This script tweens what it is attatched to between from and to over a duration that is based on the distance between from and to (beginning at each loop).
    Code (csharp):
    1.  
    2. #pragma strict
    3.  var to : Transform;
    4. var from : Transform;
    5. var speed : float = 1.0;
    6.  
    7. private function Start() : void {
    8.     StartCoroutine(MoveAndRotate(from, to));
    9. }
    10.  
    11. private function MoveAndRotate(from : Transform, to : Transform) : IEnumerator {
    12.     var curFrom : Transform = from;
    13.     var curTo : Transform = to;
    14.     while (true) {
    15.         var distance = Vector3.Distance(curFrom.position, curTo.position);    // Calculate distance.
    16.         var duration = distance / speed;                                    // Calculate duration based on speed and distance. (Will not change duration while tweening)
    17.         var currentTime : float = 0f;                                        // Current time variable.
    18.         while (currentTime < duration) {                                    // While we haven't reached our end time.
    19.             currentTime += Time.deltaTime;                                            // Progress time.
    20.             var t : float = currentTime / duration;                                    // Calculate t value.
    21.             transform.position = Vector3.Lerp(curFrom.position, curTo.position, t);        // Lerp positions.
    22.             transform.rotation = Quaternion.Slerp(curFrom.rotation, curTo.rotation, t);    // Slerp rotations.
    23.             yield null;                                                                // Yield to next frame.
    24.         }
    25.         transform.position = curTo.position;    // Pop into place when done.
    26.         transform.rotation = curTo.rotation;    // Pop into rotation when done.
    27.         var newTo : Transform = curFrom;        // Switch curFrom and curTo
    28.         curFrom = curTo;
    29.         curTo = newTo;
    30.     }
    31. }
    32.  
     
  5. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    I always use as a value for t, something like

    step = 1/totaltime

    and every frame:

    t = t + (step * Time.deltaTime)

    (where you store t between frames)

    total time is how long you want it to take to complete the slerp/lerp.
     
  6. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    This script moves what it is attatched to between two other transforms with a constant speed regardless if those transforms move.
    Code (csharp):
    1.  
    2. #pragma strict
    3.  var to : Transform;
    4. var from : Transform;
    5. var moveSpeed : float = 1.0f;
    6. var rotationSpeed : float = 100f;
    7.  
    8. private function Start() : void {
    9.     StartCoroutine(MoveAndRotate(from, to));
    10. }
    11.  
    12. private function MoveAndRotate(from : Transform, to : Transform) : IEnumerator {
    13.     var curFrom : Transform = from;
    14.     var curTo : Transform = to;
    15.     while (true) {
    16.         while (Vector3.Distance(transform.position, curTo.position) > Time.deltaTime * moveSpeed) {    // loop while distance is further than the distance we can cover this frame.
    17.             transform.position = Vector3.MoveTowards(transform.position, curTo.position, moveSpeed * Time.deltaTime);    // Move towards target poition.
    18.             transform.rotation = Quaternion.RotateTowards(transform.rotation, curTo.rotation, rotationSpeed * Time.deltaTime); // Rotate towards target rotation.
    19.             yield null; // Yield to next frame.
    20.         }
    21.         transform.position = curTo.position;    // Pop into place when done.
    22.         transform.rotation = curTo.rotation;    // Pop into rotation when done.
    23.         var newTo : Transform = curFrom;        // Switch curFrom and curTo
    24.         curFrom = curTo;
    25.         curTo = newTo;
    26.     }
    27. }
    28.  
     
  7. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    If anything I'm even more confused now than I was.. Is there not a simple way to get it to reverse the arc? Or, failing that, repeat it once it jumps back to it's original position?
     
  8. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    if A and B are to positions,

    lerp(a,b, 0) = position a
    lerp(a,b,1) = position b

    What you want to do, is go from 0 -1 and then when you hit 1, go from 1 to 0
     
  9. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Right..... that's what I'm trying to do, yes. At a consistent speed.
     
  10. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    Are the gaps between the points always the same?
     
  11. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Yes. The to, from and the swiveling object itself are all children of the same parent, so the distances between them do not shift.
     
  12. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    The error lies here:

    Code (CSharp):
    1. transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, Time.time * speed);
    2.  
    Time.time keeps increasing. So on the way up, (Time.time * speed) goes from 0 to 1. When you start rotating the object back, though, (Time.time * speed) is already 1, and it hits the last value of the lerp or slerp instantly.

    Store the starting value of Time.time in a variable, and use that to set the lerp value:

    Code (CSharp):
    1. transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, (Time.time - startTime) * speed);
    2.  
    where startTime is the value of Time.time when you started rotating in this direction.
     
    JoeStrout likes this.
  13. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Adding that in like so.....

    Code (JavaScript):
    1. function BackRotation () {
    2.     startTime = Time.time;
    3.     if (startPosition) {
    4.         transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, (Time.time - startTime) * speed);
    5.     }
    6.     if (transform.rotation == to.transform.rotation) {
    7.         endPosition = true;
    8.         startPosition = false;
    9.         StopCoroutine ("BackRotation");
    10.     }
    11. }
    12.  
    13. function ForeRotation () {
    14.     startTime = Time.time;
    15.     if (endPosition) {
    16.         transform.rotation = Quaternion.Slerp (to.rotation, from.rotation, (Time.time - startTime) * speed);
    17.     }
    18.     if (transform.rotation == from.transform.rotation) {
    19.         endPosition = false;
    20.         startPosition = true;
    21.         StopCoroutine ("ForeRotation");
    22.     }
    23. }
    Causes it to.... not move at all. I've tried putting the definition of startTime in various locations (the Start function, etc) to no avail.
     
    Last edited: May 4, 2015
  14. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Since you're already using a coroutine I'd do something like this
    Code (csharp):
    1.  
    2. while (true)
    3. {
    4.     float percent = 0;
    5.     float elapsedTime = 0;
    6.     while (percent < 1)
    7.     {
    8.         elapsedTime += Time.deltaTime;
    9.         percent = elapsedTime / totalTime;
    10.         transform.rotation = Quaternion.Slerp(to, from, percent);
    11.         yield return null;
    12.     }
    13.     while (percent > 0)
    14.     {
    15.         elapsedTime -= Time.deltaTime;
    16.         percent = elapsedTime / totalTime;
    17.         transform.rotation = Quaternion.Slerp(to, from, percent);
    18.         yield return null;
    19.     }
    20. }
    21.  
     
  15. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Trying to translate that to JS and I think I'm doing it wrong..... would it look like this?

    Code (JavaScript):
    1. #pragma strict
    2. var to : Transform;
    3. var from : Transform;
    4. var speed : float = 1.0;
    5. var startTime : float;
    6. var startPosition = true;
    7. var endPosition = false;
    8. var percent : float = 0;
    9. var elapsedTime : float = 0;
    10. var totalTime : float = 0;
    11. function Start () {
    12. }
    13. function BackRotation () {
    14.     if (startPosition) {
    15.         if (percent < 1) {
    16.             elapsedTime += Time.deltaTime;
    17.             percent = elapsedTime / totalTime;
    18.             transform.rotation = Quaternion.Slerp(from.rotation, to.rotation, percent);
    19.         }
    20.         if (percent > 0) {
    21.             elapsedTime -= Time.deltaTime;
    22.             percent = elapsedTime / totalTime;
    23.             transform.rotation = Quaternion.Slerp(to.rotation, from.rotation, percent);
    24.         }
    25.     }
    26. }
    27. function ForeRotation () {
    28. }
    29. function Update () {
    30.     if (startPosition) {
    31.         BackRotation ();
    32.     }
    33. }
     
  16. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    For the record I feel zero need to use coroutines, that's just coming from an answer I found to solve the first half of the rotation. It's not necessary (unless it's actually necessary).
     
    JoeStrout likes this.
  17. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Agreed, using coroutines for something like this is unnecessary, and confusing to newbies.

    But @wilhelmscream, I will also advise you (just this once) to make the move to C#. The industry has settled on it; almost nobody uses JS anymore, for a variety of reasons. Join us, and together we will rule the galaxy.

    Finally, even using Lerp for this is working harder than you need to. Since you want to move at a constant speed, just use MoveTowards, or in this case, MoveTowardsAngle. Easy peesy. Here's an example — untested, and in C#, but it should show you how to do it.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Rotator : MonoBehaviour {
    4.     public float targetAngle = 120; // target angle (duh)
    5.     public float speed = 10;  // movement degrees/sec
    6.  
    7.     void Update() {
    8.         float ang = transform.rotation.eulerAngles.y;
    9.         float maxMove = speed * Time.deltaTime;
    10.         ang = Mathf.MoveTowardsAngle(ang, targetAngle, maxMove);
    11.         transform.rotation = Quaternion.Euler(0, ang, 0);
    12.     }
    13. }
    Just attach this script to whatever object you want to rotate, and whenever you like, set the targetAngle to whatever you want. It will smoothly rotate (around Y) to that angle at a constant speed. Easy peasy, right?
     
  18. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    That won't ping-pong it though. Speaking of ping-pong - http://docs.unity3d.com/ScriptReference/Mathf.PingPong.html might help you keep track of your lerp percent.

    It also looks like you tried to translate and shoehorn my proposed solution into your own thing - so I'm not really sure what's going on in there. Percent keeps track of whether you're going forward or backward so your other checks are redundant. :) There was a point to the nested while loops.
     
  19. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    True — I'd rather have a script that can go to any target, and then change (perhaps via some other component) the target to make it ping-pong, as it strikes me as more general.

    Mathf.PingPong is a neat suggestion, and you're right, it would work nicely with Lerp in this case, if you're sure you don't want any delay at either end, events or other processing when it reverses direction, etc. (Which may very well be the case.)

    But, for the sake of argument, here's a version using MoveTowards, that also kicks off a UnityEvent at either end so you can trigger a sound or some other script or whatever.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEvents;
    3. public class Sweeper : MonoBehaviour {
    4.     public float targetAngle0 = 0;
    5.     public float targetAngle1 = 120;
    6.     public float speed = 10;  // movement degrees/sec
    7.     public UnityEvent hitEnd0;
    8.     public UnityEvent hitEnd1;
    9.    
    10.     bool moveTowards1 = true;
    11.     void Update() {
    12.         float ang = transform.rotation.eulerAngles.y;
    13.         float maxMove = speed * Time.deltaTime;
    14.         if (moveTowards1) {
    15.             ang = Mathf.MoveTowardsAngle(ang, targetAngle1, maxMove);
    16.             if (ang == targetAngle1) {
    17.                 hitEnd1.Invoke();
    18.                 moveTowards1 = false;
    19.             }
    20.         } else {
    21.             ang = Mathf.MoveTowardsAngle(ang, targetAngle0, maxMove);
    22.             if (ang == targetAngle0) {
    23.                 hitEnd0.Invoke();
    24.                 moveTowards1 = true;
    25.             }
    26.         }
    27.         transform.rotation = Quaternion.Euler(0, ang, 0);
    28.     }
    29. }
     
  20. Nihil688

    Nihil688

    Joined:
    Mar 12, 2013
    Posts:
    503
    You need to make sure that the from and the to transforms in your method are cached and have nothing to do with the current gameObject's transform.position
     
  21. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    PingPong looks like it's putting me closer to the answer.

    Using just this :

    Code (JavaScript):
    1. var rotSpeed : float = 10.0;
    2.  
    3. function Update () {
    4.     transform.rotation = Quaternion.Euler (0.0, Mathf.PingPong (Time.time*rotSpeed, 120.0), 0.0);
    5. }
    gives me exactly the performance and motion I need....as long as I need it rotating clockwise then back. So, naturally, I tried using -120.0 instead for a counter-clockwise rotation ...... and it went all kinds of haywire.

    Any idea how to accomplish that?

    Oh, and FYI, the object in question is a radar/sensor beam covering a fixed arc. So no particular performance requirements.
     
  22. Nihil688

    Nihil688

    Joined:
    Mar 12, 2013
    Posts:
    503
    PingPong is a glorified Lerp so I would suggest you take a step back and see what you have:
    RotA, RotB

    store those in a variable that won't change and then use a timer up to 1.0f


    Code (CSharp):
    1. float m_Timer = 0.0f;
    2. float Vector3 m_CachedRotationA, m_CachedRotationB;
    3. bool m_GoDown;
    4.  
    5. private void Update()
    6. {
    7.    if( m_GoDown )
    8.    {
    9.        m_Timer += Time.deltaTime;
    10.     }
    11.     else
    12.     {
    13.          m_Timer -= Time.deltaTime;
    14.      }
    15.      transform.rotation.eulerAngles = Vector3.Lerp(m_CachedRotationA, m_CachedRotationB, m_Timer );
    16.     if( !m_GoDown && m_Timer >= 1.0f)
    17.     {
    18.             //do stuff
    19.            m_GoDown = true;
    20.     }
    21.     else if( m_GoDown && m_Timer <= 0.0f )
    22.     {
    23.          m_GoDown = false;
    24.      }
    25. }
     
  23. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    I could be mistaken, but surely this code is going to take one second to finish? (You are just counting time.deltaTime up to 1 second worth of updates)

    If you want it to be longer than that, you might want to do something like:

    step = 1/totaltime

    and every frame:

    m_Timer = t + (step * Time.deltaTime)

    or when going in the opposite direction

    m_Timer = t - (step * Time.deltaTime)


    where totaltime is how long you want it to take.
     
  24. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Why is using PingPong a bad thing? Functionally, it works, for the minimal result that I need, I just have to figure out the angles in negative value.
     
  25. Nihil688

    Nihil688

    Joined:
    Mar 12, 2013
    Posts:
    503
    @Korno : yeah basically I wanted to simplify it a bit as I saw that this conversation moved around to different directions, it's always best to get something working and then one can experiment further
    @wilhelmscream : best advice I can give is don't go down the route of not understanding what a method does exactly just because it kinda worked, try to write the method yourself and then you can improve it whichever way you want.
     
  26. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    PingPong is fine for this. It's not the same as Lerp because it "wraps" for values bigger than 1. Honestly - either approach is fine and would work.
     
  27. Nihil688

    Nihil688

    Joined:
    Mar 12, 2013
    Posts:
    503
    Being very pedantic:
    PingPong doesn't wrap values bigger than 1, it ping pongs the value t so that it's never larger than the given length
    and not smaller than zero.

    My point is to understand what the methods you use do, Lerp is just (y2-y1)/(x2-x1) but ping pong clamps, lerps and does other stuff that are not 100% visible to the developer and thus he needs to experiment to get the desired result.
     
  28. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Ok fine. I'm not terribly interested in belaboring the point anymore. You win.
     
  29. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    I've expiremented with numerous solutions. PingPong is my preferred choice because A) I'm basically an idiot and the code is easier to understand, and B) It does everything I need it to do. Soon as I figure out this little issue with the starting rotation I'm good to go. Code currently looks like.....

    Code (JavaScript):
    1.  
    2. #pragma strict
    3.  
    4. var rotSpeed : float = 10.0;
    5. var angle : float;
    6. var startRotation : float;
    7. var targetInRange = false;
    8.  
    9. function Start () {
    10.     transform.eulerAngles = Vector3 (0, startRotation, 0);
    11. }
    12.  
    13. function Update () {
    14.     if (targetInRange) {
    15.         transform.rotation = Quaternion.Euler (0.0, Mathf.PingPong (Time.time*rotSpeed, angle), 0.0);
    16.     }
    17. }
    18.  
    The issue now is this..... when I enter play mode, the object is properly rotated on it's Y axis. However, as soon as targetInRange is true, it snaps to a rotation of 0, 0, 0 before entering it's pingpong. This is no good.

    Anyone see what I'm doing wrong there? I've tried everything I can think of.....
     
  30. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Because PingPong goes from 0 to whatever length you give it. That's what makes it different from Lerp in that Lerp let's you define that lower bound. If you're not rotating from 0 to angle then it sounds like you want to take the result of PingPong and add it to your current rotation (or subtract it if you're going backwards).

    Or use Lerp instead :)
     
    Nihil688 likes this.
  31. wilhelmscream

    wilhelmscream

    Joined:
    Jun 5, 2013
    Posts:
    223
    Oh I would much rather use Lerp if I could figure out how to make it go back and forth. Single-direction Lerp is all I can accomplish.


    However, your solution has, indeed, solved my problem. Thank you once again good sir.

    Code (JavaScript):
    1. #pragma strict
    2.  
    3. var rotSpeed : float = 10.0;
    4. var angle : float;
    5. var startRotation : float;
    6. var targetInRange = false;
    7.  
    8. function Start () {
    9.     transform.eulerAngles = Vector3 (0, startRotation, 0);
    10. }
    11. function Update () {
    12.     if (targetInRange) {
    13.         transform.rotation = Quaternion.Euler (0.0, startRotation+(Mathf.PingPong (Time.time*rotSpeed, angle)), 0.0);
    14.     }
    15. }
     
    Nihil688 likes this.
  32. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Glad you got it working.

    In terms of bi-directional lerping you'd have to take what you have and reverse it (subtract time instead of add time) once you get to the end of the lerp. You'll know when that happens because T in your lerp will be 1 (or 0 once you go backwards all the way).
     
    blizzy likes this.