読者です 読者をやめる 読者になる 読者になる

Tutti Lab

元シリコンバレー在住のおっさん技術者、モバイルVRアプリ開発に挑戦中

Vuforia for UnityのExtended Tracking機能を利用してGoogle Cardboardでポジショントラッキング

はじめに

Google CardboardやGear VRなど、スマホを利用したいわゆる「モバイルVR」は、Oculus Rift/HTC Vive等と比較し、値段が安くお手軽にVR体験を楽しむことができます。一方で、Oculus Rift/HTC Viveには搭載されているハンドデバイス(Oculus RiftのOculus Touchはこの2016/12に出荷予定)や、ポジショントラッキング等、VRの没入感を生み出すための重要な機能が、モバイルVRには搭載されておりません。
モバイルVRでは、立体視+ヘッドトラッキング(顔=デバイスの向きをトラッキング)により、顔の向きとCG空間内の視線方向を一致させることで、あたかも自分が向いている方向のCGを立体視しているような錯覚を生み出し、これが没入感を生み出します。この「立体視+ヘッドトラッキング」だけでも、相当の没入感であり、私も最初にGoogle Cardboard(Oculus Rift DK1も)を見たときには相当な感動を覚えました。しかし、ポジショントラッキングやハンドデバイスが実装されているVR、特にHTC Viveのルームスケールなんかを体験してしまうと、「立体視+ヘッドトラッキング」だけでは物足りなさを感じるようになってしまいます。
ポジショントラッキングとは、その名の通り、ユーザの位置をトラッキングする機能です。ヘッドトラッキングでは顔の向きだけ任意の方向に向けることができましたが、例えば立ち上がったり・しゃがんだり・肩を左右に振っても、ユーザの視点位置は変化しません。しかし、ポジショントラッキング搭載のものであれば、例えば向こうから飛んでくるボールを避けるゲーム、なんてものも実現することができます。
Oculus Rift (DK2/CV1)では、ユーザの正面の専用のカメラを設置し、このカメラがOculus Rift本体の位置を捉えることでポジショントラッキングを実現しています。HTC Viveは、Oculus Riftと逆の方法、部屋の2角に赤外線を発光するベースステーションを設置し、ヘッドセットやハンドデバイスに内蔵のカメラで赤外線を受光することでトラッキングをしているようです。これにより、ルームスケールと呼ばれる「CG空間を歩き回る体験」や、まるで自分の手がCG空間に現れたかのような秀悦な操作感が実現されています。
f:id:tuti107:20161127153421p:plain
しかし、これらポジショントラッキングやハンドデバイスには高度が技術が必要となり、廉価版であるモバイルVRでの実現はコスト的に困難です。Vico VRのように、ポジショントラッキングや手足による入力に対応している(らしい、コンシュマー版は未出荷)ものもありますが、それなりのお値段のようです。
今回は、有名なARエンジンである「Vuforia」を利用して、簡易なポジショントラッキングを実現してみます。

Daydream Technical Preview

はじめに、前回説明した、Daydream technical previewのUnityを起動し、適当なプロジェクトを作成してください。
おそらく通常のUnity + Google VR SDK for Unityの組み合わせでの開発も可能かと思いますが、AndroidManifestがVuforia/Google VR SDKで競合するなど、いろいろと面倒な問題が発生するため、今回は割愛します。

Vuforiaのライセンスキー登録

Vuforiaを利用したアプリを開発する際、以下の手順でライセンスキーの登録が必要となります(下記の手順の場合は無料です)。

  1. Vuforiaにログインの後、こちらより、Add License Keyボタンを押下する。
  2. Project TypeよりDevelopmentを選択、Project Detailは、App Nameには適当な名前、DeviceはMobileを選択し、Nextボタンを押下する。
  3. "By clicking "Confirm" below, ..."をチェックし、Confirmボタンを押下する。
  4. License Managerに表示されるアプリ一覧より、2でApp Nameとして入力したものを選択する。
  5. License Keyが表示されるので、これをコピーする。
  6. Unityに戻り、ARCameraのインスペクタを開く。Vuforia BehaviorコンポーネントのApp License Keyにペーストする。

Targetの登録

次に、認識対象となるイメージの登録を、以下の手順で行います。

  1. Target Managerで、Add Databaseボタンを押下する。
  2. Create Databaseポップアップにて、Nameには適当なデータベース名を、TypeはDeviceを選択し、Createボタンを押下する。
  3. Target Managerのデータベース一覧に2で追加したデータベースが追加されるので、これを選択する。
  4. Add Targetボタンを押下する。
  5. Add Targetポップアップにて、TypeはSingle Image、Fileには適当なイメージファイル、widthには実世界(プリントアウトしたイメージ)のサイズ(単位はメートル)、Nameには適当な名前を入力し、Addボタンを押下する。
  6. Download Databaseボタンを押下し、Download Databaseポップアップにて、Unity Editorを選択し、Downloadボタンを押下して、データベース(unitypackage形式)をダウンロードする。
  7. ダウンロードしたパッケージをインポートする。すると、Assets/StreamingAssets/QCARに、データベースが読み込まれる。
  8. ARCAmeraオブジェクトのDatabaseLoadBehaviorコンポーネントに、Load データベース名 Databaseというチェックが現れるので、これをチェックする。また、これをチェックすると現れる「Activate」もチェックする。

f:id:tuti107:20161127200849p:plain

VuforiaがTargetを認識した時の処理

  1. Assets/Vuforia/Prefabs/ImageTargetをシーンに追加する
  2. ImageTargetオブジェクトのImageTargetBehaviorコンポーネントについて、インスペクタにて、TypeはPredefined、Databaseはデータベース名、Image Targetはターゲットの名前を選択する。また、Enable Extended Trackingにチェックを入れる。
  3. f:id:tuti107:20161127201605p:plain
  4. ImageTargetオブジェクト配下に適当なオブジェクト(以下の図の場合はCube)を適当なサイズで配置する。なお、Extended Trackingの効果をわかりやすくするために、縦方向に長くしています。
  5. f:id:tuti107:20161127202058p:plain
  6. 認識用のイメージを印刷する。サイズはAdd Targetにて設定した横幅とする。

フロントカメラ搭載のノートPCやMacをご利用の方は、エディタ上にて実行し、印刷したターゲットのイメージにカメラを向けると、上記のCubeが撮影イメージ上に表示されます。また、Extended Trackingをチェックすると、カメラからターゲットのイメージが外れてしまってもCubeが適切な位置に表示され続けます。つまり、一度でもターゲットのイメージを認識すると、その後はターゲットのイメージの周囲の情報をリアルタイムに認識することで、ターゲットのイメージがカメラ内に無くてもまるでターゲットのイメージがカメラ内に捉えられているかのごとく振舞います(当然、周囲に動くものがある場合はダメなようです)。
www.youtube.com

Google Cardboardとのインテグレーション

  1. ARCamera配下のCameraをDisableする。また、ARCameraオブジェクトのVideoBackgroundManagerコンポーネントのEnable video backgroundチェックを外す。これらは、スマホで撮影したカメラプレビューを背景映像として表示するためのものですが、ポジショントラッキングの用途には不要なためです。
  2. ImageTarget配下のCube(もしくは上記で追加した何らかの3Dオブジェクト)をDisableする。こちらもポジショントラッキングには不要なためです。
  3. シーンのルートにEmptyObject(名前をPlayerとする)を配置、そこにCameraをぶら下げる。このCameraのTagはMainCameraとする。
  4. 以下のPositionTracker.csを作成し、Playerにコンポーネントとして設定する。
  5. using UnityEngine;
    using System.Collections;
    
    public class PositionTracker : MonoBehaviour {
      public GameObject arCamera;
    	
      void Update () {
        var p = arCamera.transform.position;
        gameObject.transform.position = new Vector3 (p.x, p.z, -p.y);
      }
    }
    

  6. Cameraの前に適当な3Dオブジェクト(Cubeなど)をシーンに配置する。

  7. Assets/Plugins/Android/AndroidManifest.xmlのandroid:minSdkVersionを"16"に変更する。
  8. 前回説明した手順にて、Virtual Reality Supportedをチェック、SDKをCardboardとして、Android向けにビルドする

品質は微妙

アプリ起動前に、ターゲットイメージを印刷した紙を(多少ごちゃごちゃした)壁・棚などに貼り付け、この紙の方を向いて端末を起動してください。
f:id:tuti107:20161127220737p:plain

紙に近づけばCubeが大きく、左右に動いたりしゃがんだりすればCubeはそれぞれの方向に動きます。ちゃんとシーンを構成すれば一応ポジショントラッキングのあるVR、な感じになります。以下は、Editorで実行し、前後上下左右にMacを動かした際のものです。
f:id:tuti107:20161128211500g:plain

ただ、品質があまりよくありません。静止していてもプルプル動くし、時々変な方向に飛んでしまうし。。シンプルにARCameraの座標を使うのでは無く、ちょっと一工夫が必要のようです。
また、私のAndroid端末(Galaxy S6)でGoogle Cardboard使用中にデバイスのカメラを利用するためには、Google Cardboardに切れ込みを入れる必要がありました。。
f:id:tuti107:20161127220810j:plain

Daydream technical previewを使ってみる

はじめに

先月注文したPixelとDaydream View、ようやく発送されました。ただ諸事情により手元に届くのは、あともう二週間後になりそうです。
今回は、実機入手後すぐに開発にとりかかるべく、Unity Daydream technical previewの導入を試してみたいと思います。

Daydream technical previewのインストール

f:id:tuti107:20161126122619p:plain
Daydream technical previewは、こちらよりダウンロードできます。本ページをずっと下にスクロールしていくと、インストーラのダウンロードボタンが見つかります。

インストールが完了しましたら、デスクトップに生成されたショートカットより、Daydream technical previewを起動します。
f:id:tuti107:20161126122959p:plain
すると、「普通」にUnityが起動します。ただしタイトルバーを見ると「Unity 5.4.2f2-GVR12」となっており、これがGoogle VR向けのものであることが確認できます。
f:id:tuti107:20161126123258p:plain

NativeサポートされたGoogle Cardboard

まずは(手元に実機もありませんので)Google Cardboard向けのアプリを作ってみます。
前回Google VR SDK for Unityを利用したアプリの作成について詳細をした際は、「GoogleVRForUnity.unitypackage」をインポートしましたが、Daydream technical previewではGoogle CardboardがNativeサポートされているため、立体視表示やヘッドトラッキング等のVRの最低限の機能については、本SDKを利用する必要はありません。その代わりに以下の通りの設定を行います。

  1. File→Build Settingsより、PlatformをAndroidとする
  2. Player SettingsのOther Settingsにて、Virtual Reality Supportedにチェックを入れる
  3. +ボタンを押下し、Cardboardを選択する
  4. 同じくOther Settingsにて、Minimum API Levelを16以上とする
  5. Bundle Identifierに適当なものを設定する

f:id:tuti107:20161126125443p:plain

f:id:tuti107:20161126125524p:plain

以上を設定の後、Build and Runにて、Android端末上にCardboard向けVRアプリが起動します。
アプリといっても、Sceneに何も配置していないので、デフォルトのSkyboxが表示されるのみですが。。
f:id:tuti107:20161126130036p:plain

さいごに

今回は、Daydream technical previewのインストールし、とりあえず実機上で動くアプリをビルド・実行してみました。
次回は、Gaze入力等、もう少し詳細まで踏み込んでみたいと思います。

Google VR SDK for Unityをより詳細に見てみる

前回は、Google VR SDK for Unityをインストールし、付属のDemo SceneをとりあえずAndroid端末上で実行してみました。特段なにもはまることなく、スムーズに実行まですすめることができました。
一方で、Demo Scene内のオブジェクトを見てみると、Google Cardboard SDKのときにはなかった色々な名前のオブジェクトがあることに気づきました。実行画面のぱっと見、Demo SceneはGoogle Cardboard SDKのものとほぼ同じなのですが、SDK自体は大きく異なるのかも?
f:id:tuti107:20161021214817p:plain
ということで、今回はGoogle Cardboard SDKとの差異を中心に、Google VR SDK for Unityついて、詳細に見てみました。

ほぼ一緒でした

