スマホVRは、手軽で楽しいが処理能力の制約がある。また、360度映像の配信において、360度全ての映像を常に送り続ける必要はない。
つまり、見ている方向を検知してその部分だけの映像を遅れるようにすれば、映像のサイズも小さくなるので遅延が少なくなるはず!(配信側PCのCPUは上がると思うが)
全て、コンピューターでやって映像だけ送ってしまおう!
そうすれば超低遅延化できるはず! という記事の続きです。
制作しながら書いてるので、いろいろなところに話が飛んだり、間違ってたりするかもしれません
webRTC 超低遅延化を目指して VRのサーバー処理(1)
双方向の通信
Unity内のカメラと視聴デバイス同期
方法
webRTCでは映像以外にもいろいろな情報をやりとりできるみたいだが、
webRTCは奥がとても深そうなのでWebsocketで実装する。
基本的には以下の方法を使う。
client ←(socket.io)→ server ←(OSC)→ Unity
カメラ姿勢の同期
前回作ったwebRTCのシステムをベースに進めていく。
このようにスマホのalpha, beta gamma回転をx, y, zにそのまま入れればいいと思っていたが、そんなに簡単じゃないようだ。
デバイスの傾き、方角、加速度を取得できる「Device Orientation Event」
カメラのジャイロセンサーをUnityのカメラと同期させるときにうまく変換しないといけない。
VRゲーム用にスマホジャイロセンサとMainCameraを同期させる
Unityアプリの場合はこのInput.gyro.attitude
を取ってくればいいのだが、
クライアント側で使う予定はない。どうすればいいか。
姿勢推定にはQuaternionという概念が重要なようだ。
組込み技術者のための四元数(クォータニオン)入門(2):応用編
結構調べたが、これを実装するのはかなり骨が折れそうだ。
奥の手
A-frameのカメラを非表示で起動してそこからデータを得よう。
前回の記事で述べたようにバージョンは0.8.2を使う。
0.8.2でカメラの回転がどうしても取れなかったのでstackoverflowにお世話になりました。
Doesn't work EventListener in a-frame 0.8.2
0.8.0より少し構文が厳しくなったのかな?
<script>
AFRAME.registerComponent("foo", {
tick: function() {
socket.emit('CameraInfo', {
x: this.el.getAttribute("rotation").x,
y: this.el.getAttribute("rotation").y,
z: this.el.getAttribute("rotation").z
});
}
});
</script>
<a-scene style="display:none">
<a-camera id="camera" foo></a-camera>
</a-scene>
この値をサーバー側で値を受けてoscに流す。
node-osc
を使ってoscメッセージを扱うのでnpmでインストールする。
前回のサーバー側コードにnode-oscの宣言を追加
// -- OSC part --
var osc = require('node-osc');
var oscClient = new osc.Client("127.0.0.1", 6001);
signaling part以下のsocketio.on(){}
内に
サーバーからの情報を受けてoscで送るコードを追加
// -- signaling part --
socketio.on('connection', function(socket) {
//追加
socket.on('CameraInfo', function(data) {
oscClient.send("/Server/CameraInfo", socket.id, data.x, data.y, data.z);
});
サーバー → Unity
OSCメッセージをUnityで受け取れるようにする。
Unity OSCのヒット数が多かったのでやってみたがUnity初心者には理解できない。
解説が丁寧なサンプルを見つけた。
UnityOSC
サンプルを開いてplay, MAXの例も梱包されているのでそれを使うとOSCメッセージが送れている事がわかる。
これをベースにOSCから受け取った値をカメラに入れていく。
A-FrameとUnityの座標変換をする(Z軸の向きが逆)。
using UnityEngine;
using System.Collections;
public class OSCReceiver : MonoBehaviour {
public OSC osc;
// Use this for initialization
void Start () {
osc.SetAddressHandler( "/Server/CameraInfo" , CameraInfo );
}
// Update is called once per frame
void Update () {
}
void CameraInfo(OscMessage message){
float x = - message.GetInt(1);
float y = - message.GetInt(2);
float z = message.GetInt(3);
transform.rotation = Quaternion.Euler(x,y,z);
}
}
アスペクト比の同期
スマホの画面にアスペクト比を合わせた映像をUnityから送らなければならない。
ので、まずはスマホから情報を送る。
クライアント側
画面サイズはwindow.innnrHeight
とwindow.innnrWidth
で取ればいいのだが、
画面回転時の対応で少しつまづいた。
Androidでのイベント実行順が
resize
→ window.innnrHeight
→ orientationchange
の順番のようだ。
だからorientationchange
イベントでwindow.innnrHeight
を取得すると値が変わらない。
resize
イベントでとる。
Android標準ブラウザの本体回転時の横幅取得に関して
<script type="text/javascript">
window.onload = function() {
socket.emit('CameraSize', {
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', function(event) {
socket.emit('CameraSize', {
w: window.innerWidth,
h: window.innerHeight
});
});
</script>
Unity側
(追記) この方法自体は間違っていないのだが、Syphon用に解像度を指定するには、この方法ではダメだった。コードの下に正しい方法を書いた。
Unityでは解像度の直接指定はできないようだ。
出力デバイスの解像度に合わせて自動で調整するらしい。
ので、アスペクト比を調整していく。
以下のサイトとUnityOSCのサンプルを大いに参考にしてOSCを受信した値でカメラのアスペクト比を調整する。
Unityで解像度に合わせて画面のサイズを自動調整する
複数の解像度に対応する(引き伸ばす)
CameraのInspectorでprojection
をnorthographic
に変更後
以下のスクリプトをカメラに追加
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraSize : MonoBehaviour {
public OSC osc;
// Use this for initialization
void Start () {
osc.SetAddressHandler( "/Server/CameraSize" , getCameraSize );
}
// Update is called once per frame
void Update () {
}
void getCameraSize(OscMessage message){
print(message);
// 開発している画面を元に縦横比取得
float developAspect = 10f / 16f;
// 実機のサイズを取得して、縦横比取得
float deviceAspect = message.GetFloat(1) / message.GetFloat(2);
// 実機と開発画面との対比
float scale = deviceAspect / developAspect;
Camera mainCamera = Camera.main;
// カメラに設定していたorthographicSizeを実機との対比でスケール
///初期値を設定(画面の回転を繰り返すと、カメラの大きさが小さくなってしまうため)
mainCamera.orthographicSize = 6;
float deviceSize = mainCamera.orthographicSize;
// scaleの逆数
float deviceScale = 1.0f / scale;
// orthographicSizeを計算し直す
mainCamera.orthographicSize = deviceSize * deviceScale;
}
}
これで、アスペクト比を同期できた。
(追記 正しい方法)
CamTwistでSyphonからの映像を確認したが、なぜかサイズがあっていない。
よこが削られて黒くなってしまう。
なぜ??
Unityからの出力はゲームビューコントロールバーのアスペクト比になってしまうみたい。
2時間くらい悩んだ末探してもいい情報がないのでSyphonのServerコードを眺めてた。
コードを変更したらなんとかなりそう!
SyphonServer.csファイルの101行目と102行目あたりを書き換える。
(追記 解決したので webRTC 超低遅延化を目指して VRのサーバー処理(3)で書きます!)
101 var width = _hasCamera ? camera.pixelWidth : _sourceTexture.width;
102 var height = _hasCamera ? camera.pixelHeight : _sourceTexture.height;
いろいろ調べたが、ここのコードをインタラクティブに制御するうまい方法がなかったので
とりあえずテストとしてcamera.pixel~
に直接、スマホの解像度を入力した。
(追記 使用していたSyphonが公式じゃないことに気づいた。公式だったらできるかもしれない)
Unityから送信した映像を表示する
問題はCamTwistの出力サイズが手動制御しかできないことだが、そこは飛ばして、クライアント側の映像表示について。
CamTwist → 配信用ブラウザ → クライアント
アスペクト比があってる映像が送られてくるので、そのまま全画面表示すればいいはず。
テストのために、CamTwistの出力設定を手動でスマホのウィンドウサイズに合わせる。
listen.jsより上にvideoタグを追加する
<video id="remote-video" style="width:100%; height:100%" autoplay loop="true"></video>
時々、勝手に映像のサイズが変わるのでstyle
を指定した。
なぜだろう。
とりあえずテスト成功。
いろいろと手動ではあるが、システムが一通り完成した。
手動部分は今後変更していきたい。
テスト
どのくらい効果があったかテストした。
同じソースを元に同じWiFi経由で、
A. 360度映像全てを送るパターン
B. 今回の一部だけ送るパターン
を検証した。
A.の場合は10802160の映像
B.の場合はスマホのウィンドウサイズに合わせて512360の映像を送る
画質
画角が多少異なっていて公平ではないが今回とった方法(B)が画質が良いのがわかるだろうか?
遅延
パターンA
CamTwist → 配信ブラウザ → スマホ
0.5~0.7秒
パターンB
準備
テストをするためにこの辺を参考にしてUnityにタイマーを追加
制限時間を表示!Unityでタイマーを扱う方法【初心者向け】
【Unity】TextMeshがぼやける!テキストをはっきり表示させる方法!
結果
Unity → CamTwist → 配信ブラウザ → スマホ
約0.3秒
これくらいの遅延だと、全然気にならなかった。
まとめ
これで一通り1:1は完成した。
今後、1:Nへの拡張、また、方法の再検討を行なっていくつもりだ。