Luna UI provides utilities for coordinate conversion between screen space and world space UI through UITKCoordinateUtility and UITKWorldSpace.
Start here — every API below exists because these two spaces measure things differently.
worldBound is in screen pixelsworldBound is in world units (meters)| Property | ScreenSpace | WorldSpace |
|---|---|---|
| worldBound | Screen pixels | World units |
| localBound | Panel pixels | Panel pixels |
| Y-axis | Down positive | Up positive |
A static utility class for coordinate conversions in UI Toolkit, supporting both ScreenSpace and WorldSpace UI panels.
Determines whether a VisualElement or IPanel belongs to a world-space panel.
public static bool IsWorldSpace(VisualElement visualElement)
public static bool IsWorldSpace(IPanel panel)Example:
bool isWorldSpace = UITKCoordinateUtility.IsWorldSpace(myElement);
if (isWorldSpace)
{
// Handle world-space specific logic
}Converts an element's worldBound to panel-local coordinates.
public static Rect WorldBoundToLocalBound(
VisualElement element,
VisualElement panelRoot,
bool worldSpace
)| Parameter | Description |
|---|---|
| element | The element to get bounds for |
| panelRoot | The root element of the panel |
| worldSpace | Whether the panel is world-space |
Returns: Rect in panel-local pixel coordinates.
Example:
// 'root' is the visual tree delivered by your PanelRenderer's reload
// callback (see the World Space Tooltip example below for registration)
var button = root.Q<Button>("MyButton");
bool isWorldSpace = UITKCoordinateUtility.IsWorldSpace(button);
Rect localBounds = UITKCoordinateUtility.WorldBoundToLocalBound(
button,
root,
isWorldSpace
);Converts a pointer/mouse position to panel-local pixel coordinates.
public static Vector2 PointerToLocalPosition(
Vector2 pointerPosition,
IPanel panel,
VisualElement referenceElement,
bool invertY,
bool worldSpace
)| Parameter | Description |
|---|---|
| pointerPosition | Screen position of the pointer |
| panel | The UI panel |
| referenceElement | Reference element for coordinate mapping |
| invertY | Whether to invert Y-axis (usually true for WorldSpace) |
| worldSpace | Whether the panel is world-space |
Example:
void OnPointerMove(PointerMoveEvent evt)
{
Vector2 localPos = UITKCoordinateUtility.PointerToLocalPosition(
evt.position,
_element.panel,
_panelRoot,
invertY: true,
worldSpace: _isWorldSpace
);
// Position element at pointer
_tooltip.style.left = localPos.x;
_tooltip.style.top = localPos.y;
}Converts a world-space position to panel-local pixel coordinates when you already have the reference bounds.
public static Vector2 WorldToLocalPosition(
Vector2 worldPosition,
IPanel panel,
Rect referenceWorldBound,
Rect referenceLocalBound,
bool worldSpace
)Gets the appropriate bounds for an element based on panel context.
public static Rect GetPanelBounds(VisualElement element, bool worldSpace)localBoundworldBoundA MonoBehaviour component that sets up a UI Toolkit panel for world-space rendering on a 3D object.
PanelSettings instance for the child PanelRendererRenderTexture for the UItargetTexture and to a child Renderer via MaterialPropertyBlock (_BaseMap)UITKWorldSpace to the parent objectHierarchy:
WorldSpaceUI (UITKWorldSpace)
├── UICanvas (PanelRenderer)
└── Quad (MeshRenderer)using UnityEngine;
using UnityEngine.UIElements;
using CupkekGames.Luna;
public class WorldSpaceUIExample : MonoBehaviour
{
// UITKWorldSpace automatically sets up the panel
// Just add your PanelRenderer as a child
private PanelRenderer _panelRenderer;
private Button _button;
void Awake()
{
_panelRenderer = GetComponentInChildren<PanelRenderer>();
if (_panelRenderer != null)
{
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
}
// PanelRenderer delivers the visual tree asynchronously —
// query elements in the reload callback, not in Awake/Start.
private void OnUIReload(PanelRenderer renderer, VisualElement root, int version)
{
if (_button != null) return; // sentinel: only init once
// UI works normally - it's just rendered to a texture
_button = root.Q<Button>("MyButton");
_button.clicked += OnButtonClicked;
}
void OnDestroy()
{
if (_panelRenderer != null)
{
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
}
}
void OnButtonClicked()
{
Debug.Log("World space button clicked!");
}
}The RenderTexture is hardcoded to 256×256 pixels (ARGB32, 16-bit depth) — UITKWorldSpace exposes no Inspector field or property for it. To change the resolution, copy the class into your project (it's ~45 lines in Runtime/Scripts/Utility/UITKWorldSpace.cs), rename it, and adjust the constructor call:
// In your copy (e.g. MyWorldSpaceUI.cs):
_renderTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.ARGB32);Don't edit the file inside the package — package updates overwrite it. Note this component implements the render-to-texture technique (UI drawn onto a quad's material). Unity 6's native World Space render mode (LunaPanelSettingsWorldSpace.asset — see Theme Stack) needs no render texture at all and is the better default for flat in-world panels.
The DragAndDropManipulator uses these utilities internally:
public class DragAndDropManipulator : Manipulator
{
private bool _worldSpace;
private VisualElement _dragArea;
protected void PointerMoveHandler(PointerMoveEvent evt)
{
// Convert pointer position to local coordinates
Vector2 localPosition = UITKCoordinateUtility.PointerToLocalPosition(
evt.position,
target.panel,
_dragArea,
invertY: true,
worldSpace: _worldSpace
);
// Position the drag element
_dragElement.style.left = localPosition.x;
_dragElement.style.top = localPosition.y;
}
protected bool OverlapsTarget(VisualElement target)
{
// Get bounds in the correct coordinate space
Rect dragBounds = UITKCoordinateUtility.WorldBoundToLocalBound(
_dragElement, _dragArea, _worldSpace);
Rect targetBounds = UITKCoordinateUtility.WorldBoundToLocalBound(
target, _dragArea, _worldSpace);
return dragBounds.Overlaps(targetBounds);
}
}using UnityEngine;
using UnityEngine.UIElements;
using CupkekGames.Luna;
public class WorldSpaceTooltip : MonoBehaviour
{
[SerializeField] private PanelRenderer _panelRenderer;
private VisualElement _panelRoot;
private VisualElement _tooltip;
private bool _isWorldSpace;
void Awake()
{
if (_panelRenderer == null)
{
_panelRenderer = GetComponent<PanelRenderer>();
}
if (_panelRenderer != null)
{
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
}
private void OnUIReload(PanelRenderer renderer, VisualElement root, int version)
{
if (_tooltip != null) return; // sentinel: only init once
_panelRoot = root;
_tooltip = root.Q<VisualElement>("Tooltip");
_isWorldSpace = UITKCoordinateUtility.IsWorldSpace(root);
var target = root.Q<Button>("HoverTarget");
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
}
void OnDestroy()
{
if (_panelRenderer != null)
{
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
}
}
void OnPointerMove(PointerMoveEvent evt)
{
Vector2 localPos = UITKCoordinateUtility.PointerToLocalPosition(
evt.position,
_tooltip.panel,
_panelRoot,
invertY: true,
worldSpace: _isWorldSpace
);
// Offset tooltip from cursor
_tooltip.style.left = localPos.x + 10;
_tooltip.style.top = localPos.y + 10;
}
}IsWorldSpace() to detect the panel type before conversionsinvertY: true for most WorldSpace pointer conversionsDragAndDropManipulator that consumes these utilitiesSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction