Trouble Attempting to Align 3 points via Orientation

Hello. I am working on creating an hub interface for a game and one of the core features is the ability to interact with a globe using buttons/attachments that are placed on the globe. The idea is that once you click the point on the globe, the globe will pivot in such a way that the attachment/point is oriented in the center of the camera.

Unfortunately I have been unable to come up with the proper math to accomplish this. I got very close, and the angles I came up with are correct. However, I neglected to consider how orientation actually works on ROBLOX and simply coming up with two angles and setting the CFrame Orientation using them results in a slightly offset Orientation.

I attempted to uncover multiple solutions, all of which provided similar results or something drastically different. Some of my attempts came from:

Here is the code I am currently using. All I really did was triangulate the three positions and solve for the necessary angle on both the Y and Z axis. The issue is that I did not consider the change to the X axis that would occur by modifying both the Y and Z, and so the resulting CFrame is offset from what I’d like it to be.

local cPos = workspace.CurrentCamera.CFrame.Position -- camera
local aPos = attachment.WorldPosition -- location of attachment on globe
local pPos = pivot.Position -- location of center of globe

local pc = pivot.CFrame:ToObjectSpace(workspace.CurrentCamera.CFrame)
local pa = pivot.CFrame:ToObjectSpace(attachment.WorldCFrame)
local ca = CFrame.new(Vector3.new(math.abs(cPos.X - aPos.X), (cPos.Y - aPos.Y), (cPos.Z - aPos.Z)))
---

local paHY = math.sqrt(pa.Z^2 + pa.X^2)
local paHZ = math.sqrt(pa.X^2 + pa.Y^2)
local caHY = math.sqrt(ca.Z^2 + ca.X^2)
local caHZ = math.sqrt(ca.X^2 + ca.Y^2)

local Z = math.acos( ((paHZ ^ 2) + (pc.X ^ 2) - (caHZ ^ 2)) / (2 * (paHZ * math.abs(pc.X))) )
local Y = math.acos( ((paHY ^ 2) + (pc.X ^ 2) - (caHY ^ 2)) / (2 * (paHY * math.abs(pc.X))) )
print(math.deg(Z)) -- 39.8
print(math.deg(Y)) -- 45

game:GetService('TweenService'):Create(pivot, TweenInfo.new(1), {CFrame = pivot.CFrame * CFrame.Angles(0, -Y, Z)}):Play()

The code above provides me with the angles I thought I could apply to orient the pivot to the camera, using the position of the attachment. By themselves, these angles work. (ie: (0, -Y, 0) will properly align the attachment on the Y axis, and same with Z), but when applied simultaneously result in the off-center orientation seen above.

Any insight is appreciated!

a possible solution could be to use only cframes, my first thought would be to use Mouse.Hit to find the 3d position of the mouse.

I achieved the result i think your looking for by creating an invisible globe surrounding your globe, and making this the primary part of the globe model. Then when you click it, set the CFrame of this invisble part to look at the mouse.hit position, then on each frame, use :SetPrimaryPartCFrame to rotate the whole globe to the cframe.lookat of the mouse.hit position.

my example is here:

local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Mouse = game.Players.LocalPlayer:GetMouse()

local Globe = game.Workspace.Globe
local HitPart = Globe.HitPart

local MouseDown -- find when the mouse is clicked
local MouseInside -- find when the mouse is inside the globe


HitPart.ClickDetector.MouseHoverEnter:Connect(function()
	MouseInside = true
end)

HitPart.ClickDetector.MouseHoverLeave:Connect(function()
	MouseInside = false
end)

UserInputService.InputBegan:Connect(function(Input,gpe)
	if gpe == false then
		if Input.UserInputType == Enum.UserInputType.MouseButton1 then
			MouseDown = true
			if MouseInside then-- if the mouse is inside the globe, rotate it
				-- set the primary part CFrame seperately so the globes descendant done jump to the new spot
				Globe.PrimaryPart.CFrame = CFrame.lookAt(Globe.PrimaryPart.Position,Mouse.Hit.Position) 
				
				--create a coroutine to rotate the globe
				local RotateCoro = coroutine.wrap(function()
					while MouseDown do -- while the mouse is pressed down, rotate the globe to the mouse position
						RunService.RenderStepped:Wait()
						Globe:SetPrimaryPartCFrame(CFrame.lookAt(Globe.PrimaryPart.Position,Mouse.Hit.Position))
					end
				end)
				
				--Start the coroutine
				RotateCoro()
			end
		end
	end
end)

