はじめに

今回は、弊社からVision Pro向けに公開されている「トリらっくす」(以下 本アプリ)をQuest3向けに移植した際に困ったことと、どのように解決したかについての記事になります。

開発環境

  • Unity 6 (6000.0.34f1)
  • Universal RP 17.0.3
  • AR Foundation 6.1.0
  • OpenXR Plugin 1.14.2
  • Unity OpenXR Meta 2.1.0
  • XR Interaction Toolkit 3.0.7

UI操作の実装

操作方法の置き換え

Vision Proでは以下の2種類の操作でUI操作を実装していました。

  • 視線 + ピンチ
  • 直接タッチ

Quest3では視線の向きが取得できないため、以下のように変更しました。

  • 手からRay + ピンチ
  • 直接タッチ

Interactorの選択

Near Far Interactor

上記の2つの操作を一括で実装できる方法はないか探してみたところ、 XR Interaction Tool Kit の Near Far Interactor で遠近両方の操作をいい感じに実装できそうでした。
試してみた結果、 Near Far Interactor でも概ね問題ありませんでしたが、RayがUIにスナップしてしまうのが少し操作しづらいと感じました。

Near Far Interactor 配下にある Curve Visual Controller の Line Bend Ratio を1にしてRayが曲がらないようにするなどを行いましたが、曲がらなくなっても直線のままスナップしてしまい解決には至りませんでした。

同じ設定の Near Far Interactor でもコントローラーで操作している時はRayのスナップが発生しなかったため、ハンドジェスチャで操作中のみスナップするようになっているようです。

Ray Interactor と Poke Interactor

上述の理由で Near Far Interactor の使用が難しいため、今回は以下のように別々のInteractorで実装しました。(今回は XR Interaction Toolkit の Starter Assets サンプルの Ray Interactor と Poke Interactorを使用しています。)

  • 手からRay + ピンチ : Ray Interactor
  • 直接タッチ : Poke Interactor

これでRayでの操作と直接の操作が分離されました。

次に Ray Interactor の Ray がスナップしないように設定します。
Ray Interactor オブジェクトにアタッチされている XR Interactor Line Visual の Line Bend Ratio を 1 に設定するとスナップしなくなります。

負荷対策

本アプリには Terrain で作成した森を表示する機能があります。
Vision Pro版で使っていた森をそのままQuest版に持ってきて使用しようとしましたが、Vision Proと比べてQuestの処理性能が低くフレームレートが大幅に低下してしまいました。

本アプリはストア公開に向けて開発しているため常時72FPSの維持が目標です。

モデルの軽量化

一番の問題はモデルのポリゴン数でした。
以下のようにVision Pro版よりもかなり軽量な木のモデルに変更しました。

Vision Pro版で使用している木のモデル

Quest版で使用することにした木のモデル

Foveated Rendering

上記の対応でほぼ72FPSが出るようになりました。
しかし後述の「テクスチャのチラつき」問題の関係で URP の Render Scale を上げる必要がありました。
Render Scale を上げると負荷も高くなるため、さらに Foveated Rendering による負荷対策を行いました。

Open XR の Foveated Rendering は以下の手順で使用できます。

1. XR Plug-in Management/OpenXR 内の Foveated Rendering を有効化する

2. コードから Foveated Rendering のレベルを指定する

var xrDisplays = new List<XRDisplaySubsystem>();

SubsystemManager.GetSubsystems(xrDisplays);

if (xrDisplays.Count >= 1)
{
    xrDisplays[0].foveatedRenderingLevel = FOVEATED_RENDERING_LEVEL; // 0 ~ 4のfloatで指定
    xrDisplays[0].foveatedRenderingFlags = XRDisplaySubsystem.FoveatedRenderingFlags.GazeAllowed;
}

テクスチャのチラつき対策

森の木がノイズのようにチラつく問題が発生しました。
この問題の解決方法として以下の対応を行いました。

  • URP の Render Scale を上げる(今回は1.4に設定)
  • Mipmap を有効にする

URP の Render Scale を上げる

この問題は解像度に対して葉っぱの透過部分が細かすぎることが根本的な原因のようでした。
そこで URP の Render Scale をデフォルトの 1 から 1.4 に上げることで解像度を高くしました。
これでチラつきはかなり軽減されました。また、ややぼやけ気味だったGUIもくっきり表示されるようになりました。

Render Scale を上げると負荷も高くなるため、前述の Foveated Rendering などの負荷対策も併せて行う必要があります。

