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

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

HoloLens のアプリ開発はじめました

HoloLens Unity C#

はじめに

先日、TMCN さんから、なんと HoloLens をお貸し頂きました。

HoloLens は Microsoft によって開発されているシースルー型の HMD で、PC やスマホを必要とせずスタンドアロンで動作し、これを被ることで実際の目に見える空間と 3D のオブジェクトやブラウザ等のウィンドウが同時に存在しているかのような世界を体験することが出来ます。トラッキング性能は異常と言っても過言ではないほどで、大抵の環境では顔をブンブン振り回しても、ピンされたオブジェクトはその場にブレることなく留まります。配置したオブジェクトは、その場から離れても保存されているので、家の各所に好きなオブジェクトを配置する、といったことも出来、色々な可能性を感じます。

本エントリでは、ドキュメントなどを読んで理解した内容を所感と共に HoloLens 開発の勘所をつかむための Tips として残しておきたいと思います。既に HoloLens で出来ること、チュートリアルなどは分かりやすくまとめて下さっているエントリが多くあるのであまり深くは触れない予定です。

リンク

最初の一歩

www.naturalsoftware.jp www.naturalsoftware.jp

公式ドキュメント

ドキュメント翻訳

littlewing.hatenablog.com

チュートリアル

公式でチュートリアルが公開されています。動画も用意されていて、字幕を日本語にすれば英語を聞けない場合でもスムーズに理解できると思います。

Holograms 100: Getting started with Unity

Unity プロジェクトを作成、必要な設定を行い、オブジェクトを配置し、ビルド用のソリューションを生成しビルド、そして Wi-Fi / USB 経由での実機インストールまたは Emulator で起動する方法が紹介されています。

Holograms 101: Introduction with Device

まず「折り紙」プロジェクトという公式が提供してくれているアセットやスクリプトを適切に配置していくことで学べる形になっています。次のような項目が動画と説明とともに用意されています。

  1. モデルを表示
  2. 視線(頭の向き)カーソルの表示
  3. 選択したものを落とす(ジェスチャ選択で RigidBody 追加)
  4. 音声入力(キーワード認識のみ)
  5. 3 次元音響(BGM と衝突音)
  6. 3 次元マッピング(部屋の形状のコライダ化)
  7. 穴の先に別の世界を出す

スクリプトの細かい解説などはされていませんが、Unity の基本的なコードを使ったものになっているので、理解は難しくないと思います。

なお 101 のチュートリアルには実機を保有していない場合のエミュレータ版(101E)もあります。

その他

101 の各項目を深掘りしたり説明していないものの解説が個別に用意されています。

  • 210: Gaze
  • 211: Gesture
  • 212: Voice
  • 220: Spacial Sound
  • 230: Spatial mapping
  • 240: Sharing Holograms

これらについては別途エントリを書こうと思います。

HoloToolkit-Unity

本エントリで主に解説する Unity 標準の API でも十分簡単に HoloLens 向けのアプリが作成できますが、MS 公式がリリースしている HoloLens のための便利なコンポーネント集の HoloToolkit-Unity を利用すると更に簡単に開発をすすめることが出来ます。

GitHub - Microsoft/HoloToolkit-Unity: This is effectively part of the existing HoloToolkit, but is the repo that will contain all Unity specific components.

API をラップして扱いやすいようにした便利なコンポーネントや、それをまとめて取り扱うゲームオブジェクト(カーソルなど)が含まれています。加えて、HoloLens の開発をより便利にするエディタ拡張(HoloLens の設定画面、ソリューションの管理など)も含まれています。

f:id:hecomi:20170211215350p:plain

開発スタイル

HoloLens には Holographic Remoting Player というアプリが用意されており、本体側でこのアプリを立ち上げておき Unity の Holographic Emulation ウィンドウから IP を指定して接続すると、インストールなしに開発中のアプリを本体側で即時実行でき、またシーンビューなどでデバッグも可能になります。

f:id:hecomi:20170211215345p:plain

