Viewmodel sway fps problem

The problem with sway (when you rotate the camera)
is that the intensity and smoothness of both are not uniformly the same at all fps. For example, the intensity of smoothness and intensity at 30 fps is not the same as at 244 fps. The fewer fps you have, the more intensity and smoothness there will be, but as soon as you have more fps, the intensity and smoothness of the viewmodel will be lower.

local player = game.Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local cam = workspace.CurrentCamera
local run = game:GetService("RunService")
local humanoid = char:WaitForChild("Humanoid")
local equipped = false
local walking = false
local jumping = false
local viewModelFolder = game.ReplicatedStorage:WaitForChild("Viewmodels")
local viewModel = viewModelFolder:WaitForChild("Fists")
local VMLeftArm = viewModel:FindFirstChild("Left Arm")
local VMRightArm = viewModel:FindFirstChild("Right Arm")
local toolHandle = script.Parent:FindFirstChild("Handle")
local lastXOffset, lastYOffset = 0, 0
local accumulatedTime = 0
local smoothingSpeed = 10

local lastCameraCFrame = cam.CFrame
local swayAngleX, swayAngleY = 0, 0
local swayRotationX, swayRotationY, swayRotationZ = 0, 0, 0
local swaySmoothing = 8
local swayIntensity = 1
local rotationSwayIntensity = 1
local rotationSwaySmoothing = 8

local function resetViewModel()
	viewModel.Parent = viewModelFolder
end

local function disableCollisionsAndAnchoring()
	for _, part in pairs(viewModel:GetChildren()) do
		if part:IsA("BasePart") then
			part.CanCollide = false
			part.Anchored = false
		end
	end
end

local function getCameraSway(deltaTime)
	local currentCFrame = cam.CFrame
	local delta = lastCameraCFrame:ToObjectSpace(currentCFrame)
	local xRot, yRot, zRot = delta:ToEulerAnglesYXZ()
	local targetSwayX = math.clamp(-xRot, -math.rad(5), math.rad(5)) * swayIntensity
	local targetSwayY = math.clamp(-yRot, -math.rad(10), math.rad(10)) * swayIntensity
	swayAngleX = swayAngleX + (targetSwayX - swayAngleX) * math.clamp(deltaTime * swaySmoothing, 0, 1)
	swayAngleY = swayAngleY + (targetSwayY - swayAngleY) * math.clamp(deltaTime * swaySmoothing, 0, 1)
	local targetRotX = math.clamp(-xRot, -math.rad(2), math.rad(2)) * rotationSwayIntensity
	local targetRotY = math.clamp(-yRot, -math.rad(3), math.rad(3)) * rotationSwayIntensity
	local targetRotZ = math.clamp(-zRot, -math.rad(1), math.rad(1)) * rotationSwayIntensity
	swayRotationX = swayRotationX + (targetRotX - swayRotationX) * math.clamp(deltaTime * rotationSwaySmoothing, 0, 1)
	swayRotationY = swayRotationY + (targetRotY - swayRotationY) * math.clamp(deltaTime * rotationSwaySmoothing, 0, 1)
	swayRotationZ = swayRotationZ + (targetRotZ - swayRotationZ) * math.clamp(deltaTime * rotationSwaySmoothing, 0, 1)
	lastCameraCFrame = currentCFrame
end

local function updateViewModelPosition(time, deltaTime)
	local playerInFirstPerson = (cam.CFrame.Position - char.Head.Position).Magnitude < 1

	
if equipped and playerInFirstPerson then
		local camOffset = CFrame.new(0, 0, 0)
		local speed = humanoid.WalkSpeed
		local balanceFactor = speed / 15
		local xOffset, yOffset = 0, 0

		if walking and not jumping then
			xOffset = math.sin(time) * (0.04 + balanceFactor * 0.04)
			yOffset = math.sin(time * 2) * (0.03 + balanceFactor * 0.03)
		end

		local t = math.clamp(smoothingSpeed * deltaTime, 0, 1)
		lastXOffset = lastXOffset + (xOffset - lastXOffset) * t
		lastYOffset = lastYOffset + (yOffset - lastYOffset) * t
		camOffset = camOffset * CFrame.new(lastXOffset, lastYOffset, 0)
		local swayPositionRotation = CFrame.Angles(swayAngleX, swayAngleY, 0)
		local swayRotation = CFrame.Angles(swayRotationX, swayRotationY, swayRotationZ)
		viewModel:SetPrimaryPartCFrame(cam.CFrame * camOffset * swayPositionRotation * swayRotation)
		viewModel.Parent = cam

		if toolHandle then
			toolHandle.Transparency = 1
		end
	else
		resetViewModel()
		if toolHandle then
			toolHandle.Transparency = 0
		end
	end
