UMG/Slate Coordinate Spaces
UMG and Slate expose five distinct coordinate spaces, and the conversion API is scattered across
FGeometry, USlateBlueprintLibrary,
and UWidgetLayoutLibrary with no single reference.
To make matters worse, "Absolute" does not mean screen-relative,
DPI scaling silently multiplies coordinates, and the two outputs of LocalToViewport
are trivially easy to mix up. This page maps every space from the outermost (OS desktop) to
the innermost (individual widget), explains how they relate, and lists every conversion function in one place.
1. The Five Spaces
Each space has a different origin, unit size, and purpose. They nest from the outermost (the OS desktop) down to each individual widget. Sections 2–6 cover each space in detail.
This is the space that UMG widget placement (
SetPositionInViewport), HUD drawing, and most gameplay math expect. A value of (960, 540) always means "the centre of a 1920×1080-logical viewport", regardless of the physical pixel count on the display.(0, 0). This is why UI code can work fine in fullscreen on
the primary monitor, but break when players run the game in windowed mode, or on a
non-primary monitor.
2. Screen / Desktop Space
Screen space is the raw OS coordinate system. Its origin is the top-left corner of the primary monitor, and it spans the entire virtual desktop (all monitors combined). Units are device pixels — no DPI scaling applied. UE exposes it mainly for mouse position queries and for ingesting coordinates that come from a platform SDK or external tool.
Mouse position functions
| Function | Returns | Notes |
|---|---|---|
| GetMousePositionOnPlatform() | Desktop device pixels | No world context. Can be negative on multi-monitor setups. |
| GetMousePositionOnViewport(World) | Viewport scaled units | Preferred for UMG work — accounts for window position and DPI automatically. |
| GetMousePositionScaledByDPI(PC, X, Y) | DPI-scaled cursor pos | Requires a PlayerController. Outputs separate float X/Y. |
Converting screen → widget
FVector2D ScreenPos = /* coord from platform SDK or OS input event */;
FVector2D LocalCoord;
// bIncludeWindowPosition: pass true in windowed mode, false in fullscreen
USlateBlueprintLibrary::ScreenToWidgetLocal(
GetWorld(),
MyWidget->GetCachedGeometry(),
ScreenPos,
LocalCoord,
true // <-- set correctly for windowed vs fullscreen!
); true in windowed mode so the window's desktop offset is subtracted before
the conversion. Pass false in fullscreen (the offset is zero, so it's a no-op,
but true is safe there too). Getting this wrong in windowed mode shifts every
coordinate by the window border — often 50–200 pixels.
GetMousePositionOnPlatform() returns coordinates in the OS virtual desktop,
which spans all connected monitors. If a secondary monitor is positioned to the left or
above the primary, X or Y can be negative. Always clamp or offset before passing into UMG.
GetMousePositionOnPlatform() still
returns the correct desktop cursor position, but that position will be somewhere inside the
editor window, not at the game viewport's origin. Use
GetMousePositionOnViewport() instead for game-relative mouse math in PIE.
3. Absolute / Window Space
"Absolute" in the Slate/UMG API means relative to the
Slate root window's top-left corner — not the desktop, and not the game viewport.
It is computed by accumulating layout transforms from the root widget down to the widget
in question (FGeometry::AccumulatedLayoutTransform).
Render transforms (rotation, scale, skew) are not included.
In a standalone game the Slate root window is the game window, so Absolute ≈ Window. In PIE the Slate root window is the entire editor window — absolute coordinates are therefore measured from the editor window's corner, not the game viewport panel's corner.
Key functions
| Function | Direction | Available in |
|---|---|---|
| FGeometry::LocalToAbsolute | Local → Absolute | C++ |
| FGeometry::AbsoluteToLocal | Absolute → Local | C++ |
| USlateBlueprintLibrary::LocalToAbsolute | Local → Absolute | C++, Blueprint |
| USlateBlueprintLibrary::AbsoluteToLocal | Absolute → Local | C++, Blueprint |
(0, 0). The comment
in Geometry.h acknowledges this ambiguity: "Absolute coordinates could
be either desktop or window space depending on what space the root of the widget hierarchy
is in." In practice, Slate's root is always the OS window, so Absolute = Window.
GetViewportWidgetGeometry() when you need viewport-relative positions —
it correctly handles both PIE and standalone.
4. Viewport Space (Pixel vs Scaled)
The game viewport is an SViewport Slate widget. Its
top-left corner is the origin of viewport space. Crucially, the viewport does not have to
fill the window:
- PIE (Play In Editor): the viewport is a panel embedded inside the editor window. The editor chrome — tabs, toolbar, content browser — surrounds it. The viewport's pixel origin is wherever that panel starts inside the editor window.
- Sub-viewport configurations: a game can render to a viewport that is smaller than the window (e.g. a picture-in-picture inset, or a split-screen sub-rect configured via
UGameViewportClient). - Standalone game: in the typical case the viewport fills the window and viewport pixel space equals absolute/window space.
Viewport space itself comes in two flavours that are always returned as a pair by conversion functions:
| Output | Units | DPI scaled? | Use when |
|---|---|---|---|
| Pixel Position | Raw device pixels | No | Render targets, scissor rects, hardware cursor APIs |
| Viewport Position | Slate units (see §5) | Yes — Pixel ÷ DPI scale | Placing UMG widgets, HUD canvas drawing, SetPositionInViewport |
FVector2D PixelPos, ViewportPos;
USlateBlueprintLibrary::LocalToViewport(
GetWorld(), MyWidget->GetCachedGeometry(),
FVector2D(0.f, 0.f), // local point (widget origin)
PixelPos, // OUT: raw device pixels
ViewportPos // OUT: Slate units (DPI-scaled)
);
// Relationship between the two:
float DPIScale = UWidgetLayoutLibrary::GetViewportScale(GetWorld());
// ViewportPos == PixelPos / DPIScale (approximately)
// Viewport logical size (Slate units):
FVector2D LogicalSize = UWidgetLayoutLibrary::GetViewportSize(GetWorld());
// Physical pixel size = LogicalSize * DPIScale Pixel Position and Viewport Position are identical at 1× DPI
(standard 1080p monitor). On a 2× HiDPI display or a console with non-default DPI scale
they differ by a factor of 2+. Always use Viewport Position for UMG widget
placement. Passing Pixel Position to SetPositionInViewport will look correct
on your dev machine then place widgets in the wrong position on a Retina display or
high-DPI monitor.
GetViewportSize() and GetViewportScale() report the size and DPI
of that sub-panel, not the editor window as a whole — so viewport-space math is PIE-safe
as long as you go through these functions rather than hardcoding sizes.
5. Local Space
Local space is the coordinate system internal to a single widget. The origin
(0, 0) is the widget's own top-left corner. The X axis
grows right and Y grows down. The "size" of the space is the widget's
FGeometry::Size — a local point at
(Size.X, Size.Y) is exactly the bottom-right corner.
Points outside those bounds are perfectly valid values in local space — they just describe
positions beyond the widget's edges. A point at (-10, 5)
is 10 units to the left of the widget's left edge. You'll encounter these when you call
AbsoluteToLocal on an absolute coordinate that isn't
inside the widget, or when doing drag-offset math.
What are Slate units?
Slate units are resolution-independent layout units —
the same concept as CSS pixels, iOS "points", or Android "dp/dip". One Slate unit always
represents the same physical size on screen regardless of monitor resolution or
pixel density. The DPI scale factor (FGeometry::Scale)
bridges them to actual device pixels:
device_pixels = slate_units × FGeometry::Scale
// Examples:
// 1× DPI (standard 1080p): 100 Slate units = 100 device pixels
// 2× DPI (Retina / 4K HiDPI): 100 Slate units = 200 device pixels
// 1.5× DPI (some 1440p setups): 100 Slate units = 150 device pixels
So Slate units are not always 1:1 with device pixels — only at 1× DPI.
However they are always 1:1 with Viewport Position
(the scaled viewport coordinate from §4), because both use the same DPI-independent unit.
The practical benefit: if you hardcode a widget position in Slate units (e.g.
SetPositionInViewport(FVector2D(960, 540))), it will
visually land at the same screen position on every DPI setting — the engine multiplies by
the DPI scale to get the final pixel placement.
Getting a widget's geometry
// C++ — call from NativeTick, NOT from NativeConstruct
const FGeometry& Geom = MyWidget->GetCachedGeometry();
// The widget's size in Slate units
FVector2D WidgetSize = FVector2D(Geom.Size);
// Centre of the widget in local space
FVector2D Centre = WidgetSize * 0.5f;
// Blueprint — "Get Cached Geometry" node on any Widget reference Converting local ↔ absolute
FVector2D LocalPt = FVector2D(50.f, 20.f);
FVector2D AbsPoint = Geom.LocalToAbsolute(LocalPt); // local → window
FVector2D BackLocal = Geom.AbsoluteToLocal(AbsPoint); // window → local
// A point outside the widget — valid, just off the edge:
FVector2D OutsidePt = FVector2D(-5.f, Geom.Size.Y + 10.f); // above-left + below-right
FVector2D AbsOutside = Geom.LocalToAbsolute(OutsidePt); // still works fine NativeConstruct, immediately after
AddToViewport,
or in any constructor context returns a zeroed
FGeometry — Size = (0, 0), AbsolutePosition = (0, 0).
Defer geometry reads to NativeTick, or guard with
if (Geom.Size.X > 0.f).
FGeometry::Scale may differ between PIE
(editor DPI setting) and standalone (game DPI setting) if they are configured differently.
6. Paint / Render Space
When you apply a render transform to a widget via
SetRenderTransform,
SetRenderTransformAngle, or
SetRenderScale, you introduce a paint space
that diverges from local/layout space. The widget looks rotated or scaled on screen,
but the layout engine and hit-test system still use the original unmodified geometry.
Render transforms are purely a rendering effect.
| Method | Includes render transforms? | Use for |
|---|---|---|
| GetCachedGeometry() | No — layout only | Coordinate conversions, hit tests, most UMG work |
| GetTickSpaceGeometry() | No — layout only | Preferred in NativeTick (equivalent to GetCachedGeometry) |
| GetPaintSpaceGeometry() | Yes | Finding the visual bounding box of a transformed widget |
| GetLayoutBoundingRect() | No | AABB in absolute space — always axis-aligned |
| GetRenderBoundingRect() | Yes | AABB enclosing the rendered widget (may be larger due to rotation) |
AbsoluteToLocal and LocalToAbsolute work in layout space only.
If you've rotated a widget 45° with SetRenderTransformAngle, clicking on its
visual centre will not give you (Width/2, Height/2) in local space — the layout
geometry is still axis-aligned at the original unrotated position. Hit tests use layout
geometry too, so the clickable area and the visible area will not match.
7. FGeometry Reference
FGeometry is the core struct that encodes a widget's
position, size, and accumulated transforms. You retrieve it from
GetCachedGeometry() and pass it into conversion functions.
| Member / Method | Type | Space | What it gives you |
|---|---|---|---|
| AbsolutePosition | FVector2f | Absolute | Widget's top-left corner in absolute (window) space, in device pixels |
| Size | FVector2f | Local (Slate units) | Widget's width and height in Slate units |
| Scale | float | — | DPI scale factor: device_pixels = slate_units × Scale |
| LocalToAbsolute(local) | FVector2D | Local → Absolute | Converts a local Slate-unit point to absolute (window) space |
| AbsoluteToLocal(abs) | FVector2D | Absolute → Local | Converts an absolute point back to local space (can be negative / outside bounds) |
| GetAbsolutePositionAtCoordinates(norm) | FVector2D | Absolute | Normalised UV → absolute: (0,0)=TL, (0.5,0.5)=centre, (1,1)=BR |
| GetAbsoluteSize() | FVector2D | Absolute (device px) | Widget size in device pixels: Size × Scale |
| GetLayoutBoundingRect() | FSlateRect | Absolute | AABB in absolute space — layout only, no render transforms |
| GetRenderBoundingRect() | FSlateRect | Absolute (paint) | AABB in absolute space — includes render transforms (larger if rotated) |
| IsUnderLocation(abs) | bool | Absolute | Hit test: is an absolute-space point inside this widget's layout bounds? |
GetCachedGeometry() and hold it across frames, but
the copy becomes stale the next time the widget is laid out (position or size changes).
For single-frame work this is safe; for multi-frame caching, re-fetch each tick.
8. Multiple Viewports & AddToPlayerScreen
Widgets can be added to the full game viewport or to a per-player screen area. In split-screen these are different regions of the window; in single-player they cover the same area but have different geometry origins.
| Function | Scope | Notes |
|---|---|---|
| AddToViewport(ZOrder) | Full game viewport | Shared across all local players. ZOrder controls draw order. |
| AddToPlayerScreen(ZOrder) | Per-player area | Correct for split-screen HUD. Requires a valid owning player. |
| GetViewportWidgetGeometry(World) | Full viewport geometry | Pair with widgets added via AddToViewport. |
| GetPlayerScreenWidgetGeometry(PC) | Player screen geometry | Pair with widgets added via AddToPlayerScreen. |
GetViewportWidgetGeometry to convert coordinates for a widget added
with AddToPlayerScreen will be slightly wrong in single-player and
completely wrong in split-screen.
SetPositionInViewport and SetDesiredSizeInViewport take
DPI-scaled Slate units, not device pixels. The optional bRemoveDPIScale
parameter (default true) divides the value by the DPI scale before applying
— pass false only if you have already done the division yourself.
9. Converting Between Spaces — C++
| From | To | Function | Class |
|---|---|---|---|
| Local | Absolute | FGeometry::LocalToAbsolute(local) | FGeometry |
| Absolute | Local | FGeometry::AbsoluteToLocal(abs) | FGeometry |
| Local | Viewport (Pixel + Scaled) | USlateBlueprintLibrary::LocalToViewport | SlateBlueprintLibrary |
| Absolute | Viewport (Pixel + Scaled) | USlateBlueprintLibrary::AbsoluteToViewport | SlateBlueprintLibrary |
| Screen | Local | USlateBlueprintLibrary::ScreenToWidgetLocal | SlateBlueprintLibrary |
| Screen | Absolute | USlateBlueprintLibrary::ScreenToWidgetAbsolute | SlateBlueprintLibrary |
| Screen | Viewport (Scaled) | USlateBlueprintLibrary::ScreenToViewport | SlateBlueprintLibrary |
| World 3D | Viewport Pixel | UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition | WidgetLayoutLibrary |
| Mouse | Viewport (Scaled) | UWidgetLayoutLibrary::GetMousePositionOnViewport | WidgetLayoutLibrary |
| Mouse | Screen / Desktop | UWidgetLayoutLibrary::GetMousePositionOnPlatform | WidgetLayoutLibrary |
LocalToViewport — annotated example
// Call from NativeTick or a mouse-event handler
const FGeometry& Geom = MyWidget->GetCachedGeometry();
FVector2D LocalPt = FVector2D(Geom.Size) * 0.5f; // widget centre in Slate units
FVector2D PixelPos; // raw device pixels — use for render targets
FVector2D ViewportPos; // DPI-scaled Slate units — use for SetPositionInViewport
USlateBlueprintLibrary::LocalToViewport(
GetWorld(), Geom, LocalPt,
PixelPos, // OUT 1
ViewportPos // OUT 2
);
// Never pass PixelPos to SetPositionInViewport — wrong on HiDPI
MyOtherWidget->SetPositionInViewport(ViewportPos); Scalar / vector helpers
When converting a distance or size (not a position) between spaces, use the scalar/vector variants — they avoid introducing any translation offset:
// Convert a 20-unit absolute offset to local-space units
float LocalOffset = USlateBlueprintLibrary::Scalar_AbsoluteToLocal(Geom, 20.f);
// Convert a local-space drag delta to absolute
FVector2D AbsDelta = USlateBlueprintLibrary::Vector_LocalToAbsolute(Geom, DragDelta); 10. Converting Between Spaces — Blueprint
All conversion functions are available as Blueprint nodes. The graphs below show each conversion pattern in a realistic context.
Local → Viewport (both pixel and scaled)
Use Get Cached Geometry on a widget reference, then feed it into Local to Viewport from the Slate Blueprint Library. The node has two output pins — Pixel Position and Viewport Position. Use Viewport Position for widget placement; Pixel Position for render targets or hardware cursor APIs.
Absolute → Viewport
Absolute to Viewport converts a window-space absolute coordinate to both viewport pixel and scaled positions. Useful when receiving an absolute coord from another system and needing it for widget placement.
Screen → Widget Local (with bIncludeWindowPosition)
Screen to Widget Local converts an OS screen coordinate into a specific
widget's local space. Set Include Window Position to true in windowed
mode to subtract the window's desktop offset.
Mouse position in viewport space
The simplest "where is the mouse in my viewport" pattern — Get Mouse Position on Viewport from Widget Layout Library returns a DPI-scaled Viewport Position directly, no geometry needed.
World 3D → Viewport (worldspace UI / nameplates)
Project World Location to Widget Position converts a 3D world position to
a 2D viewport pixel position — the core of any worldspace nameplate or ping system. Enable
Player Viewport Relative when using AddToPlayerScreen so the result
is relative to the player's sub-rect in split-screen.
11. Common Gotchas
⚡ "Absolute" doesn't mean screen-relative
The UMG/Slate "Absolute" space is window-relative, not desktop-relative. In fullscreen
the window origin is (0, 0) so this distinction is
invisible — code works fine in fullscreen then breaks in windowed mode when the window
is offset from the desktop origin. If you need actual screen (desktop) coordinates,
use GetMousePositionOnPlatform or the
ScreenToWidget* family with
bIncludeWindowPosition = true.
⚡ LocalToViewport returns TWO different things
The node outputs both Pixel Position
(device pixels, no DPI scaling) and Viewport Position
(DPI-scaled Slate units). They are identical at 1× DPI and differ by a factor of 2+ on
a Retina display or HiDPI console. Always use Viewport Position for UMG widget
placement and HUD drawing; Pixel Position is for render targets and hardware
cursor APIs only.
⚡ GetCachedGeometry() is zero on the first frame
The cached geometry is populated after the widget's first Tick/Paint pass. Calling
GetCachedGeometry() in
NativeConstruct, immediately after
AddToViewport,
or in any constructor context returns a zeroed struct —
Size = (0, 0),
AbsolutePosition = (0, 0). Defer geometry reads
to NativeTick, or guard with
if (Geom.Size.X > 0.f).
⚡ bIncludeWindowPosition must match windowed vs fullscreen
The bIncludeWindowPosition parameter on
ScreenToWidgetLocal and
ScreenToWidgetAbsolute controls whether the
window's desktop offset is subtracted. In fullscreen the offset is zero so the flag
has no visible effect. In windowed mode passing false
offsets every coordinate by the window border — often 50–200 pixels.
Safest default: always pass true.
⚡ Render transforms break coordinate conversion and hit tests
SetRenderTransformAngle /
SetRenderScale only affect how the widget is
drawn. Layout and hit-test geometry remain at the original unrotated position.
AbsoluteToLocal operates in layout space only — if
you rotate a widget 45° then call AbsoluteToLocal
on a point visually inside it, the result will be wrong. Use
GetPaintSpaceGeometry() or
GetRenderBoundingRect() when you need the actual
rendered position.
⚡ AddToPlayerScreen and GetViewportWidgetGeometry don't mix
In split-screen the player screen is a sub-region of the full viewport with a non-zero
origin offset. Even in single-player the geometry origins differ. If you add a widget
with AddToPlayerScreen but convert coordinates
using GetViewportWidgetGeometry, positions will be
shifted by that offset. Always pair:
AddToViewport
+ GetViewportWidgetGeometry, or
AddToPlayerScreen +
GetPlayerScreenWidgetGeometry(PC).
⚡ Absolute space in PIE is relative to the editor window, not the game viewport
In PIE the Slate root window is the entire editor window. Raw absolute coordinates
therefore include the offset from the editor window corner to wherever the game viewport
panel is positioned (tabs, toolbars, content browser all contribute). Code that works
with hardcoded absolute values in standalone will produce wrong results in PIE.
Always go through GetViewportWidgetGeometry()
to anchor viewport-relative math correctly in both environments.