結論からいうと、Google VR SDK for UnityとGoogle Cardboard SDKは概ねよく似ております。名前や機能分割による違いがメインかと思います。
Google Cardboard SDKは、

  1. CardboardMainというprefabを設置→これだけで左右眼分割表・樽状のゆがみ表示・ヘッドトラッキングの「VRっぽい機能」が動きます
  2. EventSystemにGaze Input Moduleを追加し、CardboardMainにGazeポインタを設置→ユーザが見ている位置にポインタが表示されます
  3. EventTriggerを実装した適当なオブジェクトを用意→上記ポインタがオブジェクトに入る・出る・クリックする等のイベントを受けることができます

との手順で、VRアプリを簡単に作成することができましたが、Google VR SDK for Unityでは、

  1. GvrViewerMain prefabを設置→左右分割・樽状のゆがみ表示・ヘッドトラッキングを行う
  2. EventSystemにGaze Input Moduleを追加し、Main CameraにGazeポインタとなるGvrReticle(円形のポインタ)を置く
  3. EventTriggerを実装した適当なオブジェクトを用意

ということでほぼ一緒です。Google Cardboard SDKでごちゃごちゃしていて複雑だったところが、すっきりした、という感じです。

GvrViewerMain

本オブジェクトは、左右分割・樽状のゆがみ表示・ヘッドトラッキングといったVR的機能を提供します。
f:id:tuti107:20161021220843p:plain

  • VR Mode Enabledは、VR表示(左右眼分割表示)をするかどうかを設定
  • Distortion Correction Enabledは、樽状の歪み表示をするかどうかを設定
  • Stereo Screen Scaleは、1より低い値とすると画質悪いが高速、1より大きい値(最大2)とすると高精細だが低速となる
  • Neck Model Scaleは、首のオフセットを考慮したより正確なヘッドトラッキングを行う。0(未使用)~1の値を設定。Daydream端末だとわかる感じなのでしょうか?私には設定時の違いがあまりわからず。。

Main Camera

こちらは、Google Cardboard SDKとほぼ一緒です。
f:id:tuti107:20161022083056p:plain
留意点としては、AudioListenerがGvrの名称に変わっていること、くらいです。なお、Google Cardboard SDKと同様なのですが、Physics Raycasterを追加しておかないと、Gazeポインタが表示されません。
GvrAudioListenerによる音空間設定については、また別途の機会に触れたいと思います。

GvrReticle

円形上のGazeポインタです。MainCamera配下にこれを配置することで、向いている方向中央に
- ポイントが乗った・外れたこと(PointerEnter/PointerExit)通知を受けるオブジェクトがある場合は、大きな円に
- それ以外は点で
ポインターが表示されます。
f:id:tuti107:20161022084337g:plain
インスペクタで、表示される円について詳細設定ができます。
f:id:tuti107:20161022091236p:plain

  • Reticle Segmentsは、円描画の点の数を指定します。この値が少ないと円ではなく多角形な感じになります(以下は5を指定の場合)

f:id:tuti107:20161022090925p:plain

  • Reticle Growth Speedは、ポインタがオブジェクト上に乗った際に、点→円になる速度を指定します。すくないほどゆっくりとなります。

GvrReticleは、後述のGazeInputModuleを利用して、オブジェクトに乗った・外れた等の判定をします。なお、この(複雑な)GazeInputModuleを使用せず、シンプルにポインタ表示を行う手段としてGvrGazeがあります(こちらもいずれ紹介しようと思っています)。

GazeInputModule

EventSystemにGazeInputModuleを追加することで、Unityにおけるマウス・キーボード等の入力イベントの共通配信システムであるEventSystemを利用してGazeポインタ関連イベントを利用できるようになります。
例えば、DemoSceneの足元には、以下のようなボタンが配置されていますが、
f:id:tuti107:20161022093134p:plain
これらボタンが押下された場合の処理は、プログラムを記述する必要なく、簡単に設定することができます。
f:id:tuti107:20161022093422p:plain
この場合は、Cubeオブジェクト、Teleportコンポーネントの、ToggleVRMode()メソッドが、ボタンクリック時に呼び出されます。
EventSystemによるGaze関連イベントはボタン等のGUI部品のみならず、自作のGameObjectにも設定可能です。
DemoScene内のCubeオブジェクトには、以下のようにEventTriggerコンポーネントが追加されており、そこでPointerEnter/PointerExit/PointerClickがそれぞれ追加されております。このようにすることで、このCubeオブジェクトはGazeポインターが入ったとき・出たとき・クリックされたときにそれぞれ指定のメソッドが呼び出されるようになります。
f:id:tuti107:20161022094323p:plain

さいごに

以上、今回はGoogle Cardboard SDKとGoogle VR SDK for Unityの差異を中心に、Google VR SDK for Unityの詳細を見ました。これら2つは非常に似た感じですが、Google VR SDK for Unityのほうが、よりシンプルにまとめられていると感じました。
今回ご紹介の内容だけで、簡単なVRコンテンツは充分に開発可能かと思います。次回は、音響周りを中心に見ていこうかな、と思っています。

Google VR SDK for Unityを使ってみる

Googleの10月4日のイベントで、最新のVRヘッドセットDaydream Viewが発表されました。軽くて装着感もよさそうだし、$79という低価格は非常に魅力的です!私も早速手に入れて色々開発してみたいところなのですが、当面日本での発売は予定されていないとのこと。残念・・
でも遅かれ早かれDaydream Viewも対応のAndroidデバイスも手に入るでしょうし、まずはDaydream向け/Cardboard向け統合のVR開発環境(?)であるGoogle VR SDKを入手し、事前に触ってみました。
今回は、Cardboard向けのデモアプリ実行まで。
f:id:tuti107:20161016150402p:plain

環境の準備

Unity用のGoogle VR SDKであるGoogle VR SDK for Unityを利用するためには、以下をインストールする必要があります。

まずは、デモをエディタ上で実行してみる

上記全てをダウンロード・インストールしましたら、Unityを起動して適当なプロジェクトを作成します。開発画面が起動しましたら、ProjectのAsset->右クリック->Import Package->Custom Packageを選択し、先程ダウンロードしたUnity VR SDK for Unityに含まれる「GoogleVRForUnity.unitypackage」をインポートします。
f:id:tuti107:20161016151320p:plain
インポートしたら、Assets→Google VR→DemoScenes→HeadsetDemoのDemoScene.unityをダブルクリックします。
f:id:tuti107:20161016151456p:plain
これでとりあえず実行してみると、
f:id:tuti107:20161016151520p:plain
f:id:tuti107:20161016151528p:plain
はい、無事に動きました。
デモは、Google Cardboard SDKと変わらない感じです。ただし、SDKの作りはGoogle Cardboard SDKとは異なる感じです(しっかりと見ていませんが、ぱっと見、シーンに配置されたオブジェクトには見覚えのないものが色々配置されているようです)。

