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

Tutti Lab

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

陰影表示の際のOnBecameVisibleとOnBecameInvisibleについて

現在開発中のアプリで、OnBecameVisible(), OnBecameInvisible()にて、オブジェクトがカメラに映り込んでいるかどうかを判定し・・、という処理を作っていたのですが、どうにも思い通りに動かない。
明らかにカメラからオブジェクトが外れているにもかかわらず、OnBecameInvisible()が呼ばれなかったり、逆にカメラ内にオブジェクトが映っていないのに、OnBecameVisible()が呼ばれたりと。
結論としては、オブジェクトの影がカメラ内にある場合は、表示状態となっている、というものでした。

例えば、以下のようにシーン配置し、Directional Lightを「もうじき沈むくらいの西日」にします。そうすると、シリンダの対面の壁にシリンダの影が映ります。
f:id:tuti107:20170409104740p:plain

シリンダには、以下のようなコードを貼り付けて、シリンダがカメラに映っているならVisible、映っていないならInvisibleが表示されるようにします。

using UnityEngine;

public class VisibleChecker : MonoBehaviour {

  public TextMesh text;

  private void OnBecameVisible()
  {
    text.text = "Visible";
  }

  private void OnBecameInvisible()
  {
    text.text = "Invisible";
  }
}

すると、シリンダがカメラ内なら、
f:id:tuti107:20170409104918p:plain

カメラ外なら、
f:id:tuti107:20170409104937p:plain

となり、期待通りの動きなのですが、シリンダの影がカメラ内に映っている場合も、
f:id:tuti107:20170409105010p:plain

となります。
影を付けながら、画面内にオブジェクトが映っている/映っていないときにOnBecameVisible(), OnBecameInvisible()が呼ばれるようにできないか頑張ってみましたが、結局対象オブジェクトは影がでないようにしました・・
f:id:tuti107:20170409105452p:plain

よい方法ご存知の方、ぜひご連絡ください。

KUDAN SDKを使って、Gear VRでどこでもドア(2)

前回は、Gear VRで「どこでもドアに近づく」体験を実現すべく、まずはマーカレスARが可能なKUDANをインストールしました。
今回は、KUDAN SDKを利用して、ポジショントラッキング機能を実装し、どこでもドアに近づいてみます。

www.youtube.com

Kudan Camera prefabをシーンに設置

KudanAR/Prefabsにある「Kudan Camera - Markerless Only」をドラッグ&ドロップし、シーン内に設置した上で、インスペクタより以下の通り設定します。

  • Camera→Depth = -2 (本カメラを表示対象にしないようにするため)
  • API Key = 前回KudanのWEBページより取得した開発用API Key

f:id:tuti107:20170325025958p:plain

ポジショントラッキング用のスクリプト作成

次にKudanPositionTrackerを作成します。
gist.github.com

そして、VR用カメラ(Main Camera)の位置に、空のGameObject(player)を生成し、VR用のカメラを、player配下にぶら下げます。
f:id:tuti107:20170325031447p:plain

また、playerに、上記で作成したKudanPositionTrackerを設置します。
f:id:tuti107:20170325031557p:plain
設置後、インスペクタより、以下の通り設定してください。

  • Kudan Tracker = シーンに設置した「Kudan Camera - Markerless Only」

動作解説

「Kudan Camera - Markerless Only」に設定されているコンポーネント「Kudan Tracker」が、KUDANのAR機能の制御を行います。
Kudan Trackerを利用してポジショントラッキングを行う手順は、以下のとおりです。

  1. FloorPlaceGetPose()を呼び出して、floorPosition, floorOrientationを取得(ドキュメントに詳細な記述はないのですが、恐らくAR空間におけるカメラの位置と向きをそれぞれ取得しているのだと思われます)
  2. floorPosition, floorOrientationを引数として、ArbiTrackStart()メソッドを呼び出す。これによりマーカレスARのトラッキングが開始される
  3. Update()内で、毎フレームArbiTrackGetPose()を呼び出し、トラック対象のポジションと向きを取得する
  4. トラック対象のポジションと向きを、カメラの位置に変換する
  5. playerのlocalPositionを、上記算出したカメラ位置とすることで、Gear VRの位置(向き)に応じた、VRカメラの位置を設定する

注意点など

  • ArbiTrackGetPose()で取得する、位置・向き情報は、前回サンプルアプリ実行時に表示された黒色カプセル型のオブジェクトの表示位置・向きとなります。RevRotateVector()は、この表示位置・向きから、カメラの位置を算出しています。
  • ArbiTrackは、ArbiTrackStart()呼び出し時にトラック対象を決定しそれをトラッキングし続けますが、トラッキング対象を見失う(頭の並行移動ではトラッキング対象を見失うことはあまりありませんが、頭を回転させるとあっさり見失います)と、ArbiTrackが終了してしまいます。このため、ポジショントラッキングをし続けるためには「ArbiTrackが終了時は再開する」処理の実装が必要となります
  • KudanTrackerのトラッキング開始直後に、ArbiTrackStart()を呼び出すと、例外が発生し、ArbiTrackを開始できません("KudanTrackerのトラッキング"と"マーカレスAR用のArbiTrack"は別物)。トラッキングはKudan TrackerのStart On Enableをチェックしていると、本コンポーネントの初期化のタイミングで開始されます(もしくはStartTracking()メソッドをスクリプトから呼び出すことで、トラッキングを開始できる)。そこで、直後ではなく少し時間をおいてから(上記コードでは5秒)、ArbiTrackStart()を呼び出すことで、本問題を回避しています。
  • これは、マーカレスARだから仕方がないことかと思いますが、ArbiTrackでは正確な距離の測定ができません(トラック対象の寸法情報がないのだから、当然ですね)。このため、ArbiTrackGetPose()で取得できる位置情報は所定の単位(メートルとか)ではなく、ArbiTrackのトラッキング対象の位置等により毎回異なります。上記コードでは、取得した位置情報にscaleFactor(0.0004)をかけて、Unityの座標に変換していますが、この0.0004は試行錯誤で見つけた「私の部屋ではそれっぽく見える値」であり、0.0004をかけることで、Unityの座標に正しく変換できる、というものではありません。このため、scaleFactor = 0.0004では、ArbiTrackトラッキング対象とGear VRの距離によっては、頭の動きに対して、動きすぎたり、逆に全然うごかなかったり、ということが起きます。このあたりを正確にしたいなら、試行錯誤scaleFactorの値を調整するか、マーカARにするしかなさそうですね。

おわりに

今回は、Gear VRで「どこでもドアに近づく」体験を実現すべく、KUDANを利用してポジショントラッキング機能を実装してみました。
Vuforiaで実装した際と比較し、マーカが不要であり、かつマーカがなくても結構高精度なトラッキングが実現されているため、Vuforiaよりも使い勝手のよいポジショントラッキングの実装が可能かと思います。
一方、KUDAN ARはポジショントラッキング用に作られたものでないため、トラッキング対象が視界から消えるとポジショントラッキングができなくなる等、ちゃんとしたポジショントラッキングの実現のためには、色々工夫が必要となりそうです。ただ、基本座って・前を向いて使用するアプリ、であれば、今回実装程度のものでも充分かと思います。

KUDAN SDKを使って、Gear VRでどこでもドア(1)

前回は、Unity Native VRでどこでもドア(ポータル)を実装しました。前回の予想どおり、前回開発したものをそのままGear VR向けにビルドしたところ、問題なく動作しました。
ただし、Gear VRにはポジショントラッキングがないため、どこでもドアに近づくことができず、どこでもドアの楽しさが半減してしまいます。
そこで今回は、前にVuforiaで実装したポジショントラッキングを応用し、Gear VRでもどこでもドアに近づく体験ができるようにしてみます。
tuti107.hatenablog.com

