How to clamp CFrame.lookAt to not exceed a conical angle?

I have an arm that’s coming out of a wall, and I want it to keep looking at the player, if I just use CFrame.lookAt the arm will clip through the wall:

I’d like to limit it to an angle, basically give it a cone that it cannot exceed, i’ve tried clamping the angles but I never got a clean enough result. How can I achieve this?

If you imagine the cone it’s allowed to move in, the height of the cone is one axis and the base is the other two. If you take the distance of just the two base axes you get a radius inside the cone:

local coneDirection = Vector3.Z
local armDirection = ...
local height = armDirection:Dot(coneDirection)
local radialDir = (armDirection - coneDirection*height)
local radius = radialDir.Magnitude

A cone has a constant angle along its side, which is the same as having a constant ratio between base and height. Measure this ratio, clamp it to some max value, then convert it back into a direction:

local maxSlope = math.tan(1.326) --about 4
local slope = radius / height

if slope > maxSlope then
  slope = maxSlope
end

local clampedDirection = radialDir * slope * height + coneDirection * height

armDirection seems to be a vector, so (armDirection - height) is not a possible operation, you cant substract a number from a vector

edited
charlimitcharlimitcharlimit

is armDirection what CFrame.lookAt gave us? Or is it (player.Position - arm.Position).Unit, then i take the clamped direction and put it in the CFrame.lookAt?

Its player position minus arm position (the point the arm is rotating around), but it does not need to be unit length.

I tried it but it’s not working, here’s the code

	local armDirection = (target.Position - pivotPosition).Unit
	local height = armDirection:Dot(lookVector)
	local radialDir = armDirection - lookVector * height
	local radius = radialDir.Magnitude

	local maxSlope = math.tan(1.326)
	local slope = radius / height

	if slope > maxSlope then slope = maxSlope end

	local clampedDirection = radialDir * slope * height + lookVector * height
	mesh.CFrame = CFrame.lookAt(pivotPosition, pivotPosition + clampedDirection) * offset

armDirection: the vector from the arm to the player
lookVector: the cone direction aka the wall normal
offset: it’s an offset to make the arm pivot around a pivot point i have

it has the same exact results as the clip i sent in post

In this case, I would use one of roblox’s mechanical constraints.
I think the BallSocketConstraint is best fit, and I would use it along with alignOrientation.

To initiate it the alignOrientation, go like this:

local attachment0 = Instance.new("Attachment", PARENTPART)
local alignOrientation  = Instance.new("AlignOrientation")
alignOrientation.Enabled = true
alignOrientation.Attachment0 = attachment0
alignOrientation.Responsiveness = 14
alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignOrientation.MaxTorque = math.huge

(note: this is a piece of code I already written in one of my games)
and to set the direction it sees:

alignOrientation.CFrame = CFrame.new(ORIGINOFPOSITION, POSITIONTOSTARE)

you can figure out the BallSocketConstraint part

I am writing this as an alternative to azgjanna’s code. I don’t wish to interrupt azgjanna. Use azgjanna’s solution instead. this is just another method… …

One second I will double check this

Thanks for the suggestion but I have already considered using this, but it requires two attachements and I only have one pivot point (with some wittiness I can manifest the 2nd point), i’d also like the mathematical representation which is gonna be more flexible and i get to enhance my maths

hmm. there were some weird bugs but this one seems to work:

	local coneDir = Vector3.xAxis
	local armDirection = rvec
	local height = armDirection:Dot(coneDir)
	local radialDir = armDirection - coneDir * height
	local radius = radialDir.Magnitude

	local maxSlope = 1/4
	local slope = math.abs(radius / height)
	local scale = 1
	
	if slope > maxSlope then
		scale = slope / maxSlope
		slope = maxSlope
	end

	local clampedDirection = radialDir * slope * height + coneDir * height * scale

Giving it random points:

Sorry, one more edit. I am trying to rotate cones in my head at 11pm:

	local coneDir = Vector3.xAxis
	local armDirection = rvec

	--Clamp height to be positive to get a one-sided cone
	local height = armDirection:Dot(coneDir)
	local radialDir = armDirection - coneDir * height
	local radius = radialDir.Magnitude

	local maxSlope = 1/4
	local slope = math.abs(radius / height)
	
	if slope > maxSlope then
		slope = maxSlope
	end

	local clampedDirection = radialDir.Unit * slope * height + coneDir * height

1 Like

This is some very nice progress, but as you can see there are two cones, same thing your image shows, how can I get rid of the second cone?

Make this change:
local height = math.max(armDirection:Dot(coneDir), 0)
If it’s backwards, flip coneDir.

This makes the arm dissapear when its on the backwards cone, I could detect when to flip the cone using the dot product, but I dont know the cone angle in radians since you have it as 1/4

Wait I’m confused. What do you want to happen if the player is on the back side of the normal cone?

Intuitively it should just not clip to the wall, there should be one cone only
same way ballsocketconstraint cone works