hz-unity-meta-quest-ui について
Meta Quest向けUnityプロジェクトでVR UIを作るためのガイドです。Canvas設定、TextMesh Pro、レイアウト検証、操作設定を扱い、サイズ基準とトラブルシューティングも示します。
主な機能
- 01World Space Canvasの設定
- 02TextMesh ProとUIサイズの設定
- 03Canvasと子要素のレイアウト検証
- 04レイ・ポーク・視線操作の追加
- 05VR UIの一般的な問題の解決
活用シーン
- 01Questのメニューや操作パネルの作成
- 02文字色や鮮明さの問題の修正
- 03ヘッドセットで反応しないボタンの診断
- 04Canvas倍率と要素配置の調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ meta-quest/ agentic-tools --skill hz-unity-meta-quest-uiインストール後、Claude Code で/hz-unity-meta-quest-uiを入力して読み込まれたか確認します。
同梱ファイルreferences/unity-mcp-fallback.md
詳細
Meta VR UI のセットアップ
このスキルを使うタイミング
次の場合は、このスキルを自動的に使用します。
- VR 用の Canvas をセットアップするとき
- VR プロジェクトで TextMesh Pro を使って UI テキストを作成するとき
- VR でボタン、スライダー、その他のインタラクティブな UI を追加するとき
- VR でピンク/マゼンタのテキスト、クリックできないボタン、UI のサイズの問題が報告されたとき
- Canvas で VR インタラクション(レイまたはポーク)を設定するとき
エディターの操作
シーンの YAML を手作業で編集するのではなく、開いているエディターに対して unity-cli を使って UI を構築してください。エディターは、実際にアクティブなシーンに変更を適用します。
unity status --format json # look for state "ready"
unity command run_script --file AgentScripts/BuildUI.cs --entry BuildUI.Run --format json
Canvas の構築は複数のコンポーネントに関わる作業なので、単一コマンドを連続して実行するのではなく、run_script の .cs ファイル(Assets/ の外側に置き、プロジェクトルートからの相対パスで指定)で行います。シーンを変更する前にスクリプトをコンパイルチェックするには、--dry_run true を追加してください。ただし、判定結果は data.result.success から確認してください。コンパイルに失敗しても、外側の success: true と終了コード 0 が返され、エラーは data.result.diagnostics に含まれます。接続方法、--project-path、Safe Mode からの復旧については unity-cli スキルを、MCP の同等機能については references/unity-mcp-fallback.md を参照してください。
単一プロパティの読み書きは、スクリプトではなく直接コマンドを使うほうが効率的です。
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
配列パラメーターには、1 つの引数として JSON 配列(--position "[0,1.5,2]")を指定します。スペース区切りやカンマ区切りの値は INVALID_COMMAND_ARGS で拒否されます。set_transform が書き込むのはローカル位置です。親が設定された Canvas については、unity-placement スキルを参照してください。
前提条件:TMP Essential Resources
VR UI を作成する前に、TMP リソースがインポートされていることを必ず確認してください。
unity command find_assets --name "LiberationSans SDF" --format json
data.result.count が 0 の場合、リソースが見つかりません。先に tmp-resources スキルを使ってください。
ステップ 1:World Space Canvas を作成する
// 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>();
canvas.renderMode = RenderMode.WorldSpace;
go.AddComponent<GraphicRaycaster>();
// Remove CanvasScaler — not appropriate for VR
var scaler = go.GetComponent<CanvasScaler>();
if (scaler != null)
Object.DestroyImmediate(scaler);
var rt = go.GetComponent<RectTransform>();
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}";
}
}
形状が適切になったら、保存してください。
unity command save_scene --format json
Canvas のルール
- Render Mode:必ず World Space にします。Screen Space モードではステレオレンダリングに問題が生じます。
- スケール:すべての軸で 0.001 にします(Canvas 上の 1 単位 = ワールド空間の 1mm)。
- CanvasScaler:削除します。物理的なサイズは画面への適応ではなく、ワールドスケールで制御します。
- 距離:ユーザーから 1.5~3m の位置に配置します。0.5m より近づけないでください。テキストを読み取れる距離は最大 5m です。
- 物理サイズの計算式:
Canvas sizeDelta * scale = meters。例:1920 * 0.001 = 幅 1.92m。
ステップ 2:子 UI 要素を作成する
すべての子要素(パネル、ボタン、テキスト)は、次のルールに従う必要があります。
- localScale:必ず
[1, 1, 1]にします。Canvas のスケールを補うために子要素を拡大縮小しないでください。 - localPosition.z:必ず
0にします。子要素は Canvas の平面上に配置してください。 - サイズの調整:スケールではなく、
RectTransform.sizeDeltaとアンカーを使います。
// 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<RectTransform>();
panelRT.localScale = Vector3.one;
panelRT.sizeDelta = new Vector2(800f, 600f);
var panelImg = panel.AddComponent<Image>();
panelImg.color = new Color(0.1f, 0.1f, 0.1f, 0.95f);
panelImg.raycastTarget = false;
var layout = panel.AddComponent<VerticalLayoutGroup>();
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<RectTransform>();
btnRT.localScale = Vector3.one;
btnRT.sizeDelta = new Vector2(400f, 120f);
var btnImg = btnGO.AddComponent<Image>();
btnImg.color = new Color(0.2f, 0.6f, 1f, 1f);
btnGO.AddComponent<Button>();
// Button text
var textGO = new GameObject("Text");
textGO.transform.SetParent(btnGO.transform, false);
var textRT = textGO.AddComponent<RectTransform>();
textRT.localScale = Vector3.one;
textRT.anchorMin = Vector2.zero;
textRT.anchorMax = Vector2.one;
textRT.sizeDelta = Vector2.zero;
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = "Start";
tmp.fontSize = 48f;
tmp.color = new Color(0.95f, 0.95f, 0.95f, 1f);
tmp.alignment = TextAlignmentOptions.Center;
tmp.raycastTarget = false;
Undo.RegisterCreatedObjectUndo(panel, "Create VR Panel");
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
return $"Created panel with button. Panel scale: {panelRT.localScale}, Button scale: {btnRT.localScale}";
}
}
ステップ 3:作成した UI を検証する
Canvas 自体を直接コマンドで確認してください。
unity command get_component_properties --target MenuUI --type Canvas --format json
unity command get_scene_hierarchy --format json # whole active scene, incl. children
get_scene_hierarchy --path には開いているシーンのパスを指定します。GameObject は指定できません。--path MenuUI を指定すると、"Scene 'MenuUI' is not open." というエラーになります。--path を省略し、アクティブなシーンから MenuUI のサブツリーを確認してください。
エディターでの確認だけでは不十分です。ビルドをデプロイし、ヘッドセットから metavr capture screenshot でスクリーンショットを取得して、ユーザーが見る状態の UI を確認してください。
すべての子要素を検査して自動修正するには、スクリプトを使います。
// AgentScripts/ValidateUI.cs
using System.Text;
using UnityEngine;
public static class ValidateUI
{
public static string Run()
{
// Replace "MenuUI" with the actual canvas name
var canvas = GameObject.Find("MenuUI");
if (canvas == null) return "ERROR: Canvas not found.";
var crt = canvas.GetComponent<RectTransform>();
var c = canvas.GetComponent<Canvas>();
var sb = new StringBuilder();
if (c.renderMode != RenderMode.WorldSpace)
sb.AppendLine("FAIL: Canvas renderMode is not World Space.");
if (Mathf.Abs(crt.localScale.x - 0.001f) > 0.0001f)
sb.AppendLine($"FAIL: Canvas scale is {crt.localScale}, expected 0.001.");
foreach (Transform child in canvas.GetComponentsInChildren<Transform>(true))
{
if (child == canvas.transform) continue;
var rt = child.GetComponent<RectTransform>();
if (rt == null) continue;
if (rt.localScale != Vector3.one)
{
sb.AppendLine($"FIXED: '{child.name}' had localScale {rt.localScale}, expected (1,1,1).");
rt.localScale = Vector3.one;
}
if (Mathf.Abs(rt.localPosition.z) > 0.01f)
{
sb.AppendLine($"FIXED: '{child.name}' had localPosition.z={rt.localPosition.z}, expected 0.");
rt.localPosition = new Vector3(rt.localPosition.x, rt.localPosition.y, 0f);
}
}
return sb.Length == 0 ? "Validation PASSED. All properties correct." : sb.ToString();
}
}
この方法で検出できる一般的な問題:
- Canvas のスケールが 0.001 ではなく 1,1,1 のままになっている
- 親を補うために子要素が自動的に拡大縮小されている(例:1000x)
- 子要素が Z 軸方向にずれている(例:localPosition.z = -2500)
ステップ 4:VR インタラクションを追加する(インタラクティブ UI のみ)
Canvas にインタラクティブな要素(ボタン、スライダー、ドロップダウン、トグル、入力フィールド)がある場合にのみ、インタラクションを追加してください。 表示専用 UI(HUD、スコア、ラベル)には追加しません。
判断フロー
Does your Canvas have buttons, dropdowns, sliders, toggles, or input fields?
YES -> Add VR interaction. Without it, interactive elements won't work in VR.
NO -> Skip. Display-only UI doesn't need interaction overhead.
接続する — ISDK クイックアクション
サポートされているエントリポイントは ISDK クイックアクション API で、run_script を通じて呼び出します。シグネチャと、これらの呼び出しに組み込まれている動作については、hz-unity-meta-interaction-sdk スキルを参照してください。
// AgentScripts/AddCanvasInteraction.cs
using Oculus.Interaction.Editor.QuickActions; // Editor-only assembly
using UnityEngine;
public static class AddCanvasInteraction
{
public static string Run()
{
var target = GameObject.Find("MenuUI");
QuickActionsAPI.AddRayCanvasInteraction(target); // or AddPokeCanvasInteraction
return "ok";
}
}
これらの呼び出しに渡すのは、キャンバスの GameObject であり、Canvas コンポーネントではありません — Canvas を渡すとコンパイルエラーになります(cannot convert from 'UnityEngine.Canvas' to 'UnityEngine.GameObject')。確認済みのシグネチャは次のとおりです。
| インタラクション | シグネチャ | 使用する場面 |
|---|---|---|
| Ray(デフォルト) | AddRayCanvasInteraction(GameObject target, bool fixPointableCanvasModule = true) | メニュー、設定パネル、手の届かない範囲にある UI |
| Poke(近距離) | AddPokeCanvasInteraction(GameObject target, bool fixPointableCanvasModule = true) | 表面上のコントロールパネル、仮想キーボード、小道具上のダイエジェティック UI(< 0.8m) |
| Gaze | AddGazeCanvasInteraction(GameObject target, bool enableRayFallback = true, bool addToChildCanvases = true, bool fixPointableCanvasModule = true) | 視線追跡による選択 |
| 両方(上級) | Ray を先に追加し、次に poke | ユーザーがポインティングまたはタッチできるハイブリッド UI |
Ray は RayInteractable、PointableCanvas、ISDK_RayCanvasInteraction の子オブジェクト、およびシーンレベルの Pointable Canvas Module を追加します。Poke は PokeInteractable、PointableCanvas、および近距離用の衝突検出を追加します。
PointableCanvasModuleがプレハブまたは未ロードの加算シーンにある場合は、fixPointableCanvasModule: falseを渡してください。そうしないと、開いているシーンにもう一つ追加されます。
Meta Unity 拡張機能を備えた Unity MCP サーバーを使用している場合は、同等のツールとして meta_add_canvas_interaction_ray / meta_add_canvas_interaction_poke を使います —
references/unity-mcp-fallback.md を参照してください。
インタラクションが追加されたことを確認する
unity command get_component_properties --target MenuUI --type PointableCanvas --format json
unity command find_gameobjects --name "Pointable Canvas Module" --format json
get_component_properties では、--target が GameObject の場合、必ず --type が必要です(指定しないと呼び出しは失敗します:"'type' is required when 'target' is a GameObject.")。また、コンポーネントの一覧ではなく1つのコンポーネントを対象とするため、確認対象のコンポーネント名を指定してください。成功すればコンポーネントが存在し、型名を示すエラーが返れば存在しません。
キャンバス上の PointableCanvas と、シーン内の Pointable Canvas Module が最低限必要です。PointableElement の配線は Awake() で割り当てられるため、プレハブアセットでは常に null と表示されます。代わりにシリアライズされたフィールドを確認してください。
unity command get_serialized_fields --target <interactable> --field _pointableElement --format json
ボタンが Editor では動作するのに Meta VR デバイスでは動作しない場合
実行時にシーンへインタラクションリグが必要です。
using Oculus.Interaction.OVR.Editor.QuickActions;
OVRQuickActionsAPI.AddOVRInteractionRig(); // also adds an OVRCameraRig if the scene has none
これにより、すべてのハンド/コントローラーインタラクターが提供されます。構築されるリグと、「リグがない状態でインタラクタブルを作成した場合」の注意事項については、hz-unity-meta-interaction-sdk を参照してください。
VR UI サイズのリファレンス
すべてのサイズはキャンバスのスケール 0.001(1 単位 = 1mm)を前提としています。
要素のサイズ(最小)
| 要素 | sizeDelta(単位) | 物理サイズ |
|---|---|---|
| ボタン(小) | 250 x 100 | 25cm x 10cm |
| ボタン(標準) | 300 x 120 | 30cm x 12cm |
| ボタン(大) | 400 x 150 | 40cm x 15cm |
| ドロップダウン | 500 x 140 | 50cm x 14cm |
| スライダー | 600 x 80 | 60cm x 8cm |
| トグル | 120 x 120 | 12cm x 12cm |
| 入力フィールド | 800 x 140 | 80cm x 14cm |
| パネル/メニュー | 1200-2000 x 800-1400 | 1.2-2m x 0.8-1.4m |
ボタンの間隔
誤操作を防ぐため、インタラクティブな要素の間は最低 50 単位(5cm)空けてください。
フォントサイズ(キャンバススケール 0.001 の場合)
| 距離 | 最小 | 快適 | 大/タイトル |
|---|---|---|---|
| 1.5m | 32pt | 40-48pt | 60-72pt |
| 2.0m | 36pt | 48-56pt | 72-84pt |
| 2.5m | 40pt | 52-64pt | 84-96pt |
| 3.0m | 48pt | 64-72pt | 96-120pt |
VR では自動サイズ調整を使用しないでください。レガシーの Text コンポーネントは使用せず、常に TextMeshPro を使用してください。
色
- テキスト:オフホワイト(0.9, 0.9, 0.9)またはダークグレー(0.1, 0.1, 0.1)。純白/純黒は避けてください。
- 背景:ダーク(0.1, 0.1, 0.1, 0.95)またはライト(0.9, 0.9, 0.9, 0.95)。
- コントラスト比:最低 4.5:1、7:1 以上を推奨します。
- 透明な背景より、不透明な背景を優先してください(レンダリングコストが低いため)。
パフォーマンスに関するヒント
- ラベル、背景、装飾用画像など、操作できない要素では
raycastTargetを無効にします。 - 再構築を最小限に抑えるため、静的コンテンツと動的コンテンツを別々のキャンバスに分けます。
- 72/90Hzを維持するには、キャンバスの再構築コストを1~2ms未満にします。
- すべてのテキスト要素で、フォントファミリーごとにSDFフォントアセットを1つ共有します。
- 表示されていないキャンバスまたはGameObjectを無効にします。
トラブルシューティング
| 症状 | 原因 | 解決方法 |
|---|---|---|
| テキストがピンク/マゼンタになる | TMPリソースがインポートされていない | tmp-resourcesスキルを使用します |
| VRでボタンをクリックできない | CanvasにVRインタラクションがない | QuickActionsAPI.AddRayCanvasInteraction(go) |
| Editorではボタンが動作するが、デバイスでは動作しない | インタラクションリグがない | OVRQuickActionsAPI.AddOVRInteractionRig() |
| UIが小さすぎる/大きすぎる | キャンバスのスケールが正しくない | 0.001に設定し、検証スクリプトで確認します |
| 子要素のZ位置がずれている | 自動配置の不具合 | すべての子要素で localPosition.z = 0 に設定します |
| 子要素のスケールが1000倍になっている | 自動スケール補正 | すべての子要素で localScale = Vector3.one に設定します |
| テキストがぼやける | アトラス解像度が低い、またはフォントサイズが小さい | SDF 2048x2048以上、フォントサイズ48以上を使用します |
| フレームレートが低下する | キャンバスが頻繁に再構築される | キャンバスを静的/動的に分けます |
unity command が接続できない | コンパイルエラーによりEditorがSafe Modeになっている | unity pipeline list を実行し、エラーを修正してUnityを再起動します |
チェックリスト
インタラクティブUI(ボタン、スライダーなど)
[ ] TMP Essential Resources imported
[ ] Canvas: renderMode = World Space
[ ] Canvas: localScale = [0.001, 0.001, 0.001]
[ ] Canvas: positioned 1.5-3m from user
[ ] All children: localScale = [1, 1, 1]
[ ] All children: localPosition.z = 0
[ ] Sizes via sizeDelta, not scale
[ ] AddRayCanvasInteraction called on Canvas
[ ] PointableCanvas component verified on Canvas
[ ] Pointable Canvas Module exists in scene
[ ] Interaction rig present (AddOVRInteractionRig)
[ ] Buttons >= 250x100 units
[ ] Button spacing >= 50 units
[ ] Text >= 48pt, raycastTarget = false on non-interactive text
[ ] Validation script run and passed
[ ] Scene saved (unity command save_scene)
表示専用UI(HUD、スコア、ラベル)
[ ] TMP Essential Resources imported
[ ] Canvas: renderMode = World Space
[ ] Canvas: localScale = [0.001, 0.001, 0.001]
[ ] All children: localScale = [1, 1, 1]
[ ] All children: localPosition.z = 0
[ ] Text >= 48pt
[ ] raycastTarget = false on all elements
[ ] NO interaction components needed
[ ] Scene saved (unity command save_scene)
他のスキルとの連携
- tmp-resources:UIを作成する前にTMP Essential Resourcesをインポートするため、最初に使用します。
- hz-unity-meta-interaction-sdk:ステップ4で使用するQuick Actions APIを担当します。
- unity-placement:他のシーンオブジェクトに対するキャンバスの配置に使用します。
- unity-cli:Editorへの接続、
--project-path、Safe Modeからの復旧に使用します。