fix: improve build performance by ~10x

CreateAsset, as it turns out, can be extremely slow, particularly when used on Mesh objects.
By adding our generated objects as sub-objects of a container AnimatorController, we can
minimize this overhead.
This commit is contained in:
bd_ 2023-01-05 21:30:01 +09:00
parent b13f60e80f
commit 250e8be54c
10 changed files with 133 additions and 100 deletions

View File

@ -52,9 +52,9 @@ namespace nadena.dev.modular_avatar.core.editor
private int controllerBaseLayer = 0; private int controllerBaseLayer = 0;
public AnimatorCombiner() public AnimatorCombiner(BuildContext context)
{ {
_combined = Util.CreateAnimator(); _combined = context.CreateAnimator();
} }
public AnimatorController Finish() public AnimatorController Finish()

View File

@ -156,16 +156,16 @@ namespace nadena.dev.modular_avatar.core.editor
var context = new BuildContext(vrcAvatarDescriptor); var context = new BuildContext(vrcAvatarDescriptor);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject); new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject); new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor); context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject); new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject); new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject); new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(); new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process();
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase); new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context.AnimationDatabase); new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject); PhysboneBlockerPass.Process(avatarGameObject);
context.AnimationDatabase.Commit(); context.AnimationDatabase.Commit();

View File

