CFrame:Lerp() camera jittery even with deltaTime

Hey! I’m trying to make the camera follow the player from behind with a bit of “weight” behind it, but it’s noticeably jittery.

I’ve tried every single variation on how to get deltaTime I could think of (heartbeat, renderstepped, directly hooking it up with the camera event) to no avail. I’ve also tried every method I could find for how to use deltaTime, equally to no avail.

Here is the script in question:

game.Workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
local dt = {0, tick()}
rs:BindToRenderStep("Target Camera", 200, function()
	local newcframe = CFrame.new((hrp.CFrame * CFrame.new(0, 5, 12)).p, hrp.Position + Vector3.new(0, 2, 0))
	camera.CFrame = camera.CFrame:Lerp(newcframe, 1 - 0.002 ^ dt[1])
	local newtime = tick()
	dt[1] = newtime - dt[2]
	dt[2] = newtime
end)

I’ve gotten better results having the deltatime update after it is used and not beforehand, so it is not the cause of the problem.

If anyone has any idea at all, please let me know! I’m stumped here.

1 Like

Make sure to set the function RenderPriority to after the camera (which is 200 + 1). Also, remember that RunService provides a deltatime by default.

rs:BindToRenderStep("Target Camera", Enum.RenderPriority.Camera + 1, function(deltaTime)
1 Like

Enum.RenderPriority.Camera + 1 throws an error (it’s an enumitem), so I set it directly to 201.
Still jitters even using the provided deltaTime.

Enum.RenderPriority.Camera.Value + 1

1 Like

Ah, yes, my bad. I forgot that you needed to add a .Value to the enum item to get its numeral value.

1 Like


Problem still persists.

Alright, so I didn’t go in-depth on how your code works. But it might just be the way you’re approaching this.

I would try something like this:

local Camera = workspace.CurrentCamera
Camera.CameraType = Enum.CameraType.Scriptable

function GetTargetCameraCF()
	return CFrame.new((hrp.CFrame * CFrame.new(0, 5, 12)).p, hrp.Position + Vector3.new(0, 2, 0))
end

local CameraCF = GetTargetCameraCF()

RunService:BindToRenderStep("CameraUpdate", Enum.RenderPriority.Camera.Value + 1, function()
	CameraCF = CameraCF:Lerp(GetTargetCameraCF(), .2)
	Camera.CFrame = CameraCF
end)
1 Like

The code you sent does the exact same thing, except it doesn’t take into account framerate, which, as you can see


is still choppy.
Therefore, something has to be done with the deltaTime. I just don’t know what it is. Documentations say to use 1 - (percentage) ^ deltaTime, even unrelated to roblox. And that clearly doesn’t work. This is why I’m so stumped about this…

Edit: The problem comes from :Lerp() being done between the stationary camera and the moving character. The new position of the character isn’t necessarily consistent each frame, which is why some sort of measure such as deltaTime needs to be employed. However I don’t think deltaTime might be the right tool for the job in this situation? Unsure which one would be and how you would go about using it.

The problem isn’t the camera movement but the character controller, you can see the background isn’t choppy. Roblox characters have always been choppy when walking, it’s just that normally the camera is fixed to the character so it’s barely noticeable.

1 Like

I got very close with the following formula:

local oldhrppos = hrp.Position
rs:BindToRenderStep("Target Camera", Enum.RenderPriority.Camera.Value + 1, function(deltaTime)
	local newcframe = CFrame.new((hrp.CFrame * CFrame.new(0, 5, 12)).p, hrp.Position + Vector3.new(0, 2, 0))
	local a = math.clamp((oldhrppos - hrp.Position).magnitude, 0.1, 0.5)
	local a = a > 0.1 and a or 0.2
	camera.CFrame = camera.CFrame:Lerp(newcframe, a)
	oldhrppos = hrp.Position
end)


Video is extremely zoomed in, but in practice it is very difficult to notice. I would still rather have it follow the character 100% accurately, but if it means I have to make everything else jittery in the process I don’t know if it’ll be worth the effort.

Pretty sure you could do a hacky lerp formula using the MoveDirection property of the Humanoid so you can get perfect results relative to the character, while sacrificing the rest of the background being jittery.

If it’s “hacky” in the same way I’m thinking of it might not be worth the performance hitch.

Is this the desired result?

Because, if so, how sure are you that your camera is staying as the Scriptable enum after the character is added? If that’s the desired result (above) I can share the source. The amount of weight in customizeable my example.

1 Like

I am 100% sure. The code chunk I gave was a very very small piece of a currently ~700 line long script, so if there was anything wacky to do with spawning in, I would have noticed it, haha.

Result above is not what I desire. I am guessing you are using a bodymover on an invisible part? Because if so, I need the camera to really behave like a lerp, like without physics based momentum, if you get what I mean. Regardless I’ve tried bodymovers before and they just didn’t feel quite right.

No, I did not use something hacky like a body mover. It’s pure interpolation.

Result above is not what I desire

Then please explain better what is.“Without physics based momentum” I don’t understand what you’re asking for. Is the “smooth movement” of my camera the only issue with your “desire?”

It’s hard to explain, so I’ll just send you a gif. If you’re able to recreate the same feel using interpolation, let me know.

Like this then? The swing weight and interpolation timeframes are adjustable as variables here. I’m recording at 27fps so excuse the choppiness of gif.

Hmm, it might work. Let me know what it is you’re doing. I’ll try incorporating it, and I’ll let you know whether it feels right or not.

Here’s a full replication (designed to be a throw-in on a blank place).

local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer
local rs = game:GetService("RunService")
local tweenService = game:GetService("TweenService")
local uis = game:GetService("UserInputService")

local char = player.Character or player.CharacterAdded:Wait()
local hrp = char:WaitForChild("HumanoidRootPart")

-- This MUST be done after the character is added
camera.CameraType = Enum.CameraType.Scriptable
char.Humanoid.AutoRotate = false

local swingWeight = 0.5
local dragTimeframe = 125 -- in milliseconds

rs:BindToRenderStep("Target Camera", 201, function()
	-- Replicate shift lock (just for my demonstration)
	local targetPosition = hrp.Position + Vector3.new(0,0,200)
	local cameraBasePosition = (hrp.CFrame * CFrame.new(0,5,12)).p -- 5 units above and 12 behind the hrp
	char:SetPrimaryPartCFrame(CFrame.new(hrp.Position, targetPosition))
	
	-- Begin camera drag adjustments
	local offsetDirectionX = math.floor(char.Humanoid.MoveDirection.X) -- -1 is left and 1 is right
	local offsetDirectionZ = math.floor(char.Humanoid.MoveDirection.Z) -- -1 is forward and 1 is backward
	local cameraTargetPosition = hrp.Position + Vector3.new(0,0,200)
	local nonAdjustCameraCFrame = CFrame.new(cameraBasePosition, cameraTargetPosition)
	local adjustedCameraCFrame = nonAdjustCameraCFrame * CFrame.new(offsetDirectionX * swingWeight,0,offsetDirectionZ * swingWeight)
	
	local tweenInfo = TweenInfo.new(dragTimeframe / 1000)
	local tween = tweenService:Create(camera, tweenInfo, {CFrame = adjustedCameraCFrame})
	tween:Play()
end)

What you’re looking for happens on line 23 and below. The lower the swingWeight value the more subtle the “drag.” The dragTimeframe can also be adjusted to change how smooth it happens in. A lower value is a sharper more instant change.

1 Like

Hrmm… Are you sure that having a tween run every single rendered frame is a good idea for performance? You won’t see the tween regardless as it’s done during the rendering process… Unless I’m missing something.