はじめに
VRM1.0-v0.108.0
を使ってWebGL
を用いて読み込みで苦労したこと、実装方法を書いていきます。
どうしてこの記事書こうと思ったか
- VRMコンソーシアム側がまだリファレンスを書いていないため自分で調べる手段がソース解析しかない。
-
Uni-VRM0.x系
からの変更点が多くて今までの記事を見ても実装が出来なかった。 - 2022年9月5日(月)~11日(日)の「
unity1week
」で実装しようとして、
その時最新の「VRM-0.98.0
」で実装しようと苦戦してほとんどの時間を費やしてしまい、あきらめたから。
VRM勉強会で聞いてみた
VRM勉強会
でコメントにて質問した結果「さんたーP」さんがTwitter
にて回答していただけて解決できました。
@fairychirno 流れてしまったのでこちらで失礼します。この RuntimeOnlyNoThreadAwaitCaller を new して VRM10.LoadAsync の引数に渡してあげれば行けると思います。ヘルプなどが追っついてなくてすみませんが… #VRM勉強会https://t.co/JRU2F0XvFq
— さんたーP (@santarh) October 19, 2022
すいません遅くなりました、VRM勉強会前に作成した時のコードを見返してみたら添付URLの131行目から始まり137行目でnewしてました。 帰って来なくなるのは164行目のVRMの展開で行ったきり音信不通になります。WebGLでのロードのサンプル又はリファレンスは無いでしょうか?https://t.co/zbCJeNgd8x
— 瑞姫 亞希乃@🉐💾🌯🦀❤️ (@fairychirno) December 7, 2022
確認が取れました!本当にありがとうございます!! https://t.co/7ZZhmvK6hG
— 瑞姫 亞希乃@🉐💾🌯🦀❤️ (@fairychirno) December 13, 2022
いつ頃に実装されたのかを調べてみた
v0.103.0
で実装WebGL
で動作確認されたと記載がありました。
https://vrm-c.github.io/UniVRM/ja/release/100/v0.103.0.html?highlight=webgl
#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
」にして実行したら返って来なかったためボタンにクスリプ等を割り当ててます。
読み込みプログラム全体
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 ===*/
解説
フォルダー選択
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()
の関数が使えます。
下記の記事を参考にさせていただきました。
ファイルの読み込み
// ファイル読み込み
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を展開と配置
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
の罠がありました。
var instance = await LoadAsync(path, new VRMShaders.RuntimeOnlyAwaitCaller());
Windows
、Android
向けのアプリケーションではRuntimeOnlyAwaitCaller()
を渡してあげれば正常に読み込めますが、
WebGL
では動作しません。
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を展開
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の読み込みができるようになっているので是非、
自分のアバターを読み込ませて見てね☆彡