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
- Set your reference parts to Transparency = 1
(or hide them later in editing) - Move your camera:
- Go to center of the map
- Switch to Top View
- Select the
Camerain 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).
- Frame your shot:
- Make sure both bound parts are visible
- Take a screenshot
- 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.
