HoloLensでFilepickerから選択したVRMファイルを読み込む


# はじめに
HoloLensでVRMファイルを読み込んで表示してみました。
VRMファイルはFilePickerを使ってフォルダから選択できるようにしました。

サンプルプロジェクトこちら ⇒ [VRMRuntimeLoaderSampleForHoloLens](https://github.com/sotanmochi/VRMRuntimeLoaderSampleForHoloLens/releases/tag/v0.1)

いくつかハマりどころがありましたが、ポイントは以下の2点です。

- Player SettingsのScripting BackendをIL2CPPに設定する。
- IL2CPPビルドでファイルを読み込む時はWWWクラスを使う。
（WWWクラスを使わないとDirectoryNotFoundExceptionが発生する）

![VRMHoloLens01.jpg](https://qiita-image-store.s3.amazonaws.com/0/162000/519ba8b5-ac86-4070-b116-8f2927d82adc.jpeg)
![VRMHoloLens02.jpg](https://qiita-image-store.s3.amazonaws.com/0/162000/9dbe05e6-a203-c03c-f93e-1cfbd5e51dac.jpeg)

# 開発・検証環境
- [UniVRM v0.43](https://github.com/dwango/UniVRM/releases/tag/v0.43)
- Unity 2017.4.9f1
- Visual Studio 2017
- HoloLens RS5 Preview (OS Build: 17720.1000)

# 事前準備・設定
## UniVRMのインポート
[UniVRM v0.43](https://github.com/dwango/UniVRM/releases/tag/v0.43)のunitypackageをダウンロードしてインポートする。
![UniVRM.png](https://qiita-image-store.s3.amazonaws.com/0/162000/aab8c275-118c-aa29-3361-fd5b19ee3ca1.png)

## IL2CPPビルドの設定
Player Settings/Other SettingsのScripting BackendをIL2CPPに設定する。
![IL2CPP.png](https://qiita-image-store.s3.amazonaws.com/0/162000/d669dd14-3877-89ad-0ac6-75c372e50d57.png)

## Capabilitiesの設定
VRMファイルが置いてあるフォルダにアクセスできるようにCapabilitiesを設定する。
今回はObjects3Dフォルダを使うため、Player Settings/Publishing Settings/CapabilitiesのObjects3Dにチェックを入れておく。
![Capabilities.png](https://qiita-image-store.s3.amazonaws.com/0/162000/04414047-be55-89e4-0d2c-8b04e7f4e9c4.png)

# VRMファイルを選択してランタイムロードする
## サンプルスクリプト
以下のようなスクリプトを空のゲームオブジェクトにアタッチします。

```VRMRuntimeLoaderUsingFilePicker.cs
using System.Collections;
using UnityEngine;
using System.IO;

#if !UNITY_EDITOR && UNITY_WSA_10_0
using System;
using System.Threading.Tasks;
using Windows.Storage.Pickers;
#endif

public class VRMRuntimeLoaderUsingFilePicker : MonoBehaviour
{
	public GameObject VRMRoot;

	void Start ()
	{
#if !UNITY_EDITOR && UNITY_WSA_10_0

		UnityEngine.WSA.Application.InvokeOnUIThread(async () =>
		{
			var openPicker = new FileOpenPicker();
			openPicker.SuggestedStartLocation = PickerLocationId.Objects3D;
			openPicker.FileTypeFilter.Add(".vrm");

			var file = await openPicker.PickSingleFileAsync();
			UnityEngine.WSA.Application.InvokeOnAppThread(() => 
			{
				if(file != null)
				{
					StartCoroutine(LoadVrmCoroutine(file.Path));
				}
			}, false);
		}, false);

#elif UNITY_EDITOR

		string path = Application.dataPath + "/Models/" + "default.vrm";
		StartCoroutine(LoadVrmCoroutine(path));

#endif
	}

	IEnumerator LoadVrmCoroutine(string path)
	{
		var www = new WWW("file://" + path);
		yield return www;
		VRM.VRMImporter.LoadVrmAsync(www.bytes, OnLoaded);
	}

	void OnLoaded(GameObject vrm)
	{
		if(VRMRoot != null)
		{
			vrm.transform.SetParent(VRMRoot.transform, false);
		}
	}
}
```

## ファイル選択
FileOpenPickerを使うことで、ファイルエクスプローラーを表示してファイルを選択することができます。
FileOpenPickerは、UnityのMainThreadでは利用できないため、UIThreadで動作させます。
選択されたファイルのパスをファイル読み込み用のメソッドに渡します。

"#if"の書き方はいくつかありますが、WINDOWS_UWPと書いてもIL2CPPで動くようです。
（参考：[HoloLensのUnity内でFilepicker使ってみた](http://akihiro-document.azurewebsites.net/post/hololens_filepicker/)）

```cs

#if !UNITY_EDITOR && UNITY_WSA_10_0

		UnityEngine.WSA.Application.InvokeOnUIThread(async () =>
		{
			var openPicker = new FileOpenPicker();
			openPicker.SuggestedStartLocation = PickerLocationId.Objects3D;
			openPicker.FileTypeFilter.Add(".vrm");

			var file = await openPicker.PickSingleFileAsync();
			UnityEngine.WSA.Application.InvokeOnAppThread(() => 
			{
				if(file != null)
				{
					StartCoroutine(LoadVrmCoroutine(file.Path));
				}
			}, false);
		}, false);

#elif UNITY_EDITOR

		string path = Application.dataPath + "/Models/" + "default.vrm";
		StartCoroutine(LoadVrmCoroutine(path));

#endif
```
## VRMファイルの読み込み
WWWクラスでファイルを読み込み、byte配列をVRM.VRMImporter.LoadVrmAsyncに渡します。
IL2CPPビルドの場合、WWWクラスを使わないとDirectoryNotFoundExceptionが発生してファイルが読み込めませんでした。

また、VRMオブジェクトの位置や向きを調整できるように、OnLoadedメソッドでSetParentしています。
SetParentの第2引数（worldPositionStays）をfalseにしておくと調整可能です。（trueだとワールド座標系の位置・回転・スケールがキープされる）

```cs

	IEnumerator LoadVrmCoroutine(string path)
	{
		var www = new WWW("file://" + path);
		yield return www;
		VRM.VRMImporter.LoadVrmAsync(www.bytes, OnLoaded);
	}

	void OnLoaded(GameObject vrm)
	{
		if(VRMRoot != null)
		{
			vrm.transform.SetParent(VRMRoot.transform, false);
		}
	}
```

# 参考情報
- [HoloLensでVRMをよむ](https://www.tattichan.work/entry/2018/07/27/HoloLens%E3%81%A7VRM%E3%82%92%E3%82%88%E3%82%80)
- [HoloLensのUnity内でFilepicker使ってみた](http://akihiro-document.azurewebsites.net/post/hololens_filepicker/)
- [UnityでVRMファイルをインポートして使うときの覚え書き](https://qiita.com/toRisouP/items/7d2c57d7d160f375e2d8)
- [WWWクラスで外部ファイルを読みこむ](http://unitygeek.hatenablog.com/entry/2012/08/05/184916)