LoginSignup
6
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-19

はじめに

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

どうしてこの記事書こうと思ったか

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

VRM勉強会で聞いてみた

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

いつ頃に実装されたのかを調べてみた

v0.103.0で実装WebGLで動作確認されたと記載がありました。
https://vrm-c.github.io/UniVRM/ja/release/100/v0.103.0.html?highlight=webgl

  • #1756

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

環境

下記を用意します。

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

ボタン配置

今回は下記の画像のように配置します。
WebGLでは、読み込みボタンとは別の「GameObject」にして実行したら返って来なかったためボタンにクスリプ等を割り当ててます。
VRM10_1.png

読み込みプログラム全体

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 ===*/

解説

フォルダー選択

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()の関数が使えます。

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

ファイルの読み込み

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を展開と配置

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の罠がありました。

ViewImport.cs
var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyAwaitCaller());

WindowsAndroid向けのアプリケーションではRuntimeOnlyAwaitCaller()を渡してあげれば正常に読み込めますが、
WebGLでは動作しません。

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を展開

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の読み込みができるようになっているので是非、
自分のアバターを読み込ませて見てね☆彡

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2