e.blog

主にUnity/UE周りのことについてまとめていきます

Magic Leap One開発入門

概要

最近、Magic Leap Oneの開発を始めたのでそれに対する諸々をメモしておこうと思います。
モバイル開発同様、ちょっとした設定や証明書の設定など、初回しかやらないことなどは忘れがちなので。

※ なお、本記事はUnity2019の、Lumin OS向けの開発がデフォルトで採用されたあとの話となります。

まずはCreator Portalにログイン

まずはMagic Leapの開発者サイトであるCreator Portalにログインします。
ドキュメントやMagicLeap向け開発のセットアップ方法などの情報、またSDKのダウンロードなどが行えるので必須です。

creator.magicleap.com

UnityのプロジェクトをMagic Leap向けに設定する

Unityに対するドキュメントはこちらにあります。

Unity2019以降をインストール

Magic Leap開発をするにはUnity2019以降のバージョンが必要です。それ以降のバージョンであればサポートプラットフォームにLumin OSが含まれています。

Settingsを調整

プラットフォームをLumin OSに

新規プロジェクトを作成、Unityを起動する際、プラットフォームをLumin OSにする必要があります。

XR Legacy Input Helperをインストール

まず、PackageManagerからXR Legacy Input Helpersをインストールします。

f:id:edo_m18:20190710133843p:plain

Lumin SDKをPreferenceに設定

次に、Preferenceから、Magic Leap向けのSDKを設定します。

f:id:edo_m18:20190710133652p:plain

Build Settingsを設定

Player SettingsColor SpaceLinearに変更します。
次に、XR SettingsVirtual Reality Supportedをオンにし、プラットフォームにLuminを追加します。
またStereo Rendering ModeSingle Pass Instancedに変更します。

パッケージのインポートとPrefabの配置

Magic LeapのUnityパッケージをインポートする

Magic LeapのUnityパッケージは、上記ポータルからDownload / InstallしたMagic Leap Package Managerを起動するとパッケージのDownloadなどができます。

起動し、インストールが済むと以下のような画面にUnityパッケージが保存されている場所が表示 されるので、そこからパッケージをインポートします。

f:id:edo_m18:20190710132640p:plain

Magic Leap向けのカメラPrefabをシーン内に配置する

該当のPrefabはAssets/MagicLeap/CoreComponents/内にあります。

f:id:edo_m18:20190528133825p:plain

アプリに署名する

Magic Leapのアプリをビルドするために、アプリに署名をする必要があります。
署名するためには証明書を作成し、適切に設定します。

Magic Leapのポータルにログインすると、証明書を作成するページがあるのでそこでIDなどを登録します。

f:id:edo_m18:20190710171053p:plain

すると、秘密鍵などがまずダウンロードされます。
その後しばらくしてページをリロードすると、上記画像のように右側のダウンロードボタンから証明書をダウンロードすることができるようになります。

それを最初にダウンロードされたフォルダに入れ、そのフォルダごとUnityのプロジェクトに追加します。
ちなみにAssets配下である必要はないので、同階層などに置いておくといいと思います。

その後、Player SettingsのPublishing Settingsで上記の証明書を設定します。

f:id:edo_m18:20190710171211p:plain

コントローラを使う

コントローラのイベントをトラッキングする

UnityEngine.XR.MagicLeap namespaceにあるMLInputを利用します。
以下は簡単に、トリガーのDown / Upのイベントを購読する例です。

using UnityEngine.XR.MagicLeap;

private void Start()
{
    MLInput.Start();
    MLInput.OnTriggerDown += OnTriggerDown;
    MLInput.OnControllerButtonDown += OnButtonDown;
}

private void OnTriggerDown(byte controllerId, float triggerValue)
{
    // do anything.
}

private void OnButtonDown(byte controllerId, MLInputControllerButton button)
{
    // do anything.
}

コントローラのバイブレーションを利用する

バイブレーションを利用するにはMLInputControllerStartFeedbackPatternVibeメソッドを使います。

private MLInputControllerFeedbackPatternVibe _pattern = MLInputControllerFeedbackPatternVibe.ForceDown;
private MLInputControllerFeedbackIntensity _intensity = MLInputControllerFeedbackIntensity.Medium;

