How to avoid jittering VFX Graph with a camera moving on FixedUpdate?

Hey!
I have a jitter issue on a visual effect (VFXGraph) seen by a moving camera whose position is updated on FixedUpdate.

I tried changing the update mode of the VFXGraph but without success (see image below). I would have thought using fixed delta time would have synced the camera movement and the particles movement but apparently not the way I thought.

Below are gifs of a reproduced example. The camera is moving in the same direction as the particles. The jitter appears in gifs 1 and 2 where the VFX graph is respectively using fixed delta time and delta time. Gif 3 shows no jitter as camera is moved on update and VFX graph is using delta time.

Animation2
Animation3
Animation1

Notes:

  • The jitter is stronger when the camera has approximately the same velocity as the particles (as in the gifs above).
  • I cannot rely on a camera moving on Update or LateUpdate because the camera is following a character whose movements are physics-based and the camera’s movement itself is smoothed. Controlling the camera on Update or LareUpdate would make the character’s movements jittery (see for example this thread for an explanation).

Correct me if I am wrong - there is no problem with updating a camera in Update or LateUpdate that is following a character whose movement is physics based. Are you using Cinemachine or doing your own camera movement?

Thanks for your answer.

I think there is a problem when you use a “smooth” camera controller as described there.
By “smooth”, I mean linearly moving towards the target with a constant velocity like this :

private void LateUpdate()
{
    cameraTransform.position = Vector3.Lerp(
        cameraTransform.position,
        targetTransform.position + offset,
        Time.deltaTime * speed);
    cameraTransform.LookAt(targetTransform);
}

Equivalently, this script can be called on FixedUpdate using Time.fixedDeltaTime instead of Time.deltaTime.

This results in the following:

Animation3
Animation4

I had a similar issue - I had a very fast object and using Cinemachine, a Lerp or a simple SmoothDamp resulted in the same problems you had. In my case the camera was doing a lazy follow of the character.

Try some of these first:

  • Try Cinemachine - should work really well in this case and is easy.
  • Use Extrapolation when moving the character with forces.
  • Use SmoothDamp instead of Lerp.
  • And what fixed the issue for me - use an average position of the character of the last few frames.

For the average position I used the last 10 frames, the last one being the current frame. Then I used a SmoothDamp to target that position in LateUpdate(). This worked well for a lazy follow effect. Maybe it will also work following the character exactly.

Where you able to get the camera moving in LateUpdate()? I want to know how this is fixed as well, if none of the above worked out I want to find a way to get that solved.

TL;DR: for following a rigidbody with a smooth camera that moves on Update and reducing the jittering effect:

  • Setting extrapolate on the rigidbody improves a lot
  • Smoothing camera movement with Vector3.SmoothDamp improves over Vector3.Lerp
  • Using an average position of the character improves if using Vector3.Lerp
  • None of these solutions are completely perfect (I always experience a bit of jitter)

Hey, sorry for the long time no-posting and thanks for your interest! I tried your suggestions and almost succeeded using extrapolate (this is the one that improves the most) + smoothdamp. However, despite the result being very convincing, there is still a bit of jittery effect.

My first approach was to rely on a camera moving on FixedUpdate as I read on other discussions that this is the way to go. I would preferably continue doing this and thus am still looking for an answer to “how to synchronize VFX Graph with a FixedUpdate camera?” (see original post).

Cinemachine

Disclaimer: I have never used Cinemachine before and therefore I may have configured it wrong. I tried a follow camera as described here in the documentation.

Cinemachine synchronizes well with the target moved on FixedUpdate with AddForce (left GIF). However, the VFX is jittery in this case. Using extrapolate on the rigidbody inverses the effect (GIF on the right).

Note: I am reluctant to using Cinemachine as I would expect that there would be a simple solution to such a common setting (target moved on FixedUpdate + Camera following it + VFX around).

Left: cinemachine
Right: cinemachine + extrapolate

cinemachine
cinemachine + extrapolate

SmoothDamp

Smoothdamp on its own does not solve the problem. It was to be expected as it is basically like a Lerp but with a damping effect (left GIF). However, using smoothdamp with extrapolate on the followed rigidbody almost solves the problem (right GIF). The jittery effect is still there but almost imperceptible (I hope you can see it on such a low res GIF: focus on the edge of the cube).

I do not understand why does extrapolate helps reducing the jittery effect. By reading the doc I would have expect it to only be used in resolving collisions, not affecting movement / rendering itself.

Left: Smoothdamp
Right: Smoothdamp + extrapolate

smoothdamp
smoothdamp_extrapolate

Moving average target position

Following a moving average position of the target’s position does not solve the problem (left GIF). I think it was to be expected as this just averages the position but does not fix the fact that the smoothed camera movement (Update) does not happen at the same rate as the target (FixedUpdate). However it improves, and it improves further using extrapolate (right GIF). Similarly, as SmoothDamp, a bit of jitter is still visible though.

Same as before, I do not understand why does extrapolate helps.

Left: Moving average
Right: Moving average + extrapolate

