VRM1.0版のWebGLでRuntimeで読み込み実装を行う

# はじめに
`VRM1.0-v0.108.0`を使って`WebGL`を用いて読み込みで苦労したこと、実装方法を書いていきます。

# どうしてこの記事書こうと思ったか
- VRMコンソーシアム側がまだリファレンスを書いていないため自分で調べる手段がソース解析しかない。
- `Uni-VRM0.x系`からの変更点が多くて今までの記事を見ても実装が出来なかった。
- 2022年9月5日(月)～11日(日)の「`unity1week`」で実装しようとして、
その時最新の「`VRM-0.98.0`」で実装しようと苦戦してほとんどの時間を費やしてしまい、あきらめたから。

# VRM勉強会で聞いてみた
`VRM勉強会`でコメントにて質問した結果「さんたーP」さんが`Twitter`にて回答していただけて解決できました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/fairychirno?ref_src=twsrc%5Etfw">@fairychirno</a> 流れてしまったのでこちらで失礼します。この RuntimeOnlyNoThreadAwaitCaller を new して VRM10.LoadAsync の引数に渡してあげれば行けると思います。ヘルプなどが追っついてなくてすみませんが… <a href="https://twitter.com/hashtag/VRM%E5%8B%89%E5%BC%B7%E4%BC%9A?src=hash&amp;ref_src=twsrc%5Etfw">#VRM勉強会</a><a href="https://t.co/JRU2F0XvFq">https://t.co/JRU2F0XvFq</a></p>&mdash; さんたーP (@santarh) <a href="https://twitter.com/santarh/status/1582700496371953665?ref_src=twsrc%5Etfw">October 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">すいません遅くなりました、VRM勉強会前に作成した時のコードを見返してみたら添付URLの131行目から始まり137行目でnewしてました。 帰って来なくなるのは164行目のVRMの展開で行ったきり音信不通になります。WebGLでのロードのサンプル又はリファレンスは無いでしょうか？<a href="https://t.co/zbCJeNgd8x">https://t.co/zbCJeNgd8x</a></p>&mdash; 瑞姫 亞希乃@🉐💾🌯🦀❤️ (@fairychirno) <a href="https://twitter.com/fairychirno/status/1600403178670858241?ref_src=twsrc%5Etfw">December 7, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">確認が取れました！本当にありがとうございます!! <a href="https://t.co/7ZZhmvK6hG">https://t.co/7ZZhmvK6hG</a></p>&mdash; 瑞姫 亞希乃@🉐💾🌯🦀❤️ (@fairychirno) <a href="https://twitter.com/fairychirno/status/1602661284960342016?ref_src=twsrc%5Etfw">December 13, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

# いつ頃に実装されたのかを調べてみた
`v0.103.0`で実装`WebGL`で動作確認されたと記載がありました。
 https://vrm-c.github.io/UniVRM/ja/release/100/v0.103.0.html?highlight=webgl

- `#1756`

https://github.com/vrm-c/UniVRM/pull/1756

`9月5日時点`ではまだ読み込みで使う「`RuntimeOnlyNoThreadAwaitCaller.cs`」が
存在してすら、いなかったんですね。
それは動かないわけだ。。。



# 環境
下記を用意します。
- Unity 2021.3.6f1
- Uni-VRM https://github.com/vrm-c/UniVRM/releases
`VRM-0.108.0_e16d.unitypackage`

- StandaloneFileBrowser https://github.com/gkngkc/UnityStandaloneFileBrowser
`StandaloneFileBrowser.unitypackage`

それでは本題のWebGLでRuntimeで読み込んでいきます。

# ボタン配置
今回は下記の画像のように配置します。
`WebGL`では、読み込みボタンとは別の「`GameObject`」にして実行したら返って来なかったためボタンにクスリプ等を割り当ててます。
![VRM10_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679847/d047e3c5-1f94-bdb3-c019-317c17c58289.png)

