How to smooth damp towards a moving target without causing jitter in the movement?

Dear forum

The title says it all. This should be really simple, but it’s killing me.

Put the script below on a game object in an empty scene and it will generate an example of to moving cubes, a target and a follower. The follower on the left side of the screen is jittering. Why? Mind you this is only visible when the speed is quite high.

~ce

using UnityEngine;

public class SnakesFollowTest : MonoBehaviour
{
	Transform targetTransform;
	Transform followerTransform;
	Transform camTransform;
	
	Vector3 followerVelocity;
	
	void Start()
	{
		camTransform = Camera.main.transform;
		targetTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
		followerTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
		targetTransform.name = "target";
		followerTransform.name = "follower";
	}
	
	void Update()
	{
		// move target //
		targetTransform.position += Vector3.right * Time.deltaTime * 200;
		
		// move follower //
		followerTransform.position = Vector3.SmoothDamp( followerTransform.position, targetTransform.position, ref followerVelocity, 0.05f );
		
		// move camera along side the target //
		camTransform.position = new Vector3( targetTransform.position.x, camTransform.position.y, camTransform.position.z );
	}
}
2 Likes

Have you tried using something like Vector3.Lerp instead?

Yes. Same shit unfortunately. You can replace line 26 with the line below and the jitter remains.

followerTransform.position = Vector3.Lerp( followerTransform.position, targetTransform.position, Time.deltaTime * 20 );
2 Likes

You shouldn’t pass a multiple of Time.deltaTime into Lerp - it’s not a linear function. But you should pass Time.deltaTime into SmoothDamp, as far as I can tell - it is asking for a time delta. Whether it applies it correctly is another matter, I’ve never used the function.

If you want to use Lerp, you probably want to pass 1 - Mathf.Exp(-k * Time.deltaTime) or something like that, varying k to change the lerp rate. Bear in mind that it will give a quite different behaviour.

These functions will both have trouble tracking a moving target though - the follower will lag behind more and more as the target velocity increases. You can write better functions to make the follower not lag behind the target point even when the target is moving with constant speed, which might be better for your purposes or not.

3 Likes

Thanks for giving it a shot George!

SmoothDamp applies Time.deltaTime by default, so it makes no difference passing it as argument. I tried it.

It also produces jitter. Try switching line 26 with the line below to test it yourself.

followerTransform.position = Vector3.Lerp( followerTransform.position, targetTransform.position, 1 - Mathf.Exp( -20 * Time.deltaTime ) );

That’s ok, I only want to avoid the jitter.

The target will need move at a varying speed. It is moving at a constant speed in the example to make the problem more obvious/visible.

What is the best practice for solving this kind of problem?

4 Likes

The easiest solution is to just do it all in FixedUpdate. It’s still worth correcting for the deltaTime, in case you change your update rate in the future, but you won’t need to be quite so careful about it.

I think the problem with your case, though, is that although the lerp with exponentiation is correct for interpolating towards a fixed target, your target is moving, and the lerp is based on the fully-integrated target position, when it should be based on a continuous integration of the motion during the timeslice.

So I guess you have three options:

  1. Do it in FixedUpdate instead
  2. Use a more linear following algorithm - which I would expect to show fewer artifacts
  3. Write the lerp as a differential equation and solve it properly

(3) might be overkill - (2) is usually simpler - but I wanted to convince myself that (3) does work in this case, so I did the maths, getting this:

	Vector3 SuperSmoothLerp(Vector3 x0, Vector3 y0, Vector3 yt, float t, float k)
	{
		Vector3 f = x0 - y0 + (yt - y0) / (k * t);
		return yt - (yt - y0) / (k*t) + f * Mathf.Exp(-k*t);
	}

x0 is the follower’s old position, y0 is the target’s old position, yt is the target’s new position, t is the elapsed time, and k is the lerp rate, as in (1 - Mathf.Exp(-k * Time.deltaTime)) before. You used 20 for that. The return value is the follower’s new position. So you call it much like Vector3.Lerp, except you pass the target’s old and new positions, rather than just its new position.

The maths assumes that the target is moving with constant velocity over the time slice, so if the target is changing velocity rapidly as well it’s still going to jitter a bit. But in any case it should be a lot more smooth than plain Lerp was, and other than being smooth, it has exactly the same behaviour (e.g. response to different k values, following distance, etc).

8 Likes

Thank’s again George, I appreciate the effort =)

In that case I’m not sure I explained myself clearly enough to begin with. I want the follower to move towards the target at high speed with it’s far away and gradually slower as it approaches. The follower should only be able to reach the target when the target stands still. This is practically what the code I posted at first does, but there is jitter in the movement.

The SuperSmoothLerp function reaches the target while the target is moving. Pretty cool, but it’s not exactly the problem I needed to solve.

Both Lerp, SmoothDamp and (even) SuperSmoothLerp causes visible jitter in the movement of the follower. Run the example below to test this.

I’m still not sure how to get this right.

~ce

using UnityEngine;

public class SnakesFollowTest : MonoBehaviour
{
	Transform targetTransform;
	Transform followerTransform;
	Transform camTransform;
	
