Creating a fullscale interactive map UI for your open-world games

Mapping world positions to a 2D UI map without plugins

This is a workaround for something Roblox doesn’t natively support well: getting consistent, accurate player/blip positions on a 2D map image when you don’t have access to tools like RoRender.

The core idea is simple:
We manually establish a relationship between world space (3D) and image space (2D pixels) using an affine transformation.

Prerequisites
you’ll need:

  • A completed or mostly completed map
  • A UI ImageLabel (your map display)
  • Two parts placed in the world:
    • UpperBounds
    • LowerBounds
  • A notepad/paper (or anything to write coordinates down)

Important:
Those two bound parts must be diagonal from each other
(e.g. top-left | bottom-right)


Step 1— Reference Points

Pick 3 locations on your map.

Rules:

  • They cannot be colinear (not all on a straight line)
  • Try to spread them out well (think triangle, not cluster)
  • A scalene triangle works best

At each location:

  • Place a part (centered if possible)
  • Ignore Y — it does not matter

Write down their world positions:

Step 2 — Map Image

  1. Set your reference parts to Transparency = 1
    (or hide them later in editing)
  2. Move your camera:
  • Go to center of the map
  • Switch to Top View
  1. Select the Camera in workspace:
  • Set FieldOfView ≈ 1–1.5

This will distort your view — fix the angle manually.

Lighting Adjustments (if needed)

Large maps may appear dark or foggy:

  • Disable:
    • Atmosphere
    • Bloom
    • Effects
  • Turn off GlobalShadows
  • Increase FogEnd
  • Increase Brightness

This is basically a fake orthographic projection
(Roblox doesn’t support real orthographic cameras).

  1. Frame your shot:
  • Make sure both bound parts are visible
  • Take a screenshot
  1. Crop the image:
  • Remove everything outside the bounds
  • You should end up with a clean rectangle/square

Optional:

  • Rotate / flip / orient the image as desired
  • Do NOT distort or rescale unevenly

Step 3 — Pixel Coordinates

Open your image in Photopea (recommended) or Photoshop

Enable:

  • Rulers (Ctrl + R)
  • Info panel (Window > Info)

Also note image resolution (bottom-left in Photopea)

Now for each reference point:

  • Hover over its location in the image
  • Record pixel coordinates

Write them in the same order as before:
pC1(X,Y)
pC2(X,Y)
pC3(X,Y).
Order consistency is critical.

Step 4 — Setup

Inside your map UI:

  • Create a LocalScript
  • Define:
    • World points (P1, P2, P3)
    • Pixel points (pC1, pC2, pC3)
  • Define your indicator/blip UI element
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local mapImage = script.Parent :: ImageLabel
local indicator = mapImage:WaitForChild("YOURINDICATOR") :: ImageLabel | Frame | TextButton | ImageButton 

--MAKE SURE THE INDICATORS ANCHOR POINT IS SET TO (0.5, 1)

local worldPoints = {
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P1
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P2
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P3
}

local imagePoints = {
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P1
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P2
	Vector2.new(YOURPOINTX, YOURPOINTY),  -- P3
}

local TEXTURE_SIZE = Vector2.new(YOURDIMENSIONX, YOURDIMENSIONY )


Step 5 — The Math

At first glance, this seems like it should be simple scaling.

It’s not.

Why:

  • Your image is not axis-aligned
  • It’s not evenly scaled
  • It has perspective distortion

However:

  • The relationship between world space and image space is still affine
    (linear transform + translation, no curvature)

Because we have:

  • 3 known world points
  • 3 corresponding pixel points

We can solve for an affine transformation matrix

Setup: WorldPosition > ImagePosition

local function invert3(m)
	local a,b,c = m[1][1], m[1][2], m[1][3]
	local d,e,f = m[2][1], m[2][2], m[2][3]
	local g,h,i = m[3][1], m[3][2], m[3][3]

	local det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g)
	
	local invDet = 1 / det

	return {
		{ (e*i - f*h) * invDet, (c*h - b*i) * invDet, (b*f - c*e) * invDet },
		{ (f*g - d*i) * invDet, (a*i - c*g) * invDet, (c*d - a*f) * invDet },
		{ (d*h - e*g) * invDet, (b*g - a*h) * invDet, (a*e - b*d) * invDet },
	}
end

local function mul3x3_3x1(m, v)
	return {
		m[1][1]*v[1] + m[1][2]*v[2] + m[1][3]*v[3],
		m[2][1]*v[1] + m[2][2]*v[2] + m[2][3]*v[3],
		m[3][1]*v[1] + m[3][2]*v[2] + m[3][3]*v[3],
	}
end


local function computeAffine(worldPts, imagePts)
	local w1, w2, w3 = worldPts[1], worldPts[2], worldPts[3]
	local i1, i2, i3 = imagePts[1], imagePts[2], imagePts[3]

	local A = {
		{ w1.X, w1.Y, 1 },
		{ w2.X, w2.Y, 1 },
		{ w3.X, w3.Y, 1 },
	}

	local Ainv = invert3(A)

	local U = { i1.X, i2.X, i3.X }
	local V = { i1.Y, i2.Y, i3.Y }

	local thetaU = mul3x3_3x1(Ainv, U)
	local thetaV = mul3x3_3x1(Ainv, V) 

	return thetaU, thetaV
end

local thetaU, thetaV = computeAffine(worldPoints, imagePoints)

local function worldToPixel(x, z): Vector2
	local u = thetaU[1]*x + thetaU[2]*z + thetaU[3]
	local v = thetaV[1]*x + thetaV[2]*z + thetaV[3]
	return Vector2.new(u, v)
end

The indicator is the blip that will show the players location.

Step 6 — Updating the Indicator

Run the transformation every frame. Use RunService:BindToRenderStep when the map is open, unbind when closed (for performance).

RunService.RenderStepped:Connect(function()
		local char = player.Character
		local hrp = char and char:FindFirstChild("HumanoidRootPart")
		if not hrp then return end
	
		local uv = worldToPixel(hrp.Position.X, hrp.Position.Z)
	
		local imgSize = mapImage.AbsoluteSize
	
		local scaleX = imgSize.X / TEXTURE_SIZE.X
		local scaleY = imgSize.Y / TEXTURE_SIZE.Y
	
		local OFFSET = Vector2.new( 0, 0)
	
		local px = uv.X * scaleX + OFFSET.X
		local py = uv.Y * scaleY + OFFSET.Y
		indicator.Position = UDim2.fromOffset(px, py)
end)


Notes / Limitations

  • Expect a small error (~10–15 pixels)
  • You can correct this with a manual offset tweak

If you use something like RoRender: you can skip the offset because it will be nearly pixel perfect.


Feedback?

This is not the cleanest solution, but it’s reliable and flexible.

Once the transformation is set up, it works consistently across:

  • Player position
  • NPCs
  • Points of interest
  • Anything with a world coordinate

The hardest part is honestly just getting clean reference points, and being precise with your pixel sampling

After that, everything falls into place.

9 Likes

I feel like this belongs in community tutorial instead, but none the less great resource!

2 Likes