Saturday, April 11, 2015

Smoothing Functions

Probably the most common feedback during development is softening camera movement. This commonly applies to tracking camera target height or relative angular offsets among many other things.

I've implemented this so many times that I've settled on a specific function that gives me pretty good control of the smoothing and doesn't cause any bumps when settling with reasonable parameters.

Here's how it looks:



The red line is the target value. Each black dash represents one frame step for the value.

To show the components of the function that work together I'm breaking up the solution. Let's start with simply applying an acceleration limit to the difference from the start value to the target value:

We have a value that is updating, a rate of change for that value that is the speed and a target value. Each update we pass in the delta time (update interval in seconds). In these graphs the initial speed is zero to illustrate the functions, the value can already be moving and the target value can change each update.


float deltaValue = target - value
float acceleration = deltaValue/deltaTime-speed
if (acceleration > maxAcc*deltaTime)
  acceleration  = maxAcc * deltaTime
else if (acceleration < -maxAcc*deltaTime)
  acceleration = -maxAcc*deltaTime
speed += acceleration
value += speed * deltaTime.

This works great for the beginning of the curve but will overshoot and swing back and forth a bit at the end:

Accelerating towards a target value

The bumps and extreme change can be dampened by filtering the speed and introducing a dampening value that is less than and near one:

speed = acceleration + damp * speed

This results in a nicer curve but that still has a bump:

Acceleration with a damping function

Let's focus on the tail of the curve. This introduces a gain parameter and ignores acceleration. The gain value is less than one and close to zero:

float deltaValue = target - value
speed = gain * deltaValue / deltaTime;
value += speed * deltaTime.

The result is a nice tail but the acceleration is harsh: 

Scaling the difference of a value towards a target value

If the frame rate is not constant or the game runs at both 60 and 50 the gain value can be expressed as 1-pow(1-gain_one_second, deltaTime) instead.

Now the individual components are defined and the complete smoothing function can be put together. We add a detection for whether the function is accelerating or decelerating to determine whether or not to apply gain instead of acceleration:

float delta = target-value;
float acceleration = delta/deltaTime - speed
if (acceleration > maxAcc*deltaTime)
  acceleration  = maxAcc * deltaTime
else if (acceleration < -maxAcc*deltaTime)
  acceleration = -maxAcc*deltaTime
speed = acceleration + damp*speed
if ((speed*delta)<0 && abs(speed*deltaTime)>abs(gain*delta))
  speed = gain*delta/deltaTime
value += speed * deltaTime

Which is represented in the graph at the top of this post.

If the smoothing is dealing with an angle or other cyclical value the function needs to apply wrapping to the delta and the result:

float Wrap(value, low, high) {
  float w = fmodf(value-low, high-low)+low;
  return w>=low ? w : (w+high-low);
}

Inserting "Wrap" into the smoothing function:

float delta = Wrap(target-value, -PI, PI)
float acceleration = delta/deltaTime - speed
if (acceleration > maxAcc*deltaTime)
  acceleration  = maxAcc * deltaTime
else if (acceleration < -maxAcc*deltaTime)
  acceleration = -maxAcc*deltaTime
speed = acceleration + damp*speed
if ((speed*delta)>=0 && abs(speed*deltaTime)>abs(gain*delta))
  speed = gain*delta/deltaTime
value = Wrap(value + speed * deltaTime, -PI, PI)

And it would look something like this if the closest delta crossed the boundary:

Smoothing function with wrapping around the limits

No comments:

Post a Comment