TL;DR1
-
VRM
をランタイムでロードするのに必要なVRMImporterContext
はIDisposable
を実装しており、Dispose
しないとメモリリークする -
VRMImporterContext
は生成済みのアバターGameObject
についているComponent
からは取得できなさそう -
VRM
のインスタンスを管理するときはGameObject
ではなくVRMImporterContext
で管理する - もしくは拡張メソッドで自作クラスをAddして
OnDestroy
のときにDispose
する
環境
説明と検証
人間牧場
わたしは人間牧場を経営したいのですが、残念ながら資金も権力もありません。
なのでかわいいアバターをいっぱい飼育することで心の慰めにしたいと思います。
というわけで、以下のようなスクリプトを書きました。
StreamingAssets/vrm/
から.vrm
を読み込んでぴょんぴょんさせます。これで好きなアバターを牧場に入れられます!
using System;
using System.Collections.Generic;
using System.IO;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VRM;
public class HumanRanch : MonoBehaviour
{
private void Start()
{
InitializeCage().ContinueWith(Rearing);
}
#region Ranch
[SerializeField]
private Transform[] _cage;
[SerializeField]
private RuntimeAnimatorController _animator;
private GameObject[] _liveStocks;
private async UniTask InitializeCage()
{
// wait for first update
await UniTask.Yield(PlayerLoopTiming.Update);
_liveStocks = new GameObject[_cage.Length];
}
private const float ProductionCycle = 3f;
private readonly Vector3 CageGap = new Vector3(0, -1, 0);
private async UniTask Rearing()
{
var token = this.GetCancellationTokenOnDestroy();
while (!token.IsCancellationRequested)
{
var changeIndex = UnityEngine.Random.Range(0, _liveStocks.Length);
if (_liveStocks[changeIndex] == null)
{
var context = await LoadVRM();
var avatar = context.Root;
avatar.GetComponent<Animator>().runtimeAnimatorController = _animator;
avatar.transform.SetPositionAndRotation(
_cage[changeIndex].position + CageGap,
_cage[changeIndex].rotation
);
_liveStocks[changeIndex] = avatar;
context.ShowMeshes();
}
else
{
Destroy(_liveStocks[changeIndex]);
_liveStocks[changeIndex] = null;
}
await UniTask.Delay(TimeSpan.FromSeconds(ProductionCycle), cancellationToken: token);
}
}
#endregion
#region LoadVRM
private readonly Dictionary<string, byte[]> _VRMBufferCache = new Dictionary<string, byte[]>();
private string[] _VRMFullPaths = null;
private static string[] SearchStreamingAssetsVRM()
{
var dirPath = Path.Combine(Application.streamingAssetsPath, "vrm");
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
return Directory.GetFiles(dirPath, "*.vrm");
}
private async UniTask<VRMImporterContext> LoadVRM()
{
if (_VRMFullPaths == null)
{
_VRMFullPaths = SearchStreamingAssetsVRM();
}
var path = _VRMFullPaths[UnityEngine.Random.Range(0, _VRMFullPaths.Length)];
if (!_VRMBufferCache.ContainsKey(path))
{
await UniTask.SwitchToThreadPool();
var buffer = File.ReadAllBytes(path);
await UniTask.SwitchToMainThread();
_VRMBufferCache[path] = buffer;
}
var context = await LoadVRMFromBuffer(_VRMBufferCache[path]);
return context;
}
private static async UniTask<VRMImporterContext> LoadVRMFromBuffer(byte[] buffer)
{
var context = new VRMImporterContext();
context.ParseGlb(buffer);
await context.LoadAsyncTask();
return context;
}
#endregion
}
人間牧場そのいち pic.twitter.com/r6An34C7wd
— ねこみみだいまおう (@CatEarEvilKing) July 18, 2020
経営の行き詰まり
適度に増えて適度に減って、永遠に眺めていられますね。
ところがだんだん動きがカクついてきて、遂にはエディタがクラッシュしてしまいます。誰の仕業でしょうか。
Profiler
で調べたところ、どうやらメモリリークしているようです。心当たりがあるとすれば破棄処理とファイル読み込みの部分ですが、特におかしいところはありません。
Destroy(_liveStocks[changeIndex]);
var buffer = File.ReadAllBytes(path);
となると残っているのはロードするときに使っているVRMImporterContext
です。
IDisposable
を実装している。実にあやしい。というわけで使い終わったらDispose
します。
context.ShowMeshes();
// 即座にDispose
context.Dispose();

そして後には誰もいない牧場が残りました。
経営再開
当然ですが、context
をDispose
したタイミングで生成したアバターも破棄されてしまうみたいです。
そして困ったことに、生成済みのアバターはVRMImporterContext
を取得できるクラスがくっついていません。2
なのでアバターをGameObject
ではなくVRMImporterContext
単位で管理することにします。
// アバターを管理する配列
// before
private GameObject[] _liveStocks;
// after
private VRMImporterContext[] _liveStocks;
// アバターの破棄処理
// before
Destroy(_liveStocks[changeIndex]);
// after
_liveStocks[changeIndex].Dispose();
人間牧場そのに pic.twitter.com/Y5IYO52zwC
— ねこみみだいまおう (@CatEarEvilKing) July 18, 2020
できました!
人間牧場の復活です!
いろんなアバターが増えたり減ったりしながら永遠にぴょんぴょんします!
更にもう一歩
とはいえリソースの開放忘れはよくあるバグの原因の一つです。
なので管理して自分でDispose
するのではなく、
拡張メソッドでアバターがOnDestroy
されたとき、自動的にDispose
されるようにします。
using UnityEngine;
namespace VRM.Extension
{
public class VRMAutoDisposer : MonoBehaviour
{
public VRMImporterContext Context;
private void OnDestroy()
{
Context?.Dispose();
}
}
public static class VRMAutoDisposerExtension
{
public static void AutoDispose(this VRMImporterContext context)
{
var disposer = context.Root.AddComponent<VRMAutoDisposer>();
disposer.Context = context;
}
}
}
// 拡張メソッドなので、こんな感じで読んでおけば自動でDisposeしてくれる
context.AutoDispose();
context.ShowMeshes();
まとめ
自分がざっと見た限りではそれらしい情報が見当たらなかったのでこの記事を書きました。
本来ならば公式にissueでも立てて要望を出すかなにかしようかとも思ったんですが、もうすぐ1.0.0版とかが出るみたいだし、いいかなって。
個人的には拡張メソッドの方式が好きです。AssetBundle
でもそうですが、リソース管理とかめんどくさすぎて自分でやりたくないんですよね。
拡張Disposeと人間牧場のコードはgist
にあげておいたので好きに使ってください。
あと、gist
に置いてあるコードのライセンスをつけておきました3。CC0ですが、作者を表記してくれたらうれしいです。
おしまい。
参考記事など
記事
使用アセット
ニコニ立体ちゃん (VRM)
ヴィータ
Bird Cage created by Poly by Google
Basic Motions FREE Pack