How would I go about smoothing Camera angles? (Quake Roll)

I’m trying to achieve a Quake Roll / Twist effect with the camera on player movement. What I’ve achieved so far is a simple good start, but I’m lost as to how to smooth it without breaking the effect.

function Roll(goal)
	local currentCFrame = Camera.CFrame
	local rotationCFrame = currentCFrame * CFrame.Angles(0, 0, math.rad(goal))
	return rotationCFrame
end

RunService.RenderStepped:Connect(function()
	local relativeVelocity = HumanoidRootPart.CFrame:vectorToObjectSpace(HumanoidRootPart.Velocity)

	if Humanoid.MoveDirection.Magnitude > 0 then
			if relativeVelocity.X > 1 then		
				Camera.CFrame = Roll(15)
			elseif relativeVelocity.X < -1 then
				Camera.CFrame = Roll(-15)
			end
	else
		Camera.CFrame = Roll(0)
	end
end)

The code I’ve written adjusts the CFrame angle of the camera if the local X (left - right) player movement changes at every frame.

I’m not sure if I’m not searching hard / thorough enough. I’ve found very little documentation about how to roll / twist the camera angles - none about how to smooth continuous CFrames - and the code above is all I could evaluate.

Pointers would be a life-saver - and If this topic has been covered before, I’d love to have a reference!

Have you heard of TweenService? It is an API that smoothly animates most properties of an instance.

So instead of just setting the CFrame of the camera, we would create a Tween object that will rotate the camera to how you desire. I don’t have time to make a full-on tutorial on how to use tween service but I will tell you what to do.

  1. At the top of your script, make a reference to the API
local TweenService = game:GetService("TweenService")
  1. Let’s make a simple value object to control the behaviour of the tween animation
local TweenService = game:GetService("TweenService")
local TweenInfomation = TweenInfo.new(1,enum.EasingStyle.Quint,enum.EasingDirection.Out)
-- 1 is the speed (in sec), enum.EasingStyle.Quint is the movement graph of the tween, and enum.EasingDirection.Out is the direction of the movement graph of the tween
  1. We now go in to the creation and playing of the tween!
    3.1. first, we need some values for our tween object, we are going to create a table that has all the target properties.
    What we will do is modify your roll function to return a table containing the desired table.
function Roll(goal)
	local currentCFrame = Camera.CFrame
	local rotationCFrame = currentCFrame * CFrame.Angles(0, 0, math.rad(goal))
    local CFrameProperties = {CFrame = rotationCFrame}
	return CFrameProperties 
end
  1. Afterwards we Make the tween and play it
if relativeVelocity.X > 1 then		
	local Properties = Roll(15)
    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
    Tween:Play()
elseif relativeVelocity.X < -1 then
	local Properties = Roll(-15)
    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
    Tween:Play()
end
local Properties = Roll(0)
local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
Tween:Play()

This should be your end result

local TweenService = game:GetService("TweenService")
local TweenInfomation = TweenInfo.new(1,enum.EasingStyle.Quint,enum.EasingDirection.Out)
-- 1 is the speed (in sec), enum.EasingStyle.Quint is the movement graph of the tween, and enum.EasingDirection.Out is the direction of the movement graph of the tween
function Roll(goal)
	local currentCFrame = Camera.CFrame
	local rotationCFrame = currentCFrame * CFrame.Angles(0, 0, math.rad(goal))
    local CFrameProperties = {CFrame = rotationCFrame}
	return CFrameProperties 
end

RunService.RenderStepped:Connect(function()
	local relativeVelocity = HumanoidRootPart.CFrame:vectorToObjectSpace(HumanoidRootPart.Velocity)

	if Humanoid.MoveDirection.Magnitude > 0 then
			if relativeVelocity.X > 1 then		
	           local Properties = Roll(15)
               local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
               Tween:Play()
            elseif relativeVelocity.X < -1 then
	           local Properties = Roll(-15)
               local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
               Tween:Play()
            end
	else
		 local Properties = Roll(0)
         local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
         Tween:Play()
	end
end)

I hope you find this tutorial helpful! :happy1:

And Im really sorry if this is too much.

For more on TweenService: TweenService | Roblox Creator Documentation

2 Likes

The final result is mildly janky but it’s a really good start. Although it’s not a solution for the problems I’m facing with the script, you gave me a really good insight about Tweening CFrames. I’m very grateful for this spontaneous response!