# 読み込みプログラム全体
```lua:ViewImport.cs
using System.Collections;
using UnityEngine;
using SFB;
using UnityEngine.Networking;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Threading.Tasks;
using UnityEngine.SceneManagement;
using System;

namespace UniVRM10.VRM10Viewer
{/*=== VRM10Viewer ===*/

    public class ViewImport : MonoBehaviour, IPointerDownHandler
    {

        [SerializeField]
        public Button LoadButton;
        public Button StartButton;
        public Button TitleButton;
        public GameObject AvatarPos;
        public GameObject ReAvatar;
        public AudioClip SE;
        AudioSource audioSource;

        public bool VrmLoadFlag = false;

        void Start()
        {

#if !(UNITY_WEBGL && !UNITY_EDITOR)
            LoadButton.onClick.AddListener(On_LoadFileSelect);
#endif
            StartButton.onClick.AddListener(OnClickGameStart);

            if (TitleButton != null)
            {
                TitleButton.onClick.AddListener(OnClickGameTitle);
            }

            audioSource = gameObject.GetComponent<AudioSource>();
            
            VrmLoadFlag = true;

        }

#if UNITY_WEBGL && !UNITY_EDITOR

        //
        // WebGL
        //

        // StandaloneFileBrowserのブラウザスクリプトプラグインから呼び出す
        [DllImport("__Internal")]
        private static extern void UploadFile(string gameObjectName, string methodName,string   filter, bool multiple);

        // ファイルを開く
        public void OnPointerDown(PointerEventData eventData) {
            audioSource.PlayOneShot(SE);
            VrmLoadFlag = false;
            UploadFile(gameObject.name, "OnFileUpload", ".vrm", false);
            VrmLoadFlag = true;
        }

        // ファイルアップロード後の処理
        public void OnFileUpload(string url) {
            StartCoroutine(Load(url));
            VrmLoadFlag = true;
        }
        
#else
        //
        // OSビルド & Unity editor上
        //
        public void OnPointerDown(PointerEventData eventData) { }

        // ファイルを開く
        public void On_LoadFileSelect()
        {

            audioSource.PlayOneShot(SE);
            VrmLoadFlag = false;

            // 拡張子フィルタ
            var extensions = new[] {
                new ExtensionFilter("Uni-VRM v0.108.0", "vrm" ),
            };
    
            // ファイルダイアログを開く
            var paths = StandaloneFileBrowser.OpenFilePanel("Open File", "", extensions,    false);
            if (paths.Length > 0 && paths[0].Length > 0)
            {
    
                StartCoroutine(Load(new System.Uri(paths[0]).AbsoluteUri));
                VrmLoadFlag = true;
            }
        }
    
#endif
        // ファイル読み込み
        private IEnumerator Load(string url)
        {
    
            UnityWebRequest webRequest = UnityWebRequest.Get(url);
            yield return webRequest.SendWebRequest();
            if (webRequest.isNetworkError)
            {
                // エラー処理
                yield break;
            }
            Debug.Log(webRequest.responseCode);
            LoadVRMClicked(webRequest.downloadHandler.data);
    
        }
    
        public async void LoadVRMClicked(Byte[] url)
        {
    
            var path = url;
    
            var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyNoThreadAwaitCaller());
    
            Transform AvatarCount = AvatarPos.GetComponentInChildren<Transform>();

            //子要素を全て消去
            if (AvatarCount.childCount != 0)
            {
                //子オブジェクトを一つずつ取得
                foreach (Transform child in AvatarCount.transform)
                {
                    //削除する
                    Destroy(child.gameObject);
                }
    
            }

            //アバターのゲームオブジェクトを取得
            var root = instance.gameObject;
            //アバターの位置を移動
            root.transform.position = new Vector3(0, 0, 0);
            root.transform.rotation = Quaternion.identity;
            //Avatarの子にVRM1(アバター本体を入れる)
            root.gameObject.transform.parent = AvatarPos.transform;

        }

        async Task<Vrm10Instance> LoadAsync(Byte[] path, VRMShaders.IAwaitCaller awaitCaller)
        {

            var instance = await Vrm10.LoadBytesAsync(path, awaitCaller: awaitCaller);
    
            // VR用 FirstPerson 設定
            await instance.Vrm.FirstPerson.SetupAsync(instance.gameObject, awaitCaller);
    
            return instance;
        }

        public void OnClickGameStart()
        {
            if (VrmLoadFlag == false)
            {
                return;
            }
            audioSource.PlayOneShot(SE);

            ReAvatar = GameObject.Find("ReAvatar");
            if (ReAvatar != null)
            {
                ReAvatar.name = "Avatar";
            }

            Invoke("SetBool", 1.0f);
        }

        public void OnClickGameTitle()
        {
            audioSource.PlayOneShot(SE);
            Destroy(GameObject.Find("ReAvatar"));
            Destroy(GameObject.Find("main"));
            Invoke("Set_StartBool", 1.0f);
        }

        void SetBool()
        {
            SceneManager.LoadScene("MainScene");
        }
        void Set_StartBool()
        {
            SceneManager.LoadScene("StartScene");
        }

    }/*=== END_ViewImport ===*/

}/*=== END_VRM10Viewer ===*/
```
# 解説
### フォルダー選択