@ -16,7 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor
*/ */
internal class BlendshapeSyncAnimationProcessor internal class BlendshapeSyncAnimationProcessor
{ {
private Object _container; private BuildContext _context;
private Dictionary<Motion, Motion> _motionCache; private Dictionary<Motion, Motion> _motionCache;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings; private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
@ -43,8 +43,11 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
public void OnPreprocessAvatar(GameObject avatar, AnimationDatabase animDb) public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{ {
_context = context;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = avatar.GetComponent<VRCAvatarDescriptor>(); var avatarDescriptor = avatar.GetComponent<VRCAvatarDescriptor>();
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>(); _bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>(); _motionCache = new Dictionary<Motion, Motion>();
@ -145,15 +148,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
var newTree = new BlendTree(); var newTree = new BlendTree();
EditorUtility.CopySerialized(tree, newTree); EditorUtility.CopySerialized(tree, newTree);
if (_container == null) _context.SaveAsset(newTree);
{
_container = newTree;
AssetDatabase.CreateAsset(_container, Util.GenerateAssetPath());
}
else
{
AssetDatabase.AddObjectToAsset(newTree, _container);
}
newTree.children = children; newTree.children = children;
motion = newTree; motion = newTree;

View File

@ -1,4 +1,9 @@
using VRC.SDK3.Avatars.Components; using System;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
@ -6,10 +11,66 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
internal readonly VRCAvatarDescriptor AvatarDescriptor; internal readonly VRCAvatarDescriptor AvatarDescriptor;
internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase(); internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase();
internal readonly AnimatorController AssetContainer;
public BuildContext(VRCAvatarDescriptor avatarDescriptor) public BuildContext(VRCAvatarDescriptor avatarDescriptor)
{ {
AvatarDescriptor = avatarDescriptor; AvatarDescriptor = avatarDescriptor;
// AssetDatabase.CreateAsset is super slow - so only do it once, and add everything else as sub-assets.
// This animator controller exists for the sole purpose of providing a placeholder to dump everything we
// generate into.
AssetContainer = new AnimatorController();
AssetDatabase.CreateAsset(AssetContainer, Util.GenerateAssetPath());
}
public void SaveAsset(Object obj)
{
if (AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
}
public AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
SaveAsset(controller);
return controller;
}
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
var merger = new AnimatorCombiner(this);
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
return merger.Finish();
}
public AnimatorController ConvertAnimatorController(AnimatorOverrideController overrideController)
{
var merger = new AnimatorCombiner(this);
merger.AddOverrideController("", overrideController, null);
return merger.Finish();
} }
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
@ -16,6 +17,8 @@ namespace nadena.dev.modular_avatar.core.editor
"Packages/nadena.dev.modular-avatar/Runtime/Icons/Icon_More_A.png" "Packages/nadena.dev.modular-avatar/Runtime/Icons/Icon_More_A.png"
); );
private BuildContext _context;
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus; private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
@ -24,9 +27,12 @@ namespace nadena.dev.modular_avatar.core.editor
private MenuTree _menuTree; private MenuTree _menuTree;
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack; private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
public void OnPreprocessAvatar(GameObject avatarRoot) public void OnPreprocessAvatar(GameObject avatarRoot, BuildContext context)
{ {
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true) _context = context;
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot
.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
.Where(menuInstaller => menuInstaller.enabled) .Where(menuInstaller => menuInstaller.enabled)
.ToArray(); .ToArray();
if (menuInstallers.Length == 0) return; if (menuInstallers.Length == 0) return;
@ -40,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (avatar.expressionsMenu == null) if (avatar.expressionsMenu == null)
{ {
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>(); var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath()); _context.SaveAsset(menu);
avatar.expressionsMenu = menu; avatar.expressionsMenu = menu;
_clonedMenus[menu] = menu; _clonedMenus[menu] = menu;
} }
@ -100,7 +106,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
// Split target menu // Split target menu
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>(); var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); _context.SaveAsset(newMenu);
const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1;
newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount));
targetMenu.controls.RemoveRange(keepCount, targetMenu.controls.RemoveRange(keepCount,
@ -131,7 +137,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (menu == null) return null; if (menu == null) return null;
if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu; if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu;
newMenu = Object.Instantiate(menu); newMenu = Object.Instantiate(menu);
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); _context.SaveAsset(newMenu);
_clonedMenus[menu] = newMenu; _clonedMenus[menu] = newMenu;
foreach (var control in newMenu.controls) foreach (var control in newMenu.controls)

View File

@ -39,6 +39,8 @@ namespace nadena.dev.modular_avatar.core.editor
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers"; private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
private BuildContext _context;
private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ = private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>(); new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
@ -48,8 +50,10 @@ namespace nadena.dev.modular_avatar.core.editor
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions = Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>(); new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
internal void OnPreprocessAvatar(GameObject avatarGameObject) internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{ {
_context = context;
defaultControllers_.Clear(); defaultControllers_.Clear();
mergeSessions.Clear(); mergeSessions.Clear();
@ -77,7 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (!mergeSessions.TryGetValue(merge.layerType, out var session)) if (!mergeSessions.TryGetValue(merge.layerType, out var session))
{ {
session = new AnimatorCombiner(); session = new AnimatorCombiner(context);
mergeSessions[merge.layerType] = session; mergeSessions[merge.layerType] = session;
if (defaultControllers_.ContainsKey(merge.layerType)) if (defaultControllers_.ContainsKey(merge.layerType))
{ {
@ -131,7 +135,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
// For non-default layers, ensure we always clone the controller for the benefit of subsequent // For non-default layers, ensure we always clone the controller for the benefit of subsequent
// processing phases // processing phases
mergeSessions[layer.type] = new AnimatorCombiner(); mergeSessions[layer.type] = new AnimatorCombiner(_context);
mergeSessions[layer.type].AddController("", controller, null); mergeSessions[layer.type].AddController("", controller, null);
} }
} }

View File

@ -80,7 +80,7 @@ namespace nadena.dev.modular_avatar.core.editor
RetainBoneReferences(c as Component); RetainBoneReferences(c as Component);
} }
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject); new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, context);
} }
private void RetainBoneReferences(Component c) private void RetainBoneReferences(Component c)

View File

@ -77,8 +77,12 @@ namespace nadena.dev.modular_avatar.core.editor
internal class RetargetMeshes internal class RetargetMeshes
{ {
internal void OnPreprocessAvatar(GameObject avatarGameObject) private BuildContext _context;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{ {
_context = context;
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true)) foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{ {
bool isRetargetable = false; bool isRetargetable = false;
@ -93,7 +97,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (isRetargetable) if (isRetargetable)
{ {
new MeshRetargeter(renderer).Retarget(); var newMesh = new MeshRetargeter(renderer).Retarget();
_context.SaveAsset(newMesh);
} }
} }
@ -139,7 +144,7 @@ namespace nadena.dev.modular_avatar.core.editor
this.renderer = renderer; this.renderer = renderer;
} }
public void Retarget() public Mesh Retarget()
{ {
var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform); var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform);
if (avatar == null) throw new System.Exception("Could not find avatar in parents of " + renderer.name); if (avatar == null) throw new System.Exception("Could not find avatar in parents of " + renderer.name);
@ -164,7 +169,7 @@ namespace nadena.dev.modular_avatar.core.editor
avatarTransform.rotation = avRot; avatarTransform.rotation = avRot;
avatarTransform.localScale = avScale; avatarTransform.localScale = avScale;
AssetDatabase.CreateAsset(dst, Util.GenerateAssetPath()); return dst;
} }
private void AdjustShapeKeys() private void AdjustShapeKeys()

View File

@ -17,13 +17,17 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7"; private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7";
private BuildContext _context;
private int internalParamIndex = 0; private int internalParamIndex = 0;
private Dictionary<string, VRCExpressionParameters.Parameter> _syncedParams = private Dictionary<string, VRCExpressionParameters.Parameter> _syncedParams =
new Dictionary<string, VRCExpressionParameters.Parameter>(); new Dictionary<string, VRCExpressionParameters.Parameter>();
public void OnPreprocessAvatar(GameObject avatar) public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{ {
_context = context;
_syncedParams.Clear(); _syncedParams.Clear();
WalkTree(avatar, ImmutableDictionary<string, string>.Empty, ImmutableDictionary<string, string>.Empty); WalkTree(avatar, ImmutableDictionary<string, string>.Empty, ImmutableDictionary<string, string>.Empty);
@ -49,7 +53,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
expParams = Object.Instantiate(expParams); expParams = Object.Instantiate(expParams);
AssetDatabase.CreateAsset(expParams, Util.GenerateAssetPath()); _context.SaveAsset(expParams);
var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet(); var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet();
var parameters = expParams.parameters.ToList(); var parameters = expParams.parameters.ToList();
@ -129,7 +133,7 @@ namespace nadena.dev.modular_avatar.core.editor
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController // RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController) if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
{ {
anim.runtimeAnimatorController = Util.ConvertAnimatorController(overrideController); anim.runtimeAnimatorController = _context.ConvertAnimatorController(overrideController);
} }
var controller = anim.runtimeAnimatorController as AnimatorController; var controller = anim.runtimeAnimatorController as AnimatorController;
@ -147,7 +151,7 @@ namespace nadena.dev.modular_avatar.core.editor
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController // RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController) if (merger.animator is AnimatorOverrideController overrideController)
{ {
merger.animator = Util.ConvertAnimatorController(overrideController); merger.animator = _context.ConvertAnimatorController(overrideController);
} }
var controller = merger.animator as AnimatorController; var controller = merger.animator as AnimatorController;
@ -192,7 +196,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (remapped.TryGetValue(menu, out var newMenu)) return newMenu; if (remapped.TryGetValue(menu, out var newMenu)) return newMenu;
newMenu = Object.Instantiate(menu); newMenu = Object.Instantiate(menu);
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); _context.SaveAsset(newMenu);
remapped[menu] = newMenu; remapped[menu] = newMenu;
ClonedMenuMappings.Add(menu, newMenu); ClonedMenuMappings.Add(menu, newMenu);
@ -222,7 +226,7 @@ namespace nadena.dev.modular_avatar.core.editor
// Deep clone the animator // Deep clone the animator
if (!Util.IsTemporaryAsset(controller)) if (!Util.IsTemporaryAsset(controller))
{ {
controller = Util.DeepCloneAnimator(controller); controller = _context.DeepCloneAnimator(controller);
} }
var parameters = controller.parameters; var parameters = controller.parameters;

View File

@ -60,23 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
EditorApplication.hierarchyChanged += () => { RuntimeUtil.InvokeHierarchyChanged(); }; EditorApplication.hierarchyChanged += () => { RuntimeUtil.InvokeHierarchyChanged(); };
} }
public static AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
AssetDatabase.CreateAsset(controller, GenerateAssetPath());
return controller;
}
public static string GenerateAssetPath() public static string GenerateAssetPath()
{ {
return GetGeneratedAssetsFolder() + "/" + GUID.Generate() + ".asset"; return GetGeneratedAssetsFolder() + "/" + GUID.Generate() + ".asset";
@ -113,31 +96,6 @@ namespace nadena.dev.modular_avatar.core.editor
}; };
} }
public static AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
var merger = new AnimatorCombiner();
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
return merger.Finish();
}
public static AnimatorController ConvertAnimatorController(AnimatorOverrideController overrideController)
{
var merger = new AnimatorCombiner();
merger.AddOverrideController("", overrideController, null);
return merger.Finish();
}
public static bool IsTemporaryAsset(Object obj) public static bool IsTemporaryAsset(Object obj)
{ {
var path = AssetDatabase.GetAssetPath(obj); var path = AssetDatabase.GetAssetPath(obj);