【Unity】UnityでAppleVisionPro を用いてARPlane/ARMeshを利用する方法と注意点【VisionOS】

Graffity でVisionPro とUnityを用いてゲーム開発をしているcovaです。
今回はVisionPro 向けAR/MRコンテンツを作るにあたって `ARPlane(平面検知)` と `ARMesh` はよく使われますが、使い方に気をつけないと意図通りの挙動にならなかったトラブルがあったので備忘録として記事化しました。

# TL;DR　（今北産業)
- Unityで行う場合はARFoundation のフレームワークに則って出来るぞ！
    - ARFoundation を使ったことがない方はスマホAR等で利用方法をざっくり理解しておくと良いです
- ARMesh を当たり判定で使う場合はCollider周りの設定を正しく行わないと、意図しない形にCollision用Mesh が変形してしまうので要注意
- ARPlane は普通に使えるし、属性も取れるのでSceneSegmentation/SceneUnderstanding とかにはMeshよりPlaneの方がよかったりします

# 検証環境
| 項目 | version等 |
|---|---|
| Unity | 2022.3.20f1 |
| PolySpatial | 1.2.3 |
| Xcode | 15.2 |
| VisionOS | 1.0.2~2.0beta |

## PolySpatial 導入について
以下の内容は様々な方がTechブログで記載されていたり、公式の導入ドキュメントがあるのでそちらをご覧ください
- Proライセンスの設定方法
- PolySpatial をUPMで入れる方法

https://docs.unity3d.com/Packages/com.unity.polyspatial.visionos@1.2/manual/Install.html

# 基本的な使い方

## 共通の設定項目
### Scene 周り

1. AR動かす用のコンポーネントを配置しましょう
    1. XROriginの設定<br>![スクリーンショット 2024-07-03 18.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/ede48a03-10b0-7a98-fa94-2bb5b68e3a2b.png)
        - TrackingOriginMode はFloor にしておくのがお勧めです
            - DeviceやNotSpecified だと現実との位置計算が面倒なので素直に床を高さ=0にするのが楽です
        - よくわからなかったらPolySpatial のMixedRealityのサンプルSceneをそのまま利用しちゃうのが楽です
    1. ARSessionの配置
        - 基本的にはデフォルトの状態で問題ないです<br>![スクリーンショット 2024-07-03 18.07.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/09a7de0b-4cc3-d987-631f-3a867df7ad75.png)
        - よくわからなかったらPolySpatial のMixedRealityのサンプルSceneをそのまま利用しちゃうのが楽です
    1. VolumeCamera がUnbounded になっているか確認しましょう<br>![スクリーンショット 2024-07-03 18.08.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/b3b52db2-efe7-d883-add6-b419c54f68f7.png)
        - Bounded だったりWindow だと基本的にはAR周りが正しくとれないです

### ProjectSettings
1. XR Plug-in Management でvisionOS を有効化しましょう<br>![スクリーンショット 2024-07-03 18.38.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/6b5522ab-691a-50cb-4771-80e35d40d995.png)

1. AppleVisionOS のところでMR向け設定をしましょう<br>![スクリーンショット 2024-07-03 18.39.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/cb2dfead-05b8-dc43-e771-79ebaec5dac9.png)
    - InitializeHandTrackingOnStartUp はハンドトラッキングをする場合は必須です
    - HandsTrackingUsageDescription はハンドトラッキング利用アプリの初回起動時に表示するメッセージです
        - ここが空だとビルドが通らなかったりします
        - Info.plist に正しく反映されないままXcodeビルドが通ると実際にアプリを起動しようとしてもすぐクラッシュするなどの問題を引き起こす原因になります
    - World Sendsing Usage Description は `入力必須` です
        - ここを入力しないと `HandsTrackingUsageDescription` と同様の問題の原因になります
    - UpperLimbVisibility は チェックを入れると手の位置と被った3DObjectが存在する場合、自動的にOpaqueでもアルファブレンドで透過する処理をOS側がやってくれます
        - ただし透過度合いとかは制御不能でOSが勝手にやります
        - 特に必要なければOffでもOKです
    - `IL2CPP Large Exe Workaround` は脳死でチェックを入れましょう
        - Xcodeビルド時にファイルサイズとかのビルドエラーが発生する場合はこれにチェックを入れれば大体解決します

## ARPlane
1.XROrigin ObjectにARPlaneManager コンポーネントをくっつけよう
     - よくわからなかったらPolySpatial のMixedRealityのサンプルSceneをそのまま利用しちゃうのが楽です
     - MixedReality のSampleだとデフォルトでコンポーネントがあるので何もしなくてもOKです
1. DetectionMode を設定しよう<br>![スクリーンショット 2024-07-03 18.13.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/d588d613-5085-cd70-a69c-0b688431400a.png)

    - 特に何もなければEverythingで問題ないです
    - 壁とか垂直面だけ検知させたいならVertical のみにチェックを入れます
    - 逆に水平面だけ検知させたい場合は Horizontal のみにチェックを入れます
1. 検知したMeshを可視化させよう
    - 基本的には検知した面はドット模様で表示されます<br>![スクリーンショット 2024-07-03 18.16.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/92a760d2-382d-d3c2-2453-062ec549ed52.png)<br>もし色をつけたいなどあればARDefaultPlane に設定されているMaterialを変更しましょう
    - ARplane に関してはデフォルトで設定されているPrefabに検知した面の属性表示用のUIがついてます<br>![スクリーンショット 2024-07-03 18.16.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/6fddfbb2-06aa-2517-c740-68d1fbeed9ae.png)<br>ただ確認用であれば特に手を加える必要はないです

