How would I lock my camera's rotation within a cone shape?

I’m making a custom camera system and I want to lock the camera’s rotation within a cone shape. The way it works is by using UserInputService.InputChanged to update two angle values, angleX and angleY with the input’s delta. Here’s a demonstration:

local UserInputService = game:GetService("UserInputService")

local angleX = 0
local angleY = 0

function onInputChanged(input, gameProcessedEvent)
	local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
	
	angleX = math.clamp((angleX - delta.Y), -45, 45)
	angleY = math.clamp((angleY - delta.X), -45, 45)
end

UserInputService.InputChanged:Connect(onInputChanged)

Then inside of a RenderStepped function I rotate the camera using those angles. I do this so I can lerp the angle values to get smoother movements. Now I want to only allow the camera to rotate within a cone shape.

I’ve tried using :Lerp() to only move the camera a certain amount of the way to the target CFrame which partially works, but for some reason the camera rotates sideways.

I’ve also tried using this post to lock the camera’s rotation: How to clamp CFrame.lookAt to not exceed a conical angle?

However that only locks the rotation of the camera and not the angle values, which causes the camera to “freeze” if you look around the corners. It’s also not restricting it to the correct angle. (Probably because I barely understand how it works) I’ve thought of locking the camera’s rotation and then extracting them by using :ToEulerAnglesYXZ() and updating the values, but I want to know if there is a better way to do this?

Any help will be appreciated.

You like trying to lower sensetivity of a camera like that:
CF:Lerp(CFrame.identity,0.9) ?

OP, can you elaborate more on what the problem is, or are you simply trying to find a better way to do this?
I don’t see a problem with just clamping the angles as you are doing. Assuming there are no additional calculations being made while Lerping, it should work fine.
If you want to reduce the cone’s angular size you can just reduce the positive and increase the negative limits on your clamps.

Currently the camera isn’t clamped to a cone, it’s allowed to move within a square shape.

This is the code I have currently in a LocalScript:

--!strict
local CAMERA_LERP_TIME: number = 1 / 10^12


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


local camera: Camera = workspace.CurrentCamera
local player: Player? = Players.LocalPlayer


local targetAngleX: number = 0
local targetAngleY: number = 0

local angleX: number = 0
local angleY: number = 0


function onRenderStepped(deltaTime: number)
	
	camera.CameraType = Enum.CameraType.Scriptable
	UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	
	if not player then return end
	
	local character: Model? = player.Character
	if not character then return end
	
	local head: Instance = character:FindFirstChild("Head")
	if not head or not head:IsA("BasePart") then return end
	
	local faceFrontAttachment: Instance = head:FindFirstChild("FaceFrontAttachment")
	if not faceFrontAttachment or not faceFrontAttachment:IsA("Attachment") then return end
	
	local lerpTime: number = 1 - CAMERA_LERP_TIME ^ deltaTime
	
	angleX = math.lerp(angleX, targetAngleX, lerpTime)
	angleY = math.lerp(angleY, targetAngleY, lerpTime)
	
	camera.CFrame = faceFrontAttachment.WorldCFrame * CFrame.fromEulerAnglesYXZ(math.rad(angleX), math.rad(angleY), 0)
	
end

function onInputChanged(input: InputObject, gameProcessedEvent: boolean)
	if gameProcessedEvent then return end
	
	local delta: Vector2 = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
	
	targetAngleX = math.clamp((targetAngleX - delta.Y), -45, 45)
	targetAngleY = math.clamp((targetAngleY - delta.X), -45, 45)
end


RunService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value + 1, onRenderStepped)
UserInputService.InputChanged:Connect(onInputChanged)

Yes it is a square because your clamps are on -45 and 45. The difference between these numbers is 90 degrees which means you have a full square range of motion. Just reduce the difference between two and you should have a more “conical” result

local UserInputService = game:GetService("UserInputService")

local angleX = 0
local angleY = 0

local maxAngleLimit = 30 -- smaller = more "conical"

function onInputChanged(input, gameProcessedEvent)
	local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
	
	angleX = math.clamp((angleX - delta.Y), -maxAngleLimit, maxAngleLimit)
	angleY = math.clamp((angleY - delta.X), -maxAngleLimit, maxAngleLimit)
end

UserInputService.InputChanged:Connect(onInputChanged)

That doesn’t restrict it to a cone shape, just a smaller square. It’s allowing the camera to rotate 30 degrees left and right, and 30 degrees up and down.

Ah alright now I realize you want a circular thing.
So for a circle you are going to have to use dynamic limits and calculate them using trigonometry.
Its late at night so I’m not going to brainstorm right now, I’ll help you when I wake up.

Alright, thank you for your help!

Nevermind, I’m insomniac so might as well figure it out.

So I just tested this out in studio and it worked.

The logic behind this is to create a new limit for both pitch and yaw based on the other component. (pitch affects yaw limit and yaw affects pitch limit). It’s essentially trying to circularize the limits.

We know that cos(1) = 0. If we divide our angles by our angular limit, we can get them as a ratio. At fully left/right, yawRatio is going to be -1/1. Because of how cosine works, we can now limit the pitch to a fraction of the angular limit, which specifically is maxAngularLimit * cos(yawRatio). This is really just how a circle works.

Imagine you’re trying to plot points on the circumference of a unit circle. When you plot a point at x = 1/-1, the y coordinate of that point is always going to be 0.

Here is my implementation:

local UserInputService = game:GetService("UserInputService")

local camPitch = 0 -- equivalent to angleY
local camYaw = 0 -- equivalent to angleX

local maxAngularLimit = 15

function onInputChanged(input, gameProcessedEvent)
	local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
	
    local pitchRatio = camPitch / maxAngularLimit
	local yawRatio = camYaw / maxAngularLimit
		
	local newPitchLimit = math.cos(yawRatio) * maxAngularLimit
	local newYawLimit = math.cos(pitchRatio) * maxAngularLimit
		
	camYaw = clamp(camYaw, -newYawLimit, newYawLimit)
	camPitch = clamp(camPitch, -newPitchLimit, newPitchLimit)
end

UserInputService.InputChanged:Connect(onInputChanged)

It’s important to note here that the true range of motion of the camera is actually twice our angular limit because the camera can move maxAngularLimit degrees positively and also negatively.

3 Likes

It works wonderfully! And the solution was so simple. Thanks for your help and explanation!

1 Like

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