lerp_averagetarget
lerp_averagetarget_extrapolate

That is starting to look good. Do you mind sharing the code that does the camera movement, specifically the turning once the object stops moving?

Sure, here is the code I used.

  • The UpdateMode defines when is the camera movement performed
  • The FollowMode defines the smoothing function the camera is using
  • The TargetMode defines how the target is computed (which creates also a smoothing effect)
public enum UpdateMode
{
    Update,
    FixedUpdate,
    LateUpdate
}

public enum FollowMode
{
    Exact,
    SmoothLerp,
    SmoothDamp
}

public enum TargetMode
{
    Exact,
    SimpleMovingAverage,
    ExponentialSmoothing
}

public class FollowCameraController : MonoBehaviour
{
    [Header("References")]
    [SerializeField] private Transform targetTransform;
    [SerializeField] private GameObject cinemachineCameraGO;

    [Header("Update Mode")]
    [SerializeField] private UpdateMode updateMode;
    [SerializeField] private bool isUsingCinemachine;

    [Header("Follow Mode")]
    [SerializeField] private FollowMode followMode;
    [SerializeField] private float lerpSpeed = 20.0f;
    [SerializeField] private float smoothDampTime = 0.1f;

    [Header("Target Mode")]
    [SerializeField] private TargetMode targetMode;
    [SerializeField] private int simpleMovingAverageRange = 3;
    [SerializeField] private float exponentialSmoothingRatio = 0.5f;

    private Transform cameraTransform;
    private CinemachineBrain cinemachineBrain;
    private Vector3 offset = new(0.0f, 2.0f, -10.0f);
    private Vector3 smoothDampVelocity;
    private Vector3 targetPosition;
    private Queue<Vector3> exactTargetPositionsBuffer;
    private bool isFollowing = true;

    private void Awake()
    {
        cameraTransform = GetComponent<Transform>();
        cinemachineBrain = GetComponent<CinemachineBrain>();

        // For SimpleMovingAverage target mode: fill FIFO buffer with the current camera position
        exactTargetPositionsBuffer = new Queue<Vector3>();
        for (int i = 0; i < simpleMovingAverageRange; i++)
            exactTargetPositionsBuffer.Enqueue(targetTransform.position + offset);
        // Set target position to the buffer mean
        targetPosition = targetTransform.position + offset;
    }

    private void Update()
    {
        if (updateMode == UpdateMode.Update)
            MoveCamera(Time.deltaTime);
    }

    private void LateUpdate()
    {
        if (updateMode == UpdateMode.LateUpdate)
            MoveCamera(Time.deltaTime);
    }

    private void FixedUpdate()
    {
        if (updateMode == UpdateMode.FixedUpdate)
            MoveCamera(Time.fixedDeltaTime);

        if (isUsingCinemachine)
        {
            if (!cinemachineCameraGO.activeInHierarchy)
                SwitchToCinemachine(true);
        }
        else
        {
            if (!isFollowing)
                SwitchToCinemachine(false);
        }
    }

    private void SwitchToCinemachine(bool x)
    {
        cinemachineCameraGO.SetActive(x);
        cinemachineBrain.enabled = x;
        isFollowing = !x;
    }

    private void MoveCamera(float deltaTime)
    {
        if (!isFollowing)
            return;

        // Update target Position
        targetPosition = GetTargetPosition();

        switch (followMode)
        {
            case (FollowMode.SmoothDamp):
                cameraTransform.position = Vector3.SmoothDamp(
                    cameraTransform.position,
                    targetPosition,
                    ref smoothDampVelocity,
                    smoothDampTime,
                    Mathf.Infinity,
                    deltaTime);
                break;

            case (FollowMode.SmoothLerp):
                cameraTransform.position = Vector3.Lerp(
                    cameraTransform.position,
                    targetPosition,
                    deltaTime * lerpSpeed);
                break;

            case (FollowMode.Exact):
                cameraTransform.position = targetPosition;
                break;
        }

        // Look at target
        cameraTransform.LookAt(targetTransform);
    }

    private Vector3 GetTargetPosition()
    {
        Vector3 exactTarget = targetTransform.position + offset;

        switch (targetMode)
        {
            case TargetMode.Exact:
                return exactTarget;

            case TargetMode.SimpleMovingAverage:
                // Dequeue oldest point
                Vector3 oldestPosition = exactTargetPositionsBuffer.Dequeue();
                // Enqueue new point
                exactTargetPositionsBuffer.Enqueue(exactTarget);
                return targetPosition + (exactTarget - oldestPosition) / simpleMovingAverageRange;

            case TargetMode.ExponentialSmoothing:
                return exponentialSmoothingRatio * exactTarget
                    + (1.0f - exponentialSmoothingRatio) * targetPosition;

            default:
                return exactTarget;
        }
    }
}

I does look better indeed! However in my case this is not really usable as the jitter is too strong, even with fine-tuning. I think I am still looking for a method for syncing exactly VFX Graph with a FixedUpdate camera.

