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

3 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

sorry for necro-ing, but for me what worked is actually setting it to RunService.Heartbeat while RunService.Stepped completely ruined my camera. The difference between these is that Heartbeat runs AFTER the physics simulation while Stepped runs before it. I’m not particularly sure why it fixed my camera in particular but it did!

So, for anybody finding this topic, try setting your camera loop to RunService.Heartbeat instead of RenderStepped or Stepped and see if that fixes it

Hi, i have encountered a similar issue not too long ago, except with a viewmodel. The fix for me was similar to what @SubtotalAnt8185 said in his post:

Changing the update frame can cause a difference with systems like these, and both RenderStepped and Stepped run in different times than we want (before rendering the frame), but what you want is changing it to PreRender, which will make all the calculations happen before rendering the following frame.

RenderStepped is actually the same as PreRender, but it has been deprecated, and was replaced by PreRender.

RenderStepped in Roblox Documentation

RenderStepped

Fires every frame, prior to the frame being rendered.

Migration Note

This event has been superseded by PreRender which should be used for new work.

Stepped runs every frame prior to the physics calculation, which is not the environment we want the calculations to run in.

Stepped in Roblox Documentation

Stepped

Fires every frame, prior to the physics simulation.

Migration Note

This event has been superseded by PreSimulation which should be used for new work.

This is not a guaranteed fix for your particular issue, but for me when the viewmodel jittered in a similar way it worked, so give it a try maybe it works. :smile:

Let me know if something isnt clear, my main language isnt english.

1 Like

Yes, that makes perfect sense as the camera system I’m using appears to set the camera at a different time. The new names for these events are very intuitive now.

This doesn’t work for me, i’ve looked through pretty much every devforum post with a remotely similar issue however the viewmodel still jitters forward and back when im moving in a linear way. Sorry for necro but if anybody else had this issue and nothing on the forum fixed it i would like to know what did.

1 Like

Hello! I have posted a dev forum about jittering cameras before, and I believe that I have the solution to fix the issue.

  1. Use RunService.Stepped or RunService.Heartbeat instead of RunService.RenderStepped. Based on my perspective, each RunService function obviously has different runtimes.
  2. If that didn’t solve the issue, try using an exponential interpolation equation. I utilized this for my custom OTS camera, and it surprisingly worked.
function Exponential(deltaTime: number, smoothness: number)
    return 1 - 0.01^(deltaTime * (smoothness or defaultSmoothness))
end

Unlike applying a linear function/equation, which results in a straight line, exponential equations, by their nature, have a rate of change in speed until they reach a certain x-y coordinate. This makes the exponential equation reasonably smoother.

If these do not work, try utilizing a Spring module, though I find this solution smoother. For me, try fixing the smoothness value, and you may find your sweet spot.

3 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.