3D world position to 2D screen position problem

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to convert a 3D world position to a 2D screen position, even if the 3D position is out of the screen

  2. What is the issue? Include screenshots / videos if possible!
    2 Issues I found so far:
    1 slight inaccuracies in the 2D position


    2 When look away from the part, it appears at the center of the screen instead of the edges of the screen

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I have tried look on the DevForum and found no one complaining about my issue, I have tried using ChatGPT only to get the same problem in a longer and different script, I have tried asking programmers I know all of them not helping at all.
    No, The problem is not with the image label that I use to Display the position returned, Anchor point is 0.5,0.5, image is centered

local function closerNumber(x, a, b)
	-- Calculate the absolute differences between x and a, and x and b
	local distToA = math.abs(x - a)
	local distToB = math.abs(x - b)

	-- Check which distance is smaller and return the corresponding value
	if distToA < distToB then
		return a
	else
		return b
	end
end

module.Get2DPosition = function(Position: Vector3)
	local ScreenPosition, inView = workspace.CurrentCamera:WorldToScreenPoint(Position)
	
	local ScreenSize = workspace.CurrentCamera.ViewportSize
	
	if inView then
		local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))
		return UDim2.fromOffset(Vector2Position.X, Vector2Position.Y)
	else
		local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))
		local scaleX = Vector2Position.X / ScreenSize.X
		local scaleY = Vector2Position.Y / ScreenSize.Y
		return UDim2.fromOffset(scaleX, scaleY)

		-- Attempt 2:
		--local x,y = math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y)
		--local xDistance, yDistance = ScreenSize.X - x, ScreenSize.Y - y
		
		--if xDistance > x then
		--	x = ScreenSize.X
		--end
		--if yDistance > y then
		--	y = ScreenSize.Y
		--end
		--local Vector2Position = Vector2.new(x,y)
		
		-- Attempt 1:
		----local Vector2Position = Vector2.new(closerNumber(x,0, ScreenSize.X), closerNumber(y, 0, ScreenSize.Y))
		
		
		--return UDim2.fromOffset(Vector2Position.X, Vector2Position.Y)
	end
end
2 Likes

Roblox has a built-in method that handles this automatically which is the CurrentCamera:WorldToScreenPoint()

1 Like

I have used it in the script and no it does not solve my issue

My apologies. I think this requires advanced knowledge of trigonometry which unfortunately I’m unable to provide, although good luck with finding a solution! I’ll try to test somethings myself and will update you if I find a solution

so changing it to :WorldToViewportPoint()
solves the inaccuracies problem however still doesnt fix the main issue which happens when you look away from the part

new Script:

module.Get2DPosition = function(Position: Vector3)
	local ScreenPosition, inView = workspace.CurrentCamera:WorldToViewportPoint(Position)
	local ScreenSize = workspace.CurrentCamera.ViewportSize
	
	if inView then
		local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))
		return UDim2.fromOffset(Vector2Position.X, Vector2Position.Y)
	else
		local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))
		local scaleX = Vector2Position.X / ScreenSize.X
		local scaleY = Vector2Position.Y / ScreenSize.Y
		return UDim2.fromOffset(scaleX, scaleY)
	end
end
1 Like

That’s due to the AnchorPoint most likely being set to 0.5, 0.5. I suggest finding a way to interpolate the AnchorPoint according to where the gui is located in the screen if my suspision is correct

1 Like

I doubt the anchor point is the reason for the 2D screen position not being in one of the edges?

In the clip it is at the edge but only partially, and that’s a common issue with AnchorPoint that’s why I made the suggestion. I’m testing for a possible solution myself atm

1 Like

This seems to work for me if it’s the effect you want:

module.Get2DPosition = function(Position: Vector3)
	local ScreenPosition = workspace.CurrentCamera:WorldToViewportPoint(Position)
	local ScreenSize = workspace.CurrentCamera.ViewportSize
	local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))

	return UDim2.fromOffset(Vector2Position.X, Vector2Position.Y)
end

nope for me it still doesn’t work

I wish I could continue helping you with this issue, but it’s very late at night where I live. I also suggest showing us an example of what the finished effect should look like to better understand the problem

1 Like

so I managed to get it to work a lot better by looking at code from SO:


New code:

Main module:

local M = game.Players.LocalPlayer:GetMouse()
local Cam = game.Workspace.CurrentCamera

local Helper = require(script.ClosestPointOnPoly)

local module = {}

module.Get2DPosition = function(Position: Vector3)
	local ScreenPosition:Vector2, inView = workspace.CurrentCamera:WorldToViewportPoint(Position)
	local ScreenSize = workspace.CurrentCamera.ViewportSize
	
	ScreenPosition = Vector2.new(ScreenPosition.X, ScreenPosition.Y)
	
	if inView then
		local Vector2Position = Vector2.new(math.clamp(ScreenPosition.X, 0, ScreenSize.X), math.clamp(ScreenPosition.Y, 0, ScreenSize.Y))
		return UDim2.fromOffset(Vector2Position.X, Vector2Position.Y)
	else
		local Rectangle = Helper.Build(Vector2.zero, ScreenSize)
		local Point = Helper.ClosestPointOnPoly(Rectangle, ScreenPosition)
		return UDim2.fromOffset(Point.X, Point.Y)
	end
end

return module

Helper Module:

local function squaredSize(v)
	return v.X^2 + v.Y^2
end

local function sub(v, w)
	return Vector2.new(v.X - w.X, v.Y - w.Y)
end

local function add(v, w)
	return Vector2.new(v.X + w.X, v.Y + w.Y)
end

local function mul(v, k)
	return Vector2.new(v.X * k, v.Y * k)
end

local function squaredDistance(a, b)
	return squaredSize(sub(a, b))
end

local function dot(v, w)
	return v.X * w.X + v.Y * w.Y
end

local function clamp(x)
	return math.max(0, math.min(1, x))
end

local function closestPointOnSegment(a, b, p)
	local ap = sub(p, a)
	local ab = sub(b, a)
	return add(a, mul(ab, clamp(dot(ap, ab) / squaredSize(ab))))
end

local function closestPointOnPoly(poly, p)
	local points = {}
	for i = 1, #poly do
		points[i] = closestPointOnSegment(poly[i], poly[(i % #poly) + 1], p)
	end

	local dists = {}
	for _, q in ipairs(points) do
		table.insert(dists, squaredDistance(p, q))
	end

	local minDist = math.min(table.unpack(dists))
	local index = table.find(dists, minDist)
	return points[index]
end

local function BuildPoly(TopLeft: Vector2, BottomRight: Vector2)
	return {
		TopLeft,
		Vector2.new(TopLeft.X, BottomRight.Y),
		BottomRight,
		Vector2.new(BottomRight.X, TopLeft.Y),
	}
end

local module = {}

module.Build = BuildPoly
module.ClosestPointOnPoly = closestPointOnPoly

return module

How ever as you see in the video there is still a small problem where I think by “flipping” the position it would fix it

here is the place file:
3DWorldPositionTo2DScreenPosition.rbxl (50.2 KB)

2 Likes