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

xin9le.net

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

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

Community Unity VR/AR 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 Windows Tips HoloLens

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 でライブコーディングしてきた

Community C#

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

f:id:xin9le:20170130232536p:plain

お品書き

今回は以下の 3 本建て。

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

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

セッション資料

資料には以下が書かれています。Docs.com で公開しているので、ご自由にご利用ください。

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

ちょっとした裏話

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

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

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

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

まとめ

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

その他の参加レポート

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

LINQ Tips

以前 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:普通ならないと思う

2016 年を振り返って

Opinion

ウチの CTO が毎年やっていて、「今年は僕も振り返ってみようかな」という気持ちになったので書いてみます。振り返りは大事だな、と言うことで!

単身赴任

2015 年 9 月に株式会社グラニに転職し、単身赴任生活が始まって早 1 年 3 ヵ月が過ぎました。転職して本当によかったって思っています。素晴らしいメンバーと毎日精一杯仕事ができているし、仕事ってこんなに楽しいんだなぁって社会人になって初めて思えました。...ただひとつを除いて。

それは家族と一緒にいる時間がものすごく少ないこと。これは単身赴任をしているので当然ですし、その選択をした自分のせいなのですが。家族って本当に大切だなぁとか愛しいなぁと改めて感じています。悲しませているとは思いつつ、自由にやらせてもらっていることに多大なる感謝が尽きません。だからこそ毎日何時間働いてもツラくないし、自分が選択したんだからって前向きになれます。そして帰省しているときは極力一緒に時間を過ごす、という (当然の?) 努力をするようになりました。単身赴任をするまでは「自分の時間をいかに確保するか」ばかりを考えていましたが、毎日が自分の時間になってしまった今はその逆ですね。以前にも増して仕事とプライベートにメリハリがついたのも良かった。

家族の理解とサポートって本当に素晴らしいですね。改めて、ありがとう。

どうして転職したの?

と、たまに聞かれます。「あんなに福井が好きって言ってたのに!」みたいな。すべてはコレだと思うんです。

プライベートではお友達 / 仕事では上司なこの方があまりにも、あまりにもブッ飛んでる。本当にリスペクトの極み。そんなスーパーな方の近くで仕事したかったから、というのが率直なところです。福井の田舎の会社で井の中の蛙にはなりたくなかった。そのためには環境を変えなきゃ / 冒険しなきゃって思えて、それで転職を決心しました。環境を変えるのが自分を変える最短かつ最高の方法だと思います。

もちろん、グラニ開発部のメンバー全員が素敵な個性と素晴らしい技量を持っていて日々尊敬していますし、毎日社内チャットでやりとりされる内容を見ているだけでレベルが高くて本当に勉強になります。

お仕事

入社してから 1 年ちょっと、グラニのメインタイトルである神獄のヴァルハラゲートの開発/運用をしてきました。日々の小さな改修はもちろんのこと、ユーザーさんが大いに盛り上がるイベントの実装までいろいろです。もちろんショックに沈む失敗もあったけれど、24 時間 365 日動き続けるゲームの運用の難しさや楽しさをいっぱい体験しました。ここで得た経験と自信はかなり大きい!

f:id:xin9le:20161231134015p:plain

夏にはグラニ開発部初のインターン生として @nanTosaka2 くんが 3 週間来てくださり、僕がメンターを担当していました。毎日「なんでこんなに優秀なの?」と目を丸くするばかりで、教えてもらうこともいっぱいありました。インターン終了後にブログを書いてくれたのを見て、ひとり泣いたのは本当です。とても有意義な時間を過ごせたみたいでこちらも感謝に絶えません。お疲れさまでした!本当にありがとね!

そして考え方や心境も結構変化しました。以前は「べき論」をかなり振りかざしていて、ちゃんとしていないことが大嫌いでした。それで喧嘩したり失いそうになったものもありました。そんなことにはもうなりたくないですし、そうしないための妥協を覚えた気がします。「人」をベースとした考え方の大切さ、忘れないようにしたいです。

C# 7

プライベートでは C# 7 のことばかり追いかけていて、重鎮 @ufcpp さんからお叱りを受けたりしつつ、新機能を先駆けて触ってみたのをベースにレビュー記事を書きまくっていました。社内で唱えていた言葉は「日本で最初に触ってみた系の記事を書く」でしたw そこだけは達成できたかなと思います。

勉強会でも C# 7 を唱え続け、C# 7 に関するセッションを半年以内で 3 回もやりました...(ぇ とは言えかなり好評で、de:code 2016 の直前にやったものが「de:code 2016 本体のセッションよりも詳しくてよかった」という評価もあって本当に嬉しかったです。C# 7 のリリースは楽しみにしています。

Hackathon

2015 年に引き続き 2016 年もハッカソンにちょこちょこ出たりしていました。SPA JAM 2016 というヤツでは東京の予選で最優秀賞を受賞して決勝に行ったりもしました。その様子はかなりの数のメディアで取り上げられ、素直に嬉しかったです。とは言え決勝ではしっかり負けたので、結果は何も残してないに等しいんですが!

紅白歌合戦の著作権表示

今日 12 月 31 日と言えば大晦日!大晦日と言えば日本中が楽しむ紅白歌合戦!そんな紅白歌合戦の公式アプリに Reactive Property が採用され、僕の名前も著作権表示に載りました

f:id:xin9le:20161231151710p:plain f:id:xin9le:20161231151716p:plain

「なんでお前がやねん!」というと、元々は @neuecc さんが作っていたライブラリなのですが、メンテが止まっていたので数年前から @okazuki さんと僕でメンテするようになりました。今は issue 対応のときの方針をどうするかを相談したり程度のただのお手伝いマンでしかないのですが、コントリビューターとして名前が入っているのでオマケとして載せていただいた感じです。これも嬉しい出来事でしたし、棚ぼたではありますが感謝に絶えません。

VR

「現在のお仕事は?」と言うと 2016 年 10 月からは新設された VR 部に異動し、その初期メンバーとして活動しています。VR 部発足の経緯などはプレスリリースが出ています。

f:id:xin9le:20161231140634p:plain

そんな Grani VR Studio 最初のプロダクトとして、弊社の六本木ヒルズオフィスを完全再現した Grani VR Office Tour を作成しました。Japan VR Summit 2 にもブース出展し、非常に良い評価をいただきました。TBS のあさチャン!でも取り上げてもらったりしました :)

f:id:xin9le:20161231141811j:plain

f:id:xin9le:20161231141819j:plain

と、最近は毎日 VR という新しい時代のためにメッチャ頑張ってます。けれど、まずまず僕自身が VR に出会ったのが 2016 年 4 月くらい。超新参者だし Unity も初心者なで毎日 ウーン...ウーン... と唸っていますが、日々新しいことにチャレンジできるのって本当に幸せなことだと思います。仲間と一緒にインパクトを残せるよう取り組んでいきます。2017 年はそんな全力の一年!

まとめ

この歳になると人のつながりの大切さに改めて気付かされます。そのためにも Team Geek にある HRT の精神は大切だなぁと痛感しますし、今後も大切にしていきたいと思っています。その上で来年はさらに加速していきます!今後とも、どうぞよろしくお願い致します。

そして、この 1 年に @neuecc さんと取締役で VR 部長の福永からいただいた金言を忘れないように。

モノを作るときに忘れちゃダメなこと 3 箇条

  • 作らない
  • 薄く作る
  • すぐ捨てる

未来はでっかく

  • 未来を作るのは、エンジニアのちょっとした創造力