📲

ARFoundationの画面をWebRTCで配信してDataChannelで操作する

2022/09/08に公開

ARFoundationの画面をWebRTCで配信してDataChannelで操作する

タイトル通りのものを実装してみました。
WebRTCを通してARFoundationの画面をUnityEditorのReceiveシーンに配信し、Receiveシーン上に配信されている画面をタップすることでDataChannelを通して座標を送信しその位置にCubeを発射します。

シーケンス

  • Receive
    映像を受信してタップ座標を送信するUnityEditor
  • AR
    映像を送信してタップ座標を受信するAndroidアプリ
  • WebSockerServer
    シグナリング用のnodeで立てたWSS

登場人物は上の3つです。
以下のシーケンスで進行します。

環境

リポジトリはこちら。
起動方法はREADME.mdにあります。
https://github.com/nekomimi-daimao/ARWithWebRTC

バージョンの組み合わせは以下です。

Platform Version
Unity 2021.3.8
ARFoundation 4.2.3
WebRTC 2.4.0-exp.8

確認した動作環境はこちら。
Android × Mac UnityEditor

ポイント

参考になりそうなポイントをそれぞれ書いていきたいと思います。

WebSocketServer

シグナリングのやり方が自由、というのはなにげに入門者にとってのつまづきポイントな気がします。「えッ自由ってつまりどうしたらいいの……?」みたいな感じで[1]。わたしはそうなりました。
今回は基本に忠実にWebSocketサーバを適当にnodeで立てました。公式のサンプルを抜いてきて、接続先となるipを表示するようにしたくらいです。

WebRTC

WebSocket

NativeWebSocketを使いました。
UPMWebSocketが使えるとはいい時代になったものです。
WebSocket.Connectは接続成功しても呼び出し元に返ってこないのにだいぶドハマリしましたが! おかしい! その設計はおかしい!

かえってこない

IObservable

ひとまずWebRTCのイベントをぜんぶIObservableに変換できる拡張メソッドをまとめたクラスを作りました。gistにも切り出したのでぜひ使ってください。たぶんこの記事とかリポジトリの価値の9割はこれです。

https://gist.github.com/nekomimi-daimao/e027c891313df418b403028faaa2813b

C#完璧に理解してるのでDelegateActionの違いがわかってないのですが、Delegateは独自クラスがどうこうとか言われてめんどくさいので、できたら全部Actionがいいな……と思いました。

データクラス

SDPのデータクラスはどれも[Serializable]ではなく、すんなりjsonにできないので間に変換クラスを噛ませる必要があります。
こういうjsonのためのデータクラスを作るときには元データ→jsonのときはfrom、json→元データのときはtoでメソッド名を始めるようにしています。

data class

IceCandidate

IceCandidateを来たそばからからつっこむと ICE!!!!! みたいなエラーを出しながら死んだので、Queueに溜めておいて順次投入するようにしています。

https://github.com/nekomimi-daimao/ARWithWebRTC/blob/main/Assets/Scripts/WebRTC/PeerController.cs#L164-L200

WebRTC.Update

ドキュメントとかに特に記載が見当たらないのですがこいつをUpdateで回さないと動かないみたいです[2]
なので初期化のInitializeと同時に行います。

https://github.com/nekomimi-daimao/ARWithWebRTC/blob/main/Assets/Scripts/Scene/ARSceneManager.cs#L21-L25

ARFoundation

Plane Collider

とりあえずCubeをぶつけるためにAR Plane Managerで床を検知しています。
GameObject -> XR -> AR Default Planeで作成したprefabのConvexがonになっていなかったので、Cubeがすり抜けてぶつかったはずのCubeがすり抜けて手間でした。なんか昔はそのまま作ったやつでぶつかっていたと思うんですが……。

MediaStream

今回、もっとも試行錯誤したのがここでした。
WebRTCのSDKがCameraの拡張メソッドで送信用のMediaStreamを作ってくれます。これは本当にいい! のですが、中でtarget textureに送信用のRenderTextureを設定しているため、AR Sessionについているカメラをそのまま使うとスマートフォンの画面が更新されなくなります。なので、作ってもらったあとにtarget textureに設定されたRenderTextureを取り出して、Cameraのtarget textureにnullを設定する必要があります。

そして問題はこのRenderTextureへの書き込みです。
実空間の映像とCubeや床などのUnityのレンダリングを重ねて書き込む必要があります。
なのでは以下のようにしていました。

