デコシノニッキ

ホロレンジャーの戦いの記録

StereoKitを使ったUnityを使わないHoloLens2アプリ開発の紹介

StereoKitとは

今回は StereoKit について紹介します。

StereoKit は、C# と OpenXR を使って HoloLens や VR アプリケーションを構築するためのライブラリです。

Nick Klingensmithさんという方が主に開発をしています。

twitter.com

StereoKitの特徴は何といってもその軽量さで、ゲームエンジンのようなレンダリング機能はありませんがビルドが兎角早く、プログラマブルに空間を設計できます。

以下抜粋

  • モデルフォーマット .gltf、.glb、.obj、.stl、プロシージャル
  • テクスチャ形式 .jpg、.png、.tga、.bmp、.psd、.gif、.hdr、.pic、等角キューブマップ、プロシージャル
  • ランタイム アセットの読み込み
  • プラットフォーム HoloLens 2、Windows Mixed Reality、最終的にはOpenXRがある場所ならどこでも!
  • アプリケーションを数分ではなく数秒で構築
  • 入力:多関節ハンド、ポインター、キーボード/マウス
  • Physics
  • 簡単でパワフルなUIとインタラクション
  • デフォルトのレンダリング パイプラインのパフォーマンス
  • 柔軟なシェーダ/マテリアルシステム
  • すべてのドキュメントはテストされ、スクリーンショットを含むコードから生成されます。

