Jitter in a "Soft" Follow-Cam

I’m attempting to implement a camera that follows a target (the player), and specifically one that is not fixed absolutely to the target, but rather that follows the target’s position smoothly over time.

To some degree I have this working. However, I keep encountering terrible jitter when the camera is near to the target, and perhaps at relatively-low frame-rates (~45fps). And thus far I have not managed to entirely get rid of it. (At least not without incurring some other issue.)

I’ve tried a variety of things–rearranging the order of operations (which did help a bit, I think), clamping the camera’s movement so that it doesn’t overshoot, re-working it to use a velocity-vector, or re-working it to draw from a set of samples over multiple rather than a single value. Thus far to little avail, I fear.

Since the actual code in question is a little complicated (involving multiple files and classes), let me describe my current approach to this in partial pseudocode:
(I’m doing so in part off the top of my head, and so may be mistaken in some of the details. However, I believe the below to be at least broadly accurate.)

updateTask = taskMgr.add(updateMethod, "update")

def updateMethod:
    dt = globalClock.getDt()
    updatePlayer(dt)
    return task.cont

def updatePlayer(dt):
    position += velocity * dt
    updateCamera(dt)

def updateCamera(dt):
    diff = targetPos - cameraPos
    scalar = cameraSpeed * dt
    if scalar > 1:
        scalar = 1
    cameraPos += diff * scalar

Does anyone see a problem with my approach here? Or have suggestions as to something that might fix the issue that I’m seeing?

It is not quite clear what you are doing with this expression.

    if scalar > 1:
        scalar = 1

In fact, you lose data about the time for rendering, hence the inaccurate positioning.

That is to prevent the camera from shooting past the target, presumably. That is one way to do it, but to be sure that that’s not the source of your jitter, you could explicitly setPos to the target in the if case and return.

Have you tried printing out the various values in your task and seeing if you see anything odd when the jitter occurs?

It’s exactly as rdb says: A means of preventing the camera from passing the target. In essence, it should result in the following:

diff = target - camPos
camPos = camPos + diff * scalar
# If scalar = 1:
camPos = camPos + diff * 1 = camPos + target - camPos = target

A good thought.

I’ve just tried it, and I’m afraid that it didn’t help. :/

Yup. I even graphed them in a spreadsheet.

The target seems to be moving just as expected. The camera seems to do so by-and-large–however, it does seem to approach and fall back over and over again.

I just re-did that graphing exercise, as I think that it’s a useful visualisation:


The values on the left represent y-axis values (I tried to keep movement close to the y-axis for the purposes of this experiment).

One line is the target-position, the other is the camera-position–I think that the red line is the camera, to be specific. (I didn’t check before closing the spreadsheet without saving, I’m afraid.)

I think that the period covered is a little under half a second.

I don’t really know whether my input will help, but I have had a similar issue previously with my model shaking around when it’s supposed to be stationary due to the player rotation constantly jumping between two values at once. My solution was to only update the rotation (or in your case, camera position) if there is some sort of user input.

Hmm… In this case the player-character may be in constant motion (as a result of movement without friction–this is set in space). Thus I’m afraid that continuous updates would seem to be called for.

(And the issue does appear even under constant motion, let me clarify.)

Still, I appreciate the input–thank you for your suggestion! :slight_smile:

1 Like

Anytime :slight_smile:

How does your varying value vary? Would it still jitter if you use integers or rounded numbers for getPos()?

Hmm… I would expect that to cause stuttering as the camera jumps from one integer/rounded value to the next.

The varying value–by which I’m guessing that you mean the target-position; please correct me if I’m misunderstanding–moves smoothly using floating-point positions. The camera is intended to follow in the same way.