KUDAN

今回は、Vuforiaではなく、日本が誇るモバイルARエンジンである「KUDAN」を利用します。KUDANは、

  • マーカレスARが可能!つまり、マーカをプリントして貼り付ける等の面倒な手続きなしで、ARが可能!
  • 収入が100万ポンド(=1.4億円)以下の個人開発者は、なんとアプリ配布用のライセンスが無料!!(Vuforiaは有料)

ということで、今の私の中ではモバイルARは、KUDAN一択です。

KUDAN SDK Unity Pluginのダウンロード

ダウンロードページより、Free-Downloadをクリックして、Unity Pllugin "KudanARUnity.unitypackage"をダウンロードします。
購入画面が表示されますが、0ポンドなのでご安心を。メールアドレス等の情報を入力し、画面一番下のDownloadボタンを押下すれば、ダウンロードがはじまります。

KUDAN SDK Unity Pluginのインストール

ダウンロードしたKudanARUnity.unitypackageを、Assets->Import Package->Custom Packageよりインポートします。

サンプルを実行してみる

まずは、SDKに同梱のサンプルを実行してみます。

  • Assets/KudanAR/Samples/AngelScene をダブルクリックして、サンプルシーンをロードします。

f:id:tuti107:20170320130414p:plain

  • Android向けにビルドをするための設定を行います。File->Build Settings..より、Build Settingsを表示し、PlatformをAndroidへスイッチします。次に、Player Settingsを押下し、インスペクタのOther Settingsー>Identifierより、以下の通りに設定します。
    • Bundle Identifier = "eu.kudan.ar"
    • Minimum API Level = 15

f:id:tuti107:20170320131705p:plain

  • 次に、シーン内のAngel Bundle/Kudan Cameraを選択し、インスペクタのKudan Trackerコンポーネントの一番下にある「Get Editor API Key」を押下します。すると以下のページが表示されるので、"click here"を書かれている箇所をクリックします。

f:id:tuti107:20170320131922p:plain
ここで、開発用のバンドルID"eu.kudan.ar"に対応するLicense Keyが取得できます。このLicense Keyをコピーし、インスペクタのKudan Trackerコンポーネント内のAPI Keyにペーストします。なお、Editor API Keyは、上記MY ACCOUNTのページより、ログインすることで取得可能です。
f:id:tuti107:20170320132242p:plain
f:id:tuti107:20170320132346p:plain

  • 次に実行してみます。適当なAndroid端末をPCと接続し、File->Build & Runを選択します。画面右下の"Place Markerless Object"ボタンを押下すると、3Dオブジェクトが設置されます。このオブジェクトは、その設置位置に貼り付けたように、スマホを傾けたり・近づけたりすると、その通りに表示がされます。

f:id:tuti107:20170320133542p:plain

次回は、KUDANでポジショントラッキングを実装

今回は、Gear VRで「どこでもドアに近づく」体験を実現すべく、まずはマーカレスARが可能なKUDANをインストールしました。
次回は、KUDANを利用したポジショントラッキングの実装に挑戦します。

どこでもドア(ポータル)をUnity NativeのVRで実現してみる

f:id:tuti107:20170319212451p:plain

VRでどこでもドア体験、ぜひやってみたい!と思うのですが、関東在住ではないので、気軽に行けそうにありません。
www.doraeiga-vr.com

そこで、Oculus Rift向けに作ってみようと思ったのですが、RenderTextureをごにょごにょして・・と色々考えたものの、どうやったらよいのかわからず、ネットで検索したところ以下の記事を発見しました。ただし、Vive用のコードで、あちこちVive専用のメソッド等が使われています。そこで、これをUnityのNative VR向けに移植してみました。
qiita.com

RenderTextureの生成

上記のサイトでは

_leftEyeRenderTexture = new RenderTexture((int)SteamVR.instance.sceneWidth, (int)SteamVR.instance.sceneHeight, 24);

として、RenderTextureを作成していますが、