	//Vector3 followerVelocity;
	Vector3 pastFollowerPosition, pastTargetPosition;
	
	void Start()
	{
		camTransform = Camera.main.transform;
		targetTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
		followerTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
		targetTransform.name = "target";
		followerTransform.name = "follower";
	}
	
	
	void Update()
	{
		// move target //
		targetTransform.position += Vector3.right * Time.deltaTime * 200;
		
		// move follower //
		followerTransform.position = SuperSmoothLerp( pastFollowerPosition, pastTargetPosition, targetTransform.position, Time.time, 0.5f );
		pastFollowerPosition = followerTransform.position;
		pastTargetPosition = targetTransform.position;
		
		// move camera along side the target //
		camTransform.position = new Vector3( targetTransform.position.x, camTransform.position.y, camTransform.position.z );
	}
	
	
	Vector3 SuperSmoothLerp( Vector3 pastPosition, Vector3 pastTargetPosition, Vector3 targetPosition, float time, float speed )
	{
    	Vector3 f = pastPosition - pastTargetPosition + (targetPosition - pastTargetPosition) / (speed * time);
    	return targetPosition - (targetPosition - pastTargetPosition) / (speed*time) + f * Mathf.Exp(-speed*time);
    }
	
}
1 Like

As I said, if you do it all in FixedUpdate, all the jitter will go away.

Otherwise, I think the problem in your usage of SuperSmoothLerp is that you’re passing in Time.time instead of Time.deltaTime. For me, it’s very smooth indeed.

Here’s a webplayer demo, the code I used to test it - based on your original code:

http://www.gfootweb.webspace.virginmedia.com/SmoothDamp/WebPlayer.html

You can choose from a few different movement profiles; “Fast” is 200m/s and “Slow” is 100m/s). The bottom cube is following using my function; the other two are using Vector3.Lerp with either linear (centre) or exponential (top) response to changes in the deltaTime.

1 Like

I have heard it’s bad practise to overload the fixed update function so I want to avoid that.

I tried using delta time as well, but I still get jitter.

However it DOES work in your web player example. Can you post the code? I must be doing something wrong.

Thanks again.

My code has got kind of long… but if I take the code you just posted and change “Time.time” to “Time.deltaTime”, and “0.5f” to “20”, it works just like the bottom cube in my web player.

You could also try using Time.smoothDeltaTime instead of Time.deltaTime. Explanation here.

1 Like

It works! George thank you for this, you totally saved my day!

That does not solve the problem, but thanks for sharing.

Here is the final example for the sake of archiving.

Yeah!

using UnityEngine;

public class SnakesFollowTest : MonoBehaviour
{
    Transform targetTransform;
    Transform followerTransform;
    Transform camTransform;
    Vector3 pastFollowerPosition, pastTargetPosition;
    
    void Start()
    {
        camTransform = Camera.main.transform;
        targetTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
        followerTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
        targetTransform.name = "target";
        followerTransform.name = "follower";
    }
    
    void Update()
    {
        // move target //
        targetTransform.position += Vector3.right * Time.deltaTime * 200;
        
        // move follower //
        followerTransform.position = SmoothApproach( pastFollowerPosition, pastTargetPosition, targetTransform.position, 20f );
        pastFollowerPosition = followerTransform.position;
        pastTargetPosition = targetTransform.position;

        // move camera along side the target //
		camTransform.position = new Vector3( targetTransform.position.x, targetTransform.position.y, targetTransform.position.z - 15 );
    }
    
    Vector3 SmoothApproach( Vector3 pastPosition, Vector3 pastTargetPosition, Vector3 targetPosition, float speed )
    {
		float t = Time.deltaTime * speed;
		Vector3 v = ( targetPosition - pastTargetPosition ) / t;
		Vector3 f = pastPosition - pastTargetPosition + v;
        return targetPosition - v + f * Mathf.Exp( -t );
    }
}
2 Likes

Sooo. Now I have encountered the same problem only with rotations. When I lerp towards a target rotation at high speeds I get jitter in the movement. I just don’t have the math smarts to make the SuperSmoothLerp differential equation for quaternions.

I made an example showing the problem. Put the script on a game object in an empty scene and the problem should be obvious. The small box is the target and big is the follower.

Anyone able to guide me or write the SuperSmoothLerp function for quaternions?

~ce

using UnityEngine;

public class RotationFollowTest : MonoBehaviour
{
	Transform targetTransform;
    Transform followerTransform;
    Transform camTransform;
	
	void Start()
	{
		camTransform = Camera.main.transform;
		camTransform.position = new Vector3( 0, 0, -2 );
        targetTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
        followerTransform = GameObject.CreatePrimitive( PrimitiveType.Cube ).transform;
        targetTransform.name = "target";
        followerTransform.name = "follower";
		followerTransform.Translate( 0, 0, 2 );
		followerTransform.localScale = new Vector3( 2, 2, 2 );
		(new GameObject()).AddComponent<Light>();
	}
	
