Coordinate Utilities

Luna UI provides utilities for coordinate conversion between screen space and world space UI through UITKCoordinateUtility and UITKWorldSpace.

Understanding Coordinate Spaces

Start here — every API below exists because these two spaces measure things differently.

Screen Space UI

  • worldBound is in screen pixels
  • No coordinate conversion needed
  • Y increases downward from top-left

World Space UI

  • worldBound is in world units (meters)
  • Must convert to panel-local pixels
  • Y-axis may need inversion (world Y increases up, UI Y increases down)
PropertyScreenSpaceWorldSpace
worldBoundScreen pixelsWorld units
localBoundPanel pixelsPanel pixels
Y-axisDown positiveUp positive

UITKCoordinateUtility

A static utility class for coordinate conversions in UI Toolkit, supporting both ScreenSpace and WorldSpace UI panels.

IsWorldSpace

Determines whether a VisualElement or IPanel belongs to a world-space panel.

csharp
public static bool IsWorldSpace(VisualElement visualElement) public static bool IsWorldSpace(IPanel panel)

Example:

csharp
bool isWorldSpace = UITKCoordinateUtility.IsWorldSpace(myElement); if (isWorldSpace) { // Handle world-space specific logic }

WorldBoundToLocalBound

Converts an element's worldBound to panel-local coordinates.

csharp
public static Rect WorldBoundToLocalBound( VisualElement element, VisualElement panelRoot, bool worldSpace )
ParameterDescription
elementThe element to get bounds for
panelRootThe root element of the panel
worldSpaceWhether the panel is world-space

Returns: Rect in panel-local pixel coordinates.

  • For WorldSpace UI: Converts from world units (meters) to panel-local pixels
  • For ScreenSpace UI: Returns worldBound as-is (already correct)

Example:

csharp
// '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 );

PointerToLocalPosition

Converts a pointer/mouse position to panel-local pixel coordinates.

csharp
public static Vector2 PointerToLocalPosition( Vector2 pointerPosition, IPanel panel, VisualElement referenceElement, bool invertY, bool worldSpace )
ParameterDescription
pointerPositionScreen position of the pointer
panelThe UI panel
referenceElementReference element for coordinate mapping
invertYWhether to invert Y-axis (usually true for WorldSpace)
worldSpaceWhether the panel is world-space

Example:

csharp
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; }

WorldToLocalPosition

Converts a world-space position to panel-local pixel coordinates when you already have the reference bounds.

csharp
public static Vector2 WorldToLocalPosition( Vector2 worldPosition, IPanel panel, Rect referenceWorldBound, Rect referenceLocalBound, bool worldSpace )

GetPanelBounds

Gets the appropriate bounds for an element based on panel context.

csharp
public static Rect GetPanelBounds(VisualElement element, bool worldSpace)
  • For WorldSpace UI: Returns localBound
  • For ScreenSpace UI: Returns worldBound

UITKWorldSpace

A MonoBehaviour component that sets up a UI Toolkit panel for world-space rendering on a 3D object.

How It Works

  1. Creates a unique PanelSettings instance for the child PanelRenderer
  2. Creates a RenderTexture for the UI
  3. Assigns the RenderTexture to the PanelSettings' targetTexture and to a child Renderer via MaterialPropertyBlock (_BaseMap)

Setup

  1. Create a 3D object (e.g., Quad) with a Renderer
  2. Add a child GameObject with PanelRenderer
  3. Add UITKWorldSpace to the parent object

Hierarchy:

WorldSpaceUI (UITKWorldSpace) ├── UICanvas (PanelRenderer) └── Quad (MeshRenderer)

Usage

csharp
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!"); } }

Configuration

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:

csharp
// 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.

Example: Drag and Drop with World Space Support

The DragAndDropManipulator uses these utilities internally:

csharp
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); } }

Example: World Space Tooltip

csharp
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; } }

Tips

  • Always use IsWorldSpace() to detect the panel type before conversions
  • Use invertY: true for most WorldSpace pointer conversions
  • The reference element should be the panel root for accurate mapping
  • WorldSpace UI requires a Renderer with appropriate material
  • Consider resolution when creating world space UI (higher res = more GPU cost)

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL