Camera jitter even when using CFrame:Lerp

I have been refactoring code from a Kart Racing game I made with the intent of creating an Authoritative Networking solution, So far everything has been working fine, except there is one problem I have been having,

I’ve noticed this very apparent jitter that appears on the physics handler for the kart whenever I try to manipulate the camera, and it does not appear to be the camera itself, but it appears to be the part itself snapping back and forth

Here’s a video example of what I’m talking about

If you watch the ground and the static parts around the baseplate you can see that those are moving smoothly, so I can deduce that this has to do with moving, unanchored parts running in the physics engine

--Just to be safe, here's my code for camera manipulation
runService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value - 1, function(dt)
	--This value is an attribute I added to the code to be able to test smoothing rates
	local smoothing = script:GetAttribute("Smoothing")
	if target == nil then
		--This simply finds whatever physical object is controlled by the player
		getTarget()
	else
		if target:FindFirstChild("Direction") ==  nil then
			target = nil
			return
		end
		
		--Direction is a part inside of the target rig, it should always be in the center of the ball
		local direction = target.Direction
		
		--Physics is a part inside of the Target rig, it should be a (5,5,5) ball
		local physics = target.Physics
		
		--Get the goal CFrame, which should be behind the target and slightly above
		local position = direction.CFrame.Position - (direction.CFrame.LookVector * 10)
		local goalCF = CFrame.new(position + Vector3.new(0, 3, 0), direction.CFrame.Position)
		
		--Lerp using a smoothing value
		camera.CFrame = camera.CFrame:Lerp(goalCF, dt * smoothing)
	end
end)

Is this just an artifact of Roblox’s physics engine? Or is there something wrong with my code?

4 Likes

Try changing from a RenderStepped loop to a Stepped loop via RunService.Stepped.

2 Likes

Seems good, I believe the issue is the visualization with the black box part representing the sphere, how are you doing it for that?

Also perhaps you can look at similar code pieces, I believe this system also uses a physics sphere to handle bike movement

1 Like

If the code that updates the position of the kart isn’t synced to RenderStepped then you’ll get this phenomenon. It makes sense if you think carefully about it. The gist of the explanation is that even though both the kart and the cam are moving “smoothly” at their respective update frequencies, because those frequencies are different it appears that the kart isn’t at the center of the screen a lot of the time. It “drifts” a bit every time the camera updates but the kart doesn’t, and when the kart finally does update it snaps back into the correct relative position. If you remove the camera smoothing then it should actually get better, because if the camera is just at some fixed offset from the kart then it will look like the camera updates at the slower physics frame rate but still in lock step with the kart, so it never drifts relative to it and so there’s no “snapping back”.

You fix it by interpolating between physics frames in your render loop. Sorry, can’t give code examples as I’m actually trying to figure this out for myself right now for my own game :sweat_smile:

https://gabrielgambetta.com/entity-interpolation.html

2 Likes

Generated by ChatGPT:

The jittering you are experiencing could be caused by the fact that the position and orientation of the kart is being controlled both by the physics engine and by the camera’s CFrame, which could lead to conflicts and inconsistencies. This is a common problem in games that use physics-based movement.

One way to solve this issue is to use the “steering” approach, where the camera doesn’t directly control the orientation of the kart, but instead gives hints to the physics engine about where the kart should go. This can be done by applying forces or impulses to the kart’s rigidbody, using the camera’s direction as a guide.

Here’s an example of how you could modify your camera script to use this approach:

runService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value - 1, function(dt)
	local smoothing = script:GetAttribute("Smoothing")
	if target == nil then
		getTarget()
	else
		if target:FindFirstChild("Direction") ==  nil then
			target = nil
			return
		end

		local direction = target.Direction
		local physics = target.Physics
		
		local targetPosition = direction.CFrame.Position - (direction.CFrame.LookVector * 10)
		local targetVelocity = (targetPosition - physics.Position) / dt

		local force = (targetVelocity - physics.Velocity) * physics:GetMass()
		physics:ApplyForce(force)

		local goalCF = CFrame.new(targetPosition + Vector3.new(0, 3, 0), direction.CFrame.Position)
		camera.CFrame = camera.CFrame:Lerp(goalCF, dt * smoothing)
	end
end)

In this modified script, we calculate the desired velocity of the kart based on the camera’s target position and apply a force to the kart’s rigidbody to match that velocity. This should result in smoother and more consistent movement, as the physics engine is responsible for maintaining the position and orientation of the kart, while the camera just provides guidance.

Note that this solution may not work; it is just a suggestion for you to fix the code. I did not test those codes or methods before posting this. Feel free to reply to this thread and ask for more help if needed.

1 Like

The black box tracks the ball using an AlignPosition

1 Like

The camera does not affect steering at all, like I said all of the physics are handled on the server using an Authoritative Networking solution.

The only thing the client does is track the ball and pass inputs to the server

1 Like

I can’t stop laughing at this.


I’ve worked on my FPS game for the past few days so I finally know what is causing this!

change this to just smoothing then increase smoothing. Although it will be incompatible with variable framerates, it should work. What I did in my FPS game is I used SpringService to get the smoothness.

So instead of using delta time, get a fixed distance between you and the kart? Wouldn’t that just make the camera more rigid and jittery?

I’m assuming SpringService is a module not unlike what Toyful games used for their Indie Game on Switch?

