ARFoundationの画面をWebRTCで配信してDataChannelで操作する
ARFoundationの画面をWebRTCで配信してDataChannelで操作する
タイトル通りのものを実装してみました。
WebRTCを通してARFoundationの画面をUnityEditorのReceiveシーンに配信し、Receiveシーン上に配信されている画面をタップすることでDataChannelを通して座標を送信しその位置にCubeを発射します。
シーケンス
- Receive
映像を受信してタップ座標を送信するUnityEditor - AR
映像を送信してタップ座標を受信するAndroidアプリ - WebSockerServer
シグナリング用のnodeで立てたWSS
登場人物は上の3つです。
以下のシーケンスで進行します。
環境
リポジトリはこちら。
起動方法はREADME.mdにあります。
バージョンの組み合わせは以下です。
| 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を使いました。
UPMでWebSocketが使えるとはいい時代になったものです。
WebSocket.Connectは接続成功しても呼び出し元に返ってこないのにだいぶドハマリしましたが! おかしい! その設計はおかしい!
かえってこない
IObservable
ひとまずWebRTCのイベントをぜんぶIObservableに変換できる拡張メソッドをまとめたクラスを作りました。gistにも切り出したのでぜひ使ってください。たぶんこの記事とかリポジトリの価値の9割はこれです。
C#完璧に理解してるのでDelegateとActionの違いがわかってないのですが、Delegateは独自クラスがどうこうとか言われてめんどくさいので、できたら全部Actionがいいな……と思いました。
データクラス
SDPのデータクラスはどれも[Serializable]ではなく、すんなりjsonにできないので間に変換クラスを噛ませる必要があります。
こういうjsonのためのデータクラスを作るときには元データ→jsonのときはfrom、json→元データのときはtoでメソッド名を始めるようにしています。
data class
IceCandidate
IceCandidateを来たそばからからつっこむと ICE!!!!! みたいなエラーを出しながら死んだので、Queueに溜めておいて順次投入するようにしています。
WebRTC.Update
ドキュメントとかに特に記載が見当たらないのですがこいつをUpdateで回さないと動かないみたいです[2]。
なので初期化のInitializeと同時に行います。
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との組み合わせが新規性があるのでセーフ! セーフです!
おしまい。
参考
Discussion