# Short form guide to solving physics-based spring jitter!

Hey guys, seen a lot of confusion around camera springs as it relates to physics jitter, just want to clear up this issue.

The main problem: This is an infographic of the roblox render pipeline, most configurations of camera springs use .RenderStepped which updates earlier than physics, creating a jitter effect you can see in this video: unknown_2021.11.21-17.41

The fix:

If you update camera movement on .Stepped instead, it will be in sync with the physics updates.
(don’t worry if you don’t understand the math jargon which I borrowed from several kind and smart robloxians mentioned below)
Hope this helps!

``````--[[
modified by SaltyPotter to include orientation
borrowed math and ideas from: @fractality, @Validark, @Quenty, @XAXA, @EgoMoose

use cases:
Camera following anything physics-based (without jitter issues):

---- I'm using dictionaries to send CFrame information ----

local position={RightVector=startCF.RightVector, UpVector=startCF.UpVector, Position=startCF.Position}
local velocity={RightVector=Vector3.new(), UpVector=Vector3.new(), Position=Vector3.new()}
local goal={RightVector=goalCF.RightVector, UpVector=goalCF.UpVector, Position=goalCF.Position}
local spring=springModule.new(position, velocity, goal)

---- you can adjust the frequency and dampness to your liking, i found a 2:1 profile is a nice smooth effect ----

spring.frequency=2
spring.dampener=1

runservice.Stepped:Connect(function(t, dt)
---- update the goal every step ----
spring.goal={RightVector=goalCF.RightVector, UpVector=goalCF.UpVector, Position=goalCF.Position}
---- call :update(), it returns a CFrame ----
camera.CFrame=spring:update(dt)
end)

other:
same as above, but you can use .RenderStepped since there's no physics involved
]]

local spring={}
spring.__index=spring

local tau = math.pi * 2
local exp = math.exp
local sin = math.sin
local cos = math.cos
local sqrt = math.sqrt

local EPSILON = 1e-4

function spring.new(position:{},velocity:{},goal:{})
setmetatable({},spring)
spring.position=position
spring.velocity=velocity
spring.goal=goal
spring.frequency=10
spring.dampener=1
return spring
end

local dampingRatio = self.dampener
local angularFrequency = self.frequency * tau
local goal = self.goal[key]
local p0 = self.position[key]
local v0 = self.velocity[key]

local offset = p0 - goal
local decay = exp(-dampingRatio * angularFrequency * dt)
local position

if dampingRatio == 1 then -- Critically damped
position = (offset * (1 + angularFrequency * dt) + v0 * dt) * decay + goal
self.velocity[key] = (v0 * (1 - angularFrequency * dt) - offset * (angularFrequency * angularFrequency * dt)) * decay
elseif dampingRatio < 1 then -- Underdamped
local e = 1 - dampingRatio * dampingRatio
local c = sqrt(e)
local y = angularFrequency * c
local i = cos(y * dt)
local j = sin(y * dt)

-- Damping ratios approaching 1 can cause division by small numbers.
-- To fix that, group terms around z=j/c and find an approximation for z.
--    z = sin(dt*angularFrequency*c)/c
-- Substitute a=dt*angularFrequency:
--    z = sin(a*c)/c
-- Take the Maclaurin expansion of z with respect to c:
--    z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6)
--    z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120
-- Rewrite in Horner form:
--    z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6

local z
if c > EPSILON then
z = j / c
else
local a = dt * angularFrequency
local a_2 = a * a
z = a * (((e*e * a_2 - 20*e) / 120) * a_2 + 1)
end

-- Frequencies approaching 0 present a similar problem.
-- We want an approximation for y as angularFrequency approaches 0, where:
--    y = sin(dt*angularFrequency*c)/(angularFrequency*c)
-- Substitute b=dt*c:
--    y = sin(b*c)/b
-- Now reapply the process from z.

if y > EPSILON then
y = j / y
else
local b = y * y
local dd = dt * dt
y = dt * (dd * (b*b*dd / 20 - b) / 6 + 1)
end

local ze = z * dampingRatio
position = (offset * (i + ze) + v0 * y) * decay + goal
self.velocity[key] = (v0 * (i - ze) - offset * (z * angularFrequency)) * decay
else -- Overdamped
local x = -angularFrequency * dampingRatio
local y = angularFrequency * sqrt(dampingRatio * dampingRatio - 1)
local r1 = x + y
local r2 = x - y

local co2 = (v0 - offset * r1) / (2 * y)
local co1 = offset - co2

local e1 = co1 * exp(r1 * dt)
local e2 = co2 * exp(r2 * dt)

position = e1 + e2 + goal
self.velocity[key] = e1 * r1 + e2 * r2
end

self.position[key] = position
end

function spring:update(dt:number)
local t={}
for key,_ in self.goal do
t[key]=self.position[key]
end
return CFrame.fromMatrix(t.Position,t.RightVector,t.UpVector):Orthonormalize()
end

return spring
``````
16 Likes

Can you also provide a video that compares the common implementation versus the fix to the jitter?

1 Like

Sure, so commonly it’s a critically damped spring but the issue is with using RenderStepped dt or heartbeat dt. (post was meant to be more of a copy paste fix, didn’t want to go too in depth or post a lot of vids)

but here’s some test vids (i’m using gyazo which is running at 30fps so might be kinda hard to tell but i’ll point out the issues)

Heartbeat test (character jitters)

RenderStepped test (part jitters)

Stepped test (neither jitter)

7 Likes

Btw if anyone still has jitter problems after using stepped make sure adaptive time stepping is off that seems to make it jitter.

1 Like

There’s… zero explanation here. “The problem is springs using RenderStepped” and the fix is just a giant code block. What? That’s not a real solution. Could you explain exactly, in the OP, what the fix is to the problem and how it fixes it instead of just dumping code - or if the explanation is somewhere in the OP, could you make it more clear?

6 Likes

So there’s no code explanation needed here, it’s just a switch of events. The critically damped spring modules are open-sourced people can figure that out on their own, not going to go in-depth explaining Hooke’s law here, this was to be a short form guide on the issue and a copy-paste solution. Just wanted to help others. Hope I did.

I think what he was asking is why does updating the spring before physics but after rendering remove the jittering, not all the nitty gritty mathematics.

2 Likes

I’m sorry if it was confusing, I intended for it to be fast and easy for everyone, I edited the OP to include the basic idea. This solved the issue for me!, great information.