xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Vuforia 6.2.x が Unity 5.5.x の Windows Store App 環境下でビルドエラーになるのを回避する方法

Tokyo HoloLens Meetup vol.2 の LT で Vuforia という AR ライブラリについて話していたので、「面白そう!」と思ってチャレンジしてみています。AR マーカー的なものはまだ作ったことがなかった上に HoloLens に対応していると聞いて俄然ヤル気に!

と思ってパッと調べてみたら昨年の //Build/ 2016 でも HoloLens を使ったデモをしていたようで。(全然覚えてないけど…

Vuforia 自体の基本的な使い方や設定については詳しい記事が山ほどあるので、ここでは割愛します。

サンプルがビルドエラーになる

Vuforia 初心者なので、まず手始めに HoloLens のサンプルを動かしてみよう!ということで Vuforia のサイトからサンプルをダウンロードします。「Digital Eyewear」のカテゴリーの Unity アイコンのものが対象です。

f:id:xin9le:20170402191358p:plain

.unitypackage が落ちてきたはずなので Unity にインポートし、「Vuforia-2-Hololens」のシーンを開きます。HoloLens で実機確認したいので何も考えずに Windows Store App 向けにビルドします。すると… ビルドエラーが出る!サンプルがいきなり動かない!/(^o^)\

f:id:xin9le:20170402194545p:plain

Reference rewriter: Error: method `System.Void UnityEngine.RenderTexture::set_generateMips(System.Boolean)` doesn't exist in target framework. It is referenced from Vuforia.UnityExtensions.dll at System.Void Vuforia.DistortionRenderingBehaviour::CreateRenderTexture().
UnityEngine.Debug:LogError(Object)
PostProcessWinRT:RunReferenceRewriter() (at C:/buildslave/unity/build/PlatformDependent/WinRT/SharedSources/CSharp/PostProcessWinRT.cs:560)
PostProcessWinRT:Process() (at C:/buildslave/unity/build/PlatformDependent/WinRT/SharedSources/CSharp/PostProcessWinRT.cs:127)
UnityEditor.HostView:OnGUI()

手元の環境は「Unity 5.5.0p4」なのですが、どうやら Unity 5.5 から RenderTexture.generateMips プロパティが RenderTexture.autoGenerateMips に名称が変更されたようで、Vuforia.UnityExtensions.dll の内部で古いバージョンの API を利用していることが理由でビルドエラーになっているっぽいです。

Graphics: Added RenderTexture.GenerateMips script API for manual control over mipmap generation. Renamed existing RenderTexture.generateMips property to autoGenerateMips.

API Updater を使って回避

Unity には API 変更の互換性を担保するための API Updater という機能が搭載されています。今回はこれを使って問題が出ているアセンブリを修正すれば OK です。

「Vuforia\Scripts\Internal\Vuforia.UnityExtensions.dll」を右クリックして、コンテキストメニューから [Run API Updater…] を実行します。

f:id:xin9le:20170402200301p:plain

これで晴れて Windows Store App 向けにビルドが通るようになります。あとは HoloLens の実機で動作確認すれば良いでしょう。

参考

これが見つけられなかったら解決できませんでした。感謝×2!

Sharing Deep Dive - HoloLens で最もクールな機能の勘所を掴む

2017/03/25 (土) に日本マイクロソフト品川本社にて開催された Tokyo HoloLens Meetup vol.2 に参加/登壇してきました。会場に行くと日本の HoloLens ブームの凄まじさに驚くばかりです。そんな圧倒的ギーク達を前に、僭越ながら HoloLens で最もクールな花形機能である「Sharing」について解説させていただきました。

Sharing 機能は僕自身がメインエンジニアとして開発している Project Sonata で実際に利用しており、開発に際しては相当苦労しましたw なので今回のセッションでは、その開発中に得た基本的な考え方やハマりポイントなど、現在日本語でも英語でも全然公開されていない知見を共有させていただきました。

Sharing はただでさえお高い HoloLens が最低 2 台必要ということで、日本でいくら HoloLens が加熱しているからと言って簡単に試せるものではありません。そういう点でも人柱としてガチでやってみた系の情報共有は、今後の HoloLens 開発に対する良い Tips になれたのではないかと思います。

大変ありがたいことにセッションの反応も想像以上に良く、セッション後には持って行った名刺が全部なくなるほどにたくさんの方々からお声掛けいただきました。こんなことは初めてだったので正直驚いていますw また、セッション中のツイートは Togetter にまとめられています。感謝×2 :)

Sharing のキモは座標系の共有