The tweens are starting every frame - there’s little time for the full rotation / roll to form. The position of the camera also tends to lag behind when the tween is playing.

I’m going to experiment with this method a bit more. Follow-ups will be made with progress.

1 Like

How about you implement a debounce system? Don’t make and start a new tween until one has finished.
My suggestion

--top of the script
local TweenService = game:GetService("TweenService")
local TweenInfomation = TweenInfo.new(1,enum.EasingStyle.Quint,enum.EasingDirection.Out)
local CanPlayTween = true
if Humanoid.MoveDirection.Magnitude > 0 then
		  if CanPlayTween then
               if relativeVelocity.X > 1 then		
	                local Properties = Roll(15)
                    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
                    Tween:Play()
               elseif relativeVelocity.X < -1 then
	                local Properties = Roll(-15)
                    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
                    Tween:Play()
               end
          end
          CanPlayTween = false
else
		local Properties = Roll(0)
        local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
        Tween:Play()
        CanPlayTween = true
end

Note this was kinda just thought on the spot so it might not work.

Final product

local TweenService = game:GetService("TweenService")
local TweenInfomation = TweenInfo.new(1,enum.EasingStyle.Quint,enum.EasingDirection.Out)
local CanPlayTween = true

RunService.RenderStepped:Connect(function()
	local relativeVelocity = HumanoidRootPart.CFrame:vectorToObjectSpace(HumanoidRootPart.Velocity)

	if Humanoid.MoveDirection.Magnitude > 0 then
		  if CanPlayTween then
               if relativeVelocity.X > 1 then		
	                local Properties = Roll(15)
                    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
                    Tween:Play()
               elseif relativeVelocity.X < -1 then
	                local Properties = Roll(-15)
                    local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
                    Tween:Play()
               end
          end
          CanPlayTween = false
	else
		local Properties = Roll(0)
        local Tween = TweenService:Create(Camera,TweenInfomation,Properties)
        Tween:Play()
        CanPlayTween = true
	end
end)

This system just prevents the script from playing the tween when unneeded

Im also really sorry if this was messy.

1 Like

Hi - once again your help is mighty appreciated! None of this is too much at all and you’ve been very concise with your assistance.

The debounce in theory would have worked but I’m facing another issue with this now.
The positioning / rotation of the camera is constantly being updated every frame by default. This means that when the tween plays (+ the wait of the debounce): it’ll have to ignore the new changes in position / mouse rotation made by the player until the tween is over. So the final result would be, while the tween plays, the camera freezing in place as the player drifts off until the tween is over and all is back to normal.

I’ve neglected using tweening for this very reason. While tweening is the most optimal method to handle smoothing - and unless I’m missing something, I feel like manually Lerping the CFrame values over the course of frames would be very optimal to achieve the effect I want. I feel like it’d naturally update / smooth the values per passing frames without having to wait.

function Lerp(a, b, t)
	return a + (b - a) * t
end

I’ve used this formula in RenderStepped before but for simpler means - I’m not sure as to how I can use it when it comes to CFrame angles. I hope there is a way to adjust the values of the CFrame specifically so I that I could be able to Lerp a value per input.
Man, in some ways, unity is a lot simpler when it comes to this stuff.

1 Like

Is it ok if you explain to me how this lerp function work? Thanks!

And yes, I do feel like this works, when you set the CFrame on a per-frame basis BUT, if the FPS is lower, the lerp will take longer.

I agree not, when I was making the camera part to my wall running system in unity, it broke everything. Still finding out how to do that today.

1 Like

Instead of doing Camera.CFrame = Roll(x) just do Camera.CFrame = Camera.CFrame:Lerp(Roll(x), 0.1)

Also, creating and destroying a tween every frame is a really silly idea. If you want to use TweenService’s directions and styles, you can use Lerp but replace the Alpha with TweenService:GetValue instead.

Edit: If you want to make the lerp remain ay the constantly speed even with low fps, simply take DeltaTime into account with the lerp alpha.

1 Like

Howdy!
It’s been a minute, hope you’ve had all the best in these 9 months

To everyone still looking for a solution, you’re in luck

Working with ROBLOX’s default camera is tricky - but I’ve managed to find a solution without having to script yourself a whole new module from scratch.

The Problem

ROBLOX’s default camera updates every passing frame. Essentially, every CFrame you’d want the camera to smooth or change into will automatically be overwritten in the next frame.

