Tutti Lab

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

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台による撮影映像を合成し、最終的なコンテンツを生成する方法について書こうと思います。

Maker Faire Tokyoに出展

8/6-7、東京ビックサイトにて開催されたMaker Faire TokyoにVR撮影システムを出展しました。この準備に多忙だったため、前回のブログからかなり期間が開いてしまいましたが。。
出展内容は、Maker Faire Bay Areaに出展した内容と基本同じですが、以下の点で変更・パワーアップを加えました。

  1. カメラをTheta S→スマホ+アタッチメント型魚眼レンズに変更
  2. 撮影映像の解像度・フレームレートを1280x1440・15fps→1920x2160・60fpsに変更
  3. 背景CGをリアルタイムレンダリング→プリレンダリングに変更

です。
1)は、Theta Sは素晴らしい360度カメラなのですが、本システムの用途上360度は不要(前方140〜150度程度で十分)であること、また将来的にたくさんの人に本システムを利用してもらいたいと考えており、ユーザがお金をかけずにVR撮影システムを作れる(すでに所有のスマホ+数千円のアタッチメント型魚眼レンズで撮影出来る)ようにすべく、泣く泣くTheta Sの利用を諦めました。

2)については、前回Maker Faire Bay Area時は開発が間に合わず、HDMIではなくUSB 2.0でカメラ-PCを接続、このためカメラからの入力画像の1280x720・15fpsでした。このため、出来上がったVR映像はドットが目立ち・若干パラパラマンガ的な感じになっていましたが、今回はHDMIで1920x1080・60fpsで左右眼それぞれの映像をPCへ入力し、1920x2160(左右360度パノラマ映像を縦に積んだ形式)にて出力するようにしました。

3)については、前回はUnityシーン内に2つのカメラ(右目・左目用)を配置して、リアルタイムで左右眼用の360度パノラマを生成・Theta Sで撮影した左右眼映像とリアルタイム重畳をしましたが、私のPC(Oculus PCなのでかなりハイスペックのはずですが)では、12-15fpsでのエンコードが限界でした(私のffmpegの使い方がまずかった、ということもありますが)。またカメラ二つでは前方百数十度程度には立体感がつくものの、それ以外は立体感なし・遠近感が逆転(後ろ方向)となるため、360度立体視はできていませんでした。そこで今回はリアルタイムレンダリングは諦め、プリレンダリングの360度立体視映像を事前に生成しておき、左右カメラ映像をキャプチャー後、プリレンダリングのCG映像と合成する、という形式にしました。

今回から数度にわたり、本技術について紹介をしていきたいと思います。

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

システム構成

本システムのハードウェア構成は以下の通りです。
f:id:tuti107:20160808095133p:plain
まずはiPhoneSEを2台用意、それぞれの背面カメラに魚眼レンズ(Gizcam)を取り付けます。
f:id:tuti107:20160808100158j:plain
なお、うまく背面カメラの上にレンズを取り付けないと、映像がにじむ・暗くなる(何も見えない)ため、レンズ中央に背面カメラがくるよう、うまく位置調整をする必要があります。コツとしては、レンズの中央が完全に透けて向こう側が見えるため、背面カメラが中央にくるよう位置調整をすればオッケーです。知っている方にとっては当たり前の話なのでしょうが、私は最初これが分からず、撮影映像を見ながらカンで上下左右レンズを動かして調整していました。
次に、それぞれにiPhoneに、Lightning - Digital AVアダプタをつなげます。iPhoneの純正アクセサリは高いです。。
次に、Lightning - Digital AVアダプタとHDMIケーブルを接続、さらにHDMIケーブルのもう一端にFEBON 198を接続します。
f:id:tuti107:20160808101159j:plain
FEBON 198は、Theta SをPCにつなぐ際の必須アイテム(?)で、実例はこちら等で取り上げられております。
HDMI映像出力をUSB 3.0のUVCへ60fps・低レイテンシで変換してくれるスグレモノです。値段は¥19,800と結構高価ですが、HDMI->USB3.0変換器としては破格の安さです。
最後に、USBケーブルにてFEBON 198をPCのUSB3.0ポートにつなぎます。これでiPhone画面の表示されている内容が1920x1080 60fpsの高画質で、UVCとしてPC側で利用可能となります。
UVCなので、UnityではWebCamTextureにて簡単に本映像をテクスチャとして貼り付け・利用可能です(WebCamTextureについてはこちらをご参照ください)。

余談ですが、当初はiPhoneではなくNexus 5を使用する予定でした。上記Lightning - Digital AVアダプタの代わりにSlimport→HDMI変換ケーブルでNexus 5とつなごうと色々頑張ってみたのですが、FEBON 198が信号をうまく認識できず結局諦めました(FEBON 198の代理店である緋斐B級技研さんには親身に相談に乗っていただけました。ありがとうございました!)。

iPhone側カメラ出力アプリ

iPhone側では、全画面カメラ映像を出力するアプリを動かします。このアプリは非常にシンプルで、Canvasに全画面のPanelを、そしてPanelいっぱいの大きさのRawImageを配置し、こちらでご紹介したWebCamControllerを貼り付けだだけのシンプルなものです。プリインのカメラアプリだとメニュー等、色々邪魔なものが表示されてしまうため、このようなアプリを用意しました。
f:id:tuti107:20160808103219p:plain

次回は

これで、左右両目用のカメラ映像を二台のiPhoneからUVC形式でPCへ入力することが可能となりました。
次回は、本映像から「360度立体視パノラマ映像との合成」映像を生成する方法について書きたいと思います。