UniVRMを使ってVRMモデルをランタイムロードする方法

#はじめに

この記事はUniVRM 0.44時点でのサンプルになります。

**UniVRM 0.68からランタイムインポートの方法がより使いやすくなりました。**
**[最新のランタイムロードの方法は公式ページのサンプルをご覧ください](https://vrm-c.github.io/UniVRM/ja/api/0_95_highlevel.html#id1)**

**VRM1.0等当時と状況が変わっていますので、公式ドキュメントやサンプルを確認してください。**
**[UniVRM Programming Document](https://vrm-c.github.io/UniVRM/ja/index.html)**
(2022/11/22更新)

UniVRMを使ってVRMモデルをランタイムロードするときのサンプル
UniVRM-RuntimeLoaderSample-0.44_67a6.unitypackage
を使用して~~VRMをランタイムロードしようとすると、多くのObsolete警告を受けたり、~~
~~.NET4.xで使用しようとするとそもそもエラーになって使用できません。~~

~~修正したプルリクエストをUniVRMTestの方に[取り込んでもらえた](https://github.com/dwango/UniVRMTest/commit/b7cb4ea6601b4de9118df7ea97068cab8ed32610)ので、~~
~~多分次のリリースでは直ってると思いますが、~~(修正されています)ただランタイムロードしたいだけであればとても簡単なので、
UniVRMTest内で行われるランタイムロードの手順を、環境ごとに分けて紹介します。

サンプルはVRMファイルのパスを固定で記述しています。ファイルを開くダイアログ等は別のサンプルや、上述のRuntimeLoaderSampleを参照してください。
この記事ではVRMファイルをランタイムロードする方法のみ記載しています。

#.NET2.0(3.5)を使用している場合
Unity 5.6.3p1や新しいUnityでScripting Runtime Versionがデフォルトの .NET 3.5 Equivalentになっている場合の読込方法です。

##同期処理

非同期処理を使用せず、一番シンプルな同期処理で読み込む方法です。
読込中動作が固まってしまうため、非同期の方を使用することをお勧めします。

```csharp
    private void ImportVRMSync()
    {
        //VRMファイルのパスを指定します
        var path = "C:\\VRM\\AliciaSolid.vrm";

        //ファイルをByte配列に読み込みます
        var bytes = File.ReadAllBytes(path);

        //VRMImporterContextがVRMを読み込む機能を提供します
        var context = new VRMImporterContext();

        // GLB形式でJSONを取得しParseします
        context.ParseGlb(bytes);
            
        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);
            
        //同期処理で読み込みます
        context.Load();

        //読込が完了するとcontext.RootにモデルのGameObjectが入っています
        var root = context.Root;

        //モデルをワールド上に配置します
        root.transform.SetParent(transform, false);

        //メッシュを表示します
        context.ShowMeshes();

    }
```

##非同期処理
モデルデータの読み込みを非同期で行います。ReadAllBytesは同期なので注意です。
また、OnLoadedが完了しないとモデルが読み込み終わっていないことに注意してください。
ImportVRMAsyncを呼んだ直後にモデルデータを開こうとしてもまだ読込終わっていないのでエラーになります。
読み込んだモデルに対して何かする場合は、OnLoaded内か、それ以降の処理で行ってください。

```csharp
    private void ImportVRMAsync()
    {
        //VRMファイルのパスを指定します
        var path = "C:\\VRM\\AliciaSolid.vrm";

        //ファイルをByte配列に読み込みます
        var bytes = File.ReadAllBytes(path);

        //VRMImporterContextがVRMを読み込む機能を提供します
        var context = new VRMImporterContext();

        // GLB形式でJSONを取得しParseします
        context.ParseGlb(bytes);
            
        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);

        //非同期処理で読み込みます
        context.LoadAsync(_ => OnLoaded(context));
    }

    private void OnLoaded(VRMImporterContext context)
    {
        //読込が完了するとcontext.RootにモデルのGameObjectが入っています
        var root = context.Root;

        //モデルをワールド上に配置します
        root.transform.SetParent(transform, false);

        //メッシュを表示します
        context.ShowMeshes();
    }
```

#.NET4.xを使用している場合
新しいUnityでScripting Runtime Versionが.NET 4.xになっている場合の方法です(可能ならこれを推奨します)

##非同期処理
.NET 4.xの場合Taskを使用してawaitすることが出来るため、最も分かりやすく書くことが出来ます。
同期処理は推奨しませんがどうしても必要な場合は.NET2.0の方をそのまま使えますのでそちらを参照してください。

```csharp
    private async Task ImportVRMAsync_Net4()
    {
        //VRMファイルのパスを指定します
        var path = "C:\\VRM\\AliciaSolid.vrm";

        //ファイルをByte配列に読み込みます
        var bytes = File.ReadAllBytes(path);

        //VRMImporterContextがVRMを読み込む機能を提供します
        var context = new VRMImporterContext();

        // GLB形式でJSONを取得しParseします
        context.ParseGlb(bytes);

        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);

        //非同期処理(Task)で読み込みます
        await context.LoadAsyncTask();

        //読込が完了するとcontext.RootにモデルのGameObjectが入っています
        var root = context.Root;

        //モデルをワールド上に配置します
        root.transform.SetParent(transform, false);

        //メッシュを表示します
        context.ShowMeshes();
    }
```
Byte配列の読込も非同期で行いたい場合は例えば以下のような実装があります。

```csharp
    // Byte列を得る
    private async static Task<Byte[]> ReadAllBytesAsync(string path)
    {
        byte[] result;
        using (FileStream SourceStream = File.Open(path, FileMode.Open))
        {
            result = new byte[SourceStream.Length];
            await SourceStream.ReadAsync(result, 0, (int)SourceStream.Length);
        }
        return result;
    }
```
```csharp
        var bytes = File.ReadAllBytes(path);
```
のかわりに

```csharp
        var bytes = await ReadAllBytesAsync(path);
```
とすることでByte配列の読込も非同期で行うことが出来ます。

#おわりに
VRMモデルをランタイムロードする方法を環境別に紹介しました。
もしMetaデータを読み込む必要がないとき(既にメタデータを表示していて、あとはモデルを表示するだけの場合)は

```csharp
        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);
```
この部分は削除しても構いません。

また、読み込んだモデルを特定の座標に配置したい場合、以前は出来ませんでしたが、最近のバージョンでは

```csharp
        //モデルをワールド上に配置します
        root.transform.SetParent(transform, false);

        //メッシュを表示します
        context.ShowMeshes();
```
このShowMeshesを実行するまでモデルが非表示になっているので、好きなParentを指定したりPositionを変えたりしてから
ShowMeshesを実行して、特定の位置に表示できるようになっています。

##アプリの紹介
同じ方法でVRMをランタイムロードしてVR機器で操作するアプリ、
バーチャルモーションキャプチャーを開発しています。
今後もUniVRMのバージョンが上がるごとに対応していく予定ですので、良かったらご覧ください。
GitHub上でソースコードも公開しています。
[公式ページ](https://sh-akira.github.io/VirtualMotionCapture/)
[GitHub](https://github.com/sh-akira/VirtualMotionCapture)