Orient 3D Viewport Arrow towards World-Space Point

Hi ~! I figured I would post my issue here, as if it were to be solved it would then be a host for said solution publicly.

As the title suggests I want a 3D arrow located in a ViewportFrame to point at a location in World-Space. This means Pitch/Yaw orientation of the Arrow.

This image shows what I’m expecting but with an arrow positioned very close to the camera with the appropriate maths being applied.

Doing this in a Viewport is very tricky because of the implications from different perspectives. Unfortunately - I do not have the mathematical prowess to properly implement this effectively - or I may just be overcomplicating/overlooking something.

This is the solution I came up with which is being used in that screenshot AND in the attached demo place:

-- Start our update method
Run.RenderStepped:Connect(
	function()
		-- Store the CFrame of our World-Camera
		local worldCameraCFrame = WorldCamera.CFrame
		
		-- Calculate the center of our viewport
		local viewportPosition = (Viewport.AbsolutePosition + (Viewport.AbsoluteSize / 2))

		-- Now determine where our viewport is in WorldSpace
		local offsetX, offsetY = ScreenSpaceToLocalSpace(WorldCamera, viewportPosition)
		local offset = Vector3.new(offsetX, offsetY, -1)
		local worldPosition = worldCameraCFrame:PointToWorldSpace(offset)
		local viewportCFrame = (worldCameraCFrame.Rotation + worldPosition)

		-- Calculate our direction to our target
		local targetOffset = viewportCFrame:PointToObjectSpace(TargetPosition)
		local orientation = CFrame.lookAt(Vector3.new(), targetOffset.Unit)
		local worldOrientation = (worldCameraCFrame.Rotation * orientation)

		-- Update our workspace-arrow
		WorkspaceArrow:PivotTo(worldOrientation + worldPosition)
	end
)

Here is the demo place:
ViewportArrowSample.rbxl (48.5 KB)

If anybody can help to get this working so the perspective is properly transformed into the ViewportFrame that would be much appreciated - it would be of great benefit to the community as well for anybody else who encounters this mathematical issue.

You’re probably not worried about this anymore, but in case it helps others (because I was stuck for a while), a probably not so performant method is to just place the oriented arrow in front of the player’s camera and set the viewport camera to the player’s camera.

Disclaimer: This is not quite perfect. If you have the arrow at the far right side of your screen, it acts as if it was in the center of your screen. You can do some modifications though to make it more accurate using workspace.CurrentCamera:ViewportPointToRay().

local ARROW_DIST_IN_FRONT = 10

local function arrowLookAt(targetPosition)
   -- Save the `workspace.CurrentCamera`'s CFrame
   local currentCameraCF = currentCamera.CFrame

   -- Orient the `arrowModel` so that it's looking 
   -- towards the `targetPosition`
   arrowModel:PivotTo(CFrame.lookAt(currentCameraCF * Vector3.new(0, 0, -ARROW_DIST_IN_FRONT), targetPosition))
   
   -- Set the `ViewportFrame.CurrentCamera`'s CFrame 
   -- to the saved `workspace.CurrentCamera`'s CFrame
   viewportCamera.CFrame = currentCameraCF
end

I appreciate your attempted solution but what you described in the Disclaimer section is exactly why your solution can’t be used.

I should’ve outlined this more clearly but ideally the solution should be able to calculate the correct orientation from any point on the screen so that the arrow is always angled towards the target.

Using the camera-center as the perspective will only work when near or at the center and doesn’t quite work as soon as you start moving the place of the arrow around the screen (because evidently the perspectives are not the same).

I myself have not had the time to correctly solve this issue so I can’t provide an update on how to do it. Possibly somebody will eventually come across this and properly solve it or I’ll find out the math behind how to make this work.

Again, I appreciate your proposed solution and the time you took to reply, thanks!

I got a mostly working product:


The main problem now is just the viewport frame.
The magic number surprisingly works for multiple screen sizes.

local viewportSize = viewportFrame.AbsoluteSize
local viewportPosition = viewportFrame.AbsolutePosition
local viewportMidPosition = viewportPosition + viewportSize/2

local distInFrontOfCamera = viewportMidPosition.X*.008 -- Magic number

local function getArrowWorldPosition()
	local ray = currentCamera:ViewportPointToRay(viewportMidPosition.X, viewportMidPosition.Y)

	return  ray.Origin + ray.Direction*distInFrontOfCamera
end

local function arrowLookAt(targetPosition)
	local currentCameraCF = currentCamera.CFrame
	local lookAtCF = CFrame.lookAt(getArrowWorldPosition(), targetPosition)

	arrowModel:PivotTo(lookAtCF)

	viewportCamera.CFrame = currentCameraCF - currentCameraCF.LookVector*ARROW_DISTANCE_IN_FRONT
end

Check it out!


If anyone wants a place file for this I’ll spend some more time to make it publicly available.

3 Likes