Mipmap を有効にする

この問題はテクスチャに細かい隙間があるのが原因なため、その隙間を目立たないようにすれば解決できます。
隙間を目立たなくする一番手っ取り早い方法は Mipmap を有効化することです。
Mipmap を有効にすることでテクスチャの解像度が下がり隙間が目立たなくなります。

しかし、Mipmap を有効化した結果、想定以上にテクスチャの解像度が下がり葉っぱ全体が潰れてしまいました。

そこで本アプリではシェーダー側で以下のように直接使用する Mipmap を指定することで解決しました。

half4 tex = SAMPLE_TEXTURE2D_BIAS(_BaseMap, sampler_BaseMap, uv, _Bias);

プレイエリア出入り時の対策

本アプリでは森表示中はパススルーを無効化して擬似的なVRモードになるようにしました。
Quest3ではプレイエリアを出るとアプリのコンテンツが非表示になり周囲のパススルー映像が表示されます。
本来であれば、この後プレイエリアに戻ったら周囲のパススルー映像は非表示、アプリのコンテンツは表示の状態に戻って欲しいのですが、アプリのコンテンツの後ろに周囲のパススルー映像が透けて見える状態になってしまいました。

本アプリでは Main Camera にアタッチされている AR Camera Manager の有効/無効を切り替ええることでパススルーの有効/無効を制御しています。

このバグ自体を防ぐ方法は見つけられなかったため、発生した場合には強制的に擬似VR状態を解除するようにしました。

AR Camera Manager の enabled でパススルーの状態を判定できれば良かったのですが、今回のバグでは AR Camera Manager は無効のままパススルーが有効化されていました。
そこでパススルーの状態で判定するのではなくプレイエリアの出入りを検知することにしました。

BoundaryVisibilityFeature で判定

本アプリではまず以下の手順でプレイエリアの出入り判定を実装しました。

1. XR Plug-in Management/OpenXR 内の Meta Quest: Boundary Visibility を有効化する

この機能は最新版(2025年4月現在)の Unity OpenXR Meta の 2.1.0 で追加された機能なため、それ以前のバージョンを使っているプロジェクトではアップデートが必要です。

2. コードからプレイエリアの出入りを検知する

/// <summary>
/// BoundaryVisibilityFeature
/// </summary>
private BoundaryVisibilityFeature _boundaryVisibilityFeature;

/// <summary>
/// Start
/// </summary>
private void Start()
{
    _boundaryVisibilityFeature = OpenXRSettings.Instance.GetFeature<BoundaryVisibilityFeature>();
    if (_boundaryVisibilityFeature == null) return;
    _boundaryVisibilityFeature.boundaryVisibilityChanged += onBoundaryVisibilityChanged;
}

/// <summary>
/// OnDestroy
/// </summary>
private void OnDestroy()
{
    if (_boundaryVisibilityFeature == null) return;
    _boundaryVisibilityFeature.boundaryVisibilityChanged -= onBoundaryVisibilityChanged;
}

/// <summary>
/// 可視性変更時
/// </summary>
/// <param name="sender"></param>
/// <param name="visibility"></param>
private void onBoundaryVisibilityChanged(object sender, XrBoundaryVisibility visibility)
{
    // 出入りを行った際に実行される想定の処理
    // Quest3だと出る時には発火せず、入った時のみ発火している模様
    var newVisibility = visibility == XrBoundaryVisibility.VisibilitySuppressed;
    Debug.Log($"visibility: {newVisibility}");
}

Unity ライフサイクルで判定

前述の方法でも問題ありませんが、実装後にUnity ライフサイクルを利用することでより簡単に実装できることが判明しました。

Metaの公式ドキュメントの アプリのライフサイクルの処理 に 「HMDがトラッキング境界空間を離れた場合」 という項目があります。
本アプリでは最終的に OnApplicationFocus と OnApplicationPause を使って判定処理を実装しました。

また、上記ドキュメントの内容通りだとホームボタンを押した時にもプレイエリアを出た判定になってしまいそうですが、実際にはホームボタンを押した時には OnApplicationFocus は発火するが、OnApplicationPause は発火されませんでした。
この違いにより、OnApplicationFocus と OnApplicationPause が両方呼ばれた時のみ判定するという実装ができました。

おわりに

いかがでしたでしょうか?
Meta Questへの移植で困っている方の手助けになれば幸いです。

次回は、Meta公式ストアへのアプリ公開についてまとめようと思っています。
是非そちらの記事もご覧ください。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる