HoloLensで始めるAzure BLOB Storage ~AssetBundleを添えて~

Azure Advent Calendar 2017の20日目の記事です！

HoloLens開発をしていて、外部からFBXなどの3Dオブジェクトを落としてくる仕組みを知りたかったので実装しました。
得意のAzureを使用し、UnityのAssetBundleという仕組みを利用して実現しています。

#環境
OS：Windows 10 Fall Creaters Update
Unity：2017.2.0f3
MRToolKit：MixedRealityToolkit-Unity-2017.2.0.1-Stabilization
**開発環境についてはご注意ください。
この組み合わせ以外でも実行できるかもしれませんが、
私のPCでは、Unityのバージョンが異なっていたりすると、
とたんに異常終了を繰り返すことになりました（かなり嵌りました・・）
UnityのバージョンとMRTKのバージョンもなかなかセンシティブです**

参考にするAzure Blob Storage DemosがUnity 2017.2を想定されているため、
必然的にMRToolkitも2017.2以降のものが必要となります。
MRToolkitが2017.2ということで、OSもFall Creaters Updateを使います。

■参考
https://github.com/Microsoft/MixedRealityToolkit-Unity/blob/master/UpgradeGuide.md

#流れ
１．Azure Blob Storageデモの動作確認
２．AssetBundleの準備
３．アップロードしたAssetBundleの動作確認＆HoloLens側の実装
４．HoloLensで実行確認

##１．Azure Blob Storageデモの動作確認
<ol>
<li>

まず、下記をダウンロードもしくはCloneします。
https://github.com/Unity3dAzure/StorageServicesDemo

</li>

<li>

プロジェクトをUnityで開いてみてください。
多くのコンパイルエラーが発生するはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/593921ad-f6ea-6873-1b86-f9c09a033831.png)

なので、このデモが依存している下記２つのライブラリを別途DLする必要があります。
https://github.com/Unity3dAzure/StorageServices
https://github.com/Unity3dAzure/RESTClient

</li>

<li>

StorageServices、RESTClientの両方をDLもしくはクローンし、
StorageServicesDemoのAsset直下に配置してください。

■StorageServices
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/0277b237-45f5-e11e-c714-3fe4b074ba30.png)

■RESTClient
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/1f645e88-9f34-9abb-cce6-930fc34f2b91.png)
httpとmodelフォルダは名前が重複しているので、そのままUnity上にドラッグ＆ドロップしてしまうと
別フォルダができあがってしまいます。
フォルダ内部にきちんとインポートしてあげてください。

インポートが終わった後のProjectビューは下記のようになります。
コンパイルエラーも出なくなっています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/3581690b-af70-996e-2db5-a218bccef694.png)

</li>

<li>

そして、下記の動画に従い、Azure側の設定、コンテナやBLOBの作成を行います。
[Azure Blob Storage for Unity3d](https://www.youtube.com/watch?v=0gpg2xwusjM)

[![ビデオが開けなかった場合に表示されるテキスト](http://img.youtube.com/vi/0gpg2xwusjM/0.jpg)](http://www.youtube.com/watch?v=0gpg2xwusjM)


途中でStorageServices (AssetBundle Demo)というGameObjectに対して
AzureBLOBのStrageAccount、AccessKey、Containerの設定も行うことになります。

*動画内ではAssetBundle以外の動作も確認しています。一通り実施してみることをおすすめします。
このQiita記事においては、AssetBundleのシーンのみを対象として記載を続けます。*


**※BLOBは有料プランしかないようなので、認識の上で作業してください。
うっかり削除を忘れて課金されていた、ということにもなりますので・・（下記はダメな例）**
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">2ヶ月くらい前にサンプルで作ったAzureのDocumentDBがずっと生きてたことを今月になって課金明細が来て気づいた。。。。くそう。。</p>&mdash; morio@12/16HoloLensMeetupVol.6 (@morio36) <a href="https://twitter.com/morio36/status/887629437654622208?ref_src=twsrc%5Etfw">2017年7月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</li>

<li>

StorageServicesDemo\Assets\Demos\AssetBundle\AssetBundle.unityのシーンを開いてみてください。
動画の通りに進めていると、StorageServicesDemo\AssetBundles配下にAssetBundle「cloud-？？？.unity3d」ができており、これをBLOBへ格納するところまで済んでいるはずです。
（？？？はプラットフォームごとに命名。Unity側の環境構築が済んでいるものについて、AssetBundleをビルドする仕組みになっている）

</li>

<li>

StrageAccount、AccessKey、Containerの設定を行った後、Playボタンを押下します。
Gameビュー内「Load Asset Bundle」を押下すると、下記のようにAzure Blobから落としてきたCubeが表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/d4c1837a-9f41-4357-3628-dcee8da72f2d.png)

</li>

ここまでで、デモの確認はOKです。
次はオリジナルコンテンツのAssetBundle化を行います。

##２．AssetBundleの準備
下記の記事を参考に、AssetBundleビルド用のプロジェクトを作成していきましょう。

[AssetBundleを使って3Dオブジェクトを動的に取り込む（その１）](http://bluebirdofoz.hatenablog.com/entry/2017/06/12/004334)

<ol>
<li>

私はそのままCreateAssetBundlesというプロジェクトを作成し、
Assets直下にCreateAssetBundles.csを作ってみました（この場所には問題があり、後程移動することになります）。

```CreateAssetBundles.cs
using UnityEditor;

public class CreateAssetBundles
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        BuildPipeline.BuildAssetBundles("Assets/AssetBundles", BuildAssetBundleOptions.None, BuildTarget.WSAPlayer);
    }
}
```

</li>

<li>

モデルは下記のページから落とせる3Dモデル「fujikyun_3d_color.fbx」を使用してみます。
https://www.city.fujisawa.kanagawa.jp/kankou/fujikyun.html

![8.PNG](https://qiita-image-store.s3.amazonaws.com/0/114630/c32ce4fa-c330-42df-215a-15226bd1a633.png)

</li>

<li>

これをCreateAssetBundlesプロジェクトにインポートします。
Assets/modelsフォルダを作成し、fbxファイルを入れてあげてください。
***下記のエラーが出ますが、実行には影響無さそうなのでいったん無視します。。***

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/8ff55d9e-56ad-8912-cbae-635e9a9cbb88.png)

</li>

<li>

フォルダmodelsをProjectタブで選択し、AssetLabelsの下部にある
AssetBundleリストを選択します。

![1a.png](https://qiita-image-store.s3.amazonaws.com/0/114630/fb7e3d92-0276-11fc-5880-5c6714207e1b.png)

Newを選択し、「fujikyun」と入力します。

![2a.png](https://qiita-image-store.s3.amazonaws.com/0/114630/c40582ea-d59a-74d1-2bd5-d8293f256c8f.png)

</li>

<li>

BuildSettingsにてUWPへスイッチプラットフォームを行い、PlayerSettingsから
ScriptingBackendを「.NET」に変更してください。

![3.PNG](https://qiita-image-store.s3.amazonaws.com/0/114630/1d8c7f11-30fa-49c5-1127-ac386571937d.png)

**ここで注意すべき点は、AssetBundleビルド時もスクリプティングバックエンドをIL2CPPではなく.NETにしておくことです。
AssetBundleビルド時と、AssetBundleの使用時（HoloLens実行時）の
スクリプティングバックエンドは同一でないといけない、という制約があるそうです。**
[Asset Bundle Can't be loaded because it was not built with the right version or build target - Unity Forum](https://forum.unity.com/threads/asset-bundle-cant-be-loaded-because-it-was-not-built-with-the-right-version-or-build-target.376688/)

**私はこちらの設定を行わないままIL2CPPでビルドし、HoloLensで利用していたため、**
> SEHException: External component has thrown an exception.

**が発生していました。**

</li>

<li>

Assets/AssetBundlesフォルダを作成し、メニューからBuild AssetBundleを選択してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/443a2df5-e6f5-b69e-53be-a9f4aa522650.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/48ce0b3d-c928-0fed-c8ca-cc3a2e927312.png)

はい。エラーが出ましたね。
これは、スクリプトCreateAssetBundles.csの配置場所がまずいようです。

■参考記事
[Unity Editor 用のスクリプトが exe を作るときにエラーになる 凛(kagring)のUE4とUnityとQt勉強中ブログ](http://kagring.blog.fc2.com/blog-entry-13.html)
[特殊フォルダーとスクリプトのコンパイル順 - Unity マニュアル](https://docs.unity3d.com/ja/current/Manual/ScriptCompileOrderFolders.html)

</li>

<li>

EditorフォルダをAsset配下に作成し、CreateAssetBundles.csを移動してください。
再度Build AssetBundleを実行します。

</li>

<li>

ビルドに成功すると、下記のようにAssetBundlesフォルダにファイルができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/6286e3da-ac3c-37e6-a3ff-2a48b9658a39.png)

</li>

<li>

作成したfuikyun（拡張子なし）をAzure BLOBにアップロードしましょう！

![4.PNG](https://qiita-image-store.s3.amazonaws.com/0/114630/90c62733-77df-8595-3e86-57abdbaf9352.png)

</li>

**これでAssetBundleの準備も完了です！**

##３．アップロードしたAssetBundleの動作確認＆HoloLens側の実装
早速HoloLensで実行といきたいところですが、一度Unityエディタ側で動作を確認しましょう。
先ほどアップロードしたFBXを、Unity側にインポートせずに直でGameビューに落とせたら成功です！

<ol>
<li>MixedReality Toolkitの設定
MRToolkitをインポートし、MixedRealityToolkitメニューからいつもの設定をチェックしてください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/7e4901ee-1f6c-a9be-31ac-f430bd7d0b90.png)


![6.png](https://qiita-image-store.s3.amazonaws.com/0/114630/f05c79fb-ed26-a24a-5a1e-e523a5a0233b.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/0fba0c79-5c1a-74f8-6a7a-44cf74ef3271.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/ba52c1c2-ef3e-6ffd-28b6-4a3228154d63.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/041b46ab-1c5e-a587-dab4-bd6159135b48.png)


スイッチプラットフォームでUWPに設定し、PlayerSettingsのScriptingBackendが.NETであることを確認してください。
</li>

<li>

次にAssetBundleDemo.csにオリジナルメソッドを追加します。
下記のメソッドでAssetBundleをロードしているようなので、
これをコピーしてオリジナルメソッドを作りましょう。

```AssetBundleDemo.cs
  public void TappedLoadAssetBundle()
  {
    UnloadAssetBundle();
    string filename = assetBundleName + "-" + GetAssetBundlePlatformName() + ".unity3d";
    string resourcePath = container + "/" + filename;
    Log.Text(label, "Load asset bundle: " + resourcePath);
    StartCoroutine(blobService.GetAssetBundle(GetAssetBundleComplete, resourcePath));
  }

  private void GetAssetBundleComplete(IRestResponse<AssetBundle> response)
  {
    if (response.IsError)
    {
      Log.Text(label, "Failed to load asset bunlde: " + response.StatusCode, response.ErrorMessage, Log.Level.Error);
    }
    else
    {
      Log.Text(label, "Loaded Asset Bundle:" + response.Url);
      assetBundle = response.Data;
      StartCoroutine(LoadAssets(assetBundle, "CloudCube")); ;
    }
  }
```

■下記のメソッドを追加

```AssetBundleDemo.cs
    public void TappedLoadOriginalAssetBundle()
    {
        UnloadAssetBundle();
        string filename = "fujikyun";
        string resourcePath = container + "/" + filename;
        Log.Text(label, "Load asset bundle: " + resourcePath);
        StartCoroutine(blobService.GetAssetBundle(GetOriginalAssetBundleComplete, resourcePath));
    }

    private void GetOriginalAssetBundleComplete(IRestResponse<AssetBundle> response)
    {
        if (response.IsError)
        {
            Log.Text(label, "Failed to load asset bunlde: " + response.StatusCode, response.ErrorMessage, Log.Level.Error);
        }
        else
        {
            Log.Text(label, "Loaded Asset Bundle:" + response.Url);
            assetBundle = response.Data;
            StartCoroutine(LoadAssets(assetBundle, "fujikyun_3d_color.fbx")); ;
        }
    }
```

</li>

<li>

エアタップをトリガーにTappedLoadOriginalAssetBundleを呼び出すためのスクリプトを新規作成します。

```LoadMyAssets.cs
using UnityEngine;
using HoloToolkit.Unity.InputModule;

public class LoadMyAssets : MonoBehaviour,IInputClickHandler {

    public AssetBundleDemo demoObject;

    public void OnInputClicked(InputClickedEventData eventData)
    {
        demoObject.TappedLoadOriginalAssetBundle();
    }
}

```

</li>

<li>

ヒエラルキーでCubeを新規作成し、スケールを0.3ほどにします。
ポジションはZ軸を2くらいでよいです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/7025f867-60da-af04-3506-91b3eb8c0e10.png)


先ほど作成したLoadMyAssets.csをCubeにアタッチしてください。
LoadMyAssetsのAssetBundleDemo にはシーン上にある「StorageServices (AssetBundle Demo)」をセットしてください。

</li>

<li>

もともとあるCanvasはインスペクタからチェックを外してDisabledにしてしまいましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/9e96afbb-6261-320d-1700-5edd82b3c4f6.png)

Projectタブはこのような様子になっているはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/ca08f343-f76c-a33e-80fd-81864e255aee.png)


ゲームビューはこのような感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/4327c30e-376a-b127-e3e9-81d2061730ac.png)

</li>

<li>

Playボタンを押下し、CubeをShift+クリックでエアタップしてみてください。

![9.PNG](https://qiita-image-store.s3.amazonaws.com/0/114630/370aa912-6351-d143-3dd8-bf22e9fa5d64.png)


姿は見えませんが、ヒエラルキー上にfujikyun_3d_color(Clone)が登場しました！（まだ先はありますが）いったん成功です！



ですが、同時にエラーも出ていますね。
>
```
MissingComponentException: There is no 'Renderer' attached to the "fujikyun_3d_color(Clone)" game object, but a script is trying to access it.
You probably need to add a Renderer to the game object "fujikyun_3d_color(Clone)". Or your script needs to check if the component is attached before using it.
AssetBundleDemo.AddPrefab (Vector3 position, Quaternion rotation, Vector3 scale, Color color) (at Assets/Demos/AssetBundle/AssetBundleDemo.cs:150)
AssetBundleDemo.AddPrefab (Vector3 position) (at Assets/Demos/AssetBundle/AssetBundleDemo.cs:138)
AssetBundleDemo+<LoadAssets>c__Iterator0.MoveNext () (at Assets/Demos/AssetBundle/AssetBundleDemo.cs:132)
UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)
```

これは、AssetBundleDemoの下記の部分が影響したものです。
GameObjectに存在しないRendererに対して操作しようとしているためですね。

```cs
gameObject.transform.GetComponent<Renderer>().material.SetColor("_EmissionColor", color);
```

</li>

<li>

モデルの位置やエラーの対応を行いましょう。

メソッドLoadAssetsおよびAddPrefab内でモデルのスケールや位置、マテリアルを設定していますので、
こちらを修正することにします（コピーしてメソッドを分ける、でもかまいません）。

デモではAddPrefab(new Vector3(0, 4, 0));という記述でモデルの生成位置をy座標+4にしていますので
これを(new Vector3(0, 0.3f, 2))に変更してみます。

スケールは Vector3.oneが設定されているので、これを大きめにしましょう。
私は今回のモデルサイズに合わせて下記のようにしました。

```cs
  private void AddPrefab(Vector3 position = default(Vector3))
  {
    AddPrefab(position, Quaternion.identity, new Vector3(5, 5, 5), Color.clear);
  }
```

gameObject.transform.GetComponent<Renderer>().material.SetColor("_EmissionColor", color);
はコメントアウトしました。
ついでにgameObject.transform.Rotate(new Vector3(0,180,0));でモデルを反転させてます。

```cs
  private void AddPrefab(Vector3 position, Quaternion rotation, Vector3 scale, Color color)
  {
    if (assetBundle == null || loadedObject == null)
    {
      Log.Text(label, "Load asset bundle first", "Error, Asset Bundle was null", Log.Level.Warning);
      return;
    }
    GameObject gameObject = Instantiate(loadedObject, position, rotation);
    gameObject.transform.localScale = scale;
    gameObject.transform.Rotate(new Vector3(0,180,0));
 //   gameObject.transform.GetComponent<Renderer>().material.SetColor("_EmissionColor", color);
  }
```

</li>

<li>

Playボタンを押下し、Cubeをエアタップしてみましょう！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/3ebbc04a-db9d-f4af-ca51-aa9f21c48568.png)


**ちょっと影がありますが、、出ました！！**

**追記**
ShaderをUnlit/Colorのように変更するとLightの影響を受けなくなりますが、
かなりフラットな仕上がりになりますので、これは見せたいモデルによって変更しましょう。

![10.PNG](https://qiita-image-store.s3.amazonaws.com/0/114630/848e5772-b96b-a8f6-8aac-688b480ff923.png)


</li>

##４．HoloLensで実行確認

いよいよ実機ビルドです！！

<ol>
<li>

AssetBundle以外のシーンのチェックを外し、Buildしてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/5eec577d-5e11-1611-6b9e-ebea2ba194f5.png)

---

ちなみに、Windows Fall Creaters Updateなしでビルドすると、下記のようなエラーが出ます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/090bf599-231d-8370-0421-51ead0bcbc95.png)

> Assets\HoloToolkit\Utilities\Scripts\Extensions\InteractionSourceExtensions.cs(113,43): error CS1929: 'SpatialInteractionController' does not contain a definition for 'TryGetRenderableModelAsync' and the best extension method overload 'InteractionSourceExtensions.TryGetRenderableModelAsync(InteractionSource)' requires a receiver of type 'InteractionSource'

エラー文言でググってみると、下記のページが出てきます。
やはりFCUが必須のように見えます。
['SpatialInteractionController' does not contain a definition for 'TryGetRenderableModelAsync' · Issue #1172 · Microsoft/MixedRealityToolkit-Unity](https://github.com/Microsoft/MixedRealityToolkit-Unity/issues/1172)

---

</li>

<li>

HoloLensで実行すると・・・

エアタップして約40秒（長い・・・）

![giphy-downsized-large.gif](https://qiita-image-store.s3.amazonaws.com/0/114630/79ba8464-38a1-f35b-3ffd-366f36941b94.gif)


**出ました！！！！**

モデルのポリゴン数が6万くらいあったので、リトポしたらもっとDLが軽くなるかもしれませんね。

</li>

##おわりに

今回、FBXファイルをAzureからダウンロードしてHoloLens上で表示するというのを試してみました。
これができれば、ダウンロードするファイルを動的に選択し、必要なものを必要なタイミングで表示させることができそうですね！
StorageServicesDemoのListSceneのサンプルを見ながら、URLを動的に作成すればいけるはず！

■ListScene
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114630/67b59275-fac7-aa94-062f-5f8f68d4d8ca.png)

Azure BLOB Storageの便利さがわかったので、次はテーブル（表形式）など試してみようと思います！