Mercari Advent Calendar 2018 の21日目の記事です。
はじめに
私はR4D XRという研究開発組織でXR領域のエンジニアをしている@nkjzmです。(R4D XRが何をしているかという話はこちら)
メルカリでは、現在セキュリティの観点から外部の方のオフィス内への立ち入りを制限しています。 ただ、実際にどのような環境ではたらいているのかというのはメルカリのカルチャーを伝達する重要な要素なので、外部の人に見せられない状況は好ましくありませんでした。
そこで『360°動画を使ったオフィスツアー』のアイデアが生まれました。今回はエンジニア方面での取り組みをご紹介します。
360°動画によるオフィス紹介の課題
社内でカメラマンをしている @chan-kuma さんがInsta360 ONE Xを使ったテスト撮影をしてくれました。
撮影した動画をOculus GoというVRゴーグルで観てみたところ、いくつかの課題浮かんできました。
- 移動や画面揺れがある部分で酔ってしまう
- どこを見ればいいのか分からない
360°動画は視聴者が自由に視点を動かすことが出来るので、映像の中に入り込んでいるような没入感を得られるという特徴があります。そのため『VR動画』と呼称されることもあります*1。
その反面、現実世界の自分の視点と異なる動きに対して違和感が生じるという性質もあります。例えば「撮影者による視点の移動」や「撮影時の揺れ」など視聴者が意図しない動きをした際に、車酔いのような不快感を感じる現象があり、感覚の不一致によるものだと言われています。
1つ目の課題はまさにこれで、対処法としては、撮影時の工夫による軽減があげられます*2。
2つ目の課題も360°動画ならではの問題です。平面の動画であれば注視してほしい箇所を横から提示することが出来ますが、360°動画は視界全体を覆ってしまうためそれが出来ません。 そのため、オフィス全体の雰囲気を掴むことは出来るのですが、現実空間の案内とは明らかに情報が落ちていることが分かります。
この問題をVRの知見を使って解決しました。
ソーシャルVR
複数人が同じ空間でコミュニケーションをとれるVRをソーシャルVRといいます。近年のVRでは、デバイスの普及などと共にソーシャル要素を持つコンテンツが台頭してきました。アバターを用いた非言語な情報(視線や手振りなど)のやりとりが出来るため、文字やビデオ通話などと比べ、より臨場感のあるコミュニケーションを行えるという特性があります。今回はこの仕組みを応用しました。
作成したプロトタイプ
「社内メンバーと共にVRオフィスツアーが体験できる」という内容になります。 これは「メルカリに訪れた方に対し、オフィスを案内する」というシナリオを想定しています。
ツアーはメルカリオフィスの入口から始まります。
上図はOculus Go越しにみた映像になっていて、相手の姿はロボットのようなアバターとなって表示されています。
手に持ったポインターで任意の球体を選択することで、見学したい場所を選択することが出来ます。
場所を選択するとオフィスツアーが始まります。視点は大きく動くのですが、開始画面と同じ床のパネルや相手のアバターが見えることで「酔い」を抑える工夫をしています。
開始画面で使用したポインターはツアー中も使用することが出来ます。例えばフロアマップや動画中の特定の領域(カフェスペースやチームの島など)を指し示し、来訪者に対して説明をすることを可能にします。
フロアマップの表示には自分達の現在位置が表示されています。VR空間上で移動をすると、現在位置表示の場所も同期して動く仕組みです。
実装について
ゲームエンジンのUnityを用いて実装を行っています。
この章はUnity開発者向けの内容となります。
360°動画の再生には、Unity2017.3で追加されたパノラマ動画機能を利用しています。デフォルトでSkyboxシェーダによる描画が可能になったため、パフォーマンスが出しやすくなりました。
使い方は簡単です。360°動画ファイルをインポートするとVideoClip
として読み込まれます(下図左)。必要であれば再生環境のスペックなどに合わせて設定を行いますが、今回は特に変更していません。次にVideoClip
を再生したいシーンのHierachyビューにドラッグします。VideoPlayer
コンポーネントがアタッチされたゲームオブジェクトが生成され、先程のVideoClip
の参照が付いていることが確認出来ると思います(下図中央)。VideoPlayer
はデフォルトでカメラに対して描画が行われる設定になっていますが、今回は最終的にSkyboxへの描画を目指しているので、仲介するためのRenderTexture
を用意します。Projectビューで右クリックをし、Create>RenderTexture
を選択します。VideoTexture
と名前をつけて保存し、下図右のように設定します。解像度(Size
)は、元のVideoClip
の大きさに合わせています。
次にSkyboxの設定をしていきます。Projectビューで右クリックをしCreate>Material
を選択、Panoramic
と名前を付けて保存します。InspectorビューでShaderをSkybox/Panoramic
に変更し、Spherical (HDR)に先程作成したVideoTexture
を設定します。Rotationは水平方向の初期位置を0°~360°の間で設定することが出来ます。後ほど動画の正面方向に合わせて調整してみてください。最後にLightingビューを開き、Skybox MaterialにPanoramic
を設定すれば完了です。
シーンを再生するとパノラマ動画が再生されることが確認できます。
次に、ソーシャルVRのネットワーク通信にはPhoton Unity Networking 2 (PUN2)を使用しています。
入室と同時に自身のアバターを生成します。PhotonNetwork.Instantiate
を使うと、ローカルクライアントだけでなく、同じルームに接続しているクライアントにも同一のオブジェクトが生成されます。自身が生成したアバターは参照を取得しておきます。
生成自体はこれで完了ですが、ソーシャルVRの要素として、頭と手(コントローラー)、それぞれの位置と向きを同期する必要があります。頭と手の情報はOculus社から配布されているOculus Integrationというアセットから取得できます。この情報を自身のアバターに渡してやり、アバター自身がOnPhotonSerializeView()
というメソッドを通じてそれぞれのクライアントに存在するアバターに反映させる流れとなっています。
上記の処理を実現しているコードの一部を以下に示します。
// 各部位の情報を持つ構造体 public struct PartStatus { public bool Enabled; public PartType Type; public Vector3 Position; public Quaternion Rotation; } // 同期用のメソッド public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.IsWriting) { var parts = new List<PartStatus>(); parts.Add(GetPartStatus(PartType.Head)); parts.Add(GetPartStatus(PartType.RightHand)); parts.Add(GetPartStatus(PartType.LeftHand)); foreach (var part in parts) { stream.SendNext(part.Enabled); stream.SendNext((int)part.Type); stream.SendNext(part.Position); stream.SendNext(part.Rotation); } stream.SendNext(PlayerManager.Instance.IsLoad); } else { var parts = new List<PartStatus>(); for (int i = 0; i < 3; ++i) { parts.Add(new PartStatus() { Enabled = (bool)stream.ReceiveNext(), Type = (PartType)(int)stream.ReceiveNext(), Position = (Vector3)stream.ReceiveNext(), Rotation = (Quaternion)stream.ReceiveNext() }); Debug.Log(parts[i].Type + ": " + parts[i].Enabled); } UpdateParts(parts); if ((bool)stream.ReceiveNext()) { UnityEngine.SceneManagement.SceneManager.LoadScene("Hoge"); } } }
OnPhotonSerializeView()
はstream.IsWriting
の値から、送信側と受信側の処理を分けることが出来ます。
送信側では自身のアバターの情報をstream
に対して追加していきます。PUN2の仕様上、独自の型を扱うためには一工夫必要なので、今回はバラバラに送受信しました。本家Photonとの差分として、PUNではVector3
やQuaternion
といったいくつかのUnityの型を扱える点が便利でした。
PartType
にはHead
,RightHand
, LeftHand
を定義しています。Oculus Goで扱える手は片手だけですが、プラットフォーム側で右手と左手のどちらかを利き手として設定できるため、両手それぞれを考慮する必要がありました。しかしそのままでは利き手でない方の手の座標を無駄に送り続けることになるので、認識していない場合については処理を分岐させています。具体的にはlocalPosition
が(0, 0, 0)
を返す時、アバターの手自体を非表示にするという情報を送るようにしました。
また、実はこのアプリはOculus Riftというハイエンド向けのVRHMD*3についても対応しています。Oculus Riftでは両手を扱うことができるのですが、前述のような処理を書いておくことでデバイス間の差異を意識することなく利用することが出来ました。また、Oculus Riftには操作していないコントローラーがスリープ状態になる仕様があるのですが、そういった場合も同様にアバターに反映される点も良かったです。
Oculus Riftの対応については、Oculus Integrationがほとんどが差分を吸収してくれていましたが、アクティブなコントローラーを取得する処理など、いくつかの箇所でラップするようなコードを書いています(下記コード参照)。
public Transform Pointer { get { // 現在アクティブなコントローラーを取得 var controller = OVRInput.GetActiveController(); // Debug.Log(controller); if (controller == OVRInput.Controller.RTrackedRemote || controller == OVRInput.Controller.RTouch || controller == OVRInput.Controller.Touch) { return RightHandAnchor; } else if (controller == OVRInput.Controller.LTrackedRemote || controller == OVRInput.Controller.LTouch) { return LeftHandAnchor; } // どちらも取れなければ目の間からポインターを出す return HeadAnchor; } }
Oculus Goの開発において、プレビューがしづらいことが課題として存在していました。
以下の記事は以前書いたもので、DaydreamのエミュレータをOculus Goでも利用するというものです。
ただ今回はOculus Riftがその役割を果たしてくれました。少しだけ個別の対応が必要ではあるものの大部分はラップされているため、Oculus Riftで動くものは大抵Oculus Goでも動きました。今回開発をしたことで得た知見です。
社内の反応
XRチームで毎週行っているVR/AR体験会で社内のメンバーにも使ってもらったところ、反応は概ね好評でした。現在は、今後面接などで実際に利用できるように社内で検討を進めています。
まとめ
今回は360°動画にソーシャルVR要素を組み込むことで、「単なる動画視聴体験」を「社内メンバーと共にオフィスを巡る体験」に引き上げることが出来ました。
今後の展望として、メルカリUSのオフィスのような「遠隔地の見学」や、ボイスチャット機能を付けることで「遠隔地での案内」なども考えています。
「見学する場所」や「案内する場所」といった制約に囚われずに利用できる今回のアプローチは、空間を自在にデザイン出来るVR技術と非常に相性が良かったと思います。
明日22日目の執筆担当もXR チームの @ash_yanagisawa です。引き続きお楽しみください !