Android端末上で実行してみる

次に、上記エディタ上で動かしたデモをAndroidの実機にて動かしてみます。
File→Build Settingsにて、Androidを選択し、
f:id:tuti107:20161016152132p:plain
Player Settingsにて、

  • Other Settingsで、Bundle Identifierを適当に、Minimum API levelをAPI level 19に設定

f:id:tuti107:20161016152322p:plain

  • Resolution and Presentationにて、Default OrientationをLandscape Leftに設定

f:id:tuti107:20161016152422p:plain
します。あとは、Build And Runボタンを押すだけです。
特段問題なく、Android端末上でデモアプリが動作しました。

おわりに

今回は、来るDaydreamに備えて、まずはGoogle VR SDK for Unityの導入と、Google Cardboard向けのデモアプリの実行までやってみました。
デモアプリを動かすまでは特に問題なく、という感じですが、上記のとおりSDKの使い方は、Google Cardboard SDKとは異なる印象です。
次回は、過去に作ったGoogle Cardboard SDK利用のアプリの移植を行って、Google VR SDK for Unityの分析を進めてみたいと思います。

ちなみに、過去Google Cardboard向けにKeyboardの開発を行っていましたが、
f:id:tuti107:20160625095127p:plain
Googleが同等のものを提供する予定のようですね。
f:id:tuti107:20161016153803j:plain
もっと早くに開発しておけばよかった。。

Unityで3DカメラIntel RealSenseを使ってみる

f:id:tuti107:20160923150741p:plain

これまで、ThetaSやスマホを2台使いすることで360度立体視映像を撮影するアプリケーションを作成してきました。これら端末を2台使いすることで左右視差のある映像を録画し、背景部分を除去して背景とCGを重畳することで、かんたんに360度立体視コンテンツをつくれる、というものです。
この方法はかんたんでそれなりのクォリティを実現可能なのですが、撮影映像とCGの重畳において問題が生じる場合があります。
右目・左目映像を用意し、Google Cardboard等のVRヘッドセットでそれぞれの目がそれぞれの目の映像だけを見るようにすれば「人間は立体的に感じる」ことが可能です。しかし、コンピュータがこれら左右眼用の映像から立体感(=奥行き)を認識することはかんたんではありません。
このため、例えば背景部分の除去には、クロマキー(緑色の部屋を用意し、そこで撮影、映像の緑色部分を透明化することで背景除去)や、背景差分(最初にだれもいない部屋を撮影(=背景画像)、背景画像と撮影した背景映像の変化分だけを抜き出す)のような簡易的な方法を利用してきました。しかしこの方法では抜き出した部分の「立体感」をコンピュータは認識できません。このためこれらの方法では、奥行感を無視したおかしな映像が出来上がってしまう恐れがあります。
f:id:tuti107:20160923151832p:plain

クロマキーや背景差分で抜き出した映像部分には奥行きの情報がないため、CG・抜き出した映像どちらを前に表示するか、判別ができないためです。

Intel RealSense等の3Dカメラは、映像に「奥行き情報(=デプス)」を付加した撮影が可能です。以下のように、カラー映像(=左側)と同時にその映像のデプス(=右側)を撮影します。
f:id:tuti107:20160923152027p:plain

デプスは黒(=最も遠い/位置を特定不能)~白(=最も近い)をグレースケール表します。真っ黒(最も遠い)と真っ白(最も近い)がぞれぞれ、カメラから具体的にどの程度の距離離れているかについては、3Dカメラ毎に異なります。例えば今回使用するIntel RealSense R200は、認識可能な距離が0.5m~4mのため、黒=概ね4m以上(又は0.5m未満)、白=概ね0.5mとなるかと思います。

デプスを利用すると、背景部分の除去もカンタンです。例えば0.5m~4mの範囲にないもの(=デプスの黒部分)は透明、としてしまえば、4m以上向こうにあるものは一切表示されなくなります。
今回は、RealSense R200を利用し、デプスによる背景除去に挑戦してみます。

RealSenseのセットアップ

まずはこちらから、ドライバとSDKのダウンロード、及びRealSense R200を購入します。
Step1では、R200 Camera Driverをダウンロードしてください。
f:id:tuti107:20160923110949p:plain
そしてStep2(SDKのダウンロード)、Step3(オプション機能、必要に応じて)を順次ダウンロードし、最後にRealSense R200を購入します。
f:id:tuti107:20160923152950p:plain
RealSense R200を購入するためには、アカウントを作成する必要があります。画面右上のSign Inを押下し、Login or Create an Account画面の右下 CREATE AN ACCOUNTボタンを押下してCreate an Account画面へ移動し、必要情報を入力してアカウントを作成してください。メール認証の後、アカウントがアクティベートされます。
サインインした状態で、Add to Cartボタンを押下し、画面右側のCHECK OUTボタンを押下してください。
f:id:tuti107:20160923111714p:plain
Billing Information画面では、領収書送付先の住所を入力します。
例えば 〒123-4567 北海道GHI市ABC町12-34 DEFビルディング1001号室 Tutti Labさん、電話番号090-1234-5678の場合は、以下の感じの入力になるかと思います。
f:id:tuti107:20160923112845p:plain
Shipping Informationでは、先程入力したBilling Addressを選択します。領収書と配送品の送り先が異なる場合のみ、こちらを別途入力します。
Shipping Methodでは配送方法を選択します。Standard (5 - 7 business days) とPriority (2 - 3 business days)が選べますが、ほとんど金額が変わらないため、Priorityのほうがよいのでは?と思います。なお、私の場合、Priorityを選択し、注文後4日で配送されました。
Payment Informationでは支払い方法が選べますが、Credit or Debit Cardを選択することになると思います。必要な情報を入力し、最後にOrder ReviewでPLACE ORDERを押せば完了です。
RealSense R200が無事配送されましたら、USB3.0でPCと接続の後、ダウンロード済みのドライバー、SDKをインストールしてください。