UserInputService.InputEnded:Connect(function(Input,gpe)
	if gpe == false then
		if Input.UserInputType == Enum.UserInputType.MouseButton1 then
			MouseDown = false -- cancels the coroutine
		end
	end
end)

https://gyazo.com/8f6886e678488bc01280e7696e8dcd08

This deviates heavily from what I’m trying to accomplish.

The 3D position of the mouse is irrelevant, as the position being referenced exists already (the WorldPosition of the attachment).

As stated, the goal is to orient the pivot (the globe) so that the attachment is between the pivot and the camera (workspace.CurrentCamera.CFrame). The means through which to rotate the pivot have already been set, I simply need help with the math to orient the globe properly.

1 Like

Using My method, you could set the CFrame LookAt of the invisible part to look at the ‘Point to Align’ then tween the CFrame using a CFrame Value to the camera’s position. it might not seem the nicest but tbh i hate using pivots.

Heres my example of doing it like that:

local RunService = game:GetService("RunService")
local Mouse = game.Players.LocalPlayer:GetMouse()

local TweenService = game:GetService("TweenService")

local RotateTweenInfo = TweenInfo.new(1,Enum.EasingStyle.Back,Enum.EasingDirection.InOut)

local Globe = game.Workspace.Globe

local MoveModel = true
Globe.Value.Changed:Connect(function(NewValue) -- Globe.Value is a CFrameValue
	if MoveModel == true then
		Globe:SetPrimaryPartCFrame(NewValue)
	else
		Globe.PrimaryPart.CFrame = NewValue
	end
end)

for _,GlobeParts in pairs(Globe:GetDescendants()) do
	if GlobeParts.Name == "Waypoint" then
		GlobeParts.ClickDetector.MouseClick:connect(function(Player)
			--Change the move model value so it doesn't rotate when we dont want to
			MoveModel = false
			Globe.Value.Value = CFrame.lookAt(Globe.PrimaryPart.Position,GlobeParts.Position)
			MoveModel = true
			--create the tween to rotate the globe
			local Tween = TweenService:Create(Globe.Value,RotateTweenInfo,{Value = CFrame.lookAt(Globe.PrimaryPart.Position,Player.Character.HumanoidRootPart.Position) })
			Tween:Play()
		end)
	end
end

https://gyazo.com/1da8d6be0cda627a82c55dd8cc610791

I know this does little to help with your math, but i hope this helps.

Seems there are various ways of solving this and when pondering, I thought calculating the new cframe by CFrame.Angles() just seems a bit convoluted, or more than it should be.

I managed to get the CFrame orientated based on the attachments position off the primary part:
https://gyazo.com/ab90a032ff1d85935dc1cf438e57509b

Although this method uses CFrame.fromMatrix rather than other things.

local thePart = workspace.TheSphere; 
local current_cf = thePart.CFrame;

-- // Creating a new cframe based on the look direction of the attachment based on world position
local direction = (thePart.Attachment.WorldPosition - thePart.Position).Unit; 

-- // This is Vector3.new(0, 1, 0) and the point is to make the cframe's up vector in relation to it; 
-- // just like how your camera is
local right_vector = direction:Cross(Vector3.yAxis); 
local up_vector = direction:Cross(right_vector); 
local point_cf = CFrame.fromMatrix(thePart.Position, right_vector, up_vector, direction); 

-- // Translating the cframe based on the original parts cframe
local inverse = current_cf:Inverse() * point_cf; 
local camera_cf = workspace.Camera.CFrame.Rotation + thePart.Position

thePart.CFrame = camera_cf * inverse;

The idea behind this was setting the original parts cframe towards the camera, then taking the translation of the attachments cframe and correcting it. Or in other words, making the part’s cframe based on the attachment.

Edit: I forgot that this is in a view port but you can easily replace the camera with the viewports camera, and do all the other stuff in relation to it as well.

1 Like

Thanks for replying!

I applied your method, swapping the necessary variables. The result was the same as my original attempt, where the final orientation is offset slightly from the center, shown here (using your method):

