--- name: hz-unity-meta-quest-ui license: Apache-2.0 description: Configures Unity UI for Meta Quest and Horizon OS VR development — world-space canvases, TextMesh Pro setup, comfortable sizing, viewing distances, and interaction readiness. --- # Meta Quest 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 ## Prerequisite: TMP Essential Resources Before creating ANY VR UI, verify TMP resources are imported. Use `Unity_RunCommand`: ```csharp using UnityEngine; using UnityEditor; using System.IO; internal class CommandScript : IRunCommand { public void Execute(ExecutionResult result) { string fontPath = "Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset"; var font = AssetDatabase.LoadAssetAtPath(fontPath); if (font != null) result.Log("TMP Essential Resources: IMPORTED. Default font present."); else result.LogError("TMP Essential Resources: NOT IMPORTED. Use tmp-resources skill first."); } } ``` If not imported, use the **tmp-resources** skill before proceeding. ## Step 1: Create World Space Canvas Use `Unity_RunCommand` to create and configure the canvas: ```csharp using UnityEngine; using UnityEditor; using UnityEngine.UI; internal class CommandScript : IRunCommand { public void Execute(ExecutionResult result) { // 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); result.RegisterObjectCreation(go); result.Log("Created VR Canvas '{0}'. Scale: {1}, Size: {2}, Position: {3}", go.name, rt.localScale, rt.sizeDelta, rt.position); } } ``` ### 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 using UnityEngine; using UnityEditor; using UnityEngine.UI; using TMPro; internal class CommandScript : IRunCommand { public void Execute(ExecutionResult result) { // Replace "MenuUI" with the actual canvas name used in Step 1 var canvas = GameObject.Find("MenuUI"); if (canvas == null) { result.LogError("Canvas 'MenuUI' not found."); return; } // 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