UnityでRealSense R200を利用する

SDKには、UnityでRealSense R200を利用するためのアセットが含まれています。
Unityを起動して適当なプロジェクトを生成の後、File→Build Settingsより、Target PlatformをWindowsに、Architectureをx86又はx86_64にそれぞれ設定します(Mac等でも使う方法があるようですが、割愛します)。
f:id:tuti107:20160923114911p:plain

次に、RealSense Unity Toolkitをインポートします。
ProjectのAssetsで右クリック→Import Package→Custom Packageより、C:\Program Files (x86)\Intel\RSSDK\framework\Unity\UnityToolkit.unitypackageをインポートしてください。
インポート後、まずはAssets/RSUnityToolkit/Prefabs/Imageをシーンにドラッグ&ドロップします。
この状態で実行すると、下記のように、画面中央に小さくカメラプレビューが表示されます。
f:id:tuti107:20160923115222p:plain

RealSense Unity Toolkitは、上記Imageプレファブのように、各種認識、3D撮像、3DスキャンやAR等の機能をプレファブとして提供しています。開発者はこれらプレファブを利用することで殆どコードを書くことなくRealSenseの機能を活用したアプリケーションを作れる、、はずなのですが、実際はそううまくはいきません。
試しに、Image以外のプレファブ、例えばDebug Viewerをシーンに放り込んで実行してみると、エラーが発生し、動作しません。

これは、R200には存在しない機能の利用を試みたために発生したエラーです。
f:id:tuti107:20160923115810p:plain

こちらのサイトに、R200とF200という2つのRealSenseの機能比較表があります。ご覧の通り、たくさんの機能に✕がついているのがわかります。
このF200(現在は後継機のSR300)とR200、用途が異なります。F200は例えばPCディスプレイの上部等に設置するインカメラであり、PC利用時に有用な機能(顔認識でのログイン、ハンドジェスチャー認識、~1.5m程度のデプス取得など)が想定されています。一方でR200は、スマホやタブレットのアウトカメラに設置されることを想定されており、建物の3Dスキャンや、~4mのデプス取得等に利用します。
F200側の機能も非常に魅力的ですので、これらを使ったアプリケーションを開発してみたい方は、F200の購入も検討いただければよいかと思います。

背景除去アプリケーションを作成してみる

Imageプレファブを利用して、背景除去機能を実装してみます。以下は、Imageプレファブのインスペクタです。Stream Outで出力される映像の種別を選択できます。
f:id:tuti107:20160923121228p:plain

  • STREAM_TYPE_COLORは、RGB映像を出力します
  • STREAM_TYPE_DEPTHは、デプスをグレースケールで出力します
  • STREAM_TYPE_IRは、赤外線センサー映像をグレースケールで出力します(F200のみ)
  • STREAM_TYPE_LEFTは、赤外線センサー(左側)映像をグレースケールで出力します(R200のみ)
  • STREAM_TYPE_RIGHTは、赤外線センサー(右側)映像をグレースケールで出力します(R200のみ)

今回は、1)STREAM_TYPE_DEPTHを指定したImageでデプスを生成、2)STREAM_TYPE_COLORを指定したImageは、1)のデプスを利用して一定距離の範囲外のピクセルを透明にする、として、背景を取り除きます。

まず、空オブジェクト「Images」を作成、その下に2つのImageプレファブを置きます。
f:id:tuti107:20160923121830p:plain
次に、Assets/RSUnityToolkit/Internal/Materials/UnlitMatを2つコピー(Ctrl+D)し、それぞれDepth, RGBという名前とします。
これらマテリアルDepth, RGBをそれぞれImageプレファブより生成したGameObject Depth, RGBにそれぞれ設定します。こうしておかないと、GameObject Depth, RGBはそれぞれ初期設定のマテリアルUnlitMatに映像を出力するため、Stream Outの設定にかかわらず両方ともRGB出力、もしくは両方ともデプス出力、となってしまいます。

次にデプスに応じて透過色設定をするためのシェーダーを作成します。
ProjectのAssets内の適当なフォルダにて、右クリック→Create→Shader→Image Effect Shaderを選択してください。ファイル名は、TransparentEffectとします。なお「シェーダーとは?」については、他の書籍・ブログ等をご参照いただければと思います。以下では、今回実装部のみ説明をしていきます。

Shader "Tuti/TransparentEffectShader"
{
  Properties
  {
    _MainTex ("Main Texture", 2D) = "white" {}
    [NoScaleOffset] _DepthTex("Depth Texture", 2D) = "white" {} // add
    _LowerLimitDepth("LowerLimitDepth", float) = 0.6 // add
    _UpperLimitDepth("UpperLimitDepth", float) = 1 // add
    _MainTexRegion("Main Texture region", Vector) = (0, 0, 1, 1) // add
    _DepthTexRegion("Depth Texture region", Vector) = (0, 0, 1, 1) // add
  }
  SubShader
  {
    // No culling or depth
    // Cull Off ZWrite Off ZTest Always // remove
    Tags{ "RenderType" = "Transparent" "Queue" = "Transparent" "ForceNoShadowCasting" = "True" } // add
    Cull Off // add
    ZWrite On // add
    Blend SrcAlpha OneMinusSrcAlpha // add

    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
			
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
      };

      struct v2f
      {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
      };

      v2f vert (appdata v)
      {
        v2f o;
        o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv = v.uv;
        return o;
      }
			
      sampler2D _MainTex;
      sampler2D _DepthTex; // add
      float _LowerLimitDepth; // add
      float _UpperLimitDepth; // add
      float4 _MainTexRegion; // add
      float4 _DepthTexRegion; // add

      // updated everything
      fixed4 frag(v2f i) : SV_Target
      {					
        fixed4 col = tex2D(_MainTex, i.uv);

        if (i.uv.x < _MainTexRegion.x || i.uv.x > _MainTexRegion.x + _MainTexRegion.z || i.uv.y < _MainTexRegion.y || i.uv.y > _MainTexRegion.y + _MainTexRegion.w) {
          col.a = 0;
        }
        else {
          float2 uv1 = float2((i.uv.x - _MainTexRegion.x)*1/_MainTexRegion.z, (i.uv.y - _MainTexRegion.y)*1/_MainTexRegion.w);
          float2 uv2 = float2(uv1.x*_DepthTexRegion.z + _DepthTexRegion.x, uv1.y*_DepthTexRegion.w + _DepthTexRegion.y);
          fixed4 dcol = tex2D(_DepthTex, uv2);
          if (dcol.r < _LowerLimitDepth || dcol.r > _UpperLimitDepth) {
            col.a = 0;
          }
        }


        return col;
      }
      ENDCG
    }
  }
}

