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