Camera CFrame:Lerp Jitter

Recently, I revisited an old Character Controller I worked on over the summer and noticed a significant issue: the Camera’s lerping behavior. The Camera would Lerp fine; however, a constant jitter occurred. I conducted research and discovered that it’s due to the frequencies of the runservice types.

So, my problem is, how would we fix that? Other engines have an option to select a LateUpdate, so that the camera code fires when the Update is done. Roblox, however, doesn’t have this. My brain is also liquid at the moment from spending three days on this problem. :sob:

I’ve tried many other alternatives, such as using a spring module, various types of run services, modifying the code’s math, and more. The closest I came to achieving camera smoothing was by utilizing the Humanoid.MoveDirection, however, the Camera behaved in a way that I personally didn’t prefer.

Video

Here’s the code.

local CameraModule = {}
--//Services\\--
local PlayerService = game:GetService("Players")
local LightingService = game:GetService("Lighting")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserGameSettings = UserSettings().GameSettings
local UIS = game:GetService("UserInputService")

local Blur = LightingService.Blur

--//Modules\\--
local RaycastModule = require(ReplicatedStorage.SharedModules.Physics.Raycast)

--//Player Stuff\\--
local Plr = PlayerService.LocalPlayer
local BodyPart : Part
local BodyPartPosition = Vector3.zero
local Cam = workspace.Camera

--//Camera Values\\--
local CamPos = Cam.CFrame
local XAngle = 0
local YAngle = 0
local MaxOffset = 10
local Offset = MaxOffset
local LerpSpeed = 20
local LastCamLookVector = Cam.CFrame.LookVector

--//Local Functions\\--
local function Heartbeat ( dT : number)
	local Char = Plr.Character or Plr.CharacterAdded:Wait()
	BodyPart = Char.HumanoidRootPart :: Part
	BodyPartPosition = BodyPart.Position

	--Raycasting
	local RayParams = RaycastParams.new()
	RayParams.FilterType = Enum.RaycastFilterType.Exclude
	RayParams.FilterDescendantsInstances = Char:GetDescendants()

	local Direction = (CamPos.Position - BodyPartPosition).Unit*10
	local RayResult = RaycastModule.Ray(BodyPartPosition, Direction, RayParams)

	if RayResult then
		if Offset ~= RayResult.Distance-0.5 then
			Offset = RayResult.Distance-0.5
		end
	else
		if Offset ~= MaxOffset then
			Offset = MaxOffset
		end
	end
	
	--Blur Effect
	local Amount = (Cam.CFrame.LookVector - LastCamLookVector).Magnitude
	Blur.Size = (math.abs(Amount/dT)/4)
	LastCamLookVector = Cam.CFrame.LookVector
end

local function PreRender(dT : number)
	if UIS.MouseBehavior ~= Enum.MouseBehavior.LockCenter then
		UIS.MouseBehavior = Enum.MouseBehavior.LockCenter
		UIS.MouseIconEnabled = false
	end
	
	--Math for Camera (Trigonometry on a 3D Plane)
	XAngle -= math.clamp((UIS:GetMouseDelta().X/UIS.MouseDeltaSensitivity)*UserGameSettings.MouseSensitivity, -10,10)
	YAngle = math.clamp(YAngle - math.clamp((UIS:GetMouseDelta().Y/UIS.MouseDeltaSensitivity)*UserGameSettings.MouseSensitivity,-10,10),-65, 45)
	local YPos = math.sin(math.rad(YAngle))*-Offset
	local C = math.sqrt((Offset^2)-(YPos^2))
	local XPos = math.sin(math.rad(XAngle))*C
	local ZPos = math.cos(math.rad(XAngle))*C

	CamPos = CFrame.new(Vector3.new(XPos,YPos,ZPos)+BodyPartPosition)
	
	Cam.CFrame = Cam.CFrame:Lerp(CamPos * CFrame.fromEulerAnglesYXZ(math.rad(YAngle),math.rad(XAngle),0), LerpSpeed*dT)
end

--//Functions\\--


--//Connect Functions\\--
function CameraModule.Initiate()
	RunService.Heartbeat:Connect(Heartbeat)
	RunService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value-1, PreRender)
end

return CameraModule
2 Likes

Try using Enum.RenderPriority.Camera.Value + 1 for your render step priority. I believe it has to be higher than the camera.

Edit: I hadn’t realized this all my time developing, but it should actually be - 1. Thanks @Tomi1231, no thanks to whoever told me to use + 1 when I first started scripting.

4 Likes

I’ve already tried that; unfortunately, there’s no change, and the jitter still occurs.

1 Like

It should be -1 instead of +1, dunno if it’s just a typo : P

Otherwise it’ll run after the camera rendered

2 Likes

BodyPartPosition is computed in Heartbeat while it is used in RenderStep.

2 Likes

Yeah, but even when computed together, the issue persists. I’d also like to mention that if BodyPartPosition were defined before the simulation, then the position would be out of date since the HRP’s position would be affected once the simulation starts.

1 Like

What have you tried for what you said here? I think @A_Mp5 is correct here

1 Like

Yes, I’ve tried it, and you can try it for yourself. The jittering, unfortunately, continues despite being on the same runservice.

1 Like

I personally think that positioning based on spring or PID math would look much better, but I believe the jitter issue you are facing is due to the use of Heartbeat for computation of offset

