SOLVED - Confused by Slerp/Lerp

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:

#pragma strict

var to : Transform;
var from : Transform;
var speed : float = 1.0;
var startPosition = false;
var endPosition = false;

function Start () {
}

function BackRotation () {
    if (startPosition) {
        transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, Time.time * speed);
    }
    if (transform.rotation == to.transform.rotation) {
        endPosition = true;
        startPosition = false;
        StopCoroutine ("BackRotation");
    }
}

function ForeRotation () {
    if (endPosition) {
        transform.rotation = Quaternion.Slerp (to.rotation, from.rotation, Time.time * speed);
    }
    if (transform.rotation == from.transform.rotation) {
        endPosition = false;
        startPosition = true;
        StopCoroutine ("ForeRotation");
    }
}

function Update () {
    if (startPosition) {
        StartCoroutine ("BackRotation");
    }
    if (endPosition) {
        StartCoroutine ("ForeRotation");
    }
}

Any and all input is appreciated.

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.

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.

1 Like

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).

#pragma strict
 var to : Transform;
var from : Transform;
var speed : float = 1.0;

private function Start() : void {
    StartCoroutine(MoveAndRotate(from, to));
}

private function MoveAndRotate(from : Transform, to : Transform) : IEnumerator {
    var curFrom : Transform = from;
    var curTo : Transform = to;
    while (true) {
        var distance = Vector3.Distance(curFrom.position, curTo.position);    // Calculate distance.
        var duration = distance / speed;                                    // Calculate duration based on speed and distance. (Will not change duration while tweening)
        var currentTime : float = 0f;                                        // Current time variable.
        while (currentTime < duration) {                                    // While we haven't reached our end time.
            currentTime += Time.deltaTime;                                            // Progress time.
            var t : float = currentTime / duration;                                    // Calculate t value.
            transform.position = Vector3.Lerp(curFrom.position, curTo.position, t);        // Lerp positions.
            transform.rotation = Quaternion.Slerp(curFrom.rotation, curTo.rotation, t);    // Slerp rotations.
            yield null;                                                                // Yield to next frame.
        }
        transform.position = curTo.position;    // Pop into place when done.
        transform.rotation = curTo.rotation;    // Pop into rotation when done.
        var newTo : Transform = curFrom;        // Switch curFrom and curTo
        curFrom = curTo;
        curTo = newTo;
    }
}

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.

This script moves what it is attatched to between two other transforms with a constant speed regardless if those transforms move.

#pragma strict
 var to : Transform;
var from : Transform;
var moveSpeed : float = 1.0f;
var rotationSpeed : float = 100f;

private function Start() : void {
    StartCoroutine(MoveAndRotate(from, to));
}

private function MoveAndRotate(from : Transform, to : Transform) : IEnumerator {
    var curFrom : Transform = from;
    var curTo : Transform = to;
    while (true) {
        while (Vector3.Distance(transform.position, curTo.position) > Time.deltaTime * moveSpeed) {    // loop while distance is further than the distance we can cover this frame.
            transform.position = Vector3.MoveTowards(transform.position, curTo.position, moveSpeed * Time.deltaTime);    // Move towards target poition.
            transform.rotation = Quaternion.RotateTowards(transform.rotation, curTo.rotation, rotationSpeed * Time.deltaTime); // Rotate towards target rotation.
            yield null; // Yield to next frame.
        }
        transform.position = curTo.position;    // Pop into place when done.
        transform.rotation = curTo.rotation;    // Pop into rotation when done.
        var newTo : Transform = curFrom;        // Switch curFrom and curTo
        curFrom = curTo;
        curTo = newTo;
    }
}

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?

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

Right… that’s what I’m trying to do, yes. At a consistent speed.

Are the gaps between the points always the same?

Yes. The to, from and the swiveling object itself are all children of the same parent, so the distances between them do not shift.

The error lies here:

transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, Time.time * speed);

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:

transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, (Time.time - startTime) * speed);

where startTime is the value of Time.time when you started rotating in this direction.

1 Like

Adding that in like so…

function BackRotation () {
    startTime = Time.time;
    if (startPosition) {
        transform.rotation = Quaternion.Slerp (from.rotation, to.rotation, (Time.time - startTime) * speed);
    }
    if (transform.rotation == to.transform.rotation) {
        endPosition = true;
        startPosition = false;
        StopCoroutine ("BackRotation");
    }
}

function ForeRotation () {
    startTime = Time.time;
    if (endPosition) {
        transform.rotation = Quaternion.Slerp (to.rotation, from.rotation, (Time.time - startTime) * speed);
    }
    if (transform.rotation == from.transform.rotation) {
        endPosition = false;
        startPosition = true;
        StopCoroutine ("ForeRotation");
    }
}

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.

Since you’re already using a coroutine I’d do something like this

while (true)
{
    float percent = 0;
    float elapsedTime = 0;
    while (percent < 1)
    {
        elapsedTime += Time.deltaTime;
        percent = elapsedTime / totalTime;
        transform.rotation = Quaternion.Slerp(to, from, percent);
        yield return null;
    }
    while (percent > 0)
    {
        elapsedTime -= Time.deltaTime;
        percent = elapsedTime / totalTime;
        transform.rotation = Quaternion.Slerp(to, from, percent);
        yield return null;
    }
}

Trying to translate that to JS and I think I’m doing it wrong… would it look like this?

#pragma strict
var to : Transform;
var from : Transform;
var speed : float = 1.0;
var startTime : float;
var startPosition = true;
var endPosition = false;
var percent : float = 0;
var elapsedTime : float = 0;
var totalTime : float = 0;
function Start () {
}
function BackRotation () {
    if (startPosition) {
        if (percent < 1) {
            elapsedTime += Time.deltaTime;
            percent = elapsedTime / totalTime;
            transform.rotation = Quaternion.Slerp(from.rotation, to.rotation, percent);
        }
        if (percent > 0) {
            elapsedTime -= Time.deltaTime;
            percent = elapsedTime / totalTime;
            transform.rotation = Quaternion.Slerp(to.rotation, from.rotation, percent);
        }
    }
}
function ForeRotation () {
}
function Update () {
    if (startPosition) {
        BackRotation ();
    }
}

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).

1 Like

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.

using UnityEngine;

public class Rotator : MonoBehaviour {
    public float targetAngle = 120; // target angle (duh)
    public float speed = 10;  // movement degrees/sec

    void Update() {
        float ang = transform.rotation.eulerAngles.y;
        float maxMove = speed * Time.deltaTime;
        ang = Mathf.MoveTowardsAngle(ang, targetAngle, maxMove);
        transform.rotation = Quaternion.Euler(0, ang, 0);
    }
}

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?

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. :slight_smile: There was a point to the nested while loops.

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.

using UnityEngine;
using UnityEvents;
public class Sweeper : MonoBehaviour {
    public float targetAngle0 = 0;
    public float targetAngle1 = 120;
    public float speed = 10;  // movement degrees/sec
    public UnityEvent hitEnd0;
    public UnityEvent hitEnd1;
   
    bool moveTowards1 = true;
    void Update() {
        float ang = transform.rotation.eulerAngles.y;
        float maxMove = speed * Time.deltaTime;
        if (moveTowards1) {
            ang = Mathf.MoveTowardsAngle(ang, targetAngle1, maxMove);
            if (ang == targetAngle1) {
                hitEnd1.Invoke();
                moveTowards1 = false;
            }
        } else {
            ang = Mathf.MoveTowardsAngle(ang, targetAngle0, maxMove);
            if (ang == targetAngle0) {
                hitEnd0.Invoke();
                moveTowards1 = true;
            }
        }
        transform.rotation = Quaternion.Euler(0, ang, 0);
    }
}

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