// ... 中略 ...

MLInputController controller = _controllerConnectionHandler.ConnectedController;
controller.StartFeedbackPatternVibe(_pattern, _intensity);

コントローラを使ってuGUIを操作する

MagicLeapのSDKの中にExamplesがあるので、それをベースにセットアップするのが早いでしょう。

CanvasのRender ModeをWorld Spaceに変更する

Magic Leapでは、uGUIを空間に配置する必要があるため、uGUIのCanvasのRender ModeをWorld Spaceに変更する必要があります。

ポイントとしては、uGUIのEventSystemオブジェクトにMLInputModuleコンポーネントを追加します。
またそのコンポーネントに、対象となるCanvasを設定します。
どうやら、Lumin SDK 0.21.0からはこの設定はいらなくなったようです。

ちなみに、0.20.0の場合は以下のように設定項目があります。

f:id:edo_m18:20190529133115p:plain

また、対象シーンにあるControllerオブジェクトをシーン内に配置します。
いちおうこれだけでも動作しますが、レーザーポインタみたいなオブジェクトなどは表示されないのでちょっと操作しづらいです。
なので、同シーンに配置されているInputExampleコンポーネントを利用するとそれらが視覚化されます。

ただ、Exampleと名前がついているので、これを複製して独自にカスタムしたほうがよいでしょう。

複数Canvasを使う場合

前述のように、MLInputModuleCanvasを設定する必要があります。
しかし複数のCanvasがシーン内にある場合は、そららを設定することができません。

こちらも前述のように、Lumin SDK 0.21.0からは不要となりました。

0.20.0時代に調べていたら、Magic Leapのフォーラムでまさに同様なことが語られていました。(フォーラムは以下)

forum.magicleap.com

どうやら、Canvasに対してMLInputRaycasterをアタッチすることで複数Canvasでも問題なく動作させることができるようです。
このMLInputRaycasterをアタッチするのは0.21.0でも同様に必要なようです。

ハンドトラッキングを使う

MLにはハンドトラッキングの機能も標準で搭載されています。

ハンドトラッキングを開始する

まずはハンドトラッキングを開始するためにMLHands.Start();を実行します。
実行に失敗したかをチェックして、問題がなければハンドトラッキングが開始されます。

MLResult result = MLHands.Start();

if (!result.IsOk)
{
    Debug.LogErrorFormat("Error: HandTrackingVisualizer failed starting MLHands, disabling script. Reason: {0}", result);
    enabled = false;
    return;
}

ハンドトラッキングを検知する

まず、(必要であれば)MLHandType型のプロパティを用意し、どちらの手のトラッキングをするかを決められるようにしておきます。

private MLHand Hand
{
    get
    {
        if (_handType == MLHandType.Left)
        {
            return MLHands.Left;
        }
        else
        {
            return MLHands.Right;
        }
    }
}

こんな感じ。

そして、対象の手の状態がenumで取得できるので、以下のように評価します。

if (Hand.KeyPose == MLHandKeyPose.Thumb)
{
    // do something.
}

上の例ではサムズアップの状態になったら呼ばれるようにしています。
こんな感じで、MagicLeapが用意してくれている手の形を検知するとそれを知ることができるので、それに応じて処理を分岐させます。

レンダリング

MagicLeapではStereo Rendering ModeにSingle Pass Instancedを使うことができます。
ただこれを利用すると、自作シェーダなどの場合はSingle Pass Instancedに対応した形にしないと正常に動作しなくなります。

このあたりについては凹みさんの記事に詳細が書かれているのでそれを参考にさせてもらいました。
ここではポイントだけ記述します。詳細について知りたい方は凹みさんの記事をご覧ください。

tips.hecomi.com

tips.hecomi.com

シングルパス対応のためにIDを適切に扱う

そもそもなぜ、Single Pass Instancedにすると正常に描画されないのでしょうか。
その理由は、左右の目用のレンダリングを一度、つまりシングルパスで行うためそれに対応する処理を追加しなければならないためです。

