VRM対応のiOSアプリを開発した際に得た知見

# はじめに
先日、VRM対応のiOSアプリをリリースしました。
VRMアバターをARで表示して好きな角度から眺めたり、写真を撮ったりできます。
この記事では私がアプリ開発で得た知見や躓いたところを共有できればと思います。
※VRMとは直接関係のないところもあります

「VConnect」
https://itunes.apple.com/us/app/v-connect/id1450810229

# VRMについて
https://dwango.github.io/vrm/vrm_about/

# どんなアプリか
↓をご覧ください。
https://www.youtube.com/watch?v=WhaJ6UYb8pQ

# 環境
Unity
ARKit2.0
UniVRM

今回の開発ではUnityを選択しました。
理由としてはUnityの勉強がしたかったのと、UniVRMを使う必要があったからです。
ですが今はネイティブでVRMを扱えるものが出ており、CocoaPodsなどで導入できるようです。

# ぶつかった問題たち
↓↓↓

## デフォルトでVRMファイルを1つ入れたい
iOSアプリのディレクトリの中には `Documents/`というディレクトリがあり、
この中にあるVRMファイルを読み込んでアプリ上で表示しています。

「VConnect」ではデフォルトで1つのVRMアバターが入っています。
そうしないとアプリをインストールした段階ではなんのアバターもいなくて遊べなくなってしまうので。

最初の問題はどうしたらアプリインストール時に`Documents/`の中にVRMファイルを入れられるかという事でした。

結論からいうとUnityの`StreamingAssets`を使う事で解決しました。
> Unity プロジェクトにおける StreamingAssets と呼ばれるフォルダーに配置したファイルはビルド先のプラットフォームの、特定のフォルダーにそのまま何も変換されない状態で保持されます。

`Assets/StreamingAssets/`の中にVRMファイルを1つ配置し、それをスクリプトで処理しています。
また、`Application.persistentDataPath`で`Documents/`までのパスを取得できます。

```c#:sample.cs
// StreamingAssets内のhogehoge.vrmを取得
var file = Application.streamingAssetsPath + "/" + "hogehoge.vrm";
// バイト配列を読み込む
byte[] byte = File.ReadAllBytes(file);
// Documents/にhogehoge.vrmという名前でbyteを保存
File.WriteAllBytes(Application.persistentDataPath + "/hogehoge.vrm", byte);
```

これで`Assets/StreamingAssets/`の中にあるファイルを`Documents/`に保存できます。

## VRMファイルのランタイムロード

ランタイムロードの方法は他の記事でもたくさん書かれていますが改めて書きます。

`/Documents`からVRMファイルを取得する

```c#:sample.cs
// Documentsディレクトリへのパスを取得
DirectoryInfo localDir = new DirectoryInfo(Application.persistentDataPath);
// GetFilesで拡張子がvrmのファイルを全て取得して配列に入れる
FileInfo[] files = localDir.GetFiles("*.vrm");
```

メタ情報を取得する

```c#:sample.cs
// VRMファイルをbyte配列に入れる
byte[] bytes = File.ReadAllBytes("hogehoge.vrm");
VRMImporterContext context = new VRMImporterContext();
context.ParseGlb(bytes);
// ReadMetaでメタオブジェクトができる
// true, falseでサムネイルの有無を選べます
VRMMetaObject meta = context.ReadMeta(true);

// 名前
Debug.Log(meta.name);
// サムネイル
Debug.Log(meta.Thumbnail);
```

VRMアバターを表示する

```c#:sample.cs
context.ParseGlb(bytes);
context.Load();
context.ShowMeshes();
```


## メモリリーク問題
### やりたい事
AR空間にアバターを配置した後に別のキャラクターに切り替えたい。
その時に前のアバターは画面上から削除し、新しいアバターを画面に表示する。

ここでは普通に`Destroy`メソッドを使えばいいと思っていました。
しかしそれではダメなのです。

### なぜか
なぜ`Destroy`ではダメなのかというと、アバターのTextureが残ってしまいメモリリークを起こしてしまうからです。
例えばiPhone8ではメモリ使用量が1400MBを越えるとアプリがクラッシュします。
1体のアバターを表示するのに300MBくらい使います。
その内、Textureで200MBくらいあります。
<img width="214" alt="スクリーンショット 2019-02-16 14.17.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/201127/cb18ccbc-1906-138f-8070-82d98c5b8ba4.png">

なのでGameObjectをDestroyで削除してもTextureが残っているので3~4体くらいアバターを読み込むとすぐにアプリがクラッシュしてしまいます。

### 解決方法

`Resources.UnloadUnusedAssets`を使います。

>使用していないアセットをアンロードします
ヒエラルキーにあるゲームオブジェクト（スクリプトコンポーネントを含む）群で総なめして、アセットが使用されていないかを判断します。

GameObjectをDestroyするところで`Resources.UnloadUnusedAssets`を使う事でアセットを解放してくれました。

###

# 番外編

## ARアプリをAppleに申請する際に気をつける事

### ガイドラインに沿った実装をする
「VConnect」もリリースまでに数回リジェクトされていますが、ARKitのガイドラインに沿った実装ができていなかったのが原因かと思います。
どのような対応をしたかいくつか紹介します。

> Anticipate that people will use your app in environments that aren’t optimal for AR.

ARが使えない状況を想定するという事ですね。
VConnectではARが使えない状況を考えてカメラを使用しない通常鑑賞モードをつけました。
https://www.youtube.com/watch?v=92HXHFBrtAU

> If your app encourages user motion, introduce it gradually

「ユーザーの動きを促す場合には徐々に紹介する」
ARのアプリではカメラを床や壁に向けて平面を検出するのですが、アプリを最初に開いたユーザーにこの動作を促す場合、徐々に紹介してくれという事です。

こちらの動画を見ていただくとわかる通り、VConnectでは2つの動作を別々に促しています。
1, 平面にカメラを向ける
2, 検出した平面をタップする
https://www.youtube.com/watch?v=WhaJ6UYb8pQ

# まとめ

Unityに慣れていなかったのが躓いた原因としては大きいですね。
しかし個人でモバイルアプリをリリースしたのは初めてだったので嬉しいです^^
C#も言語として好きになりました。

# 参考文献
https://docs.unity3d.com/ja/2018.1/Manual/StreamingAssets.html
https://docs.unity3d.com/ja/2018.1/ScriptReference/Resources.UnloadUnusedAssets.html
https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/augmented-reality/