上記で「add」「remove」及び「update」としている部分が、デフォルトのImage Effect Shaderとの差分です。
まずProperties部ですが、デプスのテスクチャー_DepthTex、画素透明化の下限(_LowerLimitDepth)と上限(_UpperLimitDepth)、RGB出力側の有効矩形(_MainTexRegion)、デプス側の有効矩形(_DepthTexRegion)です。具体的には、frag()関数部にて説明をしますが、このようにしておくことで、インスペクタ等から各値が指定可能となります。
f:id:tuti107:20160923125548p:plain

次にTags部等についてですが、透過表示を可能とするため、このような設定としています。
変数宣言部については、上記Properties部にて宣言したプロパティに対応する変数を宣言しています。なおテクスチャはsample2D型となります。
フラグメントシェーダー部(frag()関数)では、各座標(i.uv)について、

  • i.uvがRGB出力側の有効矩形(_MainTexRegion)の範囲外なら透明とする
  • RGB出力側の有効矩形(_MainTexRegion)の範囲内の座標i.uvを、デプス側の有効矩形内の座標uv2に変換
  • uv2のデプスが_LowerLimitDepth未満、又は_UpperLimitDepthより上なら、透明とする
  • それ以外の場合は、i.uvのテクスチャをそのまま表示する

との処理を行っています。
なぜこのような面倒なことをやる必要があるのか、ですが、私が現時点で調べた範囲では、残念ながらRGB映像とデプス映像の位置を一致させる方法がありません。このため、RGB映像とデプス映像の座標のずれを考慮の上で、RGB映像内のある座標aに対応するデプス映像a'(0:黒~1:白)が_LowerLimitDepth, _UpperLimitDepthの範囲内なら座標aのRGB映像の色を、範囲外なら透明を出力する、としています。
f:id:tuti107:20160923152757p:plain

また、このシェーダーにデプス映像のテクスチャを設定する目的で、以下のスクリプトも用意します。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(DrawImages))]
public class TransparentController : MonoBehaviour {

    public Material rgbMat;

    private Material depthMat;
    private Texture2D depthTex;

	// Use this for initialization
	void Start () {

        depthMat = GetComponent<Renderer>().material;
	}
	
	// Update is called once per frame
	void Update () {
	
        if (depthTex == null)
        {
            if (depthMat.mainTexture != null)
            {
                depthTex = (Texture2D)depthMat.mainTexture;
                rgbMat.SetTexture("_DepthTex", depthTex);
            }
        }
	}
}

このスクリプトは、DrawImages(Imageプレファブに設定されているコンポーネント)にて、RealSense R200から入力したデプス映像をTexture2Dとしてマテリアルに設定した際、このテクスチャを先シェーダー(TransparentEffect)の_DepthTexに設定します。
これにより、TransparentEffectは変数_DepthTexにてデプス映像を利用することが可能となります。

最後に、GameObject RGB, Depth, MainCameraを以下のとおり設定します。

f:id:tuti107:20160923135344p:plain
まずRGBですが、レイヤを新たに作成したShownとします(理由は後述します)。
画面いっぱいに映像を表示するため、Position/Rotation/Scaleは以上のとおりとします。
DrawImagesのStream OutはSTREAM_TYPE_COLORとします。RGB映像を出力するためです。
マテリアルは、上記で生成したマテリアルRGBを設定の上、Shaderは作成したTuti/TransparentEffectShaderとします。
透明化するデプス値の下限(LowerLimitDepth)、及び上限(UpperLimitDepth)はそれぞれ、0.4, 1としました。ここは任意調整してください。
RGB映像の有効矩形については、(X:0, Y:0, Width(表示はZ):0.94, Height(表示はW):1)としました。
デプス映像の有効矩形については、(X:0.0076, Y:0, Width(表示はZ):0.924, Height(表示はW):1)としました。
これら矩形範囲は、RGB映像・デプス映像のスクリーンショットから両映像の内容が一致する矩形となるよう調整してみましたが、もしかするとカメラ個体差等あるかもしれません。任意調整をお願いします。
今回は発見できませんでしたが、(必ず)RGB映像とデプス映像の位置を一致させるための設定があると信じています。見つけ次第またブログで書きたいと思います。

f:id:tuti107:20160923140254p:plain
Depthについては、こんな感じです。
レイヤは新たに作成したHiddenとします。デプス映像は画面に表示する必要がありませんので、カメラのカリング設定で非表示とするよう、レイヤを分けました。
デバッグ用にRGB映像の上に並べて配置したため、Position/Rotation/Scaleは以上としました。
DrawImagesのStream OutはSTREAM_TYPE_DEPTHとします。デプス映像を出力するためです。
上記で作成したスクリプトTransparentControllerをコンポーネントとして貼り付けます。RgbMatには、上記RGBマテリアルを設定してください。
マテリアルは、上記で生成したマテリアルDepthを設定します。

f:id:tuti107:20160923140653p:plain
最後にメインカメラは、こんな感じです。
RGB映像のみ画面表示とするため、Culling Maskは「Shown」とします。
また、ProjectionをOrthographicとします。Z方向の遠近感は不要とためです。

おわりに

今回は、(買いたてほやほやの)RealScene R200を使用して、背景除去にチャレンジしてみました。
とりあえず背景除去はできるのですが、ノイズがひどかったり、髪の毛のデプス情報がとれないため、髪の毛部分が透明になったりと、改善の余地がおおいです。今後、すこしずつ改善をしていこうと思います。
また、上記で説明のとおり、RGB映像とデプス映像の位置をあわせる設定がわかっておりません。ご存知の方おりましたら、コメント等いただけると幸いです。
最後に、USB3.0の延長ケーブルの利用には気をつけてください。付属のUSBケーブルは非常に短いため、2mの延長ケーブルを購入、接続を試みたのですが、カメラが認識されず。。ネットで調べてみると、既知の問題のようでした(延長するとカメラへの給電が落ち動かなくなる、ハブも同様)。セルフパワーのハブなら動く、との情報もありましたので、試してみようと思います。

