# Spring dynamics for production

# Introduction

Springs are an extremely useful and fundamental tool for expressing all kinds of dynamics. I’ve used them extensively when working on gameplay and physics code. They are great for doing lerps or transitions (with under/overshoot). They are also the perfect tool for moving rigidbodies around – a damped spring is mathematically equivalent to PD control.

Over the course of a few years the spring dynamics code my colleagues and I were writing at Studio Gobo evolved significantly. This post documents that evolution in the hope that it will be useful for others.

# Spring Implementations

## The basics – a simple spring implementation

Here is a simple spring implementation that follows directly from the physics of a spring:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos; // dynamic state Vector3 m_pos, m_vel; // spring param - spring constant float m_kp; // spring param - damping constant float m_kd; void Update( const float dt ) { // compute spring and damping forces Vector3 Fspring = m_kp * (m_targetPos - m_pos); Vector3 Fdamp = m_kd * -m_vel; // integrate dynamics m_pos += m_vel * dt; m_vel += (Fspring + Fdamp) * dt; } }; |

The spring has a target position and a spring force is computed as a spring constant multiplied by the offset from the current position and the target position (line 16). Typically we don’t want the spring to oscillate forever (or at all), so we dampen the system by applying a force proportional to the velocity (line 17). Finally the dynamic state (pos and vel) are updated on lines 20 and 21.

There are a few notes to get out of the way now before moving on to the fun stuff. Firstly, the code operates on vectors but could work on floats just as easily (the class could be templated on data type). The target position is an input to the spring and can be updated each frame as desired. Secondly, I never bother modelling mass explicitly. It could be added – one would then divide the forces by the mass before adding to the vel. I have not found it useful to have this parameter in the past, and have instead relied on the other spring parameters to get the desired behaviour. Finally, updating the position before the velocity may seem counter-intuitive. I wrote about getting update logic nailed here.

## Set a target velocity

The basic spring code misses an important trick – it implicitly assumes the target velocity is 0. If the spring is attached to a moving target, the position will always lag behind the target, because the spring is aiming to arrive at the target position with 0 velocity. On the other hand if you prescribe a target velocity as well as position, the point will track the target closely. We can add a little bit more code to unlock this important functionality:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos, m_targetVel; // dynamic state Vector3 m_pos, m_vel; // spring param - spring constant float m_kp; // spring param - damping constant float m_kd; void Update( const float dt ) { // compute spring and damping forces Vector3 Fspring = m_kp * (m_targetPos - m_pos); Vector3 Fdamp = m_kd * (m_targetVel - m_vel); // integrate dynamics m_pos += m_vel * dt; m_vel += (Fspring + Fdamp) * dt; } }; |

Note the nice symmetry between in the two force calculations. This spring is well suited for smoothing/filtering the position of a moving target where the velocity is known or can be estimated. This parameter is also required if the frame of reference is moving relative to the physics world. If the spring should work inside a spaceship, the target velocity should include the spaceships velocity.

## Improve parameters – replace spring constant with oscillation frequency

The relatively meaningless spring constant *m_kp* can be formulated in terms of a physically meaningful parameter: Undamped Angular Frequency (UAF). This value has strong meaning – it is the frequency that the spring will oscillate at. This is a value that anyone can understand and is much nicer than an arbitrary constant. It is used as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos, m_targetVel; // dynamic state Vector3 m_pos, m_vel; // spring param - Undamped Angular Frequency (UAF) - oscillation frequency float m_UAF; // spring param - damping constant float m_kd; void Update( const float dt ) { // compute spring and damping forces Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - m_pos); Vector3 Fdamp = m_kd * (m_targetVel - m_vel); // integrate dynamics m_pos += m_vel * dt; m_vel += (Fspring + Fdamp) * dt; } }; |

## Improve parameters – normalize the damping parameter

Now we turn attention to the second relatively meaningless parameter – the damping constant *m_kd*. Each time the spring strength/frequency UAF changes, the damping term needs to be rebalanced to maintain the desired undershoot/overshoot behaviour. This is bad news for usability/workflows and is easily fixed by changing the parameter to be Damping Ratio (DR). This has nice properties – a value of < 1 gives an underdamped spring that will overshoot, a value of > 1 gives an overdamped spring that will reach the target slowly, and a value of exactly 1 is the magic critical damping, where the target will be reached as fast as possible without overshooting.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos, m_targetVel; // dynamic state Vector3 m_pos, m_vel; // spring param - Undamped Angular Frequency (UAF) - oscillation frequency float m_UAF; // spring param - Damping Ratio (DR) float m_DR; void Update( const float dt ) { // compute spring and damping forces Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - m_pos); Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_vel); // integrate dynamics m_pos += m_vel * dt; m_vel += (Fspring + Fdamp) * dt; } }; |

Now a designer/artist/programmer needs only to decide how much overshoot/undershoot they want the spring motion to have and can set this value just once.

## Improve stability – implicit spring update

The update code so far has implemented the Euler method. It moves the state forward in time using only the current state, and is known as an explicit update. It is simple an easy to implement, but is known to add energy to the system and is not stable if a spring is sufficiently stiff and/or the update time steps are not small enough.

