Coolest camera interpolation?

Hey all,

I want to make a cutscene managing system. Right now, I’m moving the camera according to a PID controller I wrote (not sure if it’s perfect for a PID controller). I want the camera to warp between (part-defined) positions. Here’s how it looks:

If you could improve the aesthetics of this transition system, what alternatives would you propose? I’ve tried both spring and linear interpolation for the camera movement before, and I’ll say I could tolerate spring interpolation. Still, I would strongly avoid using linear interpolation for my current project. I’m not a big curves nerd, so I’m open to learning more about any suggestions that I can try.

Thanks.

1 Like

you can use something like an orbiting camera with bizarre parameters and custom acceleration factors
Desktop 2025.10.16 - 13.19.08.05

local repl = game:GetService('ReplicatedStorage')
local plr_svc = game:GetService('Players')
local tween_svc = game:GetService('TweenService')
local run_svc = game:GetService('RunService')

local plr = plr_svc.LocalPlayer
local ch = plr.Character or plr.CharacterAdded:Wait()
local hum = ch:WaitForChild('Humanoid')
local cam = workspace.CurrentCamera
cam.CameraType = Enum.CameraType.Scriptable

local CHEX = 255
function create_dummy(size_v)
	local dm = Instance.new('Part')
	dm.Name = 'dummy'
	dm.Color = Color3.fromRGB(1*CHEX, 0*CHEX, 0*CHEX)
	dm.Size = size_v
	dm.CanCollide = false
	dm.Anchored = true
	dm.Parent = workspace
	return dm
end

local target = nil
local cam_dummy = nil
function setup()
	target = create_dummy(Vector3.new(2, 2, 2))
	target:PivotTo(CFrame.new(0, 5, -32))
	
	cam_dummy = create_dummy(Vector3.new(0.25, 0.25, 0.25))
end
setup()

local ORBIT_T = {['from'] = 0, ['to'] = 10, ['by'] = 0.032, ['value'] = nil}
local CAM_R = {['from'] = 32, ['to'] = 4, ['by'] = nil, ['value'] = nil}
local CAM_ELEV = {['from'] = 20, ['to'] = 0, ['by'] = nil, ['value'] = nil}
local CAM_ROTSPEED = {['from'] = -60, ['to'] = -15, ['by'] = nil, ['value'] = nil}
local DUMMYPOS_X = {['from'] = 24, ['to'] = 0, ['by'] = nil, ['value'] = nil}
local DUMMYPOS_Y = {['from'] = 8, ['to'] = 0, ['by'] = nil, ['value'] = nil}
local DUMMYPOS_Z = {['from'] = 2.5, ['to'] = 0, ['by'] = nil, ['value'] = nil}
local flag_cutscene_pending = false
function run()
	flag_cutscene_pending = true
	local function init_values()
		ORBIT_T['value'] = ORBIT_T['from']
		ORBIT_T['n_steps'] = (ORBIT_T['to'] - ORBIT_T['from']) / ORBIT_T['by']
		CAM_R['value'] = CAM_R['from']
		CAM_R['by'] = (CAM_R['to'] - CAM_R['from']) / ORBIT_T['n_steps']
		CAM_ELEV['value'] = CAM_ELEV['from']
		CAM_ELEV['by'] = (CAM_ELEV['to'] - CAM_ELEV['from']) / ORBIT_T['n_steps']
		CAM_ROTSPEED['value'] = CAM_ROTSPEED['from']
		CAM_ROTSPEED['by'] = (CAM_ROTSPEED['to'] - CAM_ROTSPEED['from']) / ORBIT_T['n_steps']
		DUMMYPOS_X['value'] = DUMMYPOS_X['from']
		DUMMYPOS_X['by'] = (DUMMYPOS_X['to'] - DUMMYPOS_X['from']) / ORBIT_T['n_steps']
		DUMMYPOS_Y['value'] = DUMMYPOS_Y['from']
		DUMMYPOS_Y['by'] = (DUMMYPOS_Y['to'] - DUMMYPOS_Y['from']) / ORBIT_T['n_steps']
		DUMMYPOS_Z['value'] = DUMMYPOS_Z['from']
		DUMMYPOS_Z['by'] = (DUMMYPOS_Z['to'] - DUMMYPOS_Z['from']) / ORBIT_T['n_steps']
	end
	
	local function incr_values()
		ORBIT_T['value'] += ORBIT_T['by']
		CAM_R['value'] += CAM_R['by']
		CAM_ELEV['value'] += CAM_ELEV['by']
		CAM_ROTSPEED['value'] += CAM_ROTSPEED['by']
		DUMMYPOS_X['value'] += DUMMYPOS_X['by']
		DUMMYPOS_Y['value'] += DUMMYPOS_Y['by']
		DUMMYPOS_Z['value'] += DUMMYPOS_Z['by']
		cam_dummy:PivotTo(target:GetPivot() + CFrame.new(DUMMYPOS_X['value'], DUMMYPOS_Y['value'], DUMMYPOS_Z['value']).Position)
	end
	
	local function has_moresteps()
		if math.abs(ORBIT_T['value'] - ORBIT_T['to']) <= ORBIT_T['by'] then
			return false
		else
			return true
		end
	end
	
	init_values()
	local co = coroutine.create(function()
		while (flag_cutscene_pending) do
			orbit_step(cam_dummy, ORBIT_T['value'], CAM_R['value'], CAM_ELEV['value'], CAM_ROTSPEED['value'])
			task.wait(ORBIT_T['by'])
			incr_values()
			if not has_moresteps() then
				flag_cutscene_pending = false
			end
		end
	end)
	coroutine.resume(co)
end

function orbit_step(target, t, radius, elevation, rot_speed)
	local t_cf = target:GetPivot()
	local yaw = math.sin(math.rad(rot_speed) * t) * radius
	local pitch = elevation
	local roll = math.cos(math.rad(rot_speed) * t) * radius
	cam.CFrame = CFrame.lookAt((t_cf + Vector3.new(yaw, pitch, roll)).Position, t_cf.Position)
end

run()
2 Likes

I dont know if you’re doing this already but definitely seperate rotation interpolation from position, it lets you give it more personality without looking too linear.
You could use splines, they ensure that you always touch the anchor points/poles, i think theres a few open source modules on here, or just implement a basic formula.
Finally i think it would look nice if you had an effect that tweaked camera FOV based on the speed of its movement. You could have a seperate spring for this, check the delta between frames and change accordingly, theres lots of ways to do it.

Overall i think it looks quite good already

2 Likes

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