名前を出してしまえば、Unityだと一般的にUnityでプロジェクトを書き出して、更にそこからUWP用にプロジェクトをビルドします。 マシンスペックによってまちまちであるものの、長ければ10分近く、更にMRTKなどのライブラリを含んでいればもっと長い場合もあります。IL2CPP移行に伴いこのビルド時間問題はより顕著になりました。 Holographic Remoting などの開発支援ツールもありますが、ビルドしてみたら挙動が違ったなどは往々にしてあります(つれぇな

StereoKitを始める

環境構築

Guides Getting Started | StereoKit Documentation に従って環境を構築しましょう。

  • Visual Studio 2019 - 2017は非対応です。以下のワークロードを使います。

    • .NET Desktop development
    • Universal Windows Platform development
    • .NET Core cross-platform development
  • OpenXR Runtime - Open XRが必要です。HoloLensでは、Device Portal からインストールすることもできます。

  • StereoKit’s Visual Studio Template - StereoKit プロジェクトのテンプレートです。基本はここを起点にアプリを作ることになります。 NuGet packageから直接落としてくることも可能です.Preview版を使いたい人はこちらから使いましょう。
  • Developer Mode (for UWP/HoloLens builds) を有効にする。
    • Windows Settings->Update and Security->For Developers->Developer Mode

テンプレートを開く

テンプレートが追加されていれば、下記のように新規プロジェクトから作成することができます。

f:id:haikage1755:20200404013535p:plain:w250

Project.cs にテンプレートが記載されています。(エラーが出る場合は、一度ビルドしてみてください)

using System;
using StereoKit;

namespace StereoKitProject
{
    class Program
    {
        static void Main(string[] args)
        {
            if (!StereoKitApp.Initialize("StereoKitProject", Runtime.MixedReality))
                Environment.Exit(1);

            Model cube = Model.FromMesh(
                Mesh.GenerateRoundedCube(Vec3.One, 0.2f),
                Default.Material);

            while (StereoKitApp.Step(() =>
            {
                cube.Draw(Matrix.TS(Vec3.Zero, 0.1f));
            })) ;

            StereoKitApp.Shutdown();
        }
    }
}

では、これをビルドしてHoloLens 2 に入れてみましょう。
自分の環境ではだいたい1分もかからずデプロイまでいけました。超早い。

f:id:haikage1755:20200404014254j:plain:w250

Cubeが目の前に表示されるはずです。

サンプルの簡単な解説

  1. StereoKitをMRモードで初期化する
StereoKitApp.Initialize("StereoKitProject", Runtime.MixedReality)
  1. デフォルトマテリアルを使った角丸Cubeを生成する
Model cube = Model.FromMesh(
            Mesh.GenerateRoundedCube(Vec3.One, 0.2f),
            Default.Material);
  1. 各ステップごとにモデルの描画を行う
while (StereoKitApp.Step(() =>
        {
            cube.Draw(Matrix.TS(Vec3.Zero, 0.1f));
        }));

ハンドや、ライティングなどは自動で設定されるようです。

StereoKitでUIを構築する

StereoKitでUIを構築していきましょう! こちらのドキュメントに従っていきます。

StereoKitのUI思想

StereoKitでは Immediate Mode UI (即時モードUI) という思想を基としたUIとなっています。見たいフレームごとにUIを定義するというものです。 この手法の利点は、状態の保存をほとんどしないので、UIの要素追加や削除、変更が、簡単かつ標準的なコード構造で行うことができます。状態の管理が複雑ではないということは、コードの数が減る、それに伴って問題が発生する場所が減ります。 できるだけUIを早く立ち上げて実行できる、という点がStereoKitの特徴です。 ただし、APIのシンプルさとデザインの柔軟性がトレードオフの関係性になっているため、そこは課題になっている点は留意してください。

Windowを作ってみる

イメージ的にはUnityのOnGUIでUIを組み立てるのと同じ要領です。

先に述べたようにUIはほとんど状態を扱わないので、自前で管理する必要があります。 まずは、Windowのウィンドウのポーズを左側に、正面は右に向けています。このコードを初期化セクションに追加します。

Pose   windowPose        = new Pose(-.4f, 0, 0, Quat.LookDir(1,0,1));
Sprite windowPowerSprite = Sprite.FromFile("power.png", SpriteType.Single);
bool   windowShowHeader  = true;
float  windowSlider      = 0.5f;

続いてアプリケーションステップに移動して、残りのUIコードを作成します。

まず、"Window"というタイトルのウィンドウを作成します。幅20cmで、y軸上で自動でリサイズされます。StereoKitでは単位の標準はメートルですが、メートルは時に直感的ではないのでcmでも扱えるように変換用のユーティリティがあります。

ウィンドウのヘッダをオンオフするためにトグルを使います。このトグルの値は、showHeader フィールドを介してここに渡されます。

UI.WindowBegin("Window", ref windowPose, new Vec2(20, 0) * Units.cm2m, windowShowHeader);

ウィンドウを開始すると、すべてのビジュアル要素がそのウィンドウに対して相対化されます!UI は Hierarchy クラスを利用して、ウィンドウのポーズを Hierarchy スタックにプッシュします。UI は Hierarchy クラスを利用して、ウィンドウのポーズを Hierarchy スタックにプッシュします。ウィンドウを終了すると、ポーズが階層スタックからポップされ、通常の状態に戻ります。 これがトグルボタンです。UI コードの多くに 'ref' 値が使われていることにもお気づきでしょう。UI 関数は通常、フレーム内で操作されたことを示すために true/false を返すパターンに従うので、変化に反応するために 'if' ステートメントでうまくラップすることができます。 次に、'ref' パラメータで UI 要素の現在の状態を渡します。UI要素はユーザーのインタラクションに基づいて値を更新しますが、いつでも自分で値を変更することができます!

UI.Toggle("Show Header", ref windowShowHeader);

スライダーの例です。まずラベル要素から始めて、次の項目を同じ行に保つようにUIに指示します。スライダーは範囲 [0,1] に固定され、0.2 の間隔でステップします。スライダーを連続的にスライドさせたい場合は、ステップ値を0に設定します。

UI.Label("Slide");
UI.SameLine();
UI.HSlider("slider", ref windowSlider, 0, 1, 0.2f, 72 * Units.mm2m);

簡単なボタンの使い方は、 if くくってロジックを組んでいきます。この辺もUnityと同じですね。 どのUIメソッドも、値や状態が変化したときにフレーム上でtrueを返します。

if (UI.ButtonRound("Exit", windowPowerSprite))
    StereoKitApp.Quit();

最後に、ボタンを押すとアプリが終了するようにします。

UI.WindowEnd();

最終的なコードはこうなります。

using System;
using StereoKit;

namespace StereoKitProject
{
    class Program
    {
        static void Main(string[] args)
        {
            if (!StereoKitApp.Initialize("StereoKitProject", Runtime.MixedReality))
                Environment.Exit(1);

            Pose windowPose = new Pose(-.4f, 0, 0, Quat.LookDir(1, 0, 1));
            Sprite windowPowerSprite = Sprite.FromFile(@"Assets\power.png", SpriteType.Single);
            bool windowShowHeader = true;
            float windowSlider = 0.5f;

            while (StereoKitApp.Step(() =>
            {
                UI.WindowBegin("Window", ref windowPose, new Vec2(20, 0) * Units.cm2m, windowShowHeader);
                UI.Toggle("Show Header", ref windowShowHeader);
                UI.Label("Slide");
                UI.SameLine();
                UI.HSlider("slider", ref windowSlider, 0, 1, 0.2f, 72 * Units.mm2m);
                if (UI.Button("Exit"))
                    StereoKitApp.Quit();
                UI.WindowEnd();
            })) ;

            StereoKitApp.Shutdown();
        }
    }
}

f:id:haikage1755:20200404165330g:plain:w350

カスタムウィンドウを作る

これまでは2DUIでしたが、3Dモデルと2DUIを組み合わせたUIも作ることができます。 アフォーダンス(affordances)を使って、グラブ可能なUIを作ってみます。

glbや画像ファイルはこちらから取得してください。

Assetsの配下にモデルを置く際は、出力ディレクトリにコピーするようにしてください。
f:id:haikage1755:20200404170119p:plain:w350

Clipboardに必要な要素を初期化します。 3DモデルにはGLBファイルを使います。

Model clipboard = Model.FromFile(@"Assets\Clipboard.glb", Default.ShaderUI);
Sprite clipLogoSprite = Sprite.FromFile(@"Assets\StereoKitWide.png", SpriteType.Single);
Pose clipPose = new Pose(.4f, 0, 0, Quat.LookDir(-1, 0, 1));
bool clipToggle = false;
float clipSlider = 0;
int clipOption = 1;

操作可能な領域は、AffordanceBegin~AffordanceEndで囲われた領域です。描画したいオブジェクトはRendererに追加する必要があります。

UI.AffordanceBegin("Clip", ref clipPose, clipboard.Bounds);
Renderer.Add(clipboard, Matrix.Identity);

続いて、UIのレイアウトを行っていきます。スケールなどはモデルごとに異なってくるので、オブジェクトのサイズに合わせて組んでいく必要があります。

UI.LayoutArea(new Vec3(12, 13, 0) * Units.cm2m, new Vec2(24, 30) * Units.cm2m);

あとは順次必要なUIを追加してくだけです

UI.Image(clipLogoSprite, new Vec2(22, 0) * Units.cm2m);
UI.Toggle("Toggle", ref clipToggle);
UI.HSlider("Slide", ref clipSlider, 0, 1, 0, 22 * Units.cm2m);

最後にラジオボタンです。実装はサンプルなので、こういうコードになっていますが実際使うとなればEnumとかと組み合わせて使うことになるともいます。

if (UI.Radio("Radio1", clipOption == 1)) clipOption = 1;
UI.SameLine();
if (UI.Radio("Radio2", clipOption == 2)) clipOption = 2;
UI.SameLine();
if (UI.Radio("Radio3", clipOption == 3)) clipOption = 3;

最終的なコード

using System;
using StereoKit;

namespace StereoKitProject
{
    class Program
    {
        static void Main(string[] args)
        {
            if (!StereoKitApp.Initialize("StereoKitProject", Runtime.MixedReality))
                Environment.Exit(1);

            Model clipboard = Model.FromFile(@"Assets\Clipboard.glb", Default.ShaderUI);
            Sprite clipLogoSprite = Sprite.FromFile(@"Assets\StereoKitWide.png", SpriteType.Single);
            Pose clipPose = new Pose(.4f, 0, 0, Quat.LookDir(-1, 0, 1));
            bool clipToggle = false;
            float clipSlider = 0;
            int clipOption = 1;

            while (StereoKitApp.Step(() =>
            {
                UI.AffordanceBegin("Clip", ref clipPose, clipboard.Bounds);
                Renderer.Add(clipboard, Matrix.Identity);
                UI.LayoutArea(new Vec3(12, 13, 0) * Units.cm2m, new Vec2(24, 30) * Units.cm2m);
                UI.Image(clipLogoSprite, new Vec2(22, 0) * Units.cm2m);
                UI.Toggle("Toggle", ref clipToggle);
                UI.HSlider("Slide", ref clipSlider, 0, 1, 0, 22 * Units.cm2m);
                if (UI.Radio("Radio1", clipOption == 1)) clipOption = 1;
                UI.SameLine();
                if (UI.Radio("Radio2", clipOption == 2)) clipOption = 2;
                UI.SameLine();
                if (UI.Radio("Radio3", clipOption == 3)) clipOption = 3;
                UI.AffordanceEnd();
            })) ;

            StereoKitApp.Shutdown();
        }
    }
}

f:id:haikage1755:20200404172225g:plain:w350

まとめ

今回はStereoKitをGettingStartedをなぞってアプリの作り方を紹介しました。 まだ、新しいライブラリなので機能の不足もあるかと思いますが、そんなに複雑のことをしないのであれば非常に構築が簡単です。 プロジェクト時代はC#のプロジェクトなのでNugetを使ったライブラリの管理などもUnityと比較して楽そうでよいです。(CodeStrippingとか気にしなくていい...

[デコシノニッキ]は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。」