f:id:hecomi:20170211221410p:plain

ただし、レイテンシは私の環境ではおおよそ 30 ms("Enable Diagnostics" コマンドで調査)あり、デバッグする分には問題ないもののオブジェクトや空間メッシュは結構ガタガタします。なので、レイテンシが問題にならない時はこれでデバッグを行い、レイテンシを含めてアプリの動作を確かめたい時は本体にデプロイする、という形式が良いと思います。

ただ、本体にデプロイする場合も、ビルドの設定を「Remoting Machine」にすることで、WiFi 経由で本体側にインストールすることが可能で、所要時間もチュートリアルのアプリくらいなら 30 秒 - 1 分程度なので、そんなにストレスを感じることなく開発可能です。デバッグに関しても Visual Studio のデバッガがそのまま使えるので、コンソール出力もビルド後に VS の Output から確認できたり、Unity のプロファイラで IP を指定してアタッチすれば GPU のパフォーマンスを含むプロファイラを見ることが出来ます。

コーディングやコンテンツ制作に関しては、基本的に Unity の知識があれば開発可能なように作られています。視線検出も標準の Raycast、音声認識も標準のものです。基本的なインタラクションを作るのに困ることはあまりないと思います。シースルーのためオブジェクトも少なくて済み、かつ酔いもほぼ感じません。見た目も黒が完全に透明になるので、穴空き表現も簡単に作れます(穴空きの黒プレーン / ボックス作れば良い)。パフォーマンスは気にする必要がありますが、フレームレートが落ちると即気持ち悪さに繋がる VR ほどクリティカルではないように思われます。

以下、各解説のページを読みながら思ったことなどをまとめていきます。

ホログラムの安定性

ドキュメント:Hologram stability

公式では、バーチャルなオブジェクトのことをホログラムと呼んでいます(本来の意味とは違うとのツッコミがありそうですが)。基本的にホログラムはぴったりと現実の環境の位置関係に一致しますが、次のようなときに安定性が崩れる場合があります。

  • 2 つの似た部屋がある時、誤認識すると一方の部屋の配置になる
  • ジッタ(高周波ノイズ)がある場合はセンサのチューニングが必要(Sensor tuning
  • 60 fps を下回るとジャダー(不均一な動きと二重像)が起きる
    • ビュー・投影変換の予測誤差が大きくなる
  • ワールドアンカー(後述)がないオブジェクトはドリフトする可能性がある
  • 周囲の環境構築中にオブジェクトがジャンプするときがある
  • ホログラムが揺れる時は下の面が安定していないかキャリブが必要
  • 頭を早く動かすと色分解が起きやすい
    • 240 Hz で RGBG を切り替える 60 Hz の Color Sequential Display なので

ホログラムとの距離も意識することが必要です。最も快適な距離が 2 m で、1 m 以下の距離にある場合は輻輳調節矛盾を起こしやすいとのことでした。MS のベストプラクティスとしては次のようなものです。

  • 1.25 ~ 5 m になるよう物を置く
  • 1.0 ~ 0.85 m では何らかの手法でフェードアウト
  • 0.85 m 以下はニアクリップで表示しない
  • ユーザの注意をひくものは 2 m の位置に置く

フェードアウトの表現は色々工夫できそうです。Holograms アプリでは近づくと黒くなってからカリングされる形で、Asobo Studio の Fragments ではポリゴンが分解されているような表現になっていました。

HoloLens はハードウェア支援の安定化を図りますが、この安定化を良好なものにするため、安定化平面というものが選択されています。この安定化平面は手動で設定することも出来ます。

using UnityEngine.VR.WSA;

var normal = -Camera.main.transform.forward;
var position = focusedObject.transform.position;
HolographicSettings.SetFocusPointForFrame(position、normal);

安定化平面はユーザの正面にあるのことが必要なので、カメラの向きと常にチェックするようにしましょう。更に詳細についてはドキュメントをご参照ください。

最後に、色分解についてですが、白いカーソルのようなものを頭の動きに追従するようにして動かすと、頭を早く動かした場合に先述したようディスプレイの特性により RGBG の 4 つのカーソルに分解されます。これを避けるためには以下のようなことが挙げられています:

  • オブジェクトの動きをスムージングして遅らせる(5 deg / sec 以下)
  • カーソルオブジェクトの代わりにライト表現にする
  • 安定化面を注視しているものに合わせて調整
  • オブジェクトを単色(赤、青、緑いずれか)にする
  • ブラーのかかったものにする

レンダリング

ドキュメント:Rendering

  • 黒画は透明になり、白は完全な不透明ではなく少し透明(本体輝度設定によっても変化)
  • 奥行きの手がかりとして追加の接地効果があると良い、グローを足して影を落とすなど、立体音響による手助けも重要
  • ユーザの頭部の位置・姿勢の先読みを行いビュー変換および投影変換を行っておりこれを絶対に使うこと(不快感を招くため)
  • この変換のために常に待ち時間を測定して調整している、レンダリングパイプラインが短ければそれだけホログラム(バーチャルなオブジェクト)は安定
  • 解像度は 360p ~ 720p でアプリケーションに応じて調整すること
    • ただし 540p 以下は画素が荒いためテストのみの使用が推奨(ボトルネック解析用)
  • Mixed Reality Capture(写真撮影やストリーミング等)のためにビューポートは可変になるため固定パラメタを参照しないこと
    • なお、MRC 実行中はアプリケーションはシステムによって 30 fps になります

カメラ

ドキュメント: Camera in Unity

  • VR 開発同様、Virtual Reality Supported をオンにすることで、自動でステレオでレンダリングされ、頭の動きに追従するようになる
  • 黒が透明になるため、カメラの背景は Solid Color かつ (0, 0, 0, 0) にしておく
  • カメラの初期位置がスタート地点になる、自分からの距離が分かりやすいように (0, 0, 0) の位置をデフォルトにすることが推奨
  • Near Clip0.85 (m) が推奨

パフォーマンス

ドキュメント:Performance recommendations for Unity / Performance recommendations

  • HoloLens の性能はそこまで高くはない
  • 60 fps を担保、メモリの消費も 900 MB 以下に抑える
  • パフォーマンスが達成されたら電力消費も意識すること
    • Device Portal の Performance グラフからチェック可能
  • Quality の設定で Fastest をデフォルトにしておく
  • なるべく軽いシェーダを使う。特に 16 bit の fixed を使うことで 32 bit の float に比べ 2 倍の高速化が期待できる。また、なるべく CPU や頂点シェーダで計算したり、mad を使うなど通常の高速化も試みること。
  • 100 個以内のゲームオブジェクトに抑える、重い Update() を避ける、重いグラフィクスの設定を使わない(シャドウやライト)、重いグラフィクスのコンポーネントを使わない(リフレクションプローブなど)
  • GC を避ける(LINQラムダ式を使わない、Boxing に気をつける、struct を使える場面では使う、foreach を避ける、生配列を使う等)
  • .NET ネイティブによるコンパイルやシーンの非同期ロードでスタートアップ時の時間の短縮を図る
  • その他、一般的に Unity の高速化で言われていることを実施すること(大量の Update() を避けてマネージャで管理、コンポーネントのキャッシュ、FixedUpdate() を使わない等)

パフォーマンスを担保するには結構意識しないといけないようです。モバイル開発の心づもりに近い気がします。

ラッキングロス

ドキュメント:Recommended settings for Unity / Tracking loss in Unity

f:id:hecomi:20170209001655p:plain

視線入力

ドキュメント:Gaze in Unity

  • 視線と言っても目の向きではなく頭の向きを取っているだけ
  • 特別な API があるわけではなく普通のアプリ同様 Physics.Raycast()Camera.main から行う

サンプルのカーソルはメッシュの法線の向きに貼り付くようになっているのですが、部屋のメッシュも取れているため、UI だけでなく部屋の壁にも向きがあってそれだけで結構面白いです。

ジェスチャ入力

ドキュメント:Gestures in Unity

  • 低レベル API(位置や速度)と高レベル API(タップ、ダブルタップ、ホールドなど)がある
  • 高レベル APIGestureRecognizer インスタンスを生成し、そこに認識したいイベントおよびコールバックを登録すると降ってくる
  • 低レベル APIInteractionManager にコールバックを登録すると各イベントごとに InteractionSourceState が降ってくる
    • UnityEngine.VR.WSA.Input (Windows Store Apps?)を使う

高レベル API

GestureRecognizer recognizer = new GestureRecognizer();
recognizer.SetRecognizableGestures(GestureSettings.Tap | GestureSettings.Hold);
recognizer.TappedEvent += OnTapped;
recognizer.HoldEvent += OnHolded;
recognizer.StartCapturingGestures();
...
recognizer.StopCapturingGestures();
recognizer.TappedEvent -= OnTapped;
recognizer.HoldEvent -= OnHolded;

低レベル API

using UnityEngine;
using UnityEngine.VR.WSA.Input;

public class InteractionManagerTest : MonoBehaviour
{
    void Start()
    {
        InteractionManager.SourceDetected += OnSourceDetected;
        InteractionManager.SourceUpdated += OnSourceUpdated;
        InteractionManager.SourceLost += OnSourceLost;
        InteractionManager.SourcePressed += OnSourcePressed;
        InteractionManager.SourceReleased += OnSourceReleased;
    }

    void OnDestroy()
    {
        InteractionManager.SourceDetected -= OnSourceDetected;
        InteractionManager.SourceUpdated -= OnSourceUpdated;
        InteractionManager.SourceLost -= OnSourceLost;
        InteractionManager.SourcePressed -= OnSourcePressed;
        InteractionManager.SourceReleased -= OnSourceReleased;
    }

    void OnSourceDetected(InteractionSourceState state)
    {
        Debug.Log("detected: " + state.source.id);
    }

    void OnSourceLost(InteractionSourceState state)
    {
        Debug.Log("lost: " + state.source.id);
    }

    void OnSourceUpdated(InteractionSourceState state)
    {
        Debug.Log("updated: " + state.source.id);
    }

    void OnSourcePressed(InteractionSourceState state)
    {
        Debug.Log("pressed: " + state.source.id);
    }

    void OnSourceReleased(InteractionSourceState state)
    {
        Debug.Log("released: " + state.source.id);
    }
}

新しく手を検知するたびに id がインクリメントされていきます。

ジェスチャで出来ることは限られている分、設計がとてもシンプルになっていますが、更にシンプルに使えるよう Holotoolkit-Unity では IClickHandler などのインターフェースが提供されています。

音声認識

ドキュメント:Voice input in Unity

  • 音声認識には 3 つの方法が提供されている
    • キーワード認識(KeywordRecognizer
    • 文法認識(GrammarRecognizer
    • ディクテーション(DictationRecognizer
  • HoloToolkit > Configure > Apply HoloLens Capability Settings から Microphone を有効にする必要がある

音声認識の言語

現状は英語(米国)のみに最適化がされているとのことです。

Holographic Remoting を使って開発しているときは、PC 側の言語が走るようで、英語っぽく喋っていたら中々認識しなくてアレッとなりました。 はじめに英語の音声認識パックをダウンロードして規定に設定しておくのをおすすめします。また、PC 側の録音デバイスが使用されるので注意しましょう(実機デプロイしたら関係ないです)。

キーワード認識

キーワード認識は KeywordRecognizer インスタンスを生成し、そこにキーワードを登録しておくことで、認識されたタイミングでコールバックが呼ばれます。

using UnityEngine;
using UnityEngine.Windows.Speech;

public class VoiceRecognitionTest : MonoBehaviour 
{
    KeywordRecognizer recognizer;
    string[] keywords = new string[] {
        "activate", "open", "close", "select"
    };

    void Start()
    {
        recognizer = new KeywordRecognizer(keywords);
        recognizer.OnPhraseRecognized += OnPhraseRecognized;
        recognizer.Start();
    }

    void OnDestroy()
    {
        recognizer.OnPhraseRecognized -= OnPhraseRecognized;
        recognizer.Stop();
        recognizer.Dispose();
    }

    void OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        Debug.Log(args.text);
    }
}

文法認識

文法認識は文法を記述する SRGSXML)を StreamingAssets 下に置き、それを GrammarRecognizer インスタンスに与えることでコールバックが呼ばれる形です。

例えばサンプルの XML を与えてみます。

<grammar version="1.0" xml:lang="en-US" root="playCommands"
 xmlns="http://www.w3.org/2001/06/grammar">

  <rule id="playCommands">
    <ruleref uri="#playAction" />
    <item> the </item>
    <ruleref uri="#fileWords" />
  </rule>

  <rule id="playAction">
    <one-of>
      <item> play </item>
      <item> start </item>
      <item> begin </item>
    </one-of>
  </rule>

  <rule id="fileWords">
    <one-of>
      <item> song </item>
      <item> tune </item>
      <item> track </item>
      <item> item </item>
    </one-of>
  </rule>

</grammar>

コードは先程とほぼ同じです。

using UnityEngine;
using UnityEngine.Windows.Speech;

public class GrammarRecognitionTest : MonoBehaviour 
{
    [SerializeField]
    string srgsFilePath = "/SRGS/grammar.xml";

    GrammarRecognizer recognizer;

    void Start()
    {
        recognizer = new GrammarRecognizer(Application.streamingAssetsPath + srgsFilePath);
        recognizer.OnPhraseRecognized += OnPhraseRecognized;
        recognizer.Start();
    }

    void OnDestroy()
    {
        recognizer.OnPhraseRecognized -= OnPhraseRecognized;
        recognizer.Stop();
        recognizer.Dispose();
    }

    void OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        Debug.Log(args.text);
        GetComponent<TextMesh>().text = args.text;
    }
}

ディクテーション

現在は Holographic Remoting Player では UnknownError が返ってくるため使用できないようです…。

サーバ側でディクテーション処理を行うようなので、InternetClient Capabilities にチェックを入れ、インターネット接続を許可しないと動作しません。

using UnityEngine;
using UnityEngine.Windows.Speech;

public class DictationRecognizerTest : MonoBehaviour 
{
    DictationRecognizer recognizer;

    void Start()
    {
        recognizer = new DictationRecognizer();
        recognizer.InitialSilenceTimeoutSeconds = 10;
        recognizer.AutoSilenceTimeoutSeconds = 10;
        recognizer.DictationResult     += OnResult;     // 文章の区切り時
        recognizer.DictationComplete   += OnComplete;   // Stop またはタイムアウト時
        recognizer.DictationHypothesis += OnHypothesis; // 随時
        recognizer.DictationError      += OnError;      // エラー時
        recognizer.Start();
    }

    void OnDestroy()
    {
        recognizer.Stop();
        recognizer.DictationResult     -= OnResult;
        recognizer.DictationComplete   -= OnComplete;
        recognizer.DictationHypothesis -= OnHypothesis;
        recognizer.DictationError      -= OnError;
        recognizer.Dispose();
    }

    void OnResult(string text, ConfidenceLevel confidence)
    {
        Debug.Log(confidence + ": " + text);
    }

    void OnHypothesis(string text)
    {
        Debug.Log(text);
    }

    void OnComplete(DictationCompletionCause cause)
    {
        // cause: 終了の種類(Complete, Canceled, TimeoutExceeded, ...)
        Debug.Log(cause);
    }

    void OnError(string error, int hresult)
    {
        Debug.Log(hresult + ": " + error);
    }
}

たとえば「Thank you very much」と喋ると、DictationHypothesis に「Thank」「Thank you」「Thank you very」「Thank you very much」と来て、最後に無音区間検出後、DictationComplete に「Thank you very much」が飛んできます。

Fragments をやっていて、キーワード認識だけでも相当便利だなと思ったので使える場面では積極的に音声コマンドをセットしていくと良いのではないでしょうか。

アンカー

ドキュメント: World anchor in Unity / Unity - Scripting API: WorldAnchor

アンカーをオブジェクトに設定すると、そのオブジェクトをその場に固定することが出来ます。アンカーを作らなくても起動時カメラ位置からの相対座標で全オブジェクトが配置されてるからそれで良いのでは?と思うかもしれません。ただ、これがうまく動くのは単一の座標系しか存在しない場合です。まず、A 地点にオブジェクトを配置、そして 4 m 離れていると認識されている B 地点にオブジェクトを配置したとします。部屋をうろうろ歩き回ると空間の精度があがり、実は A 地点と B 地点の距離は 3.9 m だった、ということがわかる状況も起こりえます。このとき、それぞれの位置に置いたオブジェクトは最大で 0.1 m 位置関係がずれてしまいます。何もしないと、起動時のカメラ位置基準の静止座標系にオブジェクトが置かれてしまいます。そこで、HoloLens が扱っている動的な空間座標系にぴったり配置するには、WorldAnchor をアタッチすれば良い、という仕組みになっています。id:pigshape さんの翻訳が分かりやすいです:

littlewing.hatenablog.com littlewing.hatenablog.com

具体的には、アンカーは以下のようなものになります。

  • アンカーは現実の座標系にオブジェクトを固定できるもの
  • WorldAnchor というコンポーネントで提供されていてアタッチするだけで座標固定
    • 固定されたオブジェクトは transform を変更できない(動かしても戻る)
  • 固定解除はコンポーネント削除(DestroyImmediate 推奨)

アンカーは座標上の重要な点として扱われ最適化されるため、大量のアンカーを配置するのは好ましくないと思われます(具体的にどれくらいが良いかの記述は見つかりませんでしたが、使わないアンカーは解放することが推奨されています)。そこで、全てのオブジェクトにアンカーをつける代わりに、アンカーをアタッチした親オブジェクトに子オブジェクトをぶらさげてグルーピングして固定化する、といった方法が考えられます。これにより、少し面倒ですが、アンカー A 地点からアンカー B 地点へ移動するオブジェクトがあった場合には、中間ではアンカーを解除し、到達後に別のアンカーへの遷移を行う、といったことをさせる必要があります(常にダイナミックに動き続けるオブジェクトには必要ありません)。アンカーから遠くなってしまったオブジェクトはアンカーのわずかな角度変化に敏感になりノイズが大きくなってしまうため、3 m 以内の範囲にアンカーローカルなオブジェクトを留めることが推奨されています。

なお、実際に使う場合には、アンカー出来ないときもあるので WorldAnchor.OnTrackingChanged を監視してアンカー可能かそうでないかを判断するコードを書く必要があります(Handling Locatability Changes を参照)。

この固定を永続化(アプリケーション再起動時などセッションをまたいで位置を共有)する仕組みとして、Persistence 名前空間WorldAnchorStore というものが用意されています。

次のような保存/復元/削除/走査の関数を持っています。

  • WorldAnchorStore.Save(string, WorldAnchor) でユニークな名前をつけてセーブ
    • 同じ名前でセーブすると false が返ってくる
  • WorldAnchorStore.Load(string, GameObject) で指定したゲームオブジェクト下にロード
    • 見つかった場合は WorldAnchor、見つからない場合は null が返ってくる
  • WorldAnchorStore.Delete(string) で削除
  • WorldAnchorStore.Clear() で全削除
  • WorldAnchorStore.GetAllIds() で全走査

また、このアンカーは異なるデバイス間でも共有することが出来ます。これは 240 のチュートリアルで解説されています。

シェア

ドキュメント:Shared holographic experiences in Unity

複数の HoloLens でのホログラムの共有は、`WorldAnchor` をシリアライズし、別のデバイスでデシリアライズして空間を共有することで実現されます。自前でコードを書いてもそこまで多くなさそうですが、HoloToolkit-Unity にサーバ含むサンプルがあるので、そちらを利用するのが楽そうです。

www.naturalsoftware.jp

bril-tech.blogspot.jp

シェアそのものについては id:pigshape さんの翻訳をご参照ください:

littlewing.hatenablog.com

立体音響

ドキュメント:Spatial sound in Unity

Unity は標準では AudioListenerAudioSource の距離や角度のみを使っています。しかしながら実際は壁の反射や、頭の中を通じて反対側の耳に到達するまでの音の変化(頭部伝達関数:HRTF)を考慮しないと音の立体感が得られません。そこで Unity は Audio Spatializer SDK という仕組みを提供してくれています。

docs.unity3d.com

これは、AudioManager の設定の Spatilizer PluginMS HRTF Spatializer を選択することで利用できます。

f:id:hecomi:20170212132350p:plain

AudioSource 単位の設定が必要で、インスペクタから Spatialize にチェックを入れて、Volume RolloffCustom Rolloff にする必要があります。

f:id:hecomi:20170212132449p:plain

このプラグインによって AudioSource にいくつかパラメタが追加されます。

  • Minimum Gain
    • 最小のゲイン、デフォルトは -96 dB
  • Maximum Gain
    • 最大のゲイン、デフォルトは +12 dB
  • Unity Gain Distance
    • ゲインが 0 になる距離、デフォルトは 1 m
  • Room Size
    • 部屋の広さ
    • Small(小さい部屋やミーティングルーム)
    • Medium(大きいカンファレンスルーム)
    • Large(講堂)
    • None(アウトドア)

これらは直接 AudioSource.SetSpatializerFloat() を利用して書き換えも出来ますが、Holotoolkit-Unity に含まれる SpatialSoundSetting を利用すると簡単に変更できます。

ちなみに、Oculus Rift のアプリ開発時にもこの仕組を利用して OculusSpatializer という立体音響を利用していました。

qiita.com

空間マッピング

ドキュメント:Spatial mapping / Spatial mapping in Unity

https://www.youtube.com/watch?time_continue=13&v=zff2aQ1RaVo

HoloLens の周囲の形状を収集することで、現実に説得力のある形でホログラムをマージできます。具体的には以下のようなユースケースが考えられます。

  • 配置(壁にものを貼り付ける)
  • 遮蔽(現実の物体の後ろにいると隠れる)
  • 物理(ぶつかる)
  • ナビゲーション(キャラが動き回る)

基本的にはものの後ろにいると完全に消えてしまうではどこにいるか分からなくなってしまうので、薄くしたりステンシルを利用して遮蔽表現を作ってあげると良いかもしれません。レイキャストに関しては 1 本打って環境の情報を取得するよりも複数本の平均を取ったほうが安定します。

ちょっと踏み込んでどうやってメッシュが生成されているか見てみます。まず生成されたメッシュは以下のように WorldAnchor のアタッチされた複数のメッシュに分割されています。

f:id:hecomi:20170212162613p:plain

この空間マッピングのメッシュ郡を生成する方法は主に 2 通りあり、SurfaceObserver を使って手動でメッシュの追加 / 更新 / 削除をハンドルする方法と、SpatialMappingRenderer を利用して自動でオブジェクト郡を生成する方法です。

SurfaceObserver

docs.unity3d.com

Update() に登録したコールバックに、メッシュの追加 / 更新 / 削除イベントが飛んで来るので、そこに含まれる ID 情報を使って RequestMeshAsync()MeshFilterCollider などを含む SurfaceData 構造体を渡すことで、メッシュやコライダが更新される仕組みになっています。単体ではコンポーネントではないため、自前でコンポーネントを作成する必要があります。Holotoolkit-Unity の SpatialMappingObserver の実装をみると参考になります。

SpatialMappingRenderer

docs.unity3d.com

アタッチするだけで自動でメッシュが描画されるようになります。おそらく内部で SurfaceObserver を持っていると思います。occulusionMaterial および visualMaterial の 2 つのマテリアルを renderState で切り替えることが出来るようになっており、最初のキャリブレーションフェーズで visualMaterial を、ゲームになったら occlusionMaterial を、と言った用途に使えます。

f:id:hecomi:20170212205827p:plain

SpatialMappingCollider

SpatialMappingRenderer を使った場合、コライダは別途 SurfaceMappingCollider をアタッチする必要があります。

docs.unity3d.com

MeshRenderer のついかオブジェクトと MeshCollider のついたオブジェクトは別になります。

メッシュ表示の変更やオクルージョン表現

230 のチュートリアルで、シェーダを使ったメッシュ表現の変更方法や、壁の後ろに行ってしまったオブジェクトの表現の変更などが解説されています。

f:id:hecomi:20170212210812p:plain

簡単に説明すると、例えばこのストライプ表現はシェーダにフラグメントシェーダにワールド座標を渡すようにしておき、その y 座標を参照して modf してるだけになります。シェーダによる空間のメッシュ表現はとても面白いので、別途記事を分けて書こうと思います。

平面推定

230 のチュートリアルで紹介されていましたが、MS から提供されている PlaneFinding.dll を使うと平面推定が出来ます。まず部屋をスキャンして壁を検出、そこにオブジェクトを貼る、といった Fragments のようなゲームも比較的簡単に作れそうです。また、スキャンしたメッシュを消去することでポリゴン数も削減され、描画やコライダの計算が軽くなるといったメリットもあります。

メッシュ情報のダウンロード

Device Portal の 3D View から .obj 形式でメッシュをダウンロードすることが出来ます。

f:id:hecomi:20170212212011p:plain

230 のチュートリアルでは、エディタ上ではこのダウンロードしたモデルを使い、開発を早める、ということをやっていました。

穴あき表現

ドキュメント:Case study - Looking through holes in your reality

HoloLens では黒色が透明に見えます。そのため、穴の空いた黒いプレーンを通して向こう側を見るような表現を作れば、壁に穴をあけることも出来ます。しかしこの方法には欠点があり、キャプチャをする際は黒は黒として出力されてしまうのです(※ Holographic Remoting Player でのキャプチャは透明になります)。これは以下のような数式で説明できるとのことです(厳密ではないことに注意が必要)。まず実際の目に見えている画は以下のように表現できます

(現実 * バイザの黒半透明) + ホログラム

なのでホログラムが黒(= 0)の時は現実がちょっと暗くなっただけとなります。しかしながら、スクリーンショットやビデオを撮った際は以下のようになります。

// a = ホログラムのアルファ
(現実 * (1 - a)) + ホログラムの色 * a

このため、黒は透明でなく黒として映ってしまいます。これを回避するには黒ではなく ColorMask 0 をしてデプスバッファにだけ書き込むシェーダを使った遮蔽用のマテリアルを適用します。シェーダのコードは HoloToolkit に含まれています。

動的に穴をあけるにはもう少し工夫が必要だと思います。

追記(2017/02/18)

解説記事を書きました。

tips.hecomi.com

その他

更に読みたい:

まだ出来てないこと:

おわりに

MS のチュートリアルも動画でかなり丁寧に説明してくれるため分かりやすいです(英語が聞きとれない場合は、字幕表示や自動翻訳も使えば理解できると思います)。またベストプラクティスだけ抑えてしまえば、後は従来の知識がそのまま使えるのはとても楽です。加えて、Unity により HoloLens が公式サポートされていることも有り、ドキュメントも充実しているのでスクリプトを書くのもそれほど困りません。エディタ支援やプロファイラ、Device Portal などにより開発イテレーションもかなり快適です:

blogs.unity3d.com

おおむね一通り機能を見れたと思うので次からコンテンツを作っていきたいと思います。