The smaller square is the center, where the pivot is. The larger square is the position of the attachment after this method was applied.

Here is my code after applying the method you provided.

local direction = (attachment.WorldPosition - pivot.Position).Unit
local rightVector = direction:Cross(Vector3.yAxis)
local upVector = direction:Cross(rightVector)
local pointCF = CFrame.fromMatrix(pivot.Position, rightVector, upVector, direction)

local inverse = pivot.CFrame:Inverse() * pointCF
local cameraCF = workspace.CurrentCamera.CFrame.Rotation + pivot.Position

game:GetService('TweenService'):Create(pivot, TweenInfo.new(1), {CFrame = cameraCF * inverse}):Play()

My only other guess is the camera is slightly unaligned with the object you’re looking at. Which you can do a CFrame.lookAt(camera.CFrame.Position, pivot.Position) although you’ll have to translate the position of the CFrame to the pivots position.

This also makes me think that your initial equation might have also been an answer, it’s just the camera not fully being aligned. Of course I may be wrong in either situation, I’ll have to test more in a viewport, or an object with an altered pivot point.

The camera is fixed and perfectly in line with the pivot.

workspace.CurrentCamera.CFrame = CFrame.new(workspace.CameraPosition.Position, pivot.Position)

Ah, I apologize, I’ll get back to you when I find something that works, seems I didn’t test it with different attachment positions so it seemed to only work in that one scenario- I also noticed another error in what I sent, since I made 2 versions.

I appreciate you. Been going at this for a few days so any insight helps.

Still searching for solutions if possible.

Honestly, I kind of forgot about this. Now there’s an extremely easy method of handling this without exerting a lot of pure math, which may not be what you want.

It’s as simple as:

local part = workspace.TheSphere
local to_camera_cframe = CFrame.lookAt(part.Position, workspace.CurrentCamera.CFrame.Position)
local attachment = part.Attachment
-- // All it will do is change the pivot to the direction of the attachment (offset)
part.PivotOffset = CFrame.lookAt(Vector3.zero, attachment.Position)
part:PivotTo(to_camera_cframe)

And a incomplete method I’m still trying to figure out is rotating the Y axis then rotating the X axis in relation to the direction of the offset position

This is what it looks like so far, not sure if I’ll continue;

local part = workspace.TheSphere
local to_camera_cframe = CFrame.lookAt(part.Position, workspace.CurrentCamera.CFrame.Position)

local attachment = part.Attachment

-- // Getting direction for basic trig
local attachment_direction = (attachment.Position).Unit
-- // This will help find the angle it should rise up on the X axis in relation to the direction
local y_distance = math.sqrt(1 - attachment_direction.Y ^ 2)

local x_rotation = math.atan2(attachment_direction.X, attachment_direction.Z)
local y_rotation = math.atan2(attachment_direction.Y, y_distance)

local new_cframe = camera_cframe * CFrame.Angles(y_rotation, x_rotation, 0)
-- // So this won't actually work
-- // If you only use one angle like:
-- // CFrame.Angles(0, math.pi-x_rotation, 0) or CFrame.Angles(y_rotation, 0, 0)
-- // it'll turn relatively the correct amount however the next angles math will need to be different
part.CFrame = new_cframe

Turning the cube with the Y axis (CFrame.Angles(0, math.pi-x_rotation, 0))

But now you’d have to find out how to turn it up so this is where I’d get stuck.

EDIT:
This above was an extraneous method and the math behind setting a new pivot includes:

local part = workspace.TheSphere
local to_camera_cframe = CFrame.lookAt(part.Position, workspace.CurrentCamera.CFrame.Position)

local attachment = part.Attachment
local attachment_cframe = CFrame.lookAt(Vector3.zero, attachment.Position)
local inverted_cframe = attachment_cframe:Inverse()
part.CFrame = to_camera_cframe * inverted_cframe

https://gyazo.com/e8a7549d47605e5ccd9952b0f0395eec.mp4
This was basically it after I collected all attachments and ran it through a loop. (Apologies about the music in the gyazo if you watch it, didn’t realize that was being recorded)

1 Like

I’m glad you came back! This was seriously holding me up and I’m really not all too well-prepared to confront trig and the like but I guess, in the end, it wasn’t as complicated as we thought??

Thank you for your time!

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