これに尽きます。座標系の共有ができないうちは同じ空間内での Sharing は「絶対に」完成しません。まず前提知識として HoloLens における座標系は以下のようになっています。

  • アプリケーションを開始したときのヘッドセットの位置が原点
  • 視線方向 : Z 軸正方向
  • 向かって右側 : X 軸正方向
  • 上 : Y 軸正方向

つまり、同じ室内にいたとしても A さんと B さんはそれぞれ独自の座標系を持っています。この状態で A さんが (x, y, z) = (1, 2, 3) の位置にオブジェクトを生成したとして、これをネットワーク経由で B さんにも配置するように指示したとしても、B さんの座標系は全く別なので (x, y, z) = (1, 2, 3) は全然別の場所になってしまいます。そこで WorldAnchor を設定し、それを原点とした座標系を共有し合うことで、(x, y, z) = (1, 2, 3) が同じ空間内で同一の場所を指すようにします

f:id:xin9le:20170326131431p:plain

座標系の共有は完全に下準備です。これに成功したら、共有座標系に対する localPosition / localRotation でやり取りすることで、晴れて Sharing が完成します!

Sharing の実現手順

Sharing を行うまでの大まかな手順をまとめると以下のようになります。言うは易しなのですが、やってみると案外面倒なものです。

  1. 原点 (= 上図の Os) とする GameObject に対して WorldAnchor コンポーネントを引っ付ける
  2. WorldAnchorTransferBatch を使って先に設定した空間アンカーをシリアライズ
  3. ネットワーク経由でシリアライズしたデータを全ユーザーに配信
  4. 受信したデータを WorldAnchorTransferBatch を使ってデシリアライズし WorldAnchor を設定
  5. 原点となる GameObject の子要素としてオブジェクトを生成/配置
  6. オブジェクトの移動や姿勢変更があれば localPosition / localRotation をネットワーク経由で共有

空間アンカーの前回値保持

空間アンカーの設置/同期にはかなりの時間がかかります。また、UWP アプリケーションはそのライフサイクルの仕様からバックグラウンドにいるときに自動で終了させられたりします。このようなことがあるので、アプリケーション再開時に高速で空間アンカーを復旧させることには一定の意味があります。ただし、空間アンカーを保存した部屋と読み込みする部屋が別だったりすると一体どこに復旧させたら良いか分からなくなるので、同一空間での利用なのかどうかと言ったところに気を配った実装をしなければならず、思った以上に手間です。前回値保持は Sharing を実装する上では必須機能ではないので、いっそのこと思い切って無視しても良い気もします。

空間アンカーの前回値保持には WorldAnchorStore を使って行います。これはいわゆる KVS (Key Value Store : 辞書型ストレージ) として提供されています。

空間アンカーの共有におけるハマりポイント 6 選

