Quest3・Vision Proでも動くアプリを作りたい！WebXRをやろう！

本記事は[クラスター Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cluster) 9日目の記事です。

# 記事でやること
WebXRでキャラクター描画・操作を試してみる(動作確認はVision Proの例)
![ScreenRecording_12-08-2024 16-56-56_1 (online-video-cutter.com).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959874/e9df6e46-84b6-ea8f-578a-03fd3c88d6a3.gif)

# はじめに
こんにちは、こうたといいます。フロントエンドエンジニアをやっています。ただ直近はUnityでの業務が多めでやっています。

みなさん、Vision ProやQuest3は持っていますか？
clusterを遊んだり、開発している人にとっては頭につける機器を複数買ってしまうことはよくあることだと思います。
VR/ARデバイスを複数所有していると、それらを活用して何か開発したくなりますよね。しかし、Vision Pro用、Quest3用と個別にアプリを作るのは大変です。

そこで、WebXRを活用して、どちらのデバイスでも動くアプリを作ってみるのはどうでしょうか？
WebXRは、ウェブ上でARやVRを実現する技術で、Quest 3やVision Proで動作させることが可能です。

この記事では、Unityで開発しWebXRとして出力する方法を取ろうと思います。
clusterはUnityで開発されているので、この方法をとることでUnityの知識の恩恵を得ることができそうです。
今回は、[WebXR Exporter](https://github.com/De-Panther/unity-webxr-export)を利用してWebXRアプリを開発し、キャラクター描画とインタラクションを試してみます。

## 記事で解説しないこと
- WebXRについての詳細の説明
- Unityに関する操作・実現方法の詳細


# unity-webxr-exportについて
[WebXR Exporter](https://github.com/De-Panther/unity-webxr-export)は、UnityからWebXR対応アプリを出力できるツールです。導入方法は公式の[Getting Started](https://github.com/De-Panther/unity-webxr-export/blob/master/Documentation/Getting-Started.md)を参照してください。すぐ試したい場合は、[MainProject](https://github.com/De-Panther/unity-webxr-export/tree/master/MainProject)を利用するのが便利です。

このツールは更新も活発で、Unity 6にも対応しています。

# 開発をしてみよう
Unityを使ってWebXRの出力ができるようになったので、サンプルのようなものを試してみようと思います

## 作るもの
WebXRでアバターを描画し、インタラクションできる仕組みを作ります。


## 使ったアセット
- [FinalIK](https://assetstore.unity.com/packages/tools/animation/final-ik-14290)
    - キャラクターへのIKの設定に使用
- [PuppetMaster](https://assetstore.unity.com/packages/tools/physics/puppetmaster-48977)
    - キャラクターへのCollider設定とインタラクションのために使用

## やったこと

1. **キャラクターの描画とIK設定**  
   IKの設定はFinalIKを使用し、キャラクターに**FullBodyBipedIK**を適用しました。PuppetMasterを使いColliderを設定します

2. **XR Interaction WebXR Setupの利用**  
   手の動きを反映するため、`XR Interaction WebXR Setup`をシーンに配置します。このPrefabは[webxr-interactionsのサンプル](https://github.com/De-Panther/unity-webxr-export/tree/master/Packages/webxr-interactions/Samples~/XRInteractionToolkit/Prefabs)に含まれています

3. **インタラクション設定**  
   手の動きに合わせてキャラクターを操作可能にするため、手のGameObject（`Left Hand`と`Right Hand`）の子オブジェクトにColliderを追加しました。これにより、手で触れる動きがキャラクターに反映されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959874/ffc68555-8722-12a4-0e6c-410ca3b78106.png)

### 動作結果
キャラクターを描画してインタラクションすることができました（動作確認はわかりやすいようにColliderはSphereでMeshを描画してます）。
![ScreenRecording_12-08-2024 16-56-56_1 (online-video-cutter.com).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959874/e9df6e46-84b6-ea8f-578a-03fd3c88d6a3.gif)


## デバッグ方法
WebXRDeviceAPIはセキュアなコンテキストのみでしか実行することができません（[参考](https://developer.mozilla.org/ja/docs/Web/API/WebXR_Device_API/Startup_and_shutdown)）。
Vision Proで確認したい際には、Netlifyなどにデプロイなどが必要になります。
もしくは、自己署名証明書などを作成して、ローカル環境をhttps対応させるなどでも動作確認できます（あくまでのローカルのデバッグ用として）。


# 最後に
UnityでWebXR出力したプロジェクトをVisionProとQuest3で動かしてみて、普段のUnity開発と同じ開発体験でWebXRを動かせることがわかりました。
パフォーマンス面やシェーダーに関する部分は見きれてないのですが、ある程度はUnityでWebXRの開発ができそうです。

ちなみにclusterでゲーム作成やワールド作成をすると、なんとVR対応のほかにMobileでも対応してしまいます！
さらにマルチプレイでの同期という、かなり大変な問題も解決できちゃいます。
みなさんはぜひともclusterでワールド作成をしてみることをお勧めします！




# おまけ
unity-webxr-exportを導入することでWebXR出力ができるようになりました。
このパッケージは何をやっているのかの疑問がわいてきたので、中身を少し見ることができた範囲で書いてみようと思います。

## WebXRの起動について
まず、WebXRの起動がどのように行われているのかを見てみましょう。
最初の実行に必要なメニューは[WebXRMenu](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Editor/WebXRMenu.cs)に実装されていたので中身を見てみます。
こちらはHiddenのWebGLTemplateをAssetsにコピーする形となっていました。
このTemplateはPlayterのWebGLTemplateに指定しています。では、このTemplateでは何をしているのでしょうか？
index.htmlの中身には、VR/ARを有効にするための[Button](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Hidden~/WebGLTemplates/WebXR2020/index.html#L24)が実装されており、こちらはUnityの[Toggle](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Hidden~/WebGLTemplates/WebXR2020/index.html#L98)を呼び出すようになっていました。toggleVRの中身をみることで何をしているのかがわかりそうです。

では、toggleVRの中身を見てみましょう。
Unityでは[Pluginsにjslibやjspreを配置すること](https://docs.unity3d.com/2022.3/Documentation/Manual/web-interacting-browser-js.html)で相互作用を実現しており、ライブラリではwebxr.jspreで実現しているようです。
VRの開始は以下のコードで実現しています ([参考1](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Runtime/Plugins/WebGL/webxr.jspre#L615)、[参考2](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Runtime/Plugins/WebGL/webxr.jspre#L430))。
``` js
      XRManager.prototype.onRequestVRSession = function () {
        if (!this.isVRSupported) return;
        if (this.BrowserObject.pauseAsyncCallbacks) {
          this.BrowserObject.pauseAsyncCallbacks();
        }
        this.BrowserObject.mainLoop.pause();
        var thisXRMananger = this;
        var tempRender = function () {
          thisXRMananger.ctx.clearColor(0, 0, 0, 0);
          thisXRMananger.ctx.clear(thisXRMananger.ctx.COLOR_BUFFER_BIT | thisXRMananger.ctx.DEPTH_BUFFER_BIT);
        }
        window.requestAnimationFrame( tempRender );
        navigator.xr.requestSession('immersive-vr', {
          requiredFeatures: thisXRMananger.gameModule.WebXR.Settings.VRRequiredReferenceSpace,
          optionalFeatures: thisXRMananger.gameModule.WebXR.Settings.VROptionalFeatures
        }).then(function (session) {
          session.isImmersive = true;
          session.isInSession = true;
```

中身としては、WebXRのセッションの起動を要求している形になっているようです（[参考のMDN](https://developer.mozilla.org/ja/docs/Web/API/WebXR_Device_API/Startup_and_shutdown))。

## トラッキングデータの取り扱い
ここまでで、webxrのセッションを開始する方法がわかりました。
では、コントローラーや手の動きはどのように渡されているのでしょうか？
webxr.jspreの[Controller](https://github.com/De-Panther/unity-webxr-export/blob/master/Packages/webxr/Runtime/Plugins/WebGL/webxr.jspre#L753C27-L753C48)を眺めてみましょう。
``` js
      XRManager.prototype.getXRControllersData = function(frame, inputSources, refSpace, xrData) {
        Module.HEAPF32[xrData.handLeft.frameIndex] = xrData.frameNumber; // XRHandData.frame
        Module.HEAPF32[xrData.handRight.frameIndex] = xrData.frameNumber; // XRHandData.frame
        Module.HEAPF32[xrData.handLeft.enabledIndex] = 0; // XRHandData.enabled
        Module.HEAPF32[xrData.handRight.enabledIndex] = 0; // XRHandData.enabled
        Module.HEAPF32[xrData.controllerA.frameIndex] = xrData.frameNumber; // XRControllerData.frame
        Module.HEAPF32[xrData.controllerB.frameIndex] = xrData.frameNumber; // XRControllerData.frame
        Module.HEAPF32[xrData.controllerA.enabledIndex] = 0; // XRControllerData.enabled
        Module.HEAPF32[xrData.controllerB.enabledIndex] = 0; // XRControllerData.enabled
        ...
```
getXRControllersData でトラッキングデータをUnityのヒープメモリに書き込んでいるのがわかります

書き込まれたデータを扱っている個所を探してみると、HandTrackingに関しては、[WebXRSubsystemにて](https://github.com/De-Panther/unity-webxr-export/blob/7db2902ef92675e89bdf6fb5cd66fbd3b534b5cd/Packages/webxr/Runtime/XRPlugin/WebXRSubsystem.cs)扱っている様子が見えました。
このクラスでは、Nativeと共有配列を用いてデータのやり取りを行っており、手のトラッキングに関しては、`GetHandFromHandsArray`を介して、データの更新を行っているようです。

``` c#
    bool GetHandFromHandsArray(int handIndex, ref WebXRHandData handObject)
    {
      int arrayPosition = handIndex * 212;
      int frameNumber = (int)handsArray[arrayPosition++];
      if (handObject.frame == frameNumber)
      {
        return false;
      }
...
```

こうして受けとったデータは[XRPlugin](https://github.com/De-Panther/unity-webxr-export/tree/7db2902ef92675e89bdf6fb5cd66fbd3b534b5cd/Packages/webxr/Runtime/XRPlugin)にてデータをUnityEngine.XRのProviderなどで扱えるようにしているのが確認でき、あとは通常のUnity開発と同じ流れで扱えそうです。
このようにして、WebXRで受け取ったデータを扱える形に整えて実現しているようです。カメラなども同じく受信して整えている個所があったので見てみると分かるかもしれないですね。
今回はここまでで一旦中身を見るのは終わろうと思います。