--- name: hz-unity-meta-quest-ui license: Apache-2.0 description: Configures Unity UI for Meta VR and Horizon OS VR development — world-space canvases, TextMesh Pro setup, comfortable sizing, viewing distances, and interaction readiness. --- # Meta VR UI Setup ## When to use this skill Use this skill automatically when: - Setting up a Canvas for VR - Creating UI text with TextMesh Pro in a VR project - Adding buttons, sliders, or other interactive UI in VR - User reports pink/magenta text, unclickable buttons, or UI sizing issues in VR - Configuring VR interaction (ray or poke) on a Canvas ## Driving the Editor Build the UI against an open Editor with `unity-cli` rather than hand-editing scene YAML — the Editor applies changes to the actual active scene. ```bash unity status --format json # look for state "ready" unity command run_script --file AgentScripts/BuildUI.cs --entry BuildUI.Run --format json ``` Canvas construction is multi-component work, so it belongs in a `run_script` `.cs` file (kept **outside `Assets/`**, path relative to the project root) rather than a chain of single commands. Add `--dry_run true` to compile-check a script before it mutates the scene — but read the verdict from **`data.result.success`**: a compile failure still returns outer `success: true` and exit code `0`, with the errors in `data.result.diagnostics`. Connecting, `--project-path`, and Safe Mode recovery are in the **`unity-cli`** skill; MCP equivalents are in [references/unity-mcp-fallback.md](references/unity-mcp-fallback.md). Single-property reads and writes are cheaper as direct commands than as a script: ```bash unity command get_component_properties --target MenuUI --type Canvas --format json unity command set_transform --target MenuUI --position "[0,1.5,2]" --format json unity command find_gameobjects --name MenuUI --format json ``` Array parameters take a **JSON array in one argument** (`--position "[0,1.5,2]"`). Space- or comma-separated components are rejected with `INVALID_COMMAND_ARGS`. Note `set_transform` writes **local** position — see the **unity-placement** skill for parented canvases. ## Prerequisite: TMP Essential Resources Before creating ANY VR UI, verify TMP resources are imported: ```bash unity command find_assets --name "LiberationSans SDF" --format json ``` A `data.result.count` of `0` means they are missing — use the **tmp-resources** skill before proceeding. ## Step 1: Create World Space Canvas ```csharp // AgentScripts/BuildUI.cs using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; public static class BuildUI { public static string Run() { // Adapt the name to match your canvas (e.g., "MainMenu", "SettingsUI") var go = new GameObject("MenuUI"); var canvas = go.AddComponent(); canvas.renderMode = RenderMode.WorldSpace; go.AddComponent(); // Remove CanvasScaler — not appropriate for VR var scaler = go.GetComponent(); if (scaler != null) Object.DestroyImmediate(scaler); var rt = go.GetComponent(); rt.localScale = new Vector3(0.001f, 0.001f, 0.001f); rt.sizeDelta = new Vector2(1920f, 1080f); rt.position = new Vector3(0f, 1.5f, 2f); Undo.RegisterCreatedObjectUndo(go, "Create VR Canvas"); EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); return $"Created VR Canvas '{go.name}'. Scale: {rt.localScale}, Size: {rt.sizeDelta}, Position: {rt.position}"; } } ``` Persist it when the shape is right: ```bash unity command save_scene --format json ``` ### Canvas rules - **Render Mode**: Always World Space. Screen Space modes break stereo rendering. - **Scale**: 0.001 on all axes (1 unit in canvas = 1mm in world). - **CanvasScaler**: Remove it. Physical size is controlled by world scale, not screen adaptation. - **Distance**: Place 1.5-3m from user. Never closer than 0.5m. Max 5m for readable text. - **Physical size formula**: `Canvas sizeDelta * scale = meters`. Example: 1920 * 0.001 = 1.92m wide. ## Step 2: Create child UI elements All child elements (panels, buttons, text) must follow these rules: - **localScale**: Always `[1, 1, 1]`. Never scale children to compensate for canvas scale. - **localPosition.z**: Always `0`. Children must sit on the canvas plane. - **Size control**: Use `RectTransform.sizeDelta` and anchors, never scale. ```csharp // AgentScripts/BuildPanel.cs using TMPro; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; public static class BuildPanel { public static string Run() { // Replace "MenuUI" with the actual canvas name used in Step 1 var canvas = GameObject.Find("MenuUI"); if (canvas == null) return "ERROR: Canvas 'MenuUI' not found."; // Panel var panel = new GameObject("ButtonPanel"); panel.transform.SetParent(canvas.transform, false); var panelRT = panel.AddComponent(); panelRT.localScale = Vector3.one; panelRT.sizeDelta = new Vector2(800f, 600f); var panelImg = panel.AddComponent(); panelImg.color = new Color(0.1f, 0.1f, 0.1f, 0.95f); panelImg.raycastTarget = false; var layout = panel.AddComponent(); layout.spacing = 50f; layout.padding = new RectOffset(80, 80, 100, 100); layout.childAlignment = TextAnchor.MiddleCenter; // Button var btnGO = new GameObject("StartButton"); btnGO.transform.SetParent(panel.transform, false); var btnRT = btnGO.AddComponent(); btnRT.localScale = Vector3.one; btnRT.sizeDelta = new Vector2(400f, 120f); var btnImg = btnGO.AddComponent(); btnImg.color = new Color(0.2f, 0.6f, 1f, 1f); btnGO.AddComponent