見出し画像

PolySpatial の実践Tips 後編

はじめに

みなさん Vision Pro で遊んでいますか?

Unity エンジニアの我々は PolySpatial の公開直後は、さぁこれで何を作ろうかとワクワクしたものです。

しかし、いざ開発を始めてみると苦労の連続でした…

本記事では Apple Visio Proアプリ 視線de定規 の開発過程で苦労したことや、他所ではあまり語られていないことを Tips 形式で紹介していこうかと思います!

前編では、開発を行う準備段階での Tips が中心でしたが、 後編(本記事)では、開発を行っている最中の Tips が中心になっています。

注意点

Unity や PolySpatial の基礎的なことは言及しないためご容赦ください。

以降は 2024/7 時点の記事内容になります。

開発環境

  • Xcode 15.4

  • Unity 2022.3 LTS

  • PolySpatial 1.2.3

  • visionOS 1.2

Tips

1. 視線の指先の操作情報

視線の操作情報

他のXRデバイスのように常時取得はできない。
https://docs.unity3d.com/Packages/com.unity.polyspatial.visionos@1.2/manual/PolySpatialInput.html

SpatialPointerState phase will never be valid to use when polling the state. Instead, use the EnhancedTouch API to get active touches and query the phase.

SwiftUI の SpatialEventCollection を中継することでしか、Unity は視線情報を取得できないらしい。

つまりユーザが タッチやクリックを実行したタイミング でしか、視線情報は取得できない。

そのため 視線de定規 においては ARPlaneManager で生成された平面と視線が接触した場所にシンボルを常時表示したかったのだが、断念した…

操作情報の取得と制御

次のコードは、タップ時は「視線」の情報、ピンチ時は「指先」の情報を使って作ったレイが接触した平面を抽出している。

UnityEngine.InputSystem.EnhancedTouch.Touch から操作フェーズを取得している。

UnityEngine.InputSystem.EnhancedTouch.Touch を EnhancedSpatialPointerSupport.GetPointerState を使用して SpatialPointerState 型に変換。
SpatialPointerState から「視線」と「指先」の操作レイの作成に必要な情報を取得してる。

using Unity.PolySpatial.InputDevices;
using UnityEngine.InputSystem.LowLevel;
using TouchPhase = UnityEngine.InputSystem.TouchPhase;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;

.
.
.

private void OnEnable()
{
    // タッチの入力制御を有効にする (書き忘れがち注意!!)
    EnhancedTouchSupport.Enable();
}

private void Update()
{
    var activeTouches = Touch.activeTouches;
    if (activeTouches.Count <= 0)
        return;

    var touch = activeTouches[0];
    var touchPhase = touch.phase;

    // SpatialPointerState に変換する
    var pointerState = EnhancedSpatialPointerSupport.GetPointerState(touch);

    if(touchPhase  is not (TouchPhase.Began or  TouchPhase.Moved or TouchPhase.Stationary))
        return;
        
    Ray ray = default;

    // タップ開始
    if (touchPhase  is TouchPhase.Began )
    {
        // `startInteractionRayOrigin` を原点にした `startInteractionRayDirection` へ向かうレイを作成
        ray = new Ray(pointerState.startInteractionRayOrigin, pointerState.startInteractionRayDirection);
    }
    // ピンチ操作中
    if (touchPhase  is TouchPhase.Moved or TouchPhase.Stationary )
    {
        // `TouchPhase.Moved or TouchPhase.Stationary` は `inputDevicePosition` から `interactionPosition` へ向かうレイを作成
        ray =  new Ray(pointerState.inputDevicePosition, pointerState.interactionPosition - pointerState.inputDevicePosition);
    }

    // ARPlaneManager が生成した平面(ARPlane)との接触判定 
    // ( ARPlane は Collider があり, "ARPlane" タグが付いている, とする)
    var hits = new RaycastHit[10];
    var hitCnt = Physics.RaycastNonAlloc(ray, hits, maxDistance);
    if (hitCnt <= 0) return;

    // "ARPlane" タグ が付いた GameObject を近い順に並べる
    var hitObjects = hits
        .Where(hit => hit.collider != null && hit.collider.CompareTag("ARPlane"))
        .OrderBy(hit => hit.distance)
        .Select(hit => hit.collider.gameObject)
        .ToList();

    // ヒットした平面にしたいして処理を書く
    foreach (var hitObj in hitObjects)
    {
        .
        .
        .
    }
}

2. uGUI

実践的 Practice が見つからなくて、実機で動くようになるまでに苦労した。
試行錯誤の末に実機で uGUI を動かすためのシンプルな構成はこのようになった。

Project Settings/Input System Package

あえて Action Asset を作らない。

Project Settings/Input System Package/Settings
あえて Settings Asset を作らない。

Project Settings/PolySpatial/Auto-Add PolySpatial Raycaster

ON にしておく。 (デフォルトでON)
シーン内のすべての Canvas に独自の RayCaster が自動で追加されて PolySpatial の入力システムと連動される。

XR Interaction Toolkit と競合する場合は OFF にするといいらしい。


EventSystem

InputSystemUlInputModule を付ける。
(Standalone Input Module が付いていたら消す。)

Actions Assets にビルトインの DefaultInputActions を指定する。

Immersive Space の場合は加えて以降の作業が必要。

AR Session

ARSession の Attempt Update と Match FrameRate を OFF にする。

XR Origin (AR Rig)

ARRaycastManager を付ける。

