Meta Quest 3のPassthrough APIの使い方

Meta Quest 3でついにカメラアクセスができるようになりました。Meta社がすでに[サンプルプロジェクト](https://github.com/oculus-samples/Unity-PassthroughCameraApiSamples)をGitHub上に公開してくれていますが、便利に利用できる分だけ肉厚になっています。実際には権限付与とWebCamTextureを利用するだけで使えるので、必要なところだけを抜き出し、他で利用したいときなど、何を使っているかわかりやすいようにしてみました。

# 前提

- Unity 2022.3.58f1
    - Passthrough APIはUnityのバージョンがかなり重要です。初めて試すときはこのバージョンで試しておいたほうが無難です
- Meta XR All-in-One SDK 74.0.3

# 使い方の前に

Unityプロジェクトは[こちら](https://github.com/machidyo/quest-passthrough-api-sample)で公開しています。細かい点でわかなかればcloneして確認してみてください。

# 使い方

1. パススルーが使えることが前提になっているので[こちら](https://qiita.com/matchyy/items/a19de5d5ec5bae058b23)の手順を元にパススルーを設定します。
2. AndroidManifest.xmlに `<uses-permission android:name="horizonos.permission.HEADSET_CAMERA" />` を追加します
3. 以下コードを追加します。3.1、3.2で簡単な説明をして、3.3に全コードがあります。
    1. 権限周りの設定をAwakeでしています        
        ```csharp
        if (!hasPermission)
        {
            var grantedPermissionCount = 0;
            var callbacks = new PermissionCallbacks();
            callbacks.PermissionGranted += (permissionName) =>
            {
                grantedPermissionCount++;
                hasPermission = grantedPermissionCount >= PERMISSIONS.Length;
            };
            Permission.RequestUserPermissions(PERMISSIONS, callbacks);
        }
        ```
    2. この5行でカメラにアクセスして、画像をUIに設定しています
        
        ```csharp
        var devices = WebCamTexture.devices;
        var deviceName = devices[0].name;
        var webCamTexture = new WebCamTexture(deviceName, 1280, 960);
        webCamTexture.Play();
        output.texture = webCamTexture;
        ```        
    3. 全コードはこちら        
        ```csharp
        using System.Collections;
        using System.Linq;
        using UnityEngine;
        using UnityEngine.Android;
        using UnityEngine.UI;
        
        public class PassthroughAPIHelper : MonoBehaviour
        {
            private static readonly string[] PERMISSIONS =
            {
                "android.permission.CAMERA",
                "horizonos.permission.HEADSET_CAMERA"
            };
            
            [SerializeField] private RawImage output;
            [SerializeField] private Text debugMessage;
        
            private bool hasPermission = false;
        
            private void Awake()
            {
                debugMessage.text = "権限の確認をして、なければリクエストを出します";
                hasPermission = PERMISSIONS.All(Permission.HasUserAuthorizedPermission);
                if (!hasPermission)
                {
                    var grantedPermissionCount = 0;
                    var callbacks = new PermissionCallbacks();
                    callbacks.PermissionGranted += (permissionName) =>
                    {
                        grantedPermissionCount++;
                        hasPermission = grantedPermissionCount >= PERMISSIONS.Length;
                    };
                    Permission.RequestUserPermissions(PERMISSIONS, callbacks);
                }
            }
        
            void Start()
            {
                StartCoroutine(InitializeWebCamTexture());
            }
        
            private IEnumerator InitializeWebCamTexture()
            {
                debugMessage.text = "初期化を開始します";
        
        #if !UNITY_6000_OR_NEWER
                debugMessage.text = "Unity2022のバグのため、WebCamTextureの初期化を1フレーム待ち中";
                yield return new WaitForEndOfFrame();
        #endif
        
                while (!hasPermission)
                {
                    debugMessage.text = "権限の付与を待っています";
                    yield return new WaitForEndOfFrame();
                }
        
                debugMessage.text = "パススルーをWebCam経由で設定します";
                var devices = WebCamTexture.devices;
                var deviceName = devices[0].name;
                var webCamTexture = new WebCamTexture(deviceName, 1280, 960);
                webCamTexture.Play();
                output.texture = webCamTexture;
        
                debugMessage.text = "初期化完了です";
            }
        }
        
        ```        
4. PassthroughAPIHelper.csのアウトプットとしてRawImageとTextが必要なので、適当なCanvasを作成して設定してください。この例では左手にCanvasを持たせて、カメラが画像を見やすい状態にしています。
    ![2025-05-07_12h59_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/fcacf363-4e3a-4e93-88cc-ebb9652412cd.png)    
5. 後はビルド、adb installして、Questで実行するだけです。うまくいけば↑の真っ白なRawImageのところに取得したカメラ画像が表示されるはずです。
    ![com.oculus.vrshell-20250507-222659.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506075/43452cec-8de0-4d4c-a97f-60dc71b527be.jpeg)
    
# 補足
- 公式ドキュメントは[こちら](https://developers.meta.com/horizon/documentation/unity/unity-pca-overview)。Unityの前提バージョンや画像解像度など、諸々の仕様が記載されているので、細かいところはこちらを参照ください。
- Metaがすでに[サンプルプロジェクト](https://github.com/oculus-samples/Unity-PassthroughCameraApiSamples)をGitHub上に公開してくれています。これには権限周りの取得（`PassthroughCameraPermissions.cs`）やWebCamTextureの取得などを簡単にできるようにラップしてくれているクラス（`WebCamTextureManager.cs`）が含まれています。便利なのでこちらを流用すると楽です。