## ARMesh
1. ARMeshManager Component がヒエラルキーにない場合は追加しましょう. 追加する場合はXROrigin GameObjectにそのままくっつけてもOKです<br>![スクリーンショット 2024-07-03 18.35.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/415cf1ff-e2ba-f314-898c-b8363e0fb2e0.png)
    - Density(密度) は特に指定がなければデフォルトの0.5のままで.
        - 密度を上げると頂点数が増えてパフォーマンスにも影響が出るので要注意
1. Mesh を可視化しよう
    - ARMeshManager の AR Default Mesh は市松模様でMeshを表示してくれます<br>![スクリーンショット 2024-07-03 18.36.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/3525bea2-4815-adac-a5a9-203e500e536e.png)
    - またMeshはいくつかのObject毎に分かれて管理されます
        - このとき、どのObjectがどのMeshに属しているか？とかID情報が必要な場合は後述の Mesh 表示加工Scriptを適応してください


# ARPlane/ARMesh の応用的な使い方
## 当たり判定をつける

まず現実空間をスキャンしたらやりたくなること1つ目です。
基本的にはUnityのPhysics系のコンポーネントをシンプルに設定するだけで問題ないのですが、やり方によっては色々トラブルを引き起こす可能性があるので要注意です.


### 投擲物にRigidbody をつけて当たり判定を実装する場合
-　こちらの場合は特に何も気にせず実装できます
1. 　投擲物側のObjectに Rigidbody と Collider (できればSphereCollider. 次点でBox/CapsuleCollider) を設定
    -　IsTrigger のOn/Offはどちらでも良いです
1. ARPlane とARmesh表示用のPrefabそれぞれにMeshColliderを設定する
    - ARPlane に関しては厳密さを求めなければBoxColliderで代用も可能
    - ARMeshに関してはMeshColliderでないと正しくとれない
    - `MeshCollider` の `convex` はOff にしよう
        - 計算量は増えるが　　`convex を onにする` と [後述の問題]()が発生してしまう
    - ARPlane/ARMesh 側のColliderはできれば `IsTrigger = Off` にしましょう

このようにすれば基本的には現実の見た目とおおよそ一致する場所に当たり判定が生成されます

### ARPlane/ARMesh側にRigidbody をつけて当たり判定を実装する場合

コンテンツによっては投擲物の個数が当たり判定Objectより多く出るなんて場合もあります。
その場合はARPlane/ARMesh 側にRigidbody をつけて当たり判定を行わせることで投擲物側の処理コストを下げるというパフォーマンスの稼ぎ方もあります。

その場合ARPlane/ARMesh 側にRigidbodyを追加するのですが、MeshCollider において、Rigidbodyを併用する場合以下の制約があります。

:::note warn
- MeshCollider のConvexフラグをOffにする場合Rigidbody.isKinematic フラグをOnにしないと動かない
    - isKinematic フラグをOffだと当たり判定がとれない
    - Convex フラグをOnにするとARMeshの当たり判定が見た目通りにならない
:::

### ARMeshとMeshCollider のconvex フラグにまつわる問題

Unity のMeshColliderのマニュアルを確認しましょう.

https://docs.unity3d.com/ja/2021.1/Manual/class-MeshCollider.html

[バーチャルキャストさんのTechブログ](https://wiki.virtualcast.jp/wiki/vci/component/unity_meshcollider#convex) 等でも紹介されていますが

そこではConvex フラグについて以下のような記述があります.

> これを有効にすると、メッシュコライダーは他のメッシュコライダーと衝突します。 Convex (凸状) のメッシュコライダーは三角形面数の上限が 255 に制限されます。

詳細は割愛しますが凸型にすることで当たり判定の計算コストをかなり減らすことができるのtMeshCollider利用した当たり判定をとるときはConvex フラグをOnにします。

一方で、後半の文章にある `メッシュコライダーは三角形面数の上限が 255 に制限` が非常に厄介です。

一般的にゲームでMeshCollider はそこまでハイポリのものには使わないです。
（敵キャラの判定も、判定があるところにSphere ColliderやCapsuleColliderを複数置いて、Meshの範囲をカバーするように配置して軽量化を図って実装されることがほとんどです. 主人公キャラ等も胴体、腕、足それぞれにCapsuleCollider を配置で十分なため、MeshColliderは使いません)

しかし、リアルタイムスキャンして取得したARMeshについてはポリゴン数が255を優に超える場合があります。（リアルタイムでMeshのReductionは難しいため）そうするとARMesh にConvexフラグを適応すると、 `見た目のMesh` と `当たり判定用のMesh` が　　`異なる` という現象が生じます。

結果として、何もない空中に謎の当たり判定が生まれるなどの副作用が生じます


## 現実のものでOcculusion（背後に隠れてるところは見えない）させる

こちらについては以前の記事でも記載しましたが、ARPLane/ARMesh Prefabの各Renderer に設定されているMaterialのShader を `AR/Basic Occulusion` にするだけです。

https://qiita.com/Cova8bitdot/items/4e827c966ec5a14bb2bb

これだけで現実のObjectの背後にあるコンテンツは一部/全体を隠すことが出来ます。
また上記の当たり判定を併用すれば、現実の背後にある敵には、 `現実のものが邪魔で攻撃できない` ということが実現可能です。