	void Update()
	{
		// rotate target //
		targetTransform.Rotate( 0, 0, Time.deltaTime * 1000 );
		
		// move followers rotation toward targets rotation //
		followerTransform.rotation = Quaternion.Lerp( followerTransform.rotation, targetTransform.rotation, Time.deltaTime * 20 );
		
		// move cameras rotate to targets rotation //
		camTransform.rotation = targetTransform.rotation;
	}
}

There are lots of problems with this. The first is that you’re going to run into Nyquist-style sampling issues - you’re spinning the cube at 1000 rotations per second, but only running your simulation at maybe 60Hz. Anything that’s based on observing the orientation of the cube at 60Hz intervals will never be able to tell exactly how fast it is spinning. You could feed the actual angular velocity vector into the lerp function though to get around that. It’s still hard to solve.

If your rotations will be limited to one dimension then you could do the whole thing using angles instead. For that you need to change the vectors to floats, and again you need to keep track of (or at least think about) whether the target object has fully rotated, and whether you want the follower to have to spin the same number of times to keep up, or whether it’s OK for it to catch up next time around. A lot of this depends on your game.

I know you said earlier you wanted to keep things out of the FixedUpdate() because you heard it was bad to put too much in there. But sometimes it is the right solution. If putting these functions in the fixed update works, maybe you should do that?

It’s not 1000 cycles per second, it’s 1000 degrees which means 1000 / 360 = 2.77 hz.

In the example above it looks like the follower is doing a pretty good job apart from a bit of jitter, which in my case unfortunately means everything. Even when I turn down the rotation speed to 90 degrees per second (0.25hz) I can still see some minor jitter when I go fullscreen.

I’m not saying it’s an easy problem to solve.

I wish that was the case.

I’d like to keep it flexible, say, for example be able to run the code in a coroutine.

~ce

I’m still struggling with this. I uploaded the most recent example above as webplayer. Notice the minor jittering movement of the white cube. It jumps when the Computer is busy causing the deltaTime to peak.

http://sixthsensor.dk/temp/unityforum/LerpFollowJitter/index.html

If you really want to do this the correct way then check out news figure 11. If you read the short section there, you will notice the method of adding angular velocity to a quaternion. I believe this must be done if you want to avoid FixedUpdate().

One other thing to note, if you place your slerp or lerp call before rotating your target, you should at least reduce the frequency of jitters. I know it’s not the same, but in a pinch it might work for you.

Did you figure this out? I’m having the EXACT same problem and it’s making me insane, I’ve spent my whole day working on this one thing and I’m still where I was this morning.

1 Like

I ran into similar issues with the standard SmoothFollow.js script, so I checked this thread and made a new version that works even at high speeds. All I had to do is replace Time.deltaTime with (1 - Mathf.Exp( -20 * Time.deltaTime )), as shown below:

/*
This camera smoothes out rotation around the y-axis and height.
Horizontal Distance to the target is always fixed.

There are many different ways to smooth the rotation but doing it this way gives you a lot of control over how the camera behaves.

For every of those smoothed values we calculate the wanted value and the current value.
Then we smooth it using the Lerp function.
Then we apply the smoothed values to the transform's position.
*/

// The target we are following
var target : Transform;
// The distance in the x-z plane to the target
var distance = 4.0;
// the height we want the camera to be above the target
var height = 1.0;
// How much we 
var heightDamping = 25.0;
var rotationDamping = 35.0;

// Place the script in the Camera-Control group in the component menu
@script AddComponentMenu("Camera-Control/Smooth Follow")

function Start ()
{
	// Early out if we don't have a target
	if (!target)
		return;
		
	transform.position = target.position;
}

function LateUpdate () {
	// Early out if we don't have a target
	if (!target)
		return;
	
	// Calculate the current rotation angles
	var wantedRotationAngle = target.eulerAngles.y;
	var wantedHeight = target.position.y + height;
		
	var currentRotationAngle = transform.eulerAngles.y;
	var currentHeight = transform.position.y;
	
	// Damp the rotation around the y-axis
	currentRotationAngle = Mathf.LerpAngle (currentRotationAngle, wantedRotationAngle, rotationDamping * (1 - Mathf.Exp( -20 * Time.deltaTime )));

	// Damp the height
	currentHeight = Mathf.Lerp (currentHeight, wantedHeight, heightDamping * (1 - Mathf.Exp( -20 * Time.deltaTime )));

	// Convert the angle into a rotation
	var currentRotation = Quaternion.Euler (0, currentRotationAngle, 0);
	
	// Set the position of the camera on the x-z plane to:
	// distance meters behind the target
	transform.position = target.position;
	transform.position -= currentRotation * Vector3.forward * distance;
	
	// Set the rotation
	transform.rotation.z = target.rotation.z;

	// Set the height of the camera
	transform.position.y = currentHeight;
	
	// Always look at the target
	transform.LookAt (target);
}

The issue that I was having until I looked at this thread and edited SmoothFollow.js to look like the above is that I was having distracting, headache-inducing jitters in a game that involves flying around at high speeds, with the rotation and height damping set high enough to meaningfully follow the player. With the above edits, I’ve now made everything nice and smooth. Thanks for the help guys!