Implicit methods are slightly more advanced, and significantly more stable. As noted above, damped springs are closely related to PD control, and a stability result from control literature applies here. Tan et al. introduced Stable PD control which is stable for stiff dynamics or large timesteps [1]. The idea is to compute the forces based on the state at the next time step instead of the current, where this future state is predicted using a first order Taylor series. After solving for acceleration this boils down to a moderate amount of additional code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos, m_targetVel; // dynamic state Vector3 m_pos, m_vel; // spring param - Undamped Angular Frequency (UAF) - oscillation frequency float m_UAF; // spring param - Damping Ratio (DR) float m_DR; void Update( const float dt ) { // compute spring and damping forces Vector3 posNextState = m_pos + dt * m_vel; Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - posNextState); Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_vel); // integrate dynamics m_pos += m_vel * dt; m_vel += (Fspring + Fdamp) * dt / (1.0f + dt * 2.0f * m_DR * m_UAF); } }; |

To see the effect of the implicit update, we first study the case of an underdamped spring. The reference (correct) behaviour is plotted in gray, the normal explicit Euler spring update is plotted in dashed white, and the new implicit update is plotted in green:

Both implicit and explicit are overshooting here. However of the two, the implicit update maintains the correct frequency better than the explicit update which oscillates at a higher frequency and drifts away from the reference solution.

A more serious issue occurs when we increase the damping ratio. Again the implicit update over shoots significantly but comes to a rest shortly after. The explicit update on the other hand is adding too much energy to the system and has become unstable:

If we increase the damping constant further the explicit update quickly explodes. On the other hand the implicit update is unconditionally stable.

The diagrams in this post are running live as a ShaderToy here [2].

## Improve robustness – clamp or fix timesteps

The implicit update above stops the dynamics from exploding. However it does not guarantee that the behaviour of the spring will be preserved. One can see there is a significant delta between the simulated (green) and the reference solution (gray):

This behaviour will depend on the frame rate, which is a recipe for confusion and potentially disaster when working on a project to tight deadlines. I have frequently seen designers running dev builds on a mediocre PC or laptop and tweaking the game at mid to low frame rates, only to find the dynamics feel completely different at target frame rate. A typical symptom of a frame rate dependency is the dynamics feeling loose and swimmy at low frame rates. For an iron clad guarantee that behaviour is maintained at all times, we may run the spring at a constant, fixed dt. In practice, it may be enough to run with a dt that is less than a max allowable time step.

Note that if the spring is running in the physics update it is likely to already be using fixed dts. If there are issues with the spring dynamics in this scenario, then one might run the spring dynamics at a higher frequency such as 120Hz.

This section will present substepping code to exert control over the dt / simulation frequency.

### 1. Enforce a max dt per substep, allow mixed dts