Even with the case of CFrame:Lerp in RenderStepped:

Camera.CFrame = Camera.CFrame:Lerp(Camera.CFrame * CFrame.Angles(0, 0, math.rad(goal)), deltaTime)

The final result will only be snappy and watered-down by DeltaTime or whatever update speed you’ve chosen.

The Basics

We know how to adjust the angle using the method below

RunService.RenderStepped:Connect(function(deltaTime)
	Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(goal))
end)

If we consider the relative velocity of a character, we can adjust the camera’s angle per the character’s direction

RunService.RenderStepped:Connect(function(deltaTime)
	local relativeVelocity = RootPart.CFrame:VectorToObjectSpace(RootPart.Velocity)

	if relativeVelocity.X > 1 then		-- Going right
		Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(15))
	elseif relativeVelocity.X < -1 then -- Going left
		Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(-15))
	else								-- Neither
		Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(0))
	end
end)

This works just fine if you’re happy with the result, but it remains snappy.

The Trick

Let’s start over.
Instead of adjusting the Camera.CFrame per direction to change it’s angle, we want to change the rotation axis of the camera exclusively - this way, it can be smoothed.

In order to do that, we need to add two new variables to our script.

local axis = 0
local angle = 0

To smooth the angle, you’ll need a smoothing method - you can use whichever one you like
It’s recommended to use Tweening through TweenService - it’s optimized for this very reason

I prefer this neat Lerp function - It’s simple and it’s done me wonders.

function Lerp(a, b, t)
	return a + (b - a) * t
end

a - initial value
b - final value
t - time (the UpdateSpeed / DeltaTime in our case)

Onto RenderStepped: the Camera.CFrame will have to be updated every frame - regardless of direction - with consideration of the axis we want to adjust

Here, the angle alone can be adjusted per direction.

RunService.RenderStepped:Connect(function(deltaTime)
	local relativeVelocity = RootPart.CFrame:vectorToObjectSpace(RootPart.Velocity)

	if relativeVelocity.X > 1 then		
		angle = 15
	elseif relativeVelocity.X < -1 then
		angle = -15
	else
		angle = 0
	end
	
	axis = angle -- this is the roll
	Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(axis)) -- This does the job
end)

But it isn’t smooth just yet!

All you need to do is replace

axis = angle

with

axis = Lerp(axis, angle, deltaTime)

This will smooth the axis from whichever previous value it had to the new angle every frame.

You can then adjust how quick or slow the smoothing is by multiplying deltaTime with a value.

TL;DR - Final Script

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
local RootPart = Humanoid.RootPart

local Camera = workspace.CurrentCamera

function Lerp(a, b, t)
	return a + (b - a) * t
end

local roll = 0
local goal = 0

RunService.RenderStepped:Connect(function(deltaTime)
	local relativeVelocity = RootPart.CFrame:vectorToObjectSpace(RootPart.Velocity)

	if relativeVelocity.X > 1 then		
		angle = 15
	elseif relativeVelocity.X < -1 then
		angle = -15
	else
		angle = 0
	end
	
	axis = Lerp(axis, angle, 6 * deltaTime) -- this is the roll
	Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(axis)) -- This does the job
end)
3 Likes

when using your script, I got several errors, mainly due to the Lerp function that you used. I did some digging and figured out that Roblox has its own built-in lerp function, so I decided to rewrite your script to fix the problems I was having

--put in StarterCharacterScripts--
--Settings--
speed = 0.5
lerpSpeed = 1
maxtilt = 4

--Settings--






local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
local RootPart = Humanoid.RootPart

local Camera = workspace.CurrentCamera

angle = 0


RunService.RenderStepped:Connect(function(deltaTime)
	local relativeVelocity = RootPart.CFrame:vectorToObjectSpace(RootPart.Velocity)

	if relativeVelocity.X > 1 then		
		if angle ~= maxtilt then
			angle = angle + speed
		end
	elseif relativeVelocity.X < -1 then
		if angle ~= -maxtilt then
			angle = angle - speed
		end
		
	else
		if angle < 0 then
			angle =  angle + speed
		elseif angle > 0 then
			angle = angle - speed
		end
	end
		
	local fix = Camera.CFrame * CFrame.Angles(0, 0, math.rad(angle)) 
	
	Camera.CFrame = Camera.CFrame:Lerp(fix, lerpSpeed) -- This does the job
end)

This is the completed script I made, hopefully, anyone else finding this topic sees this.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.