end

local function updateViewModelAppearance()
	if equipped then
		VMLeftArm.Color = char["Left Arm"].Color
		VMRightArm.Color = char["Right Arm"].Color

		local shirt = char:FindFirstChildOfClass("Shirt")
		if shirt then
			if VMLeftArm:FindFirstChild("Shirt") and VMRightArm:FindFirstChild("Shirt") then
				VMLeftArm.Shirt.Texture = shirt.ShirtTemplate
				VMRightArm.Shirt.Texture = shirt.ShirtTemplate
			end
		end
	end
end

run.RenderStepped:Connect(function(deltaTime)
	if equipped then
		accumulatedTime = accumulatedTime + deltaTime * 6
		getCameraSway(deltaTime)
	end
	updateViewModelPosition(accumulatedTime, deltaTime)
	updateViewModelAppearance()
end)

script.Parent.Equipped:Connect(function()
	equipped = true
	disableCollisionsAndAnchoring()
end)

script.Parent.Unequipped:Connect(function()
	equipped = false
	resetViewModel()
	if toolHandle then
		toolHandle.Transparency = 0
	end
end)

humanoid.Running:Connect(function(speed)
	walking = speed > 0
end)

humanoid.Jumping:Connect(function()
	jumping = true
end)

humanoid.FreeFalling:Connect(function()
	jumping = true
end)

humanoid.StateChanged:Connect(function(_, newState)
	if newState == Enum.HumanoidStateType.Landed or newState == Enum.HumanoidStateType.Running then
		walking = humanoid.MoveDirection.Magnitude > 0
		jumping = false
	elseif newState == Enum.HumanoidStateType.Climbing then
		walking = false
		jumping = false
	end
end)

humanoid.Died:Connect(function()
	local tool = script.Parent
	if tool:IsDescendantOf(player.Backpack) or tool:IsDescendantOf(player.Character) then
		tool:Destroy()
	end
	resetViewModel()
	equipped = false
end)

player.CharacterAdded:Connect(function(newChar)
	char = newChar
	humanoid = char:WaitForChild("Humanoid")

	humanoid.Died:Connect(function()
		local tool = script.Parent
		if tool:IsDescendantOf(player.Backpack) or tool:IsDescendantOf(player.Character) then
			tool:Destroy()
		end
		resetViewModel()
		equipped = false
	end)

	humanoid.StateChanged:Connect(function(_, newState)
		if newState == Enum.HumanoidStateType.Landed or newState == Enum.HumanoidStateType.Running then
			walking = humanoid.MoveDirection.Magnitude > 0
			jumping = false
		elseif newState == Enum.HumanoidStateType.Climbing then
			walking = false
			jumping = false
		end
	end)
end)
1 Like

the way you use deltaTime removes the fps stability of the delta time concept for it to look the same across all framerates.

this algorithm that i created should be frame independent and still look like how you wanted, but if this approach has issues then i’m sure you can find another one that isn’t insignificant

local function smooth(current, target, rate, deltaTime)
	return current + (target - current) * (1 - math.exp(-rate * deltaTime))
end

local function getCameraSway(deltaTime)
	local currentCFrame = cam.CFrame
	local delta = lastCameraCFrame:ToObjectSpace(currentCFrame)
	local xRot, yRot, zRot = delta:ToEulerAnglesYXZ()
	
	local targetSwayX = math.clamp(-xRot, -math.rad(5), math.rad(5)) * swayIntensity
	local targetSwayY = math.clamp(-yRot, -math.rad(10), math.rad(10)) * swayIntensity
	swayAngleX = smooth(swayAngleX, targetSwayX, swaySmoothing, deltaTime)
	swayAngleY = smooth(swayAngleY, targetSwayY, swaySmoothing, deltaTime)
	
	local targetRotX = math.clamp(-xRot, -math.rad(2), math.rad(2)) * rotationSwayIntensity
	local targetRotY = math.clamp(-yRot, -math.rad(3), math.rad(3)) * rotationSwayIntensity
	local targetRotZ = math.clamp(-zRot, -math.rad(1), math.rad(1)) * rotationSwayIntensity
	swayRotationX = smooth(swayRotationX, targetRotX, rotationSwaySmoothing, deltaTime)
	swayRotationY = smooth(swayRotationY, targetRotY, rotationSwaySmoothing, deltaTime)
	swayRotationZ = smooth(swayRotationZ, targetRotZ, rotationSwaySmoothing, deltaTime)
	
	lastCameraCFrame = currentCFrame
end