yotiky Tech Blog

とあるエンジニアの備忘録

HoloLensの2Dアプリランチャー(ウィンドウ)の画像が更新されない場合の対処法

HoloLensでアイコンやスプラッシュなどの画像を設定する項目の中に、2Dアプリランチャー(ウィンドウ)の画像を設定するところがあります。ここの設定で画像を変更してもHoloLensで更新がかからなかったので、その場合の対処法を書いておきます。

目次

TL;DR

2Dアプリランチャー(ウィンドウ)の画像を更新した場合はHoloLens自体を再起動してください。 アイコンとスプラッシュはビルドしてデプロイすれば即時反映されます。なお、ストア周りの画像は動作未確認です。

確認環境

アイコンやスプラッシュなどの画像を生成する

アプリのアイコンやスプラッシュでは、同じ絵面でも何種類ものサイズの画像を用意しなければなりません。 まずは、もととなる画像を一枚用意しましょう。
高橋忍さんのツール「UWP Logo Maker ver.1.0」を使用するか、Visual Studio 2017であれば Package.appxmanifest の [ヴィジュアル資産] タブにある「資産ジェネレーター」でも生成できます。
資産ジェネレーターは出力ファイルを細かく設定できます。特定の資産やスケールに絞れるので何度も生成し直す場合は煩わしさが少ないかもしれません。お手軽に生成するならUWP Logo Makerといったところでしょうか。ただし、サイズなどのに制限がありますのでそこは注意してください。

f:id:yotiky:20190215172616p:plain

さて、HoloLensのアプリに必要な画像のサイズやスケールですが、Submitting an app to the Microsoft Store - Mixed Reality | Microsoft Docsに以下の表があります。

Required Asset Recommended Scale Image Format Where is this displayed?
Square 71x71 Logo Any PNG N/A
Square 150x150 Logo 150x150 (100% scale) or 225x225 (150% scale) PNG Start pins and All Apps (if 310x310 isn't provided), Store Search Suggestions, Store Listing Page, Store Browse, Store Search
Wide 310x150 Logo Any PNG N/A
Store Logo 75x75 (150% scale) PNG Partner Center, Report App, Write a Review, My Library
Splash Screen 930x450 (150% scale) PNG 2D app launcher (slate)

また、HoloLensで推奨するAssetもあるようです。なければ上記表にあるとおり Square 150x150 Logo が使われます。

Recommended Assets Recommended Scale Where is this displayed?
Square 310x310 Logo 310x310 (150% scale) Start pins and All Apps

結局最低限必要なものは、Square 150x150 Logo、Store Logo、Splash Screenでしょうか。 この3点は、UWP Logo Makerのdefault Only オプションで出力されます。Store Logoが推奨より小さい(50x50 100% scale)ですが、それ以外は200% scaleなので推奨以上かと思います。

アプリケーションに設定する

アイコンやスプラッシュなどの設定はUnity のPlayerSettingsにあります。 Iconのブロックにある「Store Logo」と「Universal 10 Tiles and Logos」にそれぞれ設定します。 また、Splash Image ブロックにある「Windows」と「Windows Holographic」にも設定します。 2Dアプリランチャー(ウィンドウ)の設定箇所は、Splash Image ブロックの「Windows」になります。

Icon ブロック

f:id:yotiky:20190215172659p:plainf:id:yotiky:20190215172703p:plainf:id:yotiky:20190215172706p:plainf:id:yotiky:20190215172710p:plainf:id:yotiky:20190215172714p:plain

Splash Image ブロック

f:id:yotiky:20190215172757p:plainf:id:yotiky:20190215172801p:plain

実機で確認する

UnityからC# プロジェクトを出力して生成されるソリューションでは、Assetsフォルダ内にアイコンなどの画像を、Package.appxmanifest の [ヴィジュアル資産] タブで各設定を確認できます。

さて本題ですが、一度も設定したことがないアプリで初めて設定した場合は即時反映されます。しかしそれ以降、画像を更新しても、他の画像に差し替えても、Unityが出力するテンポラリーやUWPフォルダなどを消しても、Unity内でもとの画像自体を消してしまっても、HoloLensの2Dアプリランチャー(ウィンドウ)ではGhostのように最初に設定した画像が出続けます。

HoloLens側で正しく反映するには、HoloLensを再起動(Goodbye&Hello)する必要があるようです。HoloLens側のOS(もしくはMR向けのミドル層あたり?)でキャッシュを持っている感じですね。知ってればなんてことはありませんが、知らないと結構な時間を食われますので気に留めておきましょう。

以下は、検証した際のキャプチャです。
並びの写真で1つ目に対して2つ目は白紙の画像で更新した結果(HoloLensは再起動していない)になります。2Dアプリランチャーだけ以前の画像が出続けています。確認した範囲で使用された画像は、IconブロックのC(Square 150x150 Logo)と、Splash ImageブロックのA(Windows Holographic)、F(Windows)の3点だけでした。

ピンどめしたメニュー
f:id:yotiky:20190215172834j:plainf:id:yotiky:20190215172838j:plain

すべてのアプリ
f:id:yotiky:20190215172852j:plainf:id:yotiky:20190215172856j:plain

スプラッシュ画面
f:id:yotiky:20190215172915j:plainf:id:yotiky:20190215172918j:plain

2Dアプリランチャー
f:id:yotiky:20190215172928j:plainf:id:yotiky:20190215172932j:plain

Visual Studio で Azure Functions に QueueTrigger のAPIを作成する

TL;DR

Visual Studio でQueue Triggerを使ったAzure Functions のAPIを実装します。
開発環境の準備は含まれないため、Visual Studio を使用する Azure Functions の開発 などを参考にしてください。

目次

開発環境

プロジェクトを作成する

  1. Visual Studioで[ファイル > 新規作成 > プロジェクト]を選択し、[Visual C# > Cloud > Azure Functions]を選択して、任意の場所に設定してOKをクリック。 f:id:yotiky:20190102000223p:plain
  2. Queue trigger を選択してOKをクリック。
    • デフォルトでv2が選択されている
    • ローカル実行の場合は、Connection string setting は空でOK f:id:yotiky:20190102000735p:plain
  3. Function1という名前のサンプルが生成される。

Function1.cs

    public static class Function1
    {
        [FunctionName("Function1")]
        public static void Run([QueueTrigger("myqueue-items", Connection = "")]string myQueueItem, ILogger log)
        {
            log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
        }
    }

local.settings.json

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet"
    }
}

QueueTriggerAttributeのQueueNameに指定したキューにデータを登録するとトリガーが発動し処理が動きます。 Local実行の場合はConnectionStringは空で大丈夫です。local.settings.jsonUseDevelopmentStorage=trueが設定されていれば、Azureストレージエミュレータに接続します。1 接続文字列に関してより詳しくは下記のBlogなどが参考になるかと思います。

beachside.hatenablog.com

ローカルで実行する

  1. F5でデバッグ実行。
  2. Cloud Explorer からキューを追加。 f:id:yotiky:20190102000907p:plain f:id:yotiky:20190102000943p:plain
  3. キューが処理されるとログに出力される。 f:id:yotiky:20190102001133p:plain

Newtonsoft.Json.JsonReaderException が発生する場合

デバッグ実行した際に、以下の例外が発生する場合は、デフォルトでセットアップされたMicrosoft.Azure.WebJobs.Extensions.Storageの3.0.0の既知のバグの可能性があるので更新してみてください。2
f:id:yotiky:20190102002419p:plain f:id:yotiky:20190102002617p:plain

インストールされている .NET Core SDK を確認する

バージョンを確認する

コマンドプロンプトもしくはPowerShellを起動して、dotnet --list-sdksと実行します。

C:\>dotnet --list-sdks
1.0.3 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.500 [C:\Program Files\dotnet\sdk]
PS C:\> dotnet --list-sdks
1.0.3 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.500 [C:\Program Files\dotnet\sdk]

.NET Core 2.2 をインストールする

ASP.NET Coreで2.2を使おうとしたら入っていなかったのでついでにインストールします。 こちら のDownload .NET Core SDKをクリックしてインストーラーをダウンロードして実行してください。

C:\>dotnet --list-sdks
1.0.3 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.500 [C:\Program Files\dotnet\sdk]
2.2.101 [C:\Program Files\dotnet\sdk]
PS C:\> dotnet --list-sdks
1.0.3 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.500 [C:\Program Files\dotnet\sdk]
2.2.101 [C:\Program Files\dotnet\sdk]

f:id:yotiky:20181227004959p:plain
before

f:id:yotiky:20181227005017p:plain
after

参考リンク

3ds Maxで地図をトレースしてUnityにオブジェクトを取り込む方法

もくじ

はじめに

対象読者は、3ds Max を触り始めたばかりの人です。(実際、触り始めて2、3日目くらいの記事)

最後まで読むと、下絵とした地図画像をトレースして、Unityに取り込むためのFBXの書き出しと、トレースした結果を画像として保存できます。

エディタで操作する箇所は赤枠で囲んだあたりになります。 f:id:yotiky:20181211163031p:plain

また、単位設定でディスプレイとシステムをcmに設定してます。

手順

下絵の設定

  1. 作成 > ジオメトリ > プリミティブ > 平面 でPlaneオブジェクトを作成
    f:id:yotiky:20181211163244p:plain

    1. 修正 > パラメータ で幅と長さを画像に合わせる(例:800 x 600)
    2. 移動でZを-0.01に

    f:id:yotiky:20181211163455p:plainf:id:yotiky:20181211163457p:plain

  2. レンダリング > マテリアルエディタ > コンパクトマテリアルエディタ を開く(もしくは[M])
    ※モードで、コンパクトとスレートを切り替えられる
    f:id:yotiky:20181211163500p:plain

    1. 自己照明を100に
    2. 拡散反射光の「なし」から、ビットマップを選んでトレースする画像を選択
    3. 終結果を表示 をクリック
    4. シェーディングマテリアルをビューポートに表示 をクリック
    5. Planeオブジェクトを選んで、マテリアルを選択へ割り当て をクリック

    f:id:yotiky:20181211163502p:plain:w300f:id:yotiky:20181211163506p:plain

  3. Planeオブジェクト選んで、右クリック > オブジェクトプロパティ f:id:yotiky:20181211163509p:plain

    1. インタラクティブ
      1. フリーズをチェック
    2. 表示プロパティ
      1. フリーズをグレーで表示をアンチェック
    3. レンダリング制御
      1. 表示を0.5
      2. レンダリング可能をアンチェック

    f:id:yotiky:20181211163512p:plain:w300

  4. Planeオブジェクトを選んで、回転でZを180に

    1. ViewPortは後ろから見た絵にすると楽(Unityのデフォルトの向きに合わせる)

    f:id:yotiky:20181211163516p:plain:w500

トレース

  1. 地面用にPlaneオブジェクトをもう一個追加して、サイズと位置(Zだけ0)を下絵のPlaneオブジェクトに重ねる
    f:id:yotiky:20181211163520p:plain:w500

  2. 作成 > シェイプ > スプライン > ライン で、スプラインを使ってトレース開始

    1. あとで調整するので大体の位置に角を置いて輪っかを作り、必要な分だけ書いてく(各スプラインは必ず閉じる)
      f:id:yotiky:20181211163524p:plainf:id:yotiky:20181211163528p:plain
  3. 各Lineオブジェクトで、頂点を選択して、座標を調整していく

    1. 地面用Planeオブジェクトと重ならないように、移動でZを0.1に
      f:id:yotiky:20181211163532p:plainf:id:yotiky:20181211163535p:plainf:id:yotiky:20181211163538p:plain
  4. 各Lineオブジェクトで、ポリゴンを選択(面としてレンダリングされる)
    f:id:yotiky:20181211163541p:plainf:id:yotiky:20181211163544p:plain:w350

  5. レンダリング > マテリアルエディタ >スレートマテリアルエディタ (もしくは[M])でマテリアルを設定
    f:id:yotiky:20181211163547p:plain

    1. フィジカルマテリアル をダブルクリック
    2. Viewで作成したマテリアルのタイトル部分をダブルクリック
    3. 基本パラメータ > ベースカラーと反射 でカラーを指定
    4. マテリアルをLineにドラッグ・アンド・ドロップ

    f:id:yotiky:20181211163551p:plain:w500f:id:yotiky:20181211163555p:plain:w500f:id:yotiky:20181211163600p:plain:w350

  6. 階層 > 基点 > 基点にのみ影響 で基点のXを90に(UnityのYupに合わせる) f:id:yotiky:20181211163605p:plain:w500

エクスポート

  1. Unityに取り込みたいオブジェクトを選択し、ファイル > 書き出し > 選択を書き出し をクリック
    f:id:yotiky:20181211163651p:plainf:id:yotiky:20181211163653p:plain:w350

  2. アニメーション、カメラ、ライトをアンチェックして書き出す f:id:yotiky:20181211163656p:plain:w500

    1. Warning出るけど無視
      f:id:yotiky:20181211163659p:plain:w500
  3. 出力したFBXをUnityにドラッグ・アンド・ドロップで完了 f:id:yotiky:20181211163705p:plainf:id:yotiky:20181211163708p:plain