冒頭の資料にあるものをピックアップしますが、メチャクチャ多いです。正直イヤになるくらいハマりますし、「もはや無理なのでは?」と挫折すると思います。これらをひとつひとつを忍耐強くクリアしていかないと、アプリにすることはおろか他人にデモを見せることすら難しいでしょう。中途半端に Sharing を搭載したアプリをリリースしたとしても「Sharing できないので ☆1」は普通にあり得る話だと思います。つまり、本当にツラいです...(ギャフン

1. 小さ過ぎるデータサイズ

シリアライズ結果が小さいとデシリアライズに失敗しやすようです。HoloToolkit-Unity のサンプルでは 100 KB を最低保証値としていて、コメントに「小さ過ぎると失敗しやすい傾向にある」とだけ書いてあります。そうらしいのでとりあえず信じましょう。

2. 滅多に成功しない保存/読み込み

まずまずシリアライズに全然成功しません。成功率は 10 % 未満なのではないかと思うほど成功しません。そして運よく成功したしたとしてもデシリアライズにも高頻度で失敗します。成功するまでリトライするような地道な実装が必要です。

3. 大き過ぎるデータサイズ

先ほどはデータサイズの最低保証値として 100 KB と書きましたが、実際には簡単に 10 MB を超えたりします。社内で実験していたときに 50 MB を超えたこともありました。それぐらいのデータ量になることもある、というのを知っておく必要があります。

4. バカにならないデータ転送量

空間アンカーを共有するにあたり、先に挙げたようなバカでかいデータを全ユーザーにブロードキャストする必要があります。が、当然カジュアルにやってしまっては非常に危険です。例えば海外でデモをしようとして、レンタル Wi-Fi (= 500 MB/day) などで通信するとすぐに上限に振り切って死にます。ですので、データサイズを減らしたり通信回数を減らすような実装上の工夫が多数必要になりますし、安定した Wi-Fi 環境が必須になります。

5. 保存/読み込みが超絶遅い

WorldAnchorTransferBatch によるシリアライズ / デシリアライズは、それだけでそれぞれ 30 秒かかるとかザラにあります。さらに先に挙げたように高頻度で失敗するので「1 分以上待って失敗!」と言ったことも発生し得ます。圧倒的にイライラが募るので、心に余裕を持って作業に当たってくださいw

6. 圧倒的デバッガビリティの低さ

Sharing の検証を行うには実機を使って部屋中を動き回る必要があります。ですが、Visual Studio のデバッガーを使おうとすると PC にケーブルを繋いでいる必要があります。動きたいのに動き回るのが困難という現実に直面することになります。そして 2 台以上にデプロイして検証する必要があるので大変煩わしいです。さらにエミュレーターは Sharing においては何の役にも立ちません...。結局僕は printf debug 的に画面にメッセージを出すことで作業していました。

SharingService.exe の是非

みんな基本は Holographic Academy : Holograms 240 から Sharing の勉強を始めるので、そのサンプル中に出てくる SharingService.exe を利用するのは当然だと思いがちです。が、ShariingService.exe を利用する以上はそのメリット / デメリットを十分に把握しておく必要があります。

メリット

  • Sharing はもちろん、音声通信など豊富な機能が用意されている
  • クライアント実装だけに集中して開発できる

デメリット

  • カスタマイズ性が一切なく、ボトルネックになった際に対処不能
  • スケールアウトできないので少人数利用に限られる
  • 自前 API がある場合、それと異なる通信の口を持つことになる

僕は Project Sonata を開発するにあたり「SharingService.exe は自由度がないのでナシ」という判断をし、MagicOnion という弊社 CTO (@neuecc) 謹製の gRPC をベースとしたハイパフォーマンス通信フレームワークを選択しました。これは相当な茨の道でしたが、大きなカスタマイズ性とパフォーマンス、そしてすべて自分で制御しきっているという安心感を手に入れました。また、Sharing の通信部分を完全に自作したことで Sharing 自体に相当詳しくなったという点でも良かったです #今となってはw

まとめ

Sharing は上手くできると本当に未来を感じます。メッチャ夢があります。でも、全然簡単じゃない。これだけは覚えておいてください。

Sharing に夢を見ても 甘く見るな

Unity における Windows Store App のアプリケーションライフサイクル

Unity で VR などの PC / Standalone 向けのアプリケーションを作っている場合、アプリの終了処理をしたかったら MonoBehaviour.OnApplicationQuit を使います。ですが、HoloLens などの Windows Store App (Universal Windows Platform) 向けのアプリケーション作りをしている場合はそうはいきません。

UWP / WSA のアプリケーションライフサイクル

以下の図にあるように、中断 / 再開という概念を伴うものになります。詳細はリンク先をご覧ください。

f:id:xin9le:20170212221534p:plain

このライフサイクルによると、アプリケーションの中断後に前触れなく突如終了 (Terminate) ということがあり得ます。実はこのとき、アプリケーションが終了しているにも関わらず Unity フレームワークから MonoBehaviour.OnApplicationQuit は呼び出されません

Unity で呼び出されるコールバック

パッと調べた範囲では、呼び出されるのは以下のふたつだけです。

呼び出し順序 中断時 再開時
1. OnApplicationFocus(false) OnApplicationPause(false)
2. OnApplicationPause(true) OnApplicationFocus(true)

つまり、中断時にはいつ勝手に終了されるかも分からないので、不測の自体に備えた終了処理に相当するものが必要ということになります。Unity を使って Windows Store App 向けのビルドを作る場合はアプリケーションライフサイクルに注意しましょう!

BuriKaigi 2017 in Toyama でライブコーディングしてきた

数えれば今回で早 5 回目。毎年恒例、北陸の恒例イベント (?) と言っても過言ではない C# 大好き MVP による、C# ドキドキ・ライブコーディング対決 !! をやってきました。これまでは @Fujiwo / @AILight / @xin9le の 3 人でしたが、今回は @RyotaMurohoshi が加わっての 4 人体制。

お品書き

今回は以下の 3 本建て。

  • ふたりペアになって FizzBuzz を 1 行交代で書く
  • 九九表を作る
  • 4 人オセロ対決

オセロは事前準備がありましたが、それ以外の問題は相変わらず当日その場まで一切知らされないという徹底ぶり。何が来るか分からないという緊張感と、できなかったらどうしようとういう焦りは本当です。どんなに簡単でも冷や汗が出ますw

セッション資料

資料には以下が書かれています。

  • 自己紹介
  • C# の好きなところ / 推したいところ
  • 4 人対戦オセロのアルゴリズム

ちょっとした裏話

@AILight さんが Twitter に書いていたので引用しておきます。

セッション直前でプログラムが動かないという自分たち自身がセッションできないかも、というドキドキ/ハラハラに見舞われて本当に焦りましたw 少し詳しく説明すると、以下のようなことが起こっていました。

  • オセロの AI (アルゴリズム) を作ってくるというお題が課せられる
  • 誰のアルゴリズムが強いか分からないので、先に 5 回試行して一番強いやつを自分のアルゴリズムにしようとした @Fujiwo さん
  • ネットワーク経由で最強と思われるアルゴリズムを自分のアルゴリズムとして利用しようとした @xin9le
  • お互い全員の AI インスタンスを内部で勝手に生成/キャッシュする仕組みだった
  • 期せずして PlayerFujiwoPlayerXin9le を生成し、PlayerXin9lePlayerFujiwo を生成するという無限ループが発生
  • このふたりが揃うと見事に StackOverflowException が飛ぶ

これまでにこんなことは一度もなかったのですが、もはやお互い何をしてくるか分からないので怖いですねw

まとめ

5 年続けてきて思うのは、一種のプログラミングエンターテイメントとしてセッションする側も見る側も楽しめていると実感できていることです。このような楽しみを今後も届けられればと思います。来年は @AILight さんからの「仕返し」が待っているようなので、僕も今から楽しみです!是非ご期待ください :)