XR Origin (AR Rig)/Camera Offset/Main Camera

ARCameraManager と ARCameraBackGround が付いていたら削除する。

3. 描画順の問題

  • Sprite に TMP (not uGUI) を重ねる → Sprite が消える

  • Canvas 配下の要素の上に Sprite を重ねる → Sprite が消える

根本的な解決策は見つけられなかった。

VisionOSSortingGroup を駆使する手段があるみたい。
しかし、 VisionOSSortingGroup は未完成な部分もあるらしく、動作が不安定なため使用は見送った。

上のような事態に陥った場合は Sprite を Mesh を置き換えることが無難な解決策だろう。(今のところは)

4. TextMesh のテキストの裏が透ける

TMP (not uGUI) で描かれたテキスト下にある Mesh が、そのテキストの周囲の空白部分で透けてしまう。

Vision Pro は片目解像度が 3800×3000 ピクセルもある。

よって、モバイルなどの他デバイスの感覚でSDFのテクスチャを作ると、相対的に1文字あたりのフォントの解像度が低くなる… というのが原因だった。

視線de定規 は下図のような調整で1文字あたりの解像度を上げた。

5. Hover Effect

GameObject に VisionOSHoverEffect を付けるだけで、エフェクトを掛けることができる。
しかし、幾つか気をつけることがある。

必要なコンポーネント

次の2つを必ず行う。

  1. VisionOSHoverEffect を付けた GameObject に Collider 系のコンポーネントを付ける

  2. VisionOSHoverEffect を付けた GameObject もしくはその子オブジェクトに MeshRenderer を付ける

uGUI の要素は 2. uGUI で述べたとおりで Project Settings/PolySpatial/Auto-Add PolySpatial Raycaster を ON の場合は上の2つは行う必要はない

現在できないこと

いずれも visionOS の制御下にあるため、現在はできない。

  • 手動でエフェクトを開始と停止

  • エフェクトの外観

エフェクトが開始されないケース

VisionOSHoverEffect を付けた GameObject が次の状態だと、エフェクトが開始されない(or見えない)と思われる。

  • 遠くにある (およそ5m以上)

  • ユーザの頭より高すぎる位置にあるとき (およそ30cm以上)

いずれも経験上のことで、根拠となるソースや確証はない。

uGUI の要素

IPointerClickHandler を継承した Button などの EventSystem の制御下にある GameObject は次のことに注意する。

  1. 自動で Hover Effect がかかる ( 2. uGUI で述べたとおり)

  2. Hover Effect はメッシュの形状に沿って描画される。そのため Image を使用していると透過部分は無視されて 矩形で描画される
    (Image の Raycast Target を OFF にすれば、エフェクトを無効化できる)

6. 対応しているレンダラーが少ない

こちらの表 (v1.2.3) にあるように対応しているコンポーネントが少ない。

Line Renderer など基本的なレンダラーも、まだサポートしていない。

アプリの制作開始前に、上のリンク先に目を通しておくことをお薦めする。

7. Camera Stacking をサポートしない

複数のカメラの出力を積層化して1つの出力にする機能である Camera Stacking は現在サポートしていない。

フォーラムのこのやりとり からすると…

> The concept the camera stack doesn’t really translate then?

No; this isn’t a concept that RealityKit supports.

Camera Stacking の使い方次第では空間の実際の奥行きを無視しての描画も可能になってしまう。

Apple の RealityKit との整合性が合わなくなるため、今後もサポートは無さそうだ。

8. 動画の再生

通常の方法では動画の再生ができない。
対処方法は2つある。

  1. VisionOSVideoComponent を使用する

  2. VideoPlayer と PolySpatialObjectUtils.MarkDirty() という関数を組み合わせる

視線de定規 で採用した、後者で一例を示す。

  1. 予め次を作っておく

    • 動画の描画先の RenderTexture

    • RenderTexture のマテリアル (Shader は URP の Unlit)

  2. シーンに Plane の Mesh をもつ GameObject を作る

  3. GameObject に VideoPlayer を付ける

  4. VideoPlayer の Render Mode を Render Texture にして Target Texture を用意した RenderTexture にする

  5. PolySpatialObjectUtils.MarkDirty() を実行するコンポーネント
    (上図だと SetRenderTextureDirty) を付ける

SetRenderTextureDirty の内容は次のとおり。
PolySpatialObjectUtils.MarkDirty() の引数に動画の描画先の RenderTexture を渡して毎フレーム実行している。

public class SetRenderTextureDirty : MonoBehaviour
{
    [SerializeField]
    private RenderTexture _videoTexture;

    void Update()
    {
        Unity.PolySpatial.PolySpatialObjectUtils.MarkDirty(_videoTexture);
    }
}

9. ローカライズ

せっかく Xcode15 を使っているため、 Info.plist の多言語の対応に String Catalog を使ってみた。

その対応の詳細は こちらの記事 を参照。

さいごに

前編 と後編に渡ってノウハウを紹介してきましたが、いかがでしたでしょうか?

厄介そうだって思いましたか?
たしかに PolySpatial はまだまだ未熟な部分はあります。
ですが、それ以上に Unity で Vision Pro の開発は楽しくて面白いです。

PolySpatial は基本的な機能のアップデートだけなく 新しい機能 についても既に発表されており、今後が楽しみです。

弊社は今後も Vision Pro & PolySpatial の最新情報を追っていきます!


この記事が気に入ったらサポートをしてみませんか?