I think it is entirely possible to get this working smoothly in LateUpdate() where it should be. The jitter that is still visible might be from cameraTransform.LookAt() - you also need to use a similar strategy [smoothDamp, min/max angle etc.] for the turning. Then it should be working smoothly in LateUpdate().

Easiest solution I can think of right now: Use a parent object for the camera. Have your camera at the desired distance from the object. That way you can use Mathf.SmoothDamp to control the rotation and distance of the camera. The parent transform then only needs to follow the characters position [Using any method that works in LateUpdate()]

Your average position is also only using the oldest and latest position and dividing that by 3. If it works it works… you can also try the true average. Have a buffer of X size - add the latest position, remove the oldest, add them all up and divide that by X. All updated in LateUpdate() of course.

I tried having an intermediate GameObject parenting the camera, this is actually my initial setting (in earlier posts, I work on a sandbox project for testing). This is something I would advise generally as it simplifies calculations for a follow camera with a focal point slightly off the target. However I do not see why it would solve the issue of having a smooth camera following a target but both of them updated at different rates (see for instance here for an explanation of what I mean). I believe inserting a parent GameObject between both does not change this fact, regardless of the rate the parent is updated.

About the simple moving average code, you can change the buffer size using simpleMovingAverageRange in the code above. This is the variable you called X. The code does exactly what you described with “true average” but with a lesser computational cost. See for instance the wikipedia page of Moving Average for the maths.

My bad - your way of getting the average is definitely the right way, and the more efficient way. I am looking into this - going to post some results later about getting that ‘Exact Follow’ working. There is a caveat though - some jitter is expected from the camera speed itself. If it is going fast enough things are generally going to jitter dependent on the relative motion to the camera. But It should not be from the RigidBody.

Before anything, in the VFX Graph Update Mode - ‘Fixed Delta Time’ needs to be unchecked. Else it will jitter because of the slower update method. I am using an anchor [parent object], it only represents the position the camera is trying to reach.

The RigidBody:
Extrapolate, Mass - 1, LinearDampening -1, No Gravity. When no force is applied I set LinearDampening to a higher value [5] to stop the RigidBody sooner.

In the camera script:

//Set the direction of the camera movement
        Vector3 directionVector = targetTransform.position - cameraAnchor.transform.position;
        float distance = directionVector.magnitude;

        float max = 0.01f;//The follow threshold
        if (directionVector.magnitude > max)//Normalize if above threshold
            directionVector /= distance;
        else
            directionVector /= max;//Else normalize to threshold

        if (directionVector.magnitude > 0.00001f)//Some tolerance to stop moving at
            cameraAnchor.transform.Translate(directionVector * Time.deltaTime * targetRb.linearVelocity.magnitude, Space.World);

Produces no jitter on my end - please post if this also works for you.
This is more or less a lazy follow with a small distance. This will also overshoot the target if the RigidBody stops too suddenly causing a short jitter. If the RigidBody does have to suddenly or very quickly stop - the overshoot would need some additional logic - maybe a different approach would work better.

When doing any additional movement on the camera it is just nice to have it on its own coordinate system i.e. the local transform. Makes it easier when using a camera distance offset or applying camera shakes, but it does not really matter. The movements all need to be smoothed out using deltaTime in LateUpdate().

This solution does not produce a smooth follow in the sense that I am referring to, maybe I should have clarified it earlier. By smooth, I mean following the target position at a gradually decreasing speed when this target gets closer (see for instance my second post with the Lerp solution). In your solution, the camera abruptly stops when the rigidbody stops.

Maybe I should give an example as to why I believe there is a fundamental issue with this kind of smooth movement performed in Update and a target moving in FixedUpdate :

  • Let us assume the game runs at 150 FPS with fixedDeltaTime = 0.02
  • Then, each second, 150 Updates are performed and 50 FixedUpdates
  • This means that the target moves once and then the camera moves three times to follow it
  • The first move of the camera is covering a distance d1 which depends on the distance D1 between the camera and the target
  • The second move of the camera is covering a distance d2 which depends on the distance D2 between the camera and the target
  • Because the target did not move between the first and the second move, we have D2 < D1
  • However, the smooth movement I defined earlier is a decreasing function of the distance between camera and target, hence d2 < d1
  • The same way, we have d3 < d2 < d1
  • In other words, when the target moves once, the camera moves 3 times, and the covered distance is decreasing each times
  • IMO, this is why we always have jitter with this kind of smooth camera movement on Update combined with a target moving on FixedUpdate

Now this explanation is not real but the fundamental issue is that both Update and FixedUpdate are not synced (see for instance the FixedUpdate Manual).

It it behaves almost like a normal smooth/lazy follow - it just has tight tolerances:
SmoothFollow
No jitter here. And yes, the camera does stop moving abruptly once the Rigid has no velocity. It is not a complete solution.

Here is Cinemachine set to LateUpdate(). In this case it works perfectly [same rigid configuration]:
CineMachine
No jitter here either.

It depends on what exactly the goal is. There are situations where Cinemachine won’t work - and thankfully getting ones own camera ‘Follow’ up and running does work. How fast are your RigidBodies going?