【追記】
こちらのセルフパワー型HUBで試したところ、無事動作しました。

プリレンダリングの360度立体視CGとカメラ入力映像を重畳し、エンコードする

前回は、Maker Faire Tokyoに出展したVR撮影システムについて、360度立体視CGのプリレンダリングのやり方について書きました。
今回は、その360度立体視CGと、カメラ(魚眼レンズを付けたiPhone2台)入力画像を重畳し、mp4動画ファイルとしてエンコードする方法について書きたいと思います。

【第1回】VR撮影システムのハードウェア構成
【第2回】360度立体視CG映像を作る
【第3回】カメラ入力映像と重畳しエンコードする(今回)

左右眼用のカメラ入力映像をFisheye形式からEquirectangular形式に変換

基本的なやり方は、Theta二台から立体視用Equirectangular映像をつくるにて書いた内容と同じです。しかし、Theta Shader Packをそのまま利用することはできません。iPhoneからのカメラ入力映像は確かにFisheye形式なのですが、Thetaからの入力映像と異なり、左右が欠けてしまっています。
以下の図はMaker Faire Bay Area時のカメラ入力映像の取り込み・変換の仕組みです。左右眼用のThetaからの入力映像のうち、前方のFisheye映像をTheta Shader Packを利用してEquirectanguler形式に変換する、という方法をとっていました。
f:id:tuti107:20160810144242p:plain
今回のMaker Faire Tokyoでのカメラ入力映像の取り込み・変換の仕組みは以下のとおりです。
f:id:tuti107:20160810174152p:plain
iPhoneには魚眼レンズが取り付けられているため、カメラ入力映像はFisheyeのように歪んだ状態となっています。ただiPhone縦持ちで撮影する際、縦方向と比較して横方向の画角は狭くなってしまいます。iPhoneSE + Gizcamの場合、図の緑の破線(画角180度を示す)がカメラ入力映像からはみ出してしまいます。つまり、iPhoneからのカメラ入力映像の中にはThetaのように完全なFisheye形式の画像は含まれません。iPhoneからの入力画像を、上記Theta Shader Packの要領でEquirectanguler形式に変換すると図のように、ひょうたん(?)のような形になります。このため、大半はTheta Shader Packの処理のままで良いのですが、

  • はみ出ている部分は透明にする
  • Fisheye部の径を適切な値とする

2点の修正が必要となります。

フルHDサイズのオフスクリーンテクスチャを60fpsでエンコードする

前回のMaker Faire Bay Areaでは、ffmpegでリアルタイムエンコードするスクリプトを生成し、それを使用しました。しかし、その方法では12-15fpsが限界であり、せっかく搭載されている GeForce970の力を生かしたハイパフォーマンスなエンコードをできるようにしたいと思っていました。
そこで見つけたのが、GPU Video Encoderです。$100とかなり高価なアセットですが、複数のFull HDサイズのRenderテクスチャを同時に60fpsでエンコードできるスグレモノです。
本アセットの利用手順は次の通りです。
※本アセットは、Windows x86_64 DX11環境かつ、NVIDIAのHWエンコーダ(nvenc)が利用できる環境でのみ動作します。それ以外の環境では使用できませんのでご注意を(高価ですし)

  • Spicy Pixel Concurrency Kitをダウンロード・インポートする(本アセットと依存関係にあります)
  • 1920x1080のRender Textureを左右眼用作成する(それぞれの名前を"LeftTex", "RightTex"とする。Render Textureは、Projectにて右クリック→Create→Render Textureで作成、インスペクタのSizeにて1920x1080を設定できる)
  • それぞれのRender Textureに描画するCameraについて、Clear FlagsをSolid Color、Backgroundを緑(R=0, G=255, B=0)とする。のちほどffmpegでクロマキー処理をする際に「抜く色」として緑を使用するため
  • GameObjectを二つ生成する(ぞれぞれの名前を"Left", "Right"とする)。それぞれにMovie Record Cameraスクリプトを追加し、それぞれインスペクタより、Textureにはぞれぞれ上記で作成したRender Texture(LeftTex, RightTex)、Output File Path/Output File Titleにはそれぞれ生成するファイルのパスと名称を設定する

f:id:tuti107:20160810182039p:plain
あとは、エンコードを開始したいタイミングで、MovieRecordCamera#StartMovieRecord()を、終了したいタイミングでMovieRecordCamera#EndMovieRecord()を呼ぶだけです。スクリプトとしては以下のような感じで、上記生成したGameObject left, rightのMovieRecordCameraコンポーネントをインスペクタで下記leftCam, rightCamに設定しておき、開始・終了のタイミング(以下の例ではRキー押下)でそれぞれのメソッドを呼び出します。

using UnityEngine;
using System.Collections;
using GPUVideoEncoder;

public class CameraController : MonoBehaviour {

	public MovieRecordCamera leftCam;
	public MovieRecordCamera rightCam;

	private bool isRecording;

	// Use this for initialization
	void Start () {
		isRecording = false;
	}
	
	// Update is called once per frame
	void Update () {
	
		if (Input.GetKeyUp (KeyCode.R)) {
			isRecording = !isRecording;
			if (isRecording) {
				leftCam.StartMovieRecord ();
				rightCam.StartMovieRecord ();
			} else {
				leftCam.EndMovieRecord ();
				rightCam.EndMovieRecord ();
			}
		}
	}
}

すると、左右それぞれ用の.h264ファイルが、それぞれ指定したパス・ファイル名にて生成されます。このファイルをffmpegにてmp4に変換すれば、完成です。

ffmpeg.exe -i movieLeft.h264 -vcodec copy movieLeft.mp4

左目用・右目用の撮影映像を縦に並べる

次に上記生成した、左目用・右目用のmp4ファイルを縦に並べたmp4ファイルを生成します。以下のコマンド一発で変換可能です。

ffmpeg.exe -i movieLeft.mp4 -i movieRight.mp4 -vcodec nvenc -filter_complex vstack -b:v 8M movie.mp4

プリレンダリングの360度立体視CGとカメラ入力映像を重畳し、エンコードする