This will ensure the update dt is less than or equal to a prescribed max. It will step along at the max dt and then deal with any remainder in the last step.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Spring { // ... code from above ... void UpdateWithSubsteps( const float dt ) { const int maxUpdates = 8; const float maxDt = 1.0f/60.0f; int c = 0; float timeLeft = dt; while( timeLeft > 0.0f && c++ < maxUpdates) { float substepDt = min( timeLeft, maxDt ); UpdateSpring( substepDt ); timeLeft -= substepDt; } } }; |

*maxUpdates* is an optional safe guard. I’ve written some considerations for this in the notes section below.

### 2. Enforce a max dt per substep, maintain uniform dts

This variant aims to compute a uniform update dt that is less than a maximum. Each step is simulated with this computed update dt.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Spring { // ... code from above ... void UpdateWithSubsteps( const float dt ) { const float maxDt = 1.0f/60.0f; float stepCountF = ceil(dt / maxDt); float substepDt = dt / stepCountF; int stepCount = (int)stepCountF; const int maxStepCount = 8; stepCount = min( stepCount, maxStepCount ); for( int i = 0; i < stepCount; i++ ) { UpdateSpring( substepDt ); } } }; |

### 3. Enforce a fixed dt

This is probably the most robust of the three options outlined here, as the dt is always fixed and behaviour is always 100% deterministic and consistent. However it’s also the most complex to implement.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
class Spring { // ... code from above ... // the time the spring dynamics needs to be simulated forward float m_timeToSimulate = 0.0f; // the previous dynamic state Vector3 m_prevPos, m_prevVel; // the result state of the spring - the interpolated dynamic state that will be correct for the current frame Vector3 m_currentFramePos, m_currentFrameVel; void UpdateWithSubsteps( const float dt ) { // we want to simulate forward by dt m_timeToSimulate += dt; const int maxUpdates = 8; const float fixedDt = 1.0f/60.0f; int c = 0; while( m_timeToSimulate > 0.0f && c++ < maxUpdates ) { // save pre-update dynamic state m_prevPos = m_pos; m_prevVel = m_vel; // compute a new latest dynamic state. always the same dt. UpdateSpring( fixedDt ); m_timeToSimulate -= fixedDt; } // at this point the spring has been updated beyond the target time. if we did not hit the max update limit, // then m_timeToSimulate will be in (-fixedDt, 0]. Convert this to a lerp alpha: float lerpAlpha = 1.0f + m_timeToSimulate / fixedDt; // if we maxed out on updates above, we won't have managed to simulate up to the current time, in which // case just use latest available state by clamping lerpAlpha lerpAlpha = min( lerpAlpha, 1.0f ); // this is the public facing state of the spring that should be queried/used this frame m_currentFramePos = lerp( m_prevPos, m_pos, lerpAlpha ); m_currentFrameVel = lerp( m_prevVel, m_vel, lerpAlpha ); } // m_currentFramePos is the position that the spring should return to outside code const Vector3& GetPos() { return m_currentFramePos; } // make sure we reset all of the state when poking data in void SetPos( const Vector3& newPos ) { m_currentFramePos = m_prevPos = m_pos = newPos; } }; |

### Misc Notes

*maxUpdates* is an optional safe guard. In general its a good thing to have – if there is a long frame hitch this will eliminate the amount of work that is done, potentially stopping the code from getting into a death cycle of long updates (although it’s unlikely the simple spring update code in the above would do that much damage).

When this update limit is hit then update will abort prematurely, and the spring won’t be simulated all the way forward in time. It may appear like the spring is moving in slow motion. It may also visibly jitter, so any large frame hitches. Ideally one can get all frames within budget and *maxUpdates* will never be hit in shipping/release builds.

One must be careful to make sure the spring is updated exactly the right amount – that the final state of the spring has been simulated forward by *dt*, otherwise the system will be prone to visible jitter. It’s really easy to get this wrong and have spent a bunch of time debugging this in the past. I wrote about jitter issues at length here.

# Conclusion

This post suggest some improvements to a naive/obvious spring implementation. The result gives consistent behaviour, is robust at low framerates, and has parameters that have intuitive meaning and are easily tweaked and maintained.

I would recommend adding substepping to any spring updates that aren’t driven from physics update, to guarantee spring behaviour works at all frame rates. The second substep variant (uniform dts) is probably my favourite due to its simplicity.

# References

[1] Tan J., Liu K., Turk G., Stable Proportional-Derivative Controllers, http://www.jie-tan.net/project/spd.pdf

[2] Bowles H., Implicit Spring, ShaderToy, https://www.shadertoy.com/view/MlB3Dm

# Appendix A – Closed form solution

There is a closed form, analytic solution for the spring dynamics plotted in gray here:

This solution is documented on wikipedia here in the Universal oscillator equation section. In the example plotted above, the target position and velocity do not change and the spring parameters (UAF, DR) are constant, and there are no external forces acting on the spring. In this case one can pick the correct transient solution based on the damping ratio and solve for the constants using the starting conditions (initial position, velocity). This is what is done in the ShaderToy (link). In this simple scenario simulation is not required – the state can be computed directly at an arbitrary time in the future, and not have to worry about frame rate/dts.

Unfortunately in practice we rarely encounter such simple scenarios and it is usually impractical or not beneficial to use a closed form expression for the dynamics.

# Appendix B – Rigidbody control

As mentioned above, damped springs are equivalent to PD control and are a useful tool for controlling physics rigidbodies. The *Spring* variant below takes a reference to a rigidbody, and applies forces to move the rigidbody to the target position/velocity:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Spring { // target for spring, set as desired by user Vector3 m_targetPos, m_targetVel; // rigidbody Rigidbody m_rb; // spring param - Undamped Angular Frequency (UAF) - oscillation frequency float m_UAF; // spring param - Damping Ratio (DR) float m_DR; void PhysicsUpdate( const float dt ) { // compute spring and damping forces Vector3 posNextState = m_rb.Pos() + dt * m_rb.Vel(); Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - posNextState); Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_rb.Vel()); // add acceleration m_rb.Vel() += (Fspring + Fdamp) * dt / (1.0f + dt * 2.0f * m_DR * m_UAF); } }; |

The dynamic state now lives inside the rigidbody. The member variables for position and velocity are no longer required and have been removed.

Line 21 will appear odd – I am applying the acceleration as an impulse/change in velocity, instead of using forces. The result of using forces will be dependent on the RB mass which is not something I’ve been interested in on previous projects. This is partly because the effect of the mass is not trivial – rotational inertia is typically asymmetrical across the axes and is something I’d rather not worry about. This is also in part because on previous projects we have ended up setting the mass to nonphysical values to get the physics to “feel” right and for stability. For this reason I found it useful to insulate the spring motion from the mass. This way, a content author can tweak the motion to her liking without having to worry about the motion changing later if the mass changes.

The downside is that the content author must tweak the UAF to give a feeling of weight to the motion. If this is undesirable, the implementation above could be changed to apply *Fspring* and *Fdamp* to the rigidbody as forces.

# Acknowledgements

The work presented here is the outcome of collaboration between myself and my colleagues at Studio Gobo including Daniel Zimmermann, Chino Noris and Tom Williams.

## Leave a Comment