```lua:ViewImport.cs

        void Start()
        {

#if !(UNITY_WEBGL && !UNITY_EDITOR)
            LoadButton.onClick.AddListener(On_LoadFileSelect);
#endif
        }

#if UNITY_WEBGL && !UNITY_EDITOR

        //
        // WebGL
        //

        // StandaloneFileBrowserのブラウザスクリプトプラグインから呼び出す
        [DllImport("__Internal")]
        private static extern void UploadFile(string gameObjectName, string methodName,string   filter, bool multiple);

        // ファイルを開く
        public void OnPointerDown(PointerEventData eventData) {
            audioSource.PlayOneShot(SE);
            VrmLoadFlag = false;
            UploadFile(gameObject.name, "OnFileUpload", ".vrm", false);
            VrmLoadFlag = true;
        }

        // ファイルアップロード後の処理
        public void OnFileUpload(string url) {
            StartCoroutine(Load(url));
            VrmLoadFlag = true;
        }
        
#else
        //
        // OSビルド & Unity editor上
        //
        public void OnPointerDown(PointerEventData eventData) { }

        // ファイルを開く
        public void On_LoadFileSelect()
        {

            audioSource.PlayOneShot(SE);
            VrmLoadFlag = false;

            // 拡張子フィルタ
            var extensions = new[] {
                new ExtensionFilter("Uni-VRM v0.108.0", "vrm" ),
            };
    
            // ファイルダイアログを開く
            var paths = StandaloneFileBrowser.OpenFilePanel("Open File", "", extensions,    false);
            if (paths.Length > 0 && paths[0].Length > 0)
            {
    
                StartCoroutine(Load(new System.Uri(paths[0]).AbsoluteUri));
                VrmLoadFlag = true;
            }
        }
    
#endif
```

`WebGL`でファイルブラウザを呼び出すコードは、ほぼこれが定型文になります。
`Start()`内ではWebGL出ない時だけボタンの関数を割り当ててます
Webブラウザの時は`StandaloneFileBrowser`の`ブラウザスクリプトプラグイン`から呼び出しています。

`Unity`からはの場合は、`StandaloneFileBrowser.OpenFilePanel()`の関数が使えます。

下記の記事を参考にさせていただきました。

https://zenn.dev/plumchang/articles/9187928bcbcf93

### ファイルの読み込み
```lua:ViewImport.cs
    // ファイル読み込み
    private IEnumerator Load(string url)
    {
    
        UnityWebRequest webRequest = UnityWebRequest.Get(url);
        yield return webRequest.SendWebRequest();
        if (webRequest.isNetworkError)
        {
            // エラー処理
            yield break;
        }

        Debug.Log(webRequest.responseCode);
        LoadVRMClicked(webRequest.downloadHandler.data);
    }
```
`UnityWebRequest.Get(url)`を使いファイルブラウザもしくは、WebサーバーからVRMのデータをリクエストしています。
データのダウンロードが終わったら`LoadVRMClicked(webRequest.downloadHandler.data);`で、
VRMファイルを展開配置処理に渡します。