より具体的に言えば、GPU Instancingを利用してオブジェクトを1ドローコールで両目用にレンダリングします。また、レンダーターゲットアレイというものを利用してレンダーターゲットを複数(左右の目分)用意しそれを利用して描画します。

つまり左右の目それぞれのオブエジェクトごとに固有の行列などを利用する必要があり、それを適切にセットアップしないとならないのがその理由です。

シェーダ内部ではunity_InstanceIDというstatic変数経由で、現在レンダリング中のオブジェクトの配列のインデックスを取得します。
つまりはこれを適切にセットアップし、配列から情報を取り出すことができれば正常にレンダリングされるようになる、というわけです。

コードセットアップ

なぜこれらの処理が必要なのかは上で紹介した凹みさんの記事にとてもとても詳しく書いてあるので、内部的にどういうことをやっているのかを知りたい方は凹みさんの記事を参考にしてください。

ここではベースとなるシンプルなシェーダに追記していく形で、ざっくりとだけまとめます。

ということで、まずはUnlitなシンプルなシェーダを載せます。
これは、Unityで「Create > Shader > UnlitShader」として生成されたものから、Fog関連の記述を消したものです。

Shader "Unlit/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

これをそのままマテリアルにして適用するとSingle Pass Instancedの設定の場合は片目になにも描画されなくなります。

pragmaを設定する

まず#pragma multi_compile_instancingを追加します。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing // 追加

これを追加すると、以下のようにマテリアルのインスペクタにEnable GPU Instancingという項目が追加されます。(当然チェックを入れます)

f:id:edo_m18:20190711165358p:plain

これでGPU Instancingを利用する準備ができました。
以下から、このインスタンシングを利用するためのコードに変更していきます。

コードをインスタンシング対応のものにする

手始めに頂点/フラグメントシェーダの入力の構造体にマクロを追加します。

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
};

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
    UNITY_VERTEX_OUTPUT_STEREO     // 追加
};

これらはGPU Instancingを利用するにあたってインスタンスのIDを適切に扱うためのものになります。

そして次に頂点シェーダにもマクロを加えます。

v2f vert (appdata v)
{
    v2f o;

    UNITY_SETUP_INSTANCE_ID(v);               // 追加
    UNITY_INITIALIZE_OUTPUT(v2f, o);          // 追加
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); // 追加

    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

インスタンスIDのセットアップとフラグメントシェーダへの出力を設定します。

次に、コンスタントバッファの宣言を追加し、フラグメントシェーダで利用できるようにします。

UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

ちなみにコンスタントバッファ(定数バッファ)は、GLSLで言うところのuniform変数やuniform blockに相当するものです。(以下の記事を参考に)

docs.microsoft.com

なので、C#側から送る値だったりインスペクタで設定するプロパティだったりは、個別に必要なデータに関してはこのコンスタントバッファの定義方法を用いて適切に設定する必要があります。

具体的には、uniformとして定義する変数はほぼそれで定義しておくと考えるといいと思います。

最後にフラグメントシェーダです。

UNITY_SETUP_INSTANCE_ID(i);

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

頂点シェーダから送られてきたインスタンスIDを取り出し、適切にパラメータを扱います。
上記のコンスタントバッファのところでも説明しましたが、通常のシェーダであればuniformな変数_Colorを定義しそれを利用するだけでよかったものを、上記のようにマクロを経由して使う必要がある、というわけです。

余談

ちなみに、凹みさんが記事を書いているときのUnityのバージョンの問題なのか、Unity2019.1.4f1では以下のようにしないとエラーになっていたので書き換えました。

        UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
        UNITY_INSTANCING_BUFFER_END(Props)

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

マクロ展開後のコード例

最後に、上記のマクロを展開したらどうなるかをコメントしたコード全体を載せておきます。
なお、以下のコードの展開例はあくまで一例です。グラフィクスAPIやその他の設定に応じていくつかの分岐が存在するため、詳細について知りたい方はUnityInstancing.cgincHLSLSupport.cgincを適宜参照してください。