You’re pretty close to treating your camera as a sprung-particle, which is not a bad way to go. (This dates back to 1989, but is still pretty common in drawing tools to smooth out input , and I still use it fairly often: http://www.graficaobscura.com/dyna/.)

If the ‘spring’ is too strong, you get jitter. If friction’s too low, you get an orbit. As you’re implemented right now, you have no friction, so you’re going to orbit. (I left ‘mass’ out, because it just drops out of the calculations, and I got used to writing this on much slower systems)

def updateCamera(dt):
    diff = targetPos - cameraPos
    cameraVel += diff * spring * dt
    cameraVel *= friction  #if dt varies a lot, you'll want  friction ** dt, and set your friction constant accordingly
    cameraPos += cameraVel * dt  

from there, you now have ‘spring’ (usually in 0.01 … 0.1) and ‘friction’ (usually 0.1 … 0.9) to play with

other things to tweak:

  • round the diff to zero if it’s below some threshold
  • round velocity to zero if it’s below some threshold
  • cap velocity or acceleration to maximum thresholds
  • cap/round in a nonuniform fashion

(disclaimer: this is a fairly different kind of easing. this eases in and eases out… my next reply will have tweaks for your lerping solution)

now, without changing the ‘shape’ of what you’re doing… which now that I look at it is equivalent to

MAX_SPEED = 1

def lerp(a, b, t):
    return a + (b - a) * t

def updateCamera(dt):
    cameraPos = lerp(targetPos, cameraPos,  min(MAX_SPEED, cameraSpeed * dt))

which I’d tweaking MAX_SPEED, which may give a fix — offhand, I’d guess to 0.05 or so

but since you mention low frame-rate… pick an expected dt and normalized for that… though my friday-afternoon brain is forgetting its logs and exponents, so here’s a “solid, and fast enough” plus an “even better, if I’m not making a dumb mistake” approach

avoid-exponent-by-looping

def updateCamera(dt):
for _ in range(dt // EXPECTED_DT):
cameraPos = lerp(targetPos, cameraPos, min(MAX_SPEED, cameraSpeed * EXPECTED_DT))

with-exponents

def updateCamera(dt):
cameraPos = lerp(targetPos, cameraPos, min(MAX_SPEED, cameraSpeed ** log(dt/EXPECTED_DT))

Hmm… I did try a velocity-based approach, and had the same problem…

However, it was pretty much as you say, as I recall: When my friction-value was too low, the camera tended to oscillate or orbit. When my friction-value was higher, the jitter reappeared.

It may perhaps be that I didn’t spend enough time tweaking the friction-value.

That said, too low a friction-value has another consequence: it causes the camera to lag further behind. And I do want the camera to stay fairly close to the target…

(Having it lag further behind introduces gameplay and diegetic-UI issues.)

Hmm… As I mentioned above, lowering the maximum speed too much introduces its own problems, I’m afraid. :/

This is interesting… I’m not sure of what to expect from it. I may try it out later when I return to this specific project.

Thank you for the suggestions, all! :slight_smile:

Okay, I’ve tried the logarithm approach, above, and it doesn’t seem to work well–presuming that I’ve implemented it correctly.

Specifically, I’ve replaced my use of “dt” with math.log(dt/<some value>), with various numbers in place “<some value>”.

If “<some value>” is low enough, then the camera is stable–but glued in place relative to the target. There’s no softness, alas.

If “<some value>” is too high, the value of dt/"<some value>" becomes less than one, and the log-function returns a negative number. This does bad things to the camera. o_o

And finally, middling values seem to vacillate between the two.

I haven’t tried the iterative approach, I’ll confess.

I think that I’ve done it! :smiley:

On another forum, it was suggested that I divide my time-step. At the time I did so, and found that it didn’t help.

But in that attempt I only applied this division to the camera’s update–not to the target’s. It then occurred to me last night (I think that it was) that perhaps applying it to the target–thus changing the relationship between camera and target over the course of the divided time-step–might be important.

I tried it just a short while ago and… it seems to have worked! :smiley:

In short, what I’ve done is as follows: In the semi-pseudocode that I showed above, I’ve changed “updatePlayer” to look something like this:

def updatePlayer(dt):
    newDt = dt

    while newDt > someVerySmallValue:
        # Update both target and camera by a small, fixed increment
        position += velocity * someVerySmallValue
        updateCamera(someVerySmallValue)
        # Keep doing so until the total delta-time for the frame is all-but
        # used up
        newDt -= someVerySmallValue

    # Perform the update one more time, using whatever delta-time remains
    position += velocity * newDt
    updateCamera(newDt)

My thanks to all who participated in this thread–it has been appreciated! :slight_smile: