TriLibで3DデータをWebからロードする

#TriLib
UnityのプラグインTriLibについてまとめました。

##AssetBundleでなくTrilibにした背景
何年か前にUnityでランタイム中に、objファイルをロードしたい！Webで動的にobjをアップロード・ダウンロードしたい！と考えていた僕は、まずは正攻法でAssetBundleでのチャレンジを行いました。（サーバー上でUnityの自動バッチ処理）
しかし、AssetBundleのファイルの扱いづらさや、アップロード時にUnityのバッチを実行する際のコスト（しかもmetaが上手く生成されないなどで心が折れたこと）もあり、結局挫折してしまいました。

そんな中、Trilibという便利なプラグインがあることを知り、これならAssetBundle使わなくていいのでは？！とテンション爆上がり。といいうことで、さっそく試していきます。

##購入
購入はアセットストアから。25ドルです。
https://assetstore.unity.com/packages/tools/modeling/trilib-model-loader-package-91777?locale=ja-JP

##参考リンク
Trilib製作者のサイト
https://ricardoreis.net/

ドキュメントはこちら
https://ricardoreis.net/trilib/manual/html/index.html

他の方の書いた記事はこちら
https://www.fast-system.jp/unity-trilib-assets-howto/

http://araruaru.hatenadiary.com/entry/2019/06/24/110711

##Unityからプラグインをインポート
Unityのバージョンは2018.4を使用しています。途中から2019.4に変更しましたが、どちらも動作しています。
まずは購入した後AssetSotreからimportします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/8de7a154-df6a-eb1c-e7ea-d809291c88ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/e7dde64a-8695-54ea-08cd-28652ef1570a.png)

警告が出るので、左側のボタンをクリックしてUnityを再起動を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/9b39d12a-f357-cbf4-24c5-617fdba307b0.png)

#サンプル
TriLibアセット内にサンプルがあるので、見ていきます。
サンプルは、Scene1からScene9まであり、それぞれの機能紹介は以下のページから確認できます。
https://ricardoreis.net/trilib/manual/html/md__c_1__repos__tri_lib187__assets__tri_lib__s_a_m_p_l_e_s.html

##Scene1 アセット ロード サンプル
このサンプルでは、​​ローカルストレージとURIからモデルをロードできます。
Scene1のパスは以下の通りです。

```
Assets/TriLib/TriLib/Samples/Scenes/Scene1 
```
シーン内はこのようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/859ccb7a-b880-897a-b471-50b5e5d40aed.png)
では、Playして動作を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/8ae29c16-3f91-69ad-a4b8-0dec63c4778c.png)

一番上のLoad Local Assetを押すと、Please select a File と出てくるので、objファイルやfbxファイルの存在するディレクトリまで移動します。
特に用意がない場合は、TriLib内のSample内を参照します。

```
C:\Users\804910\Documents\Holo2Smaple\TrilibTest\Assets\TriLib\TriLib\Samples\Models
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/4256e55a-e8dd-7153-7d21-34b1e2829983.png)

今回は、BigModel.objを選択します。このモデルは、ただの立方体です。（ちなみにBouncing.fbxは球体がバウンドするアニメーション付きのfbxデータです。）
立方体が読み込まれているのが確認できます。全体を確認する場合はSpin in X axis にチェックを入れると確認が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/71a695fe-a2ae-34db-cf4d-7f373d34a8a8.png)


Hierarchyにも追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/1e4ebf97-161c-ef0a-5ecc-55c04e1d13da.png)

##URLから取得
次に上から2つ目のボタン「Load remote Asset(URL)」から取得します。
デフォルトでは以下のzipファイルが記載されています。
https://ricardoreis.net/trilib/test1.zip

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/406c463b-98b3-b823-c5ba-d7ede010212d.png)
OKを押すとロードされて、読み込まれる・・と思いきや、エラーが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/2f6ba221-a355-94f9-a3f4-b3464103efd4.png)

No suitable reader found for the file format of file \$\$\$\_magic\_\$\$\$..zip
\$\$\$\_magic\_\$\$\$..zipファイル形式に適したリーダーが見つかりません ということは、読み込む形式を認識できていないようです。。なんで？？？（ちなみにzipファイルの中身は.3dsファイルとテクスチャです）

では、自分でアップロードしたデータではどうでしょうか。
最初にGoogleDriveで試します。GoogleDriveにfbxファイルをアップして、再度チャレンジ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/37c6fb39-984d-c7e7-856b-ca679683589d.png)
しかし、これも先ほどと同様のエラーが出現し、ファイル形式を推定できませんでした。

次に、Amazon S3にfbxファイルをアップロードさせます。パブリックアクセスをすべてブロック系のチェックを外すか、もしくはバケットポリシーできちんとIPを指定してあげます。設定方法はS3に関するほかの方の記事を参考にしてください。
リンクは短縮にならないので、Extensionも認識しているので、行けそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/7f2e44d4-0c09-ef31-9797-5f3ae1631265.png)
読み込めました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/baad53f0-5557-d284-3a91-c0edbd3c515a.png)

AWSは行けましたが、Azureはどうでしょうか。Azure StorageにもAzure Storage Explolerから同様にコンテナを作ってFBXを放り込みます。アクセスレベルの変更は、時間がないのでSet Container AcSet Container Access Level からPublic Access levelに変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ee086eac-9347-1109-d257-8321b61036ba.png)
Azure Storageも同様にExtensionも認識しているので、行けそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/115ec624-dd50-6a18-d860-a6c2585eaafc.png)
Azureも問題なく、読み込みが完了しました。
単体の3dデータは読み込みができました。

以上のように、ロードの処理は問題なく動作しています。それなのにzipファイルの3dデータが読めないのは何故なのか。Web上に上がっているdemoではzipも問題なくロードできています。何故だ！？
https://ricardoreis.net/trilib/demo/

問題の切り分けを行っていきます。
まず最初に疑ったのがUnityのバージョンです、Unity2019.2をインストールして再度確認→同様のエラーが発生
次は、Playしている間だけでBuildしたら問題ないかもしれません→Windowsでビルド→ダメ　
で、フォーラムでもいろいろ見て、、、1.9のバージョンでは解決していると書いてある。
最終的に原因は、Project Settingsのzip読み込み設定にチェックが入っていないという初歩的な見逃しでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/0049e465-06a6-5231-ba07-6c1bf69db125.png)
書いてあるやんけ～
https://ricardoreis.net/?p=341

ということで、無事zipファイルを読み込むことができました。

## Scene2 同期 アセット ロード サンプル
このサンプルは、TriLibプロジェクトフォルダーからモデルを同期的に読み込みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/fae0eb39-e6d2-3b88-93ac-9be9405a4fe8.png)
Playすると、右側に左と同じfbxファイルがロードされます。
動作としては、LoaderオブジェクトのLoadSample.csが実行されています。

```LoadSample.cs
using System;
using UnityEngine;
namespace TriLib
{
    namespace Samples
    {
        /// <summary>
        /// Represents a simple model loading sample.
        /// </summary>
        public class LoadSample : MonoBehaviour
        {
#if UNITY_EDITOR || !UNITY_WINRT
            /// <summary>
            /// Tries to load "Bouncing.fbx" model
            /// </summary>
            protected void Start()
            {
                using (var assetLoader = new AssetLoader())
                {
                    try
                    {
                        var assetLoaderOptions = AssetLoaderOptions.CreateInstance();
                        assetLoaderOptions.RotationAngles = new Vector3(90f, 180f, 0f);
                        assetLoaderOptions.AutoPlayAnimations = true;
                        assetLoaderOptions.UseOriginalPositionRotationAndScale = true;
                        var loadedGameObject = assetLoader.LoadFromFile(string.Format("{0}/Bouncing.fbx", TriLibProjectUtils.FindPathRelativeToProject("Models", "t:Model Bouncing")), assetLoaderOptions);
                        loadedGameObject.transform.position = new Vector3(128f, 0f, 0f);
                    }
                    catch (Exception e)
                    {
                        Debug.LogError(e.ToString());
                    }
                }
            }
#endif
        }
    }
}
```

やっていることは単純で、AssetLoaderOptionsのインスタンスを作り、LoadFromFileメソッドに渡す前にassetLoaderOptionsを変更しています。ロードのオプションは、以下のドキュメントを参照してください。
https://ricardoreis.net/trilib/manual/html/class_tri_lib_1_1_asset_loader_options.html
このサンプルでは、回転軸の設定、FBXのアニメーションとトランスフォームを有効に。ロードはLoadFromFileでアセット内SampleのBouncing.fbxを相対パスで呼び出して、最後にポジションを少しX軸で右側にずらしているだけです。



## Scene3 同期 アセット ダウンロード サンプル
このサンプルは、URIからモデルを同期的にロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/621cc1d0-6265-3a42-5f13-b79f4b07357e.png)

ModelDownloaderオブジェクトに、２つのコンポーネントAsset DownloaderとSimple Rotaterが適用されています。名前の通り、前者がURI読み込みの便利なコンポーネントで、後者がシンプルな回転を加えるだけの機能となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/47f428d0-0e27-c95e-49e1-09204b349e9d.png)
サンプルでは、自動スタート有効、URIは単一の.3dsファイルのURI、タイムアウトは2000（2000msec(2秒)かな、と思いきや、_unityWebRequest.timeoutにそのまま突っ込んでいるので2000sec(2000秒、33分)ですね・・・ファイルサイズがデカいと2秒は余裕で超えるとはいえ、長すぎる気もしますが）、対象の拡張子、ラップするGame Object(読み込んだアセットがこのGameObjectの子要素となります。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/584ca82c-9516-8a37-93a7-e19564e68f03.png)

## Scene4 非同期 アセット ロード サンプル
このサンプルは、TriLibプロジェクトフォルダーからモデルを非同期で読み込みます。
Scene2との違いは、assetLoaderに入れているものと、LoadFromFile時の処理の違いとなります。

Scene2では、同期のため全ての処理が完了してから処理が走っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/815a243a-8f1a-a1f4-dd35-27c59f34e6a8.png)
対して、Scene4では非同期のため、先に組み込まれているGameObjectの処理が走った結果、ロードは少し遅れて処理されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/e28ca065-6ae0-574a-b966-f89b2ce9328c.png)


### 同期アセットロードと非同期セットロードの違い
同期と非同期の読み込みの処理の違いは、製作者のサイトにある基本的な使い方を参照したほうがわかりやすいです。
https://ricardoreis.net/?p=341


```同期ロード.cs
using (var assetLoader = new AssetLoader()) {
    //AssetLoaderOptionsインスタンスを作成します。
    var assetLoaderOptions = AssetLoaderOptions.CreateInstance();   
    
    //AssetLoaderOptionsを使用すると、モデルをロードするためのオプションを指定できます。
    
    //モデルがロードされるゲームオブジェクトを設定します。
    var wrapperGameObject = gameObject;                             
                                                                    
    //モデルを同期的にロードし、myGameObjectに参照を保存します。
    var myGameObject = assetLoader.LoadFromFile("PATH TO MY FILE.FBX", assetLoaderOptions, wrapperGameObject); 
}

```



```非同期ロード.cs
using (var assetLoaderAsync = new AssetLoaderAsync()) {
    // AssetLoaderOptionsインスタンスを作成します。
    var assetLoaderOptions = AssetLoaderOptions.CreateInstance();

    // AssetLoaderOptionsを使用すると、モデルをロードするためのオプションを指定できます。

    //モデルがロードされるゲームオブジェクトを設定します。
    var wrapperGameObject = gameObject;
    
    var thread = assetLoaderAsync.LoadFromFile("PATH TO MY FILE.FBX", assetLoaderOptions, wrapperGameObject, delegate(GameObject myGameObject) {
    //ここで、ロードされたモデルへの参照をmyGameObjectを使用して取得できます。 
    }); //モデルを非同期にロードし、作成されたタスク/スレッドへの参照を返します。
}
```

注：非同期ローダーは読み込みリクエストごとに使用されることを想定しているため、読み込むモデルごとに新しいAssetLoaderAssyncを作成するか、処理が完了したらAssetLoaderAsyncインスタンスを再利用する必要があります。

## Scene5 同期 バッチ アセット ダウンロード サンプル
このサンプルでは、TriLibサーバーから一連のモデルをダウンロードしてロードできます。サンプルが読み込まれた後、読み込みを高速化するためにローカルに保存されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/2560a924-3384-44b9-c33c-201a1f2b2091.png)

DownloadSample.csでは、スタート時にまず、_fileDownloaders = new UnityWebRequest[urls.Length];で必要なURIを入れておき、OnGUI内の、Loadボタンが押された際に既にローカルにデータが存在しているかのチェックを行います。ここで初回は当然未ダウンロードなので、StartCoroutine(DownloadFile(・・・のWebからのダウンロード処理が走ります。(ローカルに保持されている場合はLoadFileで読み込み）
このローカルってどこ？というと、僕の環境だと以下の場所にデータがダウンロードされていました。

```
C:/Users/804910/AppData/LocalLow/DefaultCompany/New Unity Project/test3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/f38d66ae-63be-2054-3faa-052bda8fea10.png)

これはApplication.persistentDataPathで指定しているためであって、用途に合わせてどこに保存するかは調整が必要になるはずです。参考：[unityでplatformによって取得できるパス](https://qiita.com/w_yang/items/8458cd790607d14b1b36)

※LocalLow：このフォルダ（%appdata%/../LocalLow）には移動できないデータが含まれていますが、アクセスレベルも低くなっています。たとえば、保護モードまたはセーフモードでWebブラウザを実行している場合、アプリはLocalLowフォルダのデータにしかアクセスできません。

そして、UnityWebRequest.Getでダウンロードしたファイルが、objやfbxのように単体のファイルであればそのままディレクトリ作成・データコピー・ファイル読み込みを行います。例外として、zip形式の場合はUnzipFromStream関数で、zipを展開してから同様の処理を行います。

```DownloadSample.cs
using System;
using System.IO;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
#if TRILIB_USE_ZIP
#if !UNITY_EDITOR && (NETFX_CORE || NET_4_6 || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
using System.IO.Compression;
#else
using ICSharpCode.SharpZipLib.Zip;
#endif
#endif
namespace TriLib
{
    namespace Samples
    {
        public class DownloadSample : MonoBehaviour
        {
            //Here are our asset URLs
            private string[] urls =
            {
                "http://ricardoreis.net/trilib/test1.zip",
                "http://ricardoreis.net/trilib/test2.zip",
                "http://ricardoreis.net/trilib/test3.zip",
                "http://ricardoreis.net/trilib/test1.3ds"
            };

            //Stores a reference for file downloaders
            private UnityWebRequest[] _fileDownloaders;

            //Reference for the latest loaded GameObject
            private GameObject _loadedGameObject;

            //Start logic
            private void Start()
            {
                //For each asset on the list, we create a slot for a new WWW instance
                _fileDownloaders = new UnityWebRequest[urls.Length];
            }

            //GUI logic
            private void OnGUI()
            {
                //Loop thru all avariable URLs
                for (var i = 0; i < urls.Length; i++)
                {
                    //Gets current URL
                    var url = urls[i];

                    //Gets current WWW instance (if avariable)
                    var fileDownloader = _fileDownloaders[i];

                    GUILayout.BeginHorizontal();

                    //Shows the URL on GUI
                    GUILayout.Label(url);

                    //Checks if current file downloader exists
                    if (fileDownloader == null)
                    {
                        //When clicking the "Load" button
                        if (GUILayout.Button("Load"))
                        {
                            //Destroys the latest loaded GameObject, if avariable
                            if (_loadedGameObject != null)
                            {
                                Destroy(_loadedGameObject);
                            }

                            //Gets current file name, extension, local path and local filename
                            var fileName = FileUtils.GetFilenameWithoutExtension(url);
                            var fileExtension = FileUtils.GetFileExtension(url);
                            var localFilePath = string.Format("{0}/{1}", Application.persistentDataPath, fileName);
                            var localFilename = string.Format("{0}/{1}{2}", localFilePath, fileName, fileExtension);

                            //Checks if local path exists, which indicates the file has been downloaded
                            if (Directory.Exists(localFilePath))
                            {
                                //Loads local file
                                LoadFile(fileExtension, localFilename);
                            }
                            else
                            {
                                //If local path doesn't exists, download the file and create the local folder
                                StartCoroutine(DownloadFile(url, i, fileExtension, localFilePath, localFilename));
                            }
                        }
                    }
                    else
                    {
                        //Shows the current file download progress
                        GUILayout.Label(string.Format("Downloaded {0:P2}", fileDownloader.downloadedBytes == 0 ? 0f : fileDownloader.downloadProgress));
                    }

                    GUILayout.EndHorizontal();
                }
            }

            //Searches inside a path and returns the first path of an asset loadable by TriLib
            private string GetReadableAssetPath(string path)
            {
                var supportedExtensions = AssetLoaderBase.GetSupportedFileExtensions();
                foreach (var file in Directory.GetFiles(path))
                {
                    var fileExtension = FileUtils.GetFileExtension(file);
                    if (supportedExtensions.Contains("*" + fileExtension + ";"))
                    {
                        return file;
                    }
                }

                foreach (var directory in Directory.GetDirectories(path))
                {
                    var assetPath = GetReadableAssetPath(directory);
                    if (assetPath != null)
                    {
                        return assetPath;
                    }
                }

                return null;
            }

            //Loads an existing local file
            private void LoadFile(string fileExtension, string localFilename)
            {
                //Creates a new AssetLoader instance
                using (var assetLoader = new AssetLoader())
                {
                    //Checks if the URL is a ZIP file
                    if (fileExtension == ".zip")
                    {
#if TRILIB_USE_ZIP
                    var localFilePath = FileUtils.GetFileDirectory(localFilename);

                    //Gets the first asset loadable by TriLib on the folder
                    var assetPath = GetReadableAssetPath(localFilePath);
                    if (assetPath == null)
                    {
                        Debug.LogError("No TriLib readable file could be found on the given directory");
                        return;
                    }

                    //Loads the found asset
                    _loadedGameObject = assetLoader.LoadFromFile(assetPath);
#else
                        throw new Exception("Please enable TriLib ZIP loading");
#endif
                    }
                    else
                    {
                        //If the URL is not a ZIP file, loads the file inside the folder
                        _loadedGameObject = assetLoader.LoadFromFile(localFilename);
                    }

                    //Move camera away to fit the loaded object in view
                    Camera.main.FitToBounds(_loadedGameObject.transform, 3f);
                }
            }

            //Downloads a file to a local path or extract all ZIP file contents to the local path in case of ZIP files, then loads the file
            private IEnumerator DownloadFile(string url, int index, string fileExtension, string localFilePath, string localFilename)
            {
                _fileDownloaders[index] = UnityWebRequest.Get(url);
                yield return _fileDownloaders[index].SendWebRequest();
                if (fileExtension == ".zip")
                {
#if TRILIB_USE_ZIP
                using (var memoryStream = new MemoryStream(_fileDownloaders[index].downloadHandler.data))
                {
                    UnzipFromStream(memoryStream, localFilePath);
                }
#else
                    throw new Exception("Please enable TriLib ZIP loading");
#endif
                }

                Directory.CreateDirectory(localFilePath);
                File.WriteAllBytes(localFilename, _fileDownloaders[index].downloadHandler.data);
                LoadFile(fileExtension, localFilename);
                _fileDownloaders[index] = null;
            }

#if TRILIB_USE_ZIP
//Helper function to extract all ZIP file contents to a local folder
        private void UnzipFromStream(Stream zipStream, string outFolder)
        {
#if !UNITY_EDITOR && (NETFX_CORE || NET_4_6 || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
            var zipFile = new ZipArchive(zipStream, ZipArchiveMode.Read);
            foreach (ZipArchiveEntry zipEntry in zipFile.Entries)
            {
                var zipFileStream = zipEntry.Open();
#else
                var zipFile = new ZipFile(zipStream);
            foreach (ZipEntry zipEntry in zipFile)
            {
                if (!zipEntry.IsFile)
                {
                    continue;
                }
                var zipFileStream = zipFile.GetInputStream(zipEntry);
#endif
                var entryFileName = zipEntry.Name;
                var buffer = new byte[4096];
                var fullZipToPath = Path.Combine(outFolder, entryFileName);
                var directoryName = Path.GetDirectoryName(fullZipToPath);
                if (!string.IsNullOrEmpty(directoryName))
                {
                    Directory.CreateDirectory(directoryName);
                }
                var fileName = Path.GetFileName(fullZipToPath);
                if (fileName.Length == 0)
                {
                    continue;
                }
                using (var streamWriter = File.Create(fullZipToPath))
                {
#if !UNITY_EDITOR && (NETFX_CORE || NET_4_6 || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
                    zipFileStream.CopyTo(streamWriter);
#else
                    ICSharpCode.SharpZipLib.Core.StreamUtils.Copy(zipFileStream, streamWriter, buffer);
#endif
                }
            }
        }
#endif
        }
    }
}
```

## Scene6 非同期 アセット ダウンロード サンプル
このサンプルは、URIからモデルを非同期に読み込みます。
このScene6は、ほぼScene4と同じで、4との違いはAsyncにチェックが入っているかどうかです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/b38b436e-c3e0-db8f-c0b9-b0554269e377.png)

## Scene7 同期永続データパス アセット ロード サンプル
このサンプルでは、​​デバイスの永続データパスからモデルをロードできます。
まずはPlayで実行させます。しかし、ローカルのパスが出るだけで、特に何も出てきません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/e4c59013-7cec-2c25-bda8-a72e118cab01.png)
Listening・・とあるので、このローカルパスに対して、ファイルを置いてあげる必要があります。
New Unity Project直下にfbxファイルもしくはobjファイル（TriLibがサポートしている形式）を配置します。そして再度実行するとボタンが現れました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ad93282f-8ab6-1c70-252a-278815b1ed28.png)
ボタンを押すと、データがロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/413cb824-b130-92f4-c13a-7623bac316f4.png)

## Scene8 カスタムIO アセット ロード サンプル
このサンプルは、カスタムデータソースからモデルとテクスチャを読み込みます（データはbase-64文字列でエンコードされます）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/32138f2c-55b0-d16c-e26f-196ab5604089.png)
CustomIOLoadSample.csの中身を見ていきます。
処理内容としては、obj形式のファイルをテキスト形式から読み込む形で実現しています。テクスチャデータもBase64なので、すべて文字列です。
Obj、Mtl、テクスチャ、の文字列からassetLoader.LoadFromMemory(…の関数で読み込んでいます。

```CustomIOLoadSample.cs
using System;
using UnityEngine;
using System.Text;
using STB;
using AOT;

namespace TriLib
{
    namespace Samples
    {
        /// <summary>
        /// Represents a custom IO system file loading.
        /// You can load data from any source that returns a byte array with TriLib.
        /// This sample loads a series of data from embedded data strings.
        /// </summary>
        public class CustomIOLoadSample : MonoBehaviour
        {
            /// <summary>
            /// Obj file data.
            /// </summary>
            private const string ObjData = "mtllib cube.mtl\n\nv -1.000000 -1.000000 1.000000\nv 1.000000 -1.000000 1.000000\nv -1.000000 1.000000 1.000000\nv 1.000000 1.000000 1.000000\nv -1.000000 1.000000 -1.000000\nv 1.000000 1.000000 -1.000000\nv -1.000000 -1.000000 -1.000000\nv 1.000000 -1.000000 -1.000000\n\nvt 0.000000 0.000000\nvt 1.000000 0.000000\nvt 0.000000 1.000000\nvt 1.000000 1.000000\n\nvn 0.000000 0.000000 1.000000\nvn 0.000000 1.000000 0.000000\nvn 0.000000 0.000000 -1.000000\nvn 0.000000 -1.000000 0.000000\nvn 1.000000 0.000000 0.000000\nvn -1.000000 0.000000 0.000000\n\ng cube\nusemtl cube\ns 1\nf 1/1/1 2/2/1 3/3/1\nf 3/3/1 2/2/1 4/4/1\ns 2\nf 3/1/2 4/2/2 5/3/2\nf 5/3/2 4/2/2 6/4/2\ns 3\nf 5/4/3 6/3/3 7/2/3\nf 7/2/3 6/3/3 8/1/3\ns 4\nf 7/1/4 8/2/4 1/3/4\nf 1/3/4 8/2/4 2/4/4\ns 5\nf 2/1/5 8/2/5 4/3/5\nf 4/3/5 8/2/5 6/4/5\ns 6\nf 7/1/6 1/2/6 5/3/6\nf 5/3/6 1/2/6 3/4/6";

            /// <summary>
            /// MTL file name.
            /// </summary>
            private const string MtlFilename = "cube.mtl";

            /// <summary>
            /// MTL file data.
            /// </summary>
            private const string MtlData = "newmtl cube\n  Ns 10.0000\n  Ni 1.5000\n  d 1.0000\n  Tr 0.0000\n  Tf 1.0000 1.0000 1.0000 \n  illum 2\n  Ka 0.0000 0.0000 0.0000\n  Kd 0.5880 0.5880 0.5880\n  Ks 0.0000 0.0000 0.0000\n  Ke 0.0000 0.0000 0.0000\n  map_Ka cube.png\n  map_Kd cube.png";

            /// <summary>
            /// Texture file name.
            /// </summary>
            private const string TextureFilename = "cube.png";

            /// <summary>
            /// Texture file data.
            /// </summary>
            private const string TextureData = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABTSURBVDhPrYrJDcAwDMO8/7hdoAegKAbtJo+W4EdH/MB5TDfka7YHJ0gwt05QOdZGIDvWRjhvFWgXCgcPMH8e6pDFQTTV4HXy0NqDk12B6+03Ii7udAgYy29ORgAAAABJRU5ErkJggg==";

            /// <summary>
            /// Load our custom model from embedded data strings.
            /// </summary>
            private void Start()
            {
                //Creates a new AssetLoader instance to load our model.
                using (var assetLoader = new AssetLoader())
                {
                    var objBytes = Encoding.UTF8.GetBytes(ObjData); //Transforms the OBJ file data into a char array.

                    //Loads the model contained in objBytes, passing:
                    //CustomDataCallback to load our mtlData contents;
                    //CustomExistsCallback to tell our mtlData contents exists;
                    //CustomTextureDataCallback to load our textureData;
                    assetLoader.LoadFromMemory(objBytes, ".obj", null, gameObject, null, CustomDataCallback, CustomExistsCallback, CustomTextureDataCallback);
                }
            }

            /// <summary>
            /// Checks if our custom texture has been requested and returns a new <see cref="EmbeddedTextureData"></see>.
            /// </summary>
            /// <param name="path">Requested texture path.</param>
            /// <param name="basePath">Requested texture base path.</param>
            /// <returns>If our custom texture has been requested, returns a new <see cref="EmbeddedTextureData"></see>, otherwise, returns <c>null</c></returns>
            private static EmbeddedTextureData CustomTextureDataCallback(string path, string basePath)
            {
                //Checks if our custom texture has been requested.
                if (path == TextureFilename)
                {
                    var embeddedTextureData = new EmbeddedTextureData(); //Creates a new EmbeddedTextureData instance.
                    var textureBytes = Convert.FromBase64String(TextureData); //Transforms texture data from a Base64 string into a byte array.
                    embeddedTextureData.DataPointer = STBImageLoader.LoadTextureDataFromByteArray(textureBytes, out embeddedTextureData.Width, out embeddedTextureData.Height, out embeddedTextureData.NumChannels, out embeddedTextureData.DataLength); //Loads our custom texture data from the given byte array.
                    embeddedTextureData.OnDataDisposal = STBImageLoader.UnloadTextureData;
                    return embeddedTextureData;
                }

                //Returns null if our custom texture hasn't been requested.
                return null;
            }

            /// <summary>
            /// Checks if our custom mtl file has been requested and returns a new GC pointer handle for it.
            /// </summary>
            /// <param name="resourceFilename">Requested resource filename.</param>
            /// <param name="resourceId">Requested resource ID.</param>
            /// <param name="fileSize">Output requested resource file size.</param>
            /// <returns>A new <see cref="IntPtr"/> if our custom mtl file has been requested, otherwise, returns a null <see cref="IntPtr"/>.</returns>
            [MonoPInvokeCallback(typeof(AssimpInterop.DataCallback))]
            private static IntPtr CustomDataCallback(string resourceFilename, int resourceId, ref int fileSize)
            {
                //Checks if our custom mtl file has been requested.
                if (resourceFilename == MtlFilename)
                {
                    var mtlBytes = Encoding.UTF8.GetBytes(MtlData); //Transforms our mtlData into a byte array.
                    fileSize = mtlBytes.Length; //Important: sets the fileSize output to our data array length.
                    var dataBuffer = AssimpInterop.LockGc(mtlBytes); //Lock the GC to don't destroy our array while being used.
                    var fileLoadData = AssetLoaderBase.FilesLoadData[resourceId]; //Retrieves the fileLoadData created for this request.
                    fileLoadData.AddBuffer(dataBuffer); //Adds the locked GC buffer to the request buffers list.
                    return dataBuffer.AddrOfPinnedObject(); //Returns our locked buffer pointer.
                }

                //Returns a null IntPtr if our custom mtl file hasn't been requested.
                return IntPtr.Zero;
            }

            /// <summary>
            /// Checks if it's our custom mtl file being requested and returns <c>true</c> to indicate the requested resource exists.
            /// </summary>
            /// <param name="resourceFilename">Requested resource filename.</param>
            /// <param name="resourceId">Requested resource ID.</param>
            /// <returns><c>true</c> if our custom mtl file has been requested, otherwise <c>false</c>.</returns>
            [MonoPInvokeCallback(typeof(AssimpInterop.ExistsCallback))]
            private static bool CustomExistsCallback(string resourceFilename, int resourceId)
            {
                return resourceFilename == MtlFilename;
            }
        }
    }
}
```

## Scene9 進行処理のサンプル
このサンプルは、ロードの進行状況インジケーターを表示しながら、TriLibプロジェクトフォルダーからモデルをロードします。
ProgressHandlingSample.csで処理を行っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/b27dcc4f-2430-49df-3c5a-012bfbdcaade.png)

#HoloLens2用にビルドして読み込んでみる（現在調査中）
HoloLens2アプリでTriLibを利用してみます。
以下の記事が非常に参考になります。
http://araruaru.hatenadiary.com/entry/2019/06/24/110711

新しいSceneを作ります。
Assetsの直下にStreamingAssetsフォルダを作成し、objファイルを配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/2e1c8c92-d0d1-0a61-5cfa-535351fb6e1a.png)

SceneにCreateEmptyでGameObjectを追加して、名前をloadedGameObjectに変更。
NewScriptのコンポーネント追加で以下のように記載します。

```trilibimport.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TriLib;
using System.IO;
using System;

public class trilibimport : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

        var filePath = Application.streamingAssetsPath + "/BigModel.obj";
        byte[] fileBytes = File.ReadAllBytes(filePath);

        using (var assetLoader = new AssetLoaderAsync())
        {
            var assetLoaderOptions = AssetLoaderOptions.CreateInstance();
            assetLoaderOptions.UseOriginalPositionRotationAndScale = true;

            // バイト配列と拡張子がzipだと伝える
            var loadingThread = assetLoader.LoadFromMemoryWithTextures(fileBytes, "zip", assetLoaderOptions, null, delegate (GameObject loadedGameObject)
            {
                // ロード完了後に行われる処理
                // ロードされたfbxのモデルはloadedGameObject割り当てられる
                Debug.Log("Load Finished");
            });

        }
    }
}

```

これでPlayすると、立方体が読み込めているはずです。※GameObjectの位置とカメラの関係は調整が必要です

##HoloLens2の下準備
ホロラボの中村さんの記事が非常に参考になります。
https://speakerdeck.com/hololab/hololens-hands-on-labs
下準備としては、[このリンク](https://github.com/Microsoft/MixedRealityToolkit-Unity/releases)から[Microsoft.MixedReality.Toolkit.Unity.Foundation.2.4.0.unitypackage](https://github.com/microsoft/MixedRealityToolkit-Unity/releases/download/v2.4.0/Microsoft.MixedReality.Toolkit.Unity.Foundation.2.4.0.unitypackage)をダウンロードして取り込みます。読み込んだらデフォルト設定をApplyします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/d9286e0e-90f8-b6e9-4d59-af4128d7aa6b.png)
そして、BuildSettingでSceneを追加した後、UWPにSwitchしてProjectSettingのPlayer＞PublishSettingsでPackageNameを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ce7c6267-2c6b-5f03-7c46-1a405f005641.png)
同じくPlayer内のXR Settingsで、Virtual Reality SDKsにWindows Mixed Realityを設定します。（Depthは16bit）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/10133d76-78e3-2970-b518-65d0ec8afddf.png)
Add to Scene and Configure...でMixedRealityToolkitとMixedRealityPlayspace追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/36d596b2-cb64-f007-e7d6-74980866e1ca.png)
プロファイルをDefaultHoloLens2ConfigurationProfileに変更した後、Copy&Customize→Clone
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/6da19e43-4d85-a3ce-2a91-d1f578bf081c.png)
再度BuildSettingを開き、Buildを行う。

するとエラーが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/49b09494-acd3-079a-7f9d-cc3f491b85ec.png)

重複しているらしいです。何も触ってないぞ！
調べたところ、UWPのPluginsにはx86、x64、arm64の３つのフォルダがあるのですが、arm64の設定がx86になっています。これをarm64に変更してやる必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ebc27310-d649-8031-da55-8fbfcfb11212.png)

トラップはもう一つあります。arm64に入っているdllですが、なぜかarm64でなくただのarmでビルドされています。CMakeでビルドしなおす必要があります。CPUをx86からarm64に変更してApplyします。対象のdllは２つ、あるので忘れずに。assimp.dll、stb_image.dll。

ただし、現状arm64でビルドしても上手く動作しません。
https://forum.unity.com/threads/trilib-on-hololens-2-unable-to-load-dll-assimp.856954/
なぜか
Unable to load DLL 'assimp': The specified module could not be found.
の表示が出続けています。CMakeの方法が悪いのか、それともUnity側の設定が悪いのか、再度確認する必要があります。

現在調査中　後日追記します。