I’m working on a realistic camera effects script that adds head bob and banking based on velocity. I have two banking effects:
- rollZ (banking left/right based on sideways velocity) - works perfectly
- rollX (banking forward/back based on forward velocity) - extremely unexpected behavior
The Problem:
Both rollX and rollZ have identical values when I print them. (both lerp to their targets and clamp to ± 10), but:
rollZhovers around its target value and behaves correctlyrollXkeeps accumulating until it reaches +=82~ in the Camera objects actual orientation
Code:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local player = Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local root: Part = char:WaitForChild("HumanoidRootPart")
local camera = workspace.CurrentCamera
UserInputService.MouseIconEnabled = false
local velMagnitude = 0
local elapsed = 0
local rollZ = 0
local rollX = 0
RunService.RenderStepped:Connect(function(dt)
elapsed += dt
local velocityWorld = Vector3.new(root.AssemblyLinearVelocity.X, 0, root.AssemblyLinearVelocity.Z)
local velocityLocal = root.CFrame:VectorToObjectSpace(velocityWorld)
-- Get the AssemblyLinearVelocity (Only on the X and Z)
velMagnitude = math.lerp(velMagnitude, (velocityWorld).Magnitude, 1 - math.exp(-2 * dt))
velMagnitude = math.min(velMagnitude, 50)
local angleY = math.sin(elapsed * 5) * math.cos(elapsed * 3) * (velMagnitude * 0.01)
local angleX = math.cos(elapsed * 7) * (velMagnitude * 0.01)
-- Get the mouse delta X value, multiply by a small scalar
local mouseDeltaX = -UserInputService:GetMouseDelta().X * 0.025
mouseDeltaX = math.clamp(mouseDeltaX, -0.2, 0.2)
-- rollZ is just lerped to -velocityLocal.X, scaled accordingly
rollZ = math.lerp(rollZ, (-velocityLocal.X / 5), 1 - math.exp(-2 * dt))
rollZ += mouseDeltaX
rollZ = math.clamp(rollZ, -10, 10) -- Clamped to prevent too much roll issues
-- noiseX and noiseY add random X and Y jitter to both rotation and position of the camera
local noiseY = math.noise(elapsed * 0.5) * 0.05
local noiseX = math.noise(elapsed * 0.8, 42) * 0.05
-- rollX is calculated the exact same as rollZ...
rollX = math.lerp(rollX, (velocityLocal.Z / 5), 1 - math.exp(-2 * dt))
rollX = math.clamp(rollX, -10, 10)
-- Multiply all our transformations
camera.CFrame *=
CFrame.new(noiseX * 5, angleY + noiseY, 0) *
CFrame.Angles(
math.rad(angleY * 0.1 + (noiseY * 0.35)),
math.rad(angleX * 0.1),
0
) *
CFrame.Angles(math.rad(rollX), 0, math.rad(rollZ))
end)
Here’s a video of what’s happening:
I’m completely stumped. My best guess is Roblox’s default camera script is interfering, or a gimbal lock-type issue is happening.
If anyone could help that’d be great.
(You can also paste in the script above, so you can test it yourself as well)