Shader "Unlit/Sample"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加

                // 以下のマクロを経由して「stereoTargetEyeIndexSV, stereoTargetEyeIndex」が追加される。
                // #define UNITY_VERTEX_OUTPUT_STEREO DEFAULT_UNITY_VERTEX_OUTPUT_STEREO
                // #define DEFAULT_UNITY_VERTEX_OUTPUT_STEREO uint stereoTargetEyeIndexSV : SV_RenderTargetArrayIndex; uint stereoTargetEyeIndex : BLENDINDICES0;
                UNITY_VERTEX_OUTPUT_STEREO     // 追加
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            // #define UNITY_INSTANCING_BUFFER_START(buf) CBUFFER_START(buf)
            // #define CBUFFER_START(name) cbuffer name { 
            UNITY_INSTANCING_BUFFER_START(Props)

                // #define UNITY_DEFINE_INSTANCED_PROP(type, var)  type var;
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

            // #define CBUFFER_END };
            UNITY_INSTANCING_BUFFER_END(Props)

            v2f vert (appdata v)
            {
                v2f o;

                // DEFAULT_UNITY_SETUP_INSTANCE_IDはいくつか定義が分散しているので詳細は「UnityInstancing.cginc」を参照。
                // #define UNITY_SETUP_INSTANCE_ID(input) DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
                // #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UnitySetupCompoundMatrices(); }
                UNITY_SETUP_INSTANCE_ID(v);

                UNITY_INITIALIZE_OUTPUT(v2f, o);

                // DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREOもいくつか定義があるので「UnityInstancing.cginc」を参照。
                // #define UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)
                // #define DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) output.stereoTargetEyeIndexSV = unity_StereoEyeIndex; output.stereoTargetEyeIndex = unity_StereoEyeIndex;
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

                fixed4 col = tex2D(_MainTex, i.uv);

                // #define UNITY_ACCESS_INSTANCED_PROP(arr, var) arr##Array[unity_InstanceID].var
                col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

                return col;
            }
            ENDCG
        }
    }
}

開発環境についてのメモ

Magic Leap Remote for Unityを使って実機でプレビューする

まずはドキュメント。

creator.magicleap.com

Magic Leap Remoteを起動する

Remoteでの確認をするのに、専用のアプリを利用します。
ドキュメントは以下。

Learn | Magic Leap

適切にセットアップが終わっていれば、Magic Leap Package Managerがインストールされているはずなので、その中のLumin SDKをリストから選択、さらにその後Use ML Remoteボタンを押してアプリを起動します。

f:id:edo_m18:20190528161618j:plain

また、Unity Editorから実機に転送できるようセットアップを行います。

Player Settingsから、Windows向け設定のAuto Graphics API for Windowsのチェックをはずし、OpenGL Coreをリストに追加します。

f:id:edo_m18:20190528162003g:plain

続いて、必要なパッケージをインポートします。
Magic Leap向けのプラグインをインポートしている場合は、メニューにMagic Leap用のものが追加されているので、そこから必要なパッケージをインポートすることができます。

Magic Leap > ML Remote > Import Support Librariesに該当のライブラリがあります。

f:id:edo_m18:20190528162232p:plain

そして同じメニュー内にMagic Leap > ML Remote > Launch MLRemoteと、MLRemoteを起動する項目があるので起動します。

起動すると以下のようなウィンドウが表示されるので、Start Deviceボタンを押下して実機に接続します。

f:id:edo_m18:20190528163228j:plain

あとはUnity Editorのプレイモードに入れば、自動的に描画結果が実機に転送されプレビューできるようになります。

コマンドラインを扱う

Magic Leap OneのSDKにはコマンドラインツールも含まれています。

利用するのはmldb.exeです。
コマンド自体は以下のようなパスに保存されています。(デフォルト設定の場合)

C:\Users\{USER_NAME}\MagicLeap\mlsdk\{VERSION}\tools\mldb\

接続されているデバイスのリストを表示する

$ mldb devices

コマンドラインからmpkファイルをインストールする

mpkファイルをコマンドラインからインストールするには以下のようにします。

$ mldb install /path/to/any.mpk

また、すでにインストール済のものを上書きインストールする場合は-uオプションを使用します。

$ mldb install -u /path/to/any.mpk

コマンドでできることをまとめてくれているサイトがあったので紹介しておきます。