トレース結果を画像として保存

  1. 作成 > カメラ > フリー でカメラ追加
    f:id:yotiky:20181211163610p:plain

    1. ストックレンズは200mm
    2. 回転でZを180に
      f:id:yotiky:20181211163613p:plain:w300f:id:yotiky:20181211163620p:plain
  2. レンダリング > レンダリング設定 で出力サイズを元画像に合わせる(例:800 x 600)
    f:id:yotiky:20181211163622p:plain:w230f:id:yotiky:20181211163628p:plain:w230

  3. ビューポートをカメラ[C]にしてセーフフレーム[Shift+F]を表示 f:id:yotiky:20181211163630p:plain:w500

  4. カメラの位置を(X, Y) = (0, 0)にして、黄色い枠にPlaneの縁が合うようにZを遠ざけて調整

    1. 必ずカメラのビューポートを確認しながら
  5. カメラのビューポートをアクティブにして、レンダリング > レンダリング からイメージを保存で画像書き出し
    f:id:yotiky:20181211163633p:plainf:id:yotiky:20181211163641p:plain:w500

地面用Planeオブジェクトを抜いて画像で出力し、下絵の画像に重ねた結果。 f:id:yotiky:20181211163446p:plain:w600

エントリーポイントを探してUnityの森を彷徨う

Unity 基礎シリーズ 目次

Unityの開発を始めてまず探したくなるのがエントリーポイント、アプリケーションが始まる場所である。
サンプルアプリ程度の短いコード量ならサクッと処理を追えるだろうと思うわけだ。だがしかし、これが一向に見つからない。
あるはずのものがなくてちゃぶ台をひっくり返す。冷静さを取り戻し再度Unityと向き合うも何度探してもわからない。
お前は一体どこから走り始めたんだ、、、やがてUnityをそっと閉じる。

目次

エントリーポイントが見つからない理由

Unityはゲームループで動くエンジンである。
エンジンとMonoBehaviourを継承したスクリプトプラグインアーキテクチャのような仕組みになっており、規定の名前のイベント関数を実装することで各々のタイミングでエンジンがキックしてくれる。 f:id:yotiky:20181129031839p:plainf:id:yotiky:20181129031841p:plain

開発者が書くスクリプトは、基本的にはこのMonoBehaviourを継承したクラスになり先の仕組みで動く。イベント関数で一番早いのはAwakeだが、実行タイミングはシーンがロードされ、各GameObject(Component)のインスタンスが生成し終わった後くらいに走り出すと言ったところ。アプリケーションのエントリーポイント、ゲームループが始まるよりも前の処理はエンジン側が隠し持ち、開発者が触れる表層ではゲームループ以降となる。

f:id:yotiky:20181129031935p:plain

では、「どの順でAwakeが呼ばれ、どのGameObject(Component)が最初になるのか」だが、UnityではComponentの実行順は保証されていない。恐らくInstanceID順で実行はされるのだが、このIDは保存されておらず編集やUnityの再起動などで簡単に変わってしまう。(詳しくは「複数のComponentのイベント関数の実行順」を参照)

このような構成のため、Unityを触り始めた人がエントリーポイントを探しても簡単には見つからない、理解できないということになる。

エントリーポイントを作る

最近書くアプリケーションでは、エントリーポイントを意図的に作るようにしている。『MVP4U』1記事でEngineの初期化(こちらはアプリ側のEngine)で軽く触れたが、単一のエントリーポイントを定義し、そこを起点にScript側から波及的に初期化を呼び出す設計だ。 f:id:yotiky:20181129032034p:plain

ここではScenePresenterとしているが、これがエントリーポイントとなる。
ScenePresenter以外の全Presenter(以降Presenter)は、ScenePresenter(以降EntryPoint)に登録されている。
PresenterにはInitializeメソッドが定義されておりここに初期化処理を集約する。AwakeやStartといったイベント関数は持たない。
View側も同様にInitializeを用意し、Presenterの初期化の中でViewのそれを呼び出す。Viewの先にあるComponentについてもInitializeに初期化を定義し、PresenterのInitializeを起点として波及的に呼び出されるようになっている。

    // EntryPoint.cs
    public class EntryPoint : MonoBehaviour
    {
        public PresenterBase[] presenters;

        void Start()
        {
            foreach (var p in presenters)
            {
                p.InitializeOnStart();
            }
        }
    }
    // SamplePresenter.cs
    public class SamplePresenter : MonoBehaviour
    {
        private SampleView view;

        public void InitializeOnStart()
        {
            view = GetComponent<FloorView>();
            view.Initialize();
            // その他必要な初期化処理
        }
    }
    // SampleView.cs
    public class SampleView : MonoBehaviour
    {
        public HogeComponent hoge;

        public void Initialize()
        {
            hoge.Initialize();
        }

イベント関数以外のメソッドの実行に関しては「Scriptのイベント関数の実行順と実行可否」の最後に書いたが、GameObject/Componentの有効/無効に関係なく、Awakeが呼ばれて無くてもメソッドの呼び出しは可能である。これは初期化を行うためにGameObjectをアクティブにしたり、編集中にHierarchy上での状態を気にする必要がないことも意味する。
さらにEntryPointはStart(もしくはAwake)となるため、この時点でGameObject/Componentのインスタンス生成はすべて完了しており、Initializeの波及が問題なく実行される。

注意点としては、Initializeで実行順序を担保するので、AwakeやStartなどと初期化を混同しないことだろうか。意図的に分ける場合は除くが。

余談だがこの実装の場合、UniRxのメソッドチェーンは多くのケースでInitializeの中で宣言的に定義することができる。


  1. Unity向けにMVPを解釈したアーキテクチャ(MVP for Unity)

MonoBehaviourのコンストラクタ/デストラクタ

Unity 基礎シリーズ 目次

目次

検証環境

Unity:2017.4.5f1

MonoBehaviourのコンストラクタ/デストラク

MonoBehaviourを継承したクラスではコンストラクタを書いて初期化をせず、イベント関数のAwakeやStartの中で初期化する。Unityの鉄則の一つ。
理由は、クラスがインスタンス化されるタイミングが分からない上に、実行時以外にもインスタンス化と破棄が繰り返し行われているのと、もうひとつはUnityEngine.Objectのコンストラクタは別スレッドで呼ばれるとのこと。1 実際にMonoBehaviourのコンストラクタ/デストラクタの処理が走るタイミングは次のようになった。GameObject3-1はNon-activeにしてある。末尾はインスタンス判別用の識別子。

f:id:yotiky:20181122094239p:plain

  1. Editor上でPlay

    1. GameObject1.Destructor: 0e031e
    2. GameObject2.Destructor: 9ac3b8
    3. GameObject2-1.Destructor: 8551b3
    4. GameObject3.Destructor: dc8392
    5. GameObject3-1.Destructor: 808b20
    6. GameObject1-1.Destructor: 8f06af

    7. GameObject1-1.Constructor : 9884aa

    8. GameObject2.Constructor : 9fb890
    9. GameObject2-1.Constructor : f9fe82
    10. GameObject1.Constructor : 55321f
    11. GameObject3-1.Constructor : 28671f
    12. GameObject3.Constructor : 1fe677

    13. GameObject3-1.Constructor : 8841c6 (GameObject non-active)

    14. GameObject1-1.Constructor : 216283
    15. GameObject2.Constructor : fac40a
    16. GameObject2-1.Constructor : bcab4a
    17. GameObject3.Constructor : 3f2e53
    18. GameObject1.Constructor : c7c2ac

    19. GameObject1-1.Awake : 216283

    20. GameObject2.Awake : fac40a
    21. GameObject2-1.Awake : bcab4a
    22. GameObject3.Awake : 3f2e53
    23. GameObject1.Awake : c7c2ac

    24. GameObject2-1.Destructor: f9fe82

    25. GameObject2.Destructor: 9fb890
    26. GameObject1.Destructor: 55321f
    27. GameObject3-1.Destructor: 28671f
    28. GameObject1-1.Destructor: 9884aa
    29. GameObject3.Destructor: 1fe677
  2. Editor上でStop

    1. GameObject3-1.Constructor : 2d3d8b
    2. GameObject1-1.Constructor : c72899
    3. GameObject2.Constructor : 28c597
    4. GameObject2-1.Constructor : 7e0981
    5. GameObject3.Constructor : 3f5c8f
    6. GameObject1.Constructor : a22ef8

    7. GameObject3-1.Destructor: 8841c6

    8. GameObject2-1.Destructor: bcab4a
    9. GameObject2.Destructor: fac40a
    10. GameObject1-1.Destructor: 216283
    11. GameObject1.Destructor: c7c2ac
    12. GameObject3.Destructor: 3f2e53

Editor上でPlayするとまずインスタンスの破棄が行われる。その後2度インスタンスが生成されひとつはすぐに破棄される。また、Stopした際にはPlay中のインスタンスは破棄され新たなインスタンスが生成される。 Play/Stopの有無にかかわらず、Editor上での編集中やComponentをアタッチした際にも裏で生成と破棄が繰り返し行われている。最初に破棄されたインスタンスや最後に生成されたインスタンスは編集用のインスタンスであろう。

まとめ

MonoBehaviourを継承したクラスではコンストラクタで初期化は行わない鉄則は守る。
GameObjectはActiveかどうかにかかわらずまずインスタンスが生成され、イベント関数同様にGameObject全体のインスタンスを生成した後、Awakeの処理に移る。