quot;Panel Settings render mode is {doc.panelSettings.renderMode}, expected WorldSpace."); problems++; } if (doc.visualTreeAsset == null) { Report("UIDocument has no Source Asset (UXML)."); problems++; } if (!HasComponentNamed("XRUIToolkitManager")) { Report("No XRUIToolkitManager in the scene (add 'XR UI Toolkit Manager')."); problems++; } if (!HasComponentNamed("PanelInputConfiguration")) { Report("No Panel Input Configuration in the scene (set Panel Input Redirection to 'No input redirection')."); problems++; } if (GetComponentInChildren<Collider>() == null) { Report("No collider yet. With Collider Update Mode = Match 2-D/3-D, UI Toolkit creates it after the first render. " + "Rechecking after two frames."); StartCoroutine(RecheckCollider()); } else if (!HasInteractable()) { Report("No XR interactable on the panel; ray hover works, but poke needs XRSimpleInteractable + XRPokeFilter."); } Debug.Log(problems == 0 ? "[XRUIToolkitPanelDiagnostics] no obvious setup problems found." : quot;[XRUIToolkitPanelDiagnostics] {problems} problem(s) found."); } System.Collections.IEnumerator RecheckCollider() { yield return null; yield return null; bool has = GetComponentInChildren<Collider>() != null; Debug.Log(has ? "[XRUIToolkitPanelDiagnostics] collider appeared after first render (expected)." : "[XRUIToolkitPanelDiagnostics] still no collider. Set Collider Update Mode or add one manually."); } static bool HasComponentNamed(string typeName) { return Object.FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Exclude, FindObjectsSortMode.None) .Any(m => m != null && m.GetType().Name == typeName); } bool HasInteractable() { return GetComponents<MonoBehaviour>().Any(m => m != null && m.GetType().Name.Contains("Interactable")); } void Report(string message) => Debug.LogWarning(quot;[XRUIToolkitPanelDiagnostics] {message}", this); } ``` ## 8.4 Quest 2 design rules for VR UI Toolkit panels - **Large text.** Use font sizes around 34-48 px for a 800 px wide panel viewed at roughly 1 m (the worked example uses these). Quest 2 resolution is lower than a monitor, and foveated rendering ([[5.Quest 2 Performance and Rendering]]) blurs the periphery. - **Update sparsely.** Do not write labels every frame. Use `schedule.Execute(...).Every(ms)` ([[7.UI Toolkit Pitfalls and Performance]]). - **Keep it world-space.** VRC.Quest.Functional.10 recommends avoiding head-locked UI. - **Keep draw cost small.** The cost of a world-space UI Toolkit panel on Quest 2 is **not documented**; profile on the device. - **Prefer ray first, poke second.** Poke needs a collider with depth (Match 3-D bounding box) and careful z-depth. ## 8.5 Route B: Meta Interaction SDK ISDK's documented UI route uses uGUI canvases. If you must keep ISDK as the only interaction system, use Route C. If you want UI Toolkit with XRI interactors, use Route A and let ISDK own only the hands or grabs, never both on the same hand ([[2.Quest 2 SDK Stack]]). ## 8.6 Route C (fallback): RenderTexture panel with ray-to-UV input When the XRI route is not available, render the panel into a `RenderTexture` shown on a quad (the approach of [[6.Diegetic Terminal and Wrist Display]]) and forward your own ray hits into the panel. > [!warning] Unverified fallback > This route uses public UI Toolkit APIs (`panel.Pick`, `NavigationSubmitEvent`) as I understand them, but I did not compile or run it. Test on the Quest 2 early. ### Setup 1. Create a `RenderTexture` (for example 1024x640) asset. 2. Create a **Panel Settings** asset with **Render Mode = Render Texture** and assign the `RenderTexture` as the **Target Texture**. 3. Put a **UI Document** (UXML as in [[7.Quest 2 Worked Example Room]]) on a GameObject using that Panel Settings. 4. Create a **Quad** (it comes with a MeshCollider; UVs only come back from a **MeshCollider**). Give it an **Unlit** material whose base map is the `RenderTexture`. 5. Add `RenderTexturePanelPointer` below to the quad. Assign the document, a `rayOrigin` (the controller transform, or the XRI ray interactor's attach/origin transform), and a trigger `InputActionProperty` (for example the right controller's **Activate** or **Select** action). ### Script: RenderTexturePanelPointer ```csharp using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UIElements; /// <summary> /// Forwards a VR controller ray to a UI Toolkit panel rendered on a quad via RenderTexture. /// Hover is shown with the USS class "xr-hover"; the trigger sends a NavigationSubmitEvent /// to the nearest focusable element (Button, Toggle). Not compiled in an editor. /// </summary> [RequireComponent(typeof(MeshCollider))] public class RenderTexturePanelPointer : MonoBehaviour { const string HoverClass = "xr-hover"; [SerializeField] UIDocument document; [SerializeField] Transform rayOrigin; [SerializeField] InputActionProperty triggerAction; [SerializeField] float maxDistance = 6f; [Tooltip("Flip if hover/clicks appear mirrored vertically.")] [SerializeField] bool flipV = true; MeshCollider meshCollider; VisualElement hovered; bool wasPressed; void Awake() { meshCollider = GetComponent<MeshCollider>(); } void OnEnable() { if (triggerAction.action != null) triggerAction.action.Enable(); } void OnDisable() { if (triggerAction.action != null) triggerAction.action.Disable(); SetHover(null); } void Update() { var root = document != null ? document.rootVisualElement : null; if (root == null || root.panel == null || rayOrigin == null) return; VisualElement under = null; if (TryGetPanelPoint(root, out Vector2 panelPoint)) { under = root.panel.Pick(panelPoint); } SetHover(under); bool pressed = triggerAction.action != null && triggerAction.action.IsPressed(); if (pressed && !wasPressed && under != null) { Submit(under); } wasPressed = pressed; } bool TryGetPanelPoint(VisualElement root, out Vector2 panelPoint) { panelPoint = default; var ray = new Ray(rayOrigin.position, rayOrigin.forward); if (!meshCollider.Raycast(ray, out RaycastHit hit, maxDistance)) return false; Vector2 uv = hit.textureCoord; // valid only for MeshCollider hits Vector2 size = root.panel.visualTree.layout.size; float v = flipV ? 1f - uv.y : uv.y; panelPoint = new Vector2(uv.x * size.x, v * size.y); return true; } void SetHover(VisualElement next) { if (ReferenceEquals(hovered, next)) return; hovered?.RemoveFromClassList(HoverClass); hovered = next; hovered?.AddToClassList(HoverClass); } static void Submit(VisualElement picked) { // Walk up to the nearest focusable element so clicking a Button's inner label works. VisualElement target = picked; while (target != null && !target.focusable) { target = target.parent; } if (target == null) return; target.Focus(); using (var submit = NavigationSubmitEvent.GetPooled()) { submit.target = target; target.SendEvent(submit); } } } ``` > [!note] How the fallback maps coordinates > `hit.textureCoord` gives UV in 0..1 with the origin at the bottom-left; UI Toolkit panel coordinates have the origin at the top-left, so the script flips V. The panel size comes from the panel's root layout. If your panel scale differs from the texture size, adjust the multiplication. > [!note] What this fallback does not do > No pointer-drag (scroll views, sliders) and no text input. For those you need real pointer events; consider the virtual-mouse approach (an Input System `Mouse` device driven with `InputState.Change`, plus `SetScreenToPanelSpaceFunction` as in note 6), which I did not verify for VR. ## 8.7 Troubleshooting | Symptom | Likely cause | |---|---| | Ray passes through the panel | No collider yet, or collider on a layer the interactor ignores | | Hover works, click does nothing | `Panel Input Configuration` not set to No input redirection, or the Game view is not focused (Link) | | Poke completes instantly | Collider Update Mode = Match 2-D document rect (zero thickness); use Match 3-D bounding box | | Poke z-depth stuck | Nested child of a z-depth element; apply depth to the Button/Toggle itself | | Panel interactable steals the ray | Nested UIDocument collider adopted by an interactable; set Is Trigger or assign colliders manually | | Keyboard does not appear | **Enable System Keyboard** off, or `Hide Soft Keyboard` / `Hide Mobile Input` ticked on the TextField | ## Official references - [XRI: XR Interaction Toolkit + UI Toolkit](https://docs.unity3d.com/Packages/[email protected]/manual/ui-world-space-ui-toolkit-support.html) - [XRI: XR UI Input Module](https://docs.unity3d.com/Packages/[email protected]/manual/xr-ui-input-module.html) - [Unity: Panel Settings (UI Toolkit)](https://docs.unity3d.com/6000.3/Documentation/Manual/UIE-Runtime-Panel-Settings.html) Back to [[UI Toolkit and Diegetic UI]]. Quest 2 project: [[Meta Quest 2]], [[7.Quest 2 Worked Example Room]].