littlewing.hatenablog.com

その他Tips

Lumin OSを選択している場合のPlatform Dependent Compilation

PLATFORM_LUMINを利用する。

#if PLATFORM_LUMIN
// for lumin
#endif

Magic Leap Oneの映像をモバイルのコンパニオンアプリにミラーリングする

まだbeta版のようですが、コンパニオンアプリを使うことでミラーリングすることができます。
(ただし、コンパニオンアプリはアメリカのStoreでしか落とせないのでちょっとごにょごにょしないと手に入りません。無料です)

www.magicleap.care

ハマった点

ARKit関連でLinker error

ARKitを使っているARプロジェクトなどをMagic Leapに移植しようとして、ARKit関連のSDKが残ったままだと以下のようなエラーが出てしまいます。

In function `UnityARVideoFormat_EnumerateVideoFormats_m1076262586' : undefined reference to `EnumerateVideoFormats'

利用している箇所で#if UNITY_EDITORのみとなっている箇所が、Luming OSプラットフォームだとDLLを参照しにいこうとしてコケるやつです。
なので、分岐を追加することで回避できます。

こちらの記事にも似たようなことが書かれています。
(ただ、バージョン違いなのか自分の環境ではARVideoFormat.csというファイル名でした)

bitbucket.org

PlacenoteなどのライブラリをSymboliclinkで参照を作る

上記と似たような問題ですが、今回の開発では元々ARKit向けに作っていたものを改修する形で対応しました。
なので元々ARKit用のプラグインなどが入っていて、いくつかのライブラリに関してはそのままでも大丈夫だったのですが、場合によってはビルドがまったくできなくなってしまいます。

そこで、各プラットフォームごとに必要なSDKなどをSymboliclinkにして読み込ませる、という方法を取りました。

もっとスマートなやり方がある気もしますが、ビルド時に対象フォルダを外す、などはあまり簡単にできそうではなかったのでこの方法を選択しました。
(もし他の方法を知っている人いたら教えてください)

$ new-item -itemtype symboliclink -path D:\MyDesktop\UnityProjects\078_ar_city\Assets -name Placen
ote -value D:\MyDesktop\UnityProjects\078_ar_city\Placenote

satococoa.hatenablog.com

ネットワークで通信ができない

最初にビルドしたときはできていた気がしたんですが、途中からなぜかネットワークに接続できない現象が。

調べてみたら、以下の記事がヒット。
ただ、最終的にはマニフェストファイルで解決したんですが、ランタイムで個別に権限の確認などが行えるスクリプトが最初から用意されているらしく、メモとして残しておきます。

medium.com

OSやSDKのupdateに伴う変更

Magic LeapにはPackageManagerがあるので、それでSDKなどのバージョン管理などを行います。
なのでアップデートがあったときはそこからインスールし、さらにバージョン管理も(フォルダ分けも)自動的に行ってくれるので非常に楽です。

が、Unity側で指定しているSDKのパスは、エディタ上で設定を変更しないとなりません。
設定自体は特にむずかしいことはないのですが、しばらく開発をしていて久々にアップデートした際に忘れがちになるので、メモとして残しておきます。

設定自体はAndroid SDKなどと同様に、Editor > Preferencesから開く設定画面で、以下の箇所に設定項目があります。

f:id:edo_m18:20190603140823p:plain

イベントを登録しているとuGUIへのイベントが発火しない?

ちょっとまだしっかりと調査していないのですが、ちょっとハマったのでメモ。
MLInputの各種イベントを購読して処理をしていたら、その処理を追加したあとなぜかuGUIへのイベントの発火が止まってしまい、uGUIを操作できなくなった。

処理終了後にそれらのイベントを購読解除したら正常に動いた。

最後に

ARKitを用いたモバイルARと、いわゆるARグラスを用いたAR体験はだいぶ色々なものが異なるので、プロジェクト的に一緒に開発していくのはややきびしいかもなーというのが正直な感想でした。

一番感じた点としては、ARKitなどのモバイルAR開発であったとしても、ARグラス向けを意識して開発しておくと後々幸せになれるかもしれません。(Hololens2も控えているし)