その他の参加レポート

1 次元配列から 2 次元辞書を作ろう

以前 2 次元配列の要素をインデックス付きで 1 次元配列に落とす、というのを紹介しました。

となると、その逆もやってみたくなりませんか…?*1

拡張メソッドを作る

通常の ToDictionary メソッドはひとつのプロパティをキーにして辞書を作りますが、このキーをふたつにすればよいでしょう。以下のような感じになります。

//--- キーだけ選択してそのまま要素を格納するバージョン
public static Dictionary<TKeyX, Dictionary<TKeyY, TSource>> ToDictionary2<TSource, TKeyX, TKeyY>
    (
        this IEnumerable<TSource> self,
        Func<TSource, TKeyX> xSelector,
        Func<TSource, TKeyY> ySelector
    )
{
    if (self == null)      throw new ArgumentNullException(nameof(self));
    if (xSelector == null) throw new ArgumentNullException(nameof(xSelector));
    if (ySelector == null) throw new ArgumentNullException(nameof(ySelector));

    return  self.GroupBy(xSelector)
            .ToDictionary(x => x.Key, xs => xs.ToDictionary(ySelector));
}

//--- キーを選択するだけでなく、要素として何を格納するかを選ぶバージョン
public static Dictionary<TKeyX, Dictionary<TKeyY, TElement>> ToDictionary2<TSource, TKeyX, TKeyY, TElement>
    (
        this IEnumerable<TSource> self,
        Func<TSource, TKeyX> xSelector,
        Func<TSource, TKeyY> ySelector,
        Func<TSource, TElement> elementSelector
    )
{
    if (self == null)            throw new ArgumentNullException(nameof(self));
    if (xSelector == null)       throw new ArgumentNullException(nameof(xSelector));
    if (ySelector == null)       throw new ArgumentNullException(nameof(ySelector));
    if (elementSelector == null) throw new ArgumentNullException(nameof(elementSelector));

    return  self.GroupBy(xSelector)
            .ToDictionary(x => x.Key, xs => xs.ToDictionary(ySelector, elementSelector));
}

使ってみよう

こんなものが一体どんなケースで役に立つというのだ…。と思うかもしれませんが、例えば以下のようなものがあります。

//--- こんなクラスがあるとする
class Block
{
    public X { get; set; }
    public Y { get; set; }
    public Color Color { get; set; }
}

//--- こういう 1 次元配列を 2 次元辞書化してしまえば...
var blocks = new []
{
    new Block{ X = 0, Y = 0, Color = Colors.Red },
    new Block{ X = 0, Y = 1, Color = Colors.Blue },
    new Block{ X = 1, Y = 0, Color = Colors.Green },
    new Block{ X = 1, Y = 1, Color = Colors.Yellow },
}
.ToDictionary2(x => x.X, x => Y);

//--- こんなに分かりやすくなる!
var color = blocks[0][1].Color;

…滅多に使わないとは思います(ハイ

*1:普通ならないと思う