_leftEyeRenderTexture = new RenderTexture((int)VRSettings.eyeTextureWidth, (int)VRSettings.eyeTextureHeight, 24);

としました。

ポータル用マテリアルへRenderTextureを設定する処理

まず、左右カメラ視差をつくるためのオフセット設定は、

var v = _cameraForPortal.transform.localPosition;
_cameraForPortal.transform.localPosition = v + new Vector3(-VrEye.stereoSeparation/2f, 0f, 0f);

としました(左目の場合)。
また、

Valve.VR.HmdMatrix44_t leftMatrix = SteamVR.instance.hmd.GetProjectionMatrix(Valve.VR.EVREye.Eye_Left, VrEye.nearClipPlane, VrEye.farClipPlane, Valve.VR.EGraphicsAPIConvention.API_DirectX);
_cameraForPortal.projectionMatrix = HMDMatrix4x4ToMatrix4x4(leftMatrix);

として、プロジェクションマトリクスの設定を行っている箇所は、

_cameraForPortal.projectionMatrix = VrEye.GetStereoProjectionMatrix(Camera.StereoscopicEye.Left);

としました(左目の場合)。

ポータル用マテリアルのシェーダー処理

左右どちらのレンダリングか、により使用するテクスチャを切り替えている以下の処理

if (unity_CameraProjection[0][2] < 0)
{
    col = tex2D(_LeftEyeTexture, sUV);
}
else {
    col = tex2D(_RightEyeTexture, sUV);
}

は、

if (unity_StereoEyeIndex == 0)

としました。
その他は、上記サイトにて記載されているソース等の通りです。実行すると、以下のような感じに動きます。
f:id:tuti107:20170319212415g:plain

UnityのNativeのVR機能のみでの実装のため、Oculus Riftだけでなく、Gear VR、Google Cardboard、Daydreamでの動くと思われます。試していませんが。。

シーン内GameObjectの初期化の順序などについて

シーン内GameObjectの初期化の順序について、私はごく最近まで「シーン内のGameObjectは入れ子構造の親から順に初期化、子はシーンの上にあるものから順に初期化」される、と勘違いしていました。

+GameObject1
|    |
|    +---GameObject2
|    +---GameObject3

なら、GameObject1, GameObject2, GameObject3の順で・・
ただ実際は「初期化順は不定」なのですね。今開発中のアプリで、起動時は期待した順にAwake()が呼び出されるのに、SceneManager.LoadScene()するとAwake()の呼び出し順序が入れ替わってしまう、という問題が発生し、この事実を知りました。

そうすると、GameObjectの初期化時に他のGameObjectを参照する必要がある場合、どうするのだとうと。例えばGameObject2の初期化時に、GameObject1のなんらかを参照する場合、GameObject1が初期化されていないと期待の動作となりません。
これについては、

  • 自身の初期化はAwake()で
  • 他のGameObjectの参照を伴う初期化処理はStart()で

行う、と徹底すればよいようですね。

初歩的なことなのですが、いままでずっと勘違いをしていました。これまで開発したアプリは「たまたま動いていた」ようで・・

VR広告SDK「Immersv」を組み込んでみました

モバイルVRアプリ開発において「マネタイズ」は大きな課題です。一般的にモバイル向けアプリ(特に個人開発者)のマネタイズ手段は広告であり、各種提供されている広告SDKをアプリに組み込むことで、アプリ内で広告を表示し、実績に応じて広告収入を得る、というものです。
しかし、これらモバイル向けアプリの広告SDKは、VR空間上に貼り付けて使用するような使用形態が想定されていないため、モバイルVRアプリにそのまま適用することはできません。
今回は、モバイルVRに特化された広告SDKである「Immersv」を、開発中のアプリに組み込み、動作確認をしてみました。
f:id:tuti107:20170307200728p:plain

SDKの請求