RenderingCameraはARSessionOriginのCameraを複製したもので、ParentConstraintでAR SessionのCameraの子オブジェクトとして振る舞うようにしてあります。ARCameraManagerから取得した実空間の映像をUnityのCameraの背景として設定することで、カメラの映像+Unityのレンダリングを重ねていたわけです。
……あきらかにこうりつがわるい……毎フレームRenderTexture×2を書き換えるのはどう考えてもよくない。シェーダ書きたくなかったからやっていなかっただけで、素直にやるならなんらかのシェーダで合成したほうがよいはずです。

という苦労をしていたのですが、そもそもの話、Graphics.Blitの引数としてnullを渡して、画面をそのままレンダリングするだけでよかったです。3つめの引数としてarCameraBackground.materialを渡さなければいけないような気がして渡していましたが、これをすると背景の実空間だけになってしまいます。引数は2つだけにするのが正解です。

最終形
// WebRTCSDKに配信用のRenderTextureを作ってもらって
var cs = arSessionOrigin.camera.CaptureStream(Screen.width, Screen.height, 1000000);
// arSessionOrigin.cameraは画面の更新をさせる
var targetTexture = arSessionOrigin.camera.targetTexture;
arSessionOrigin.camera.targetTexture = null;

// 毎フレーム配信用のRenderTextureを更新
// 第一引数のsourceをnullにすると現在の画面へのレンダリングを指定したことになる
Observable.EveryUpdate()
                .Subscribe(_ => Graphics.Blit(null, targetTexture))
                .AddTo(_compositeDisposable);

まとめ

WebRTC

WebRTCのSDKはAPIがブラウザのjsそのままなので使ったことがある人なら即座に使えると思います。それでいてUnity向けに便利な拡張メソッドなども切られていてすごく使いやすいです。statsもあるし。
映像の遅延はそんなでもなかったです。ちょっとはありました。Androidアプリの方は無理してる感じはなかったので、通信が詰まっているか受信側のデコードの負荷が高いかだと思います。まあEditorなのでビルドしたら最適化かかってもうちょっと楽になるかも。
ブラウザとネットワーク越しのシグナリングは成立するのかな……普通にしてくれそうですが試してないです。たぶんできる。
今回はローカル環境内でしか試していないですが、βとはいえUnity公式が出してるものですし、これなら実プロダクトに投入してみてもいいんじゃないかな? というのが感想です[3]

ARFoundation

ARFoundationの画面配信は思ったより大変でした。以前の試行で実空間の画像を取るだけならそんなでもないのはわかっていたのですが、Unity部分のレンダリング込みだとめちゃくちゃ苦労しました……素直にURPに切り替えてシェーダ書いたほうがよかったかもしれない。
これUI部分は配信したくない、とかなったらどうしたらいいんでしょうね……つらい。

まとめ

作ってからユースケースを考えるに、遠くの人と一緒にARコンテンツを楽しめる、というのは面白いのではないでしょうか。ARコンテンツを誰かと一緒にやろうとするとやっぱり実空間でいっしょにいる必要がどうしてもありますが、WebRTCと組み合わせることで、自分が今見ているAR空間を遠隔の人に配信して、遠隔の人のインタラクションによって自分が今いるAR空間が変化していく! みたいな。

……ちなみに、作り終わってから
Unity Render Streamingがあることに気づいたけどARFoundationとの組み合わせが新規性があるのでセーフ! セーフです!

おしまい。

参考

https://zenn.dev/5ena/articles/184f208f7a1d03e1d876
https://html5experts.jp/mganeko/5181/
https://answers.unity.com/questions/799941/blit-camera-targettexture-to-screen.html
https://forum.unity.com/threads/ar-foundation-camera-output-to-render-texture.1075068/
https://stackoverflow.com/questions/36061163/background-of-maincamera-unity-c
https://befool.co.jp/blog/8823-scholar/unirx-from-event-args/
https://stackoverflow.com/questions/14822708/how-to-get-client-ip-address-with-websocket-websockets-ws-library-in-node-js
https://github.com/endel/NativeWebSocket

脚注
  1. 「手動」で「シグナリング」するという「選択肢」も「ある」。ちなみにHoloLens 2のときにWebSocketサーバを立てるのがめんどくさくて最初に2回くらいやりました。ちゃんと繋がってえらい。 ↩︎

  2. 動かなくて諦める寸前でした。Sampleちゃんと最初から読み返してよかった。 ↩︎

  3. スマートフォン、しかもブラウザじゃなくてアプリでのWebRTCだとなんかよくわからないことがいっぱい起こって相当つらい目に遭うと思いますが……。 ↩︎

Discussion