The difference is that the framerate is variable, meaning that it will go the same speed if you use a fixed alpha.

Exactly. Here’s the code for the module:

local physics = {}

do
  	physics.spring = {}
  	do
    	local spring = {}
    	physics.spring = spring
		local e = 2.718281828459045
    	function spring.new(init)
      		local null = 0 * (init or 0)
      		local d = 1
      		local s = 1
      		local p0 = init or null
      		local v0 = null
      		local p1 = init or null
      		local t0 = os.clock()
      		local h = 0
      		local c1 = null
      		local c2 = null
      		local self = {}
      		local meta = {}
      		local function UpdateConstants()
        		if s == 0 then
          			h = 0
          			c1 = null
          			c2 = null
        		elseif d < 0.99999999 then
          			h = (1 - d * d) ^ 0.5
          			c1 = p0 - p1
          			c2 = d / h * c1 + v0 / (h * s)
        		elseif d < 1.00000001 then
          			h = 0
          			c1 = p0 - p1
          			c2 = c1 + v0 / s
        		else
         			h = (d * d - 1) ^ 0.5
          			local a = -v0 / (2 * s * h)
          			local b = -(p1 - p0) / 2
          			c1 = (1 - d / h) * b + a
          			c2 = (1 + d / h) * b - a
        		end
      		end
      		local function Pos(x)
        		if x < 0.001 then
          			return p0
        		end
        		if s == 0 then
          			return p0
        		elseif d < 0.99999999 then
          			local co = math.cos(h * s * x)
          			local si = math.sin(h * s * x)
          			local ex = e ^ (d * s * x)
          			return co / ex * c1 + si / ex * c2 + p1
        		elseif d < 1.00000001 then
          			local ex = e ^ (s * x)
          			return (c1 + s * x * c2) / ex + p1
        		else
         			local co = e ^ ((-d - h) * s * x)
          			local si = e ^ ((-d + h) * s * x)
          			return c1 * co + c2 * si + p1
        		end
      		end
      		local function Vel(x)
        		if x < 0.001 then
          			return v0
       			end
        		if s == 0 then
          			return p0
        		elseif d < 0.99999999 then
          			local co = math.cos(h * s * x)
          			local si = math.sin(h * s * x)
          			local ex = e ^ (d * s * x)
          			return s * (co * h - d * si) / ex * c2 - s * (co * d + h * si) / ex * c1
        		elseif d < 1.00000001 then
          			local ex = e ^ (s * x)
          			return -s / ex * (c1 + (s * x - 1) * c2)
        		else
          			local co = e ^ ((-d - h) * s * x)
          			local si = e ^ ((-d + h) * s * x)
          			return si * (h - d) * s * c2 - co * (d + h) * s * c1
        		end
      		end
      		local function PosVel(x)
        		if s == 0 then
          			return p0
        		elseif d < 0.99999999 then
          			local co = math.cos(h * s * x)
          			local si = math.sin(h * s * x)
          			local ex = e ^ (d * s * x)
          			return co / ex * c1 + si / ex * c2 + p1, s * (co * h - d * si) / ex * c2 - s * (co * d + h * si) / ex * c1
        		elseif d < 1.00000001 then
          			local ex = e ^ (s * x)
          			return (c1 + s * x * c2) / ex + p1, -s / ex * (c1 + (s * x - 1) * c2)
        		else
          			local co = e ^ ((-d - h) * s * x)
          			local si = e ^ ((-d + h) * s * x)
          			return c1 * co + c2 * si + p1, si * (h - d) * s * c2 - co * (d + h) * s * c1
        		end
      		end
			UpdateConstants()
      		function self.GetPosVel()
				return PosVel(os.clock() - t0)
      		end
      		function self.SetPosVel(p, v)
        		local time = os.clock()
        		p0, v0 = p, v
        		t0 = time
				UpdateConstants()
      		end
      		function self:Accelerate(a)
        		local time = os.clock()
				local p, v = PosVel(time - t0)
        		p0, v0 = p, v + a
        		t0 = time
				UpdateConstants()
      		end
      		function meta:__index(index)
        		local time = os.clock()
        		if index == "p" then
					return Pos(time - t0)
        		elseif index == "v" then
					return Vel(time - t0)
        		elseif index == "t" then
          			return p1
        		elseif index == "d" then
          			return d
        		elseif index == "s" then
          			return s
        		end
      		end
      		function meta:__newindex(index, value)
        		local time = os.clock()
        		if index == "p" then
					p0, v0 = value, Vel(time - t0)
        		elseif index == "v" then
					p0, v0 = Pos(time - t0), value
        		elseif index == "t" then
					p0, v0 = PosVel(time - t0)
          			p1 = value
        		elseif index == "d" then
          			if value == nil then
            			warn("nil value for d")
            			warn(debug.stacktrace())
            			value = d
          			end
					p0, v0 = PosVel(time - t0)
          			d = value
        		elseif index == "s" then
          			if value == nil then
            			warn("nil value for s")
            			warn(debug.stacktrace())
            			value = s
          			end
					p0, v0 = PosVel(time - t0)
          			s = value
        		elseif index == "a" then
					local p, v = PosVel(time - t0)
          			p0, v0 = p, v + value
        		end
        		t0 = time
				UpdateConstants()
      		end
			return setmetatable(self, meta)
    	end
	end
end

return physics