最後に、上記縦に並べた左右眼用の撮影映像と、前回生成したプリレンダリングの360度立体視CGを重畳します。これもffmpegのコマンド一発でオッケーです。

ffmpeg.exe -i movie.mp4 -f lavfi -t 10 -i \"movie = filename = a.mp4:loop = 0, setpts = N / (FRAME_RATE * TB)\" -filter_complex [0:0]colorkey=0x00ff00:0.5:.2[a1];[1:0][a1]overlay out.mp4
  • "t"オプションには作成する映像の時間(秒)を設定します
  • "movie=..."部分は重畳する360度立体視CG映像についての設定です。"loop = 0, setpts = N / (FRAME_RATE * TB)"とすることで、360度立体視CG映像の再生時間が-tで指定した時間より短い場合、これをループ再生します
  • "filter_complex"オプションには、クロマキーと映像重畳を設定します。colorkeyに緑色(0x00ff00)を指定することで緑色が透過します。なお":"で句切られた他の2つの数値は、クロマキーする範囲について指定しています。この2つの数字は試行錯誤で設定したものであり、数値とクロマキーの度合いの関係はいまいちわかっておりません。

これで、プリレンダリングの360度立体視CGとカメラ入力映像を重畳した動画ファイルが完成です!

終わりに

3回に渡り、Maker Faire Tokyoに出展した「スマホ2台を利用したVR映像作成システム」について書きました。記載量が莫大になってしまうため、実装詳細については触れませんでしたが、ご興味おありの方はその旨いろいろご質問いただけると嬉しいです。

Unityで360度立体視CG映像を作る

前回は、Maker Faire Tokyoに出展したVR映像撮影システムのハードウェア構成について書きました。
今回は、背景となる360度立体視CG映像をUnityで作成する方法について書きたいと思います。

【第1回】VR撮影システムのハードウェア構成
【第2回】360度立体視CG映像を作る(今回)
【第3回】カメラ入力映像と重畳しエンコードする

360 Panorama Capture

360 Panorama Captureを利用することで、Unityで構成したシーンから簡単に360度立体視映像を作成することができます(ただしWindowsのみ、DirectX 11以上、ハイスペックなGPU必要)。
本アセットの利用方法は非常にシンプルです。まず、本アセットをインポートし、Edit->Project Setting->PlayerでPCのPlayer Settingsを表示、Other Settings->OptimizationのAPI Compatibility Levelを".NET 2.0 subset"から".NET 2.0"に変更します。これで、Editorでの実行でキャプチャーが可能となります
次にシーン内に適当なGameObjectを配置し、Project/Capture Panorama/CapturePanorama.csをそのGameObjectへドラッグ&ドロップします。
するとインスペクタに以下のようにたくさんの設定項目が表示されますが、360度立体視映像の生成に必要なのは、下記の設定のみです。

  • Panorama Name: 生成するイメージファイルの接頭語を設定します。例えば"Img"を設定すると、本アセットは"Img****_000001.png", "Img****_000002.png"の名前の画像を出力します。
  • Image Format: 出力するイメージのフォーマットを指定します。JPG, PNG, BMPを指定できます
  • Capture Stereoscopic: これをチェックすることで立体視用の画像が出力されます
  • Panorama Width: 生成するイメージの幅を指定します。デフォルトで8096とかなり大きな数字となっているため、4096, 2048等、必要に応じて低い値を設定すると良いです
  • Save Image Path: イメージを出力するフォルダ名を指定します。大量のイメージが出力されることになるため、本ファイル出力用に新たにフォルダを生成する方が良いです
  • Capture Every Frame: これをチェックすることで、全フレームイメージとして出力されます(その代わりよほどハイスペックなPCでない限りパフォーマンスが大幅に低下します。私の環境だと60fps 10秒分のファイルを出力するために5分程度かかりました)
  • Frame Rate: フレームレートを設定します。私は60としましたが、用途に応じて30や15を設定すると良いです

その他の設定項目については、Project/Capture Panorama/READMEを確認ください。なお、古いOculus Pluginを使用している場合は、一部スクリプトの変更が必要のようです。

上記設定したら、あとは(Editor上で)実行し、pキーを押下します。これでキャプチャーが開始されます(上記の通り非常に動作が重くなります)。再度pキーを押下することでキャプチャーは終了します(しばらくpキーを押しっぱなしにしないと、動作が重すぎて当方環境ではキャプチャー終了となりませんでした)。

次に上記生成したイメージファイルをmp4等の動画に変換します。動画への変換は、ffmpegを利用して、コマンドラインから行います。なお、入力すべき内容は、Project/Panorama Capture/assemble.cmdとして用意されています。しかし、シンプルに以下の入力で問題ありません。

ffmpeg -f image2 -r 60 -i ****_%06d.png -r 60 -an -vcodec nvenc -b:v 8M -y -pix_fmt yuv420p -s 1920x2160 a.mp4

生成されるファイルの名前は、例えばImg_2016-08-06_10-34-06-377_000001.png, Img_2016-08-06_10-34-06-377_000002.pngのように、6桁の数字の前部分は全て同一となえいます。****には、この6桁の数字より前の部分を指定します。

Urban City 1.0

Maker Faire Tokyoでのデモでは、背景の360度立体視映像として、Urban City 1.0を利用しました。
本アセットのデモシーンProject/City/Scene.unity内の適当なビル間に長細いCubeを配置して橋にして、その橋の上にカメラを設置、まるで橋の上にいるような360度立体視映像としました(Maker Faire Tokyoでのデモで一番人気でした)。
f:id:tuti107:20160808182858p:plain
なお、本アセットには車等の3Dモデルも含まれております。Maker Faire Tokyoのデモでは、車道に車を走らせるアニメーションを追加し、橋の下を見ると、遠くに小さな車が走っている、それが「高さ」を感じさせて足がすくむ、という効果を狙いました。
youtu.be
※iPhone/Android PhoneのYoutube Appにて本ビデオを視聴することで、360度立体視視聴が可能です(こちらをクリック)。ただし立体視をするためにはGoogle Cardboardが必要です。


今回は、360度立体視映像をプリレンダリングする方法について書きました。次回は、このプリレンダリングした360度立体視映像と、前回説明したiPhone 2台による撮影映像を合成し、最終的なコンテンツを生成する方法について書こうと思います。