まずはこちら(注:2017年3月7日現在、本サイトに接続できなくなっております。一時的なものかと思いますが。。)にアクセスし、SDKを請求します。フォームに必要情報を入力・送信すると、数日で先方から応答のメールがあります。
その後、私のプロジェクト(今、Gear VR、Daydream向けにモバイルVRアプリを開発しています)の説明をし、何度かのインタラクションを経て、SDKを入手することができました。なお、Immersvは米国の会社であり、全編英語を覚悟したのですが、対応いただいた日本法人の担当の方は日本語が堪能で、問題なくコミュニケーションをとることができました。

SDKのダウンロード、組み込み

SDKは、Gear VR、Daydream/Cardbord用に、これらOculus SDK/Google VR SDKやUnityのバージョン毎に用意されており、自分の開発環境に合わせて取得・インポートします。
私は、Daydream向けビルドを考慮し、Unity Daydream Preview 5.4.2f2-GVR12で、かつ(まずは)Gear VR向けに開発をしているので、本条件に相当する「ImmersvSDK-1.31-GearVR-1.0.3.unitypackage」を使用しました。
f:id:tuti107:20170223073524p:plain

実装

Immersv SDKは非常にシンプルです。基本、SDKに同梱のPDFドキュメントに記載されているサンプルコードをそのまま使えば、ほぼ問題ありません。一応、処理の流れを記しておくと、

  • ImmersvSDK.Init(APPLICATION_ID)を呼び出す。APPLICATION_IDはSDK利用許可時に通知される。
  • ImmersvSDK.OnInitSucces()コールバック(初期化成功)にて、ImmersvSDK.Ads.LoadAd(PLACEMENT_ID)を呼び出し、広告を読み込む。なお、PLACEMENT_IDはSDK利用許可時に通知される。
  • ImmersvSDK.Ads.OnAdReady()コールバック(広告準備完了)を受け取った後、なんらかのユーザ操作(「広告を見る」ボタン押下など)を受け付けた後、ImmersvSDK.Ads.StartAd()を呼び出して、広告を再生する
  • ImmersvSDK.Ads.OnAdComplete(result)コールバック(広告再生完了)にて、ユーザが広告を最後まで見る等の条件を満たした場合(result.BillableCriteriaMetがtrue)、アプリ内仮想通貨を与える等の、ユーザに対する報酬処理を行う

という形になります。

なお、以下の公式ビデオ(英語)にて、SDK請求から実装までの詳細が公開されています。
www.youtube.com

ちなみに私の開発中アプリにSDKを組み込んだものは、このような感じです。ビデオ画質が悪いため細かな部分、特に広告閲覧後の報酬の様子がわかりづらいですが、一通り動作しています。
www.youtube.com
チケット券売機の右中央辺り(ビデオでは解像度が低くてよく見えませんが、いわゆる「広告を見てコインをゲット」を表示しています)をGAZEすることで、Immersvの広告が始まります。
広告は、2D広告を映画館風に再生するものや、360度動画等様々であり、充分楽しめる(?)内容です。
広告が終わると、コインが手に入ります(こちらも見えづらい・・)

GearVRでは、私がいつも利用しているキャプチャーアプリLollipop screen recorderが動作せず、MirrorOP for Galaxy+OBSを使用しました。ただこれだと録音ができず、解像度が低く、微妙な感じです。。

getterのケアレスミスでUnityクラッシュ

以下のコードですが、

private int _curNum;
public int curNum {
  get {
    return curNum;
  }
  set {
    _curNum = value;
  }
}

正しくは、「return _curNum;」としなければなりません。
なお、上記の間違いコードを実行すると、Unityがクラッシュします。
f:id:tuti107:20170306192256p:plain
スタックがえらいことになっています。循環参照をしまくったためでしょうね。
この不具合の検証をさらに困難にさせるのが、このgetterにアクセスしなくても、例えば本ゲッターを持つクラスの別の変数にアクセスしても落ちます(ただ、落ちない変数もある・・謎・・)。まさかgetterのミスとは気づかず、小一時間解析に要してしまいました。