Camera X-axis accumulates beyond clamped values while Z-axis works fine (same values different behavior)

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:

  • rollZ hovers around its target value and behaves correctly
  • rollX keeps 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)