Unity+WebRTC+three.js+WebVRを使ってRICOH THETAの全天球画像を遠隔地のOculusでみる

Unity(WebGL出力)+WebRTC+three.js+WebVRを使ってRICOH THETAの全天球画像を遠隔地のOculusでみることができたので技術的要点についてまとめる。

# 従来やっていたこと

![従来手法 (1).png](https://qiita-image-store.s3.amazonaws.com/0/16768/b457176d-a64b-a445-2fab-bc4b79f9736b.png)

今まではTHETAのストリーミング画像をそのままWebRTCで送って受信側で以下の表示方法で見ていた。
（Dualfisheye画像を送る場合は[THETAのDualfisheye動画をThree.jsで表示してみた - Qiita](https://qiita.com/mechamogera/items/b6eb59912748bbbd7e5d)の方法などで表示していた）

three.jsにはWebVR用のライブラリもあり技術的にはシンプルに実現できる。
ただし、このような方法だと巨大な全天球画像を送るため、通信状況が悪くなるとWebRTCで画質が落とされてさらにその1部分を見ることになるためモザイクに近い状態になることが起きていた。

# 今回したこと

![新手法 (1).png](https://qiita-image-store.s3.amazonaws.com/0/16768/6bc6c74e-fa6b-91d0-4d16-d3dab71d2db7.png)

THETA画像の送信側で描画を行いその画像を転送することによって転送量を抑える。
HMDの傾き情報はWebRTCのデータチャンネルを利用して受信側から送信側に送り回転を同期する。
これによって従来やっていた方法より綺麗な画像が見られるようになった。

## 今回したことの課題

とりあえず以下のような感じかな。

* 1:1通信しかできない
* 操作遅延がある

また、接続直後はトラッキングがうまくいかず時間がたつとHMDの動きと映像が同期するという現象も確認した。

## 今回試した環境

* Unity:5.6.3p4
* three.js
* WebRTCプラットフォーム：skyway
* Firefox 57.0.1(送受信側)
* Oculus(Software Version 1.20.02)

# 送信側の技術要点

## UnityでTHETAのライブストリーミング画像表示

[Unity+THETA S+UVC Blenderでライブストーリミング画像を表示してみる - Qiita](https://qiita.com/mechamogera/items/fe701e36206faaa3d7a4)
上記のようにしてUnityでTHTEAのライブストリーミング画像を表示する。
WebCamTextureはUnityのWebGL出力にも対応している。

## UnityのWebVR対応

[UnityのWebGLを一番シンプルだと思う実装でWebVRに対応させてみた。(アセット追加) - Qiita](https://qiita.com/gtk2k/items/82b4b63c82c118049223)
を参照してUnityをWebVR対応する。

記事中で紹介されているAssetsは新しいUnityには対応していなかったのでforkして動くようにしてみた。
[mechamogera/Unity-WebVR-Assets: UnityでWebVRに対応したWebGLビルドを行うテンプレートおよびアセット](https://github.com/mechamogera/Unity-WebVR-Assets)

基本的にはUnity外部のjsスクリプトから内部のスクリプトとSendMessage()で連携しているところをWebRTCのデータチャンネルで受信してUnityに送り込むように改造すればよい。

## Unityで描画した映像の転送

[HTMLCanvasElement.captureStream() - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/captureStream)を利用して、UnityがWebGL出力で描きだしたcanvasの映像を以下のようにstreamを取得してWebRTCで送る。
UnityはWebGL出力で#canvasというIDのcanvasを作ってそこに描画する。

```js
var canvasStream = document.getElementById("#canvas").captureStream(30);
```

## 音声と映像ストリームの合成

このままだと音声が送れないので以下のようにして音声も送る。
(ブラウザで実行時にビデオと音声の2回に分けて許可を求められるのがいまいち)

[how to add a audio stream on canvas stream in webrtc - Stack Overflow](https://stackoverflow.com/questions/42927903/how-to-add-a-audio-stream-on-canvas-stream-in-webrtc) を参照
以下のような感じでいける。

```js
var p = navigator.mediaDevices.getUserMedia({ audio: true, video: false });
p.then(function(stream) {
  var audioTrack = stream.getAudioTracks()[0];
  canvasStream.addTrack(audioTrack);
});
```

# 受信側の技術要点

## WebVRでの閲覧

最初はWebRTCの映像を書き出したvideoの画像をcanvasに描き出してWebVRで見ようとしたが無理だった。
現状2D canvasはWebVRでサポートされておらずWebGLで描画されたcanvasしかサポートされていないらしい。
[2D Canvas Support in WebVR · Issue #153 · w3c/webvr](https://github.com/w3c/webvr/issues/153)

仕方がないので以下の記事あたりを参考にしてthree.jsでcanvasに書き出す。
[WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける - 自習室](http://izmiz.hateblo.jp/entry/2015/04/19/223456)
映像が上下反転していたのでGeometryで反転している。

```js
(function() {

  var WIDTH = 2688, HEIGHT = 1600; // oculus用の画像サイズ
  var renderer, scene2d, camera2d;

  init();
  animate();

  function init() {
    var canvas = document.getElementById("canvas")
    renderer = new THREE.WebGLRenderer({ canvas: canvas });
    renderer.setSize(WIDTH, HEIGHT);
    renderer.setClearColor(0x000000, 1);
    
    (function init2D() {
      camera2d = new THREE.OrthographicCamera(WIDTH/-2, WIDTH/2, HEIGHT/-2, HEIGHT/2, 1, 2000);
      camera2d.position.z = 500;
      scene2d = new THREE.Scene();
    
      var video = document.getElementById( 'their-video' );
      var texture = new THREE.VideoTexture( video );
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.format = THREE.RGBFormat;
      
      var movieMaterial = new THREE.MeshBasicMaterial({side: THREE.DoubleSide, map: texture});
      var movieGeometry = new THREE.PlaneGeometry(1, -1, 1, 1);
      var movieScreen = new THREE.Mesh(movieGeometry, movieMaterial);
      movieScreen.scale.x = WIDTH;
      movieScreen.scale.y = HEIGHT
      scene2d.add(movieScreen);
    })();
  }
  
  function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene2d, camera2d);
  }
  
})();
```

本当はUnityでしたかったが今回は時間制限があったのであきらめた。
VideoPlayer.urlにstreamのblobを渡してみたりしたのだが表示できなかった。
Simple MovieTexturesのアセットを利用すればうまくいく？

WebVRでの閲覧は上で書いたUnityのWebVR対応のコードの閲覧部分を参考にする。
three.jsで描き出したcanvasに対してVRDisplay.requestPresent()する。
HMDの傾き情報もそのコードを参考にしてデータチャンネルで送るだけ。