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.
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
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.
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.
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
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.
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?)
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)
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.
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
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