### VRMを展開と配置
```lua:ViewImport.cs
    public async void LoadVRMClicked(Byte[] url)
    {
    
        var path = url;
    
        var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyNoThreadAwaitCaller());
    
        Transform AvatarCount = AvatarPos.GetComponentInChildren<Transform>();

        //子要素を全て消去
        if (AvatarCount.childCount != 0)
        {
            //子オブジェクトを一つずつ取得
            foreach (Transform child in AvatarCount.transform)
            {
                //削除する
                Destroy(child.gameObject);
            }
    
        }

        //アバターのゲームオブジェクトを取得
        var root = instance.gameObject;
        //アバターの位置を移動
        root.transform.position = new Vector3(0, 0, 0);
        root.transform.rotation = Quaternion.identity;
        //Avatarの子にVRM1(アバター本体を入れる)
        root.gameObject.transform.parent = AvatarPos.transform;

    }
```
このコードでVRMを展開と配置するのですが、ここで`WebGL`の罠がありました。

```lua:ViewImport.cs
var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyAwaitCaller());
```
`Windows`、`Android`向けのアプリケーションでは`RuntimeOnlyAwaitCaller()`を渡してあげれば正常に読み込めますが、
`WebGL`では動作しません。

```lua:ViewImport.cs
var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyNoThreadAwaitCaller());
```
`RuntimeOnlyNoThreadAwaitCaller()`を`new`して渡してあげると動作します。

「`.\Asset\VRMShaders\GLTF\IO\Runtime\AwaitCaller`」にあるファイルを見て見ると
コメントに下記の記述がありました。

- `RuntimeOnlyAwaitCaller`
Runtime (Build 後と、Editor Playing) での非同期ロードを実現する AwaitCaller.
 NOTE: 簡便に実装されたものなので、最適化の余地はある.

- `RuntimeOnlyNoThreadAwaitCaller`
Runtime (Build 後と、Editor Playing) での非同期ロードを実現する AwaitCaller.
WebGL など Thread が無いもの向け

`RuntimeOnlyNoThreadAwaitCaller`のみ`Thread`がない物向けと記述されています。
今後最適化はされそうな感じはしますが確かに`WebGL`向けになります。

### VRMを展開
```lua:ViewImport.cs
    async Task<Vrm10Instance> LoadAsync(Byte[] path, VRMShaders.IAwaitCaller awaitCaller)
    {

        var instance = await Vrm10.LoadBytesAsync(path, awaitCaller: awaitCaller);
    
        // VR用 FirstPerson 設定
        await instance.Vrm.FirstPerson.SetupAsync(instance.gameObject, awaitCaller);
    
        return instance;
    }
```
`RuntimeOnlyNoThreadAwaitCaller()`で受け取った後に`LoadBytesAsync()`に渡して`Byte配列`データで受け取ったVRMファイルを展開します。

`FirstPerson`を取得する場合はVRM1.0からは`instance.Vrm.FirstPerson.SetupAsync()`で取得できます。

# まとめ
- リファレンスマニュアルはまだ不十分ではあるけど何とか動作した。
- Webなどダウンロードが必要な場合は`Byte配列`でデータを取り扱うため`Vrm10.LoadBytesAsync()`を使う。
- VRM0.x系では複数行を使い読み込むが、シンプルに1行で読み込める。
- 今後のVRM1.0が主力になるので、積極的に実装し行きましょう。

## Unity1Week
今回の作ったゲームにVRM1.0の読み込みができるようになっているので是非、
自分のアバターを読み込ませて見てね☆彡

https://unityroom.com/games/akino_unity1week_re