1 Like

Sorry, I am not worrying about “trying for myself.” I was wondering what you had implemented

1 Like

Alright, I appreciate the feedback, though. The Spring Module is something I looked into and temporarily implemented; I’ll probably add it back in later. Additionally, the Math PID would also be something I’ll explore later.

1 Like

Looking at the video, it kinda looks like the camera is smooth, but the character isn’t? What kind of character controller are you using, it seems like an older one (unless you are using the default one, but with different animations?)

2 Likes

It’s something I made myself. I plan to make it a kinematic character controller; however, I currently only have a basic movement module and camera module in-game. All Roblox character and player scripts are also disabled. The character appears to jitter due to being a rigid body, which causes it to behave according to the physics simulation. At the same time, any environments that are not anchored within it remain unaffected. A better way to illustrate this is the RBXL I found in another post, which features a follower and background block operating on different Runservice types
, which you can switch with the F key.
CamTest.rbxl (26.3 KB)

1 Like

I’m confused then, what is the problem we are supposed to be observing?

1 Like

The problem is the jittering that occurs whenever the character moves. I’ve been trying to find ways around it, and so far I’ve had no luck.

1 Like

Looking back on what you said, I realized that I misinterpreted what you were asking; I apologize, especially since I came out aggressively.

To the best of my knowledge, these are all the things I’ve tried implementing so far.

  • Combining the computations on multiple different types of run services.
  • Attempting to create a fixed step for both run services to operate, which I probably did completely wrong, and I will look back on it.
  • Changing the math used, as well as attempting to create an interpolation, which was the closest I’ve gotten to solving the jitter issue by implementing the MoveDirection; however, the behavior of the camera wasn’t what I was looking for, I’ll have to see if there was something else I could’ve done.
  • I’ve also attempted to lerp the HRP so that it’s consistent with the renderstepped, utilizing the fixedstep method, but as I stated previously, I probably completely butchered it.
1 Like

He currently has -1. I may be wrong; however, I believe that the higher the numeric priority, the higher the actual priority.

Edit: Oh my god. I’ve been doing it wrong all this time. Thank you for bringing this to my attention!

2 Likes

Hi, @ImadeJohn.

I don’t think it’s good to use CharacterAdded in Heartbeat, just put it in outside heartbeat,
it does change when CharacterAdded is fired and :Wait() receives the signal fire of it (no need to use loop)

-- this
local Char = Plr.Character or Plr.CharacterAdded:Wait()
-- outside of loop and runtime

@A_Mp5 is correct on BodyPartPosition since PreRender fires before Heartbeat

The lower is number, the sooner it is called ( RunService #BindToRenderStep | Documentation - Roblox Creator Hub)

2 Likes

Hello, @Microlosofto.
I went ahead and removed the CharacterAdded:Wait() from the heartbeat itself, but unfortunately, even when combining them into one computation, in this case it’s now being fired all under PreRender, the jitter still occurs.

local function PreRender(dT : number)
	Char = Plr.Character
	BodyPart = Char.HumanoidRootPart :: Part
	BodyPartPosition = BodyPart.Position

	--Raycasting
	local RayParams = RaycastParams.new()
	RayParams.FilterType = Enum.RaycastFilterType.Exclude
	RayParams.FilterDescendantsInstances = Char:GetDescendants()

	local Direction = (CamPos.Position - BodyPartPosition).Unit*10
	local RayResult = RaycastModule.Ray(BodyPartPosition, Direction, RayParams)

	if RayResult then
		if Offset ~= RayResult.Distance-0.5 then
			Offset = RayResult.Distance-0.5
		end
	else
		if Offset ~= MaxOffset then
			Offset = MaxOffset
		end
	end
	
	if UIS.MouseBehavior ~= Enum.MouseBehavior.LockCenter then
		UIS.MouseBehavior = Enum.MouseBehavior.LockCenter
		UIS.MouseIconEnabled = false
	end
	
	--Math for Camera (Trigonometry on a 3D Plane)
	XAngle -= math.clamp((UIS:GetMouseDelta().X/UIS.MouseDeltaSensitivity)*UserGameSettings.MouseSensitivity, -10,10)
	YAngle = math.clamp(YAngle - math.clamp((UIS:GetMouseDelta().Y/UIS.MouseDeltaSensitivity)*UserGameSettings.MouseSensitivity,-10,10),-65, 45)
	local YPos = math.sin(math.rad(YAngle))*-Offset
	local C = math.sqrt((Offset^2)-(YPos^2))
	local XPos = math.sin(math.rad(XAngle))*C
	local ZPos = math.cos(math.rad(XAngle))*C

	CamPos = CFrame.new(Vector3.new(XPos,YPos,ZPos)+BodyPartPosition)
	
	Cam.CFrame = Cam.CFrame:Lerp(CamPos * CFrame.fromEulerAnglesYXZ(math.rad(YAngle),math.rad(XAngle),0), LerpSpeed*dT)
	
	--Blur Effect
	local Amount = (Cam.CFrame.LookVector - LastCamLookVector).Magnitude
	Blur.Size = (math.abs(Amount/dT)/4)
	LastCamLookVector = Cam.CFrame.LookVector
end
1 Like

What about getting the current position, not setted-position ?