mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-16 19:25:01 +08:00
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:
parent
b13f60e80f
commit
250e8be54c
@ -52,9 +52,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private int controllerBaseLayer = 0;
|
||||
|
||||
public AnimatorCombiner()
|
||||
public AnimatorCombiner(BuildContext context)
|
||||
{
|
||||
_combined = Util.CreateAnimator();
|
||||
_combined = context.CreateAnimator();
|
||||
}
|
||||
|
||||
public AnimatorController Finish()
|
||||
|
@ -156,16 +156,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
var context = new BuildContext(vrcAvatarDescriptor);
|
||||
|
||||
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject);
|
||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
|
||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
|
||||
|
||||
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject);
|
||||
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
|
||||
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
|
||||
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process();
|
||||
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
|
||||
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context.AnimationDatabase);
|
||||
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||
PhysboneBlockerPass.Process(avatarGameObject);
|
||||
|
||||
context.AnimationDatabase.Commit();
|
||||
|
@ -16,7 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
*/
|
||||
internal class BlendshapeSyncAnimationProcessor
|
||||
{
|
||||
private Object _container;
|
||||
private BuildContext _context;
|
||||
private Dictionary<Motion, Motion> _motionCache;
|
||||
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>();
|
||||
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
||||
_motionCache = new Dictionary<Motion, Motion>();
|
||||
@ -145,15 +148,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
var newTree = new BlendTree();
|
||||
EditorUtility.CopySerialized(tree, newTree);
|
||||
if (_container == null)
|
||||
{
|
||||
_container = newTree;
|
||||
AssetDatabase.CreateAsset(_container, Util.GenerateAssetPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(newTree, _container);
|
||||
}
|
||||
_context.SaveAsset(newTree);
|
||||
|
||||
newTree.children = children;
|
||||
motion = newTree;
|
||||
|
@ -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
|
||||
{
|
||||
@ -6,10 +11,66 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal readonly VRCAvatarDescriptor AvatarDescriptor;
|
||||
internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase();
|
||||
internal readonly AnimatorController AssetContainer;
|
||||
|
||||
public BuildContext(VRCAvatarDescriptor 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
@ -16,31 +17,36 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
"Packages/nadena.dev.modular-avatar/Runtime/Icons/Icon_More_A.png"
|
||||
);
|
||||
|
||||
private BuildContext _context;
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
||||
|
||||
|
||||
|
||||
private VRCExpressionsMenu _rootMenu;
|
||||
|
||||
private MenuTree _menuTree;
|
||||
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)
|
||||
.ToArray();
|
||||
if (menuInstallers.Length == 0) return;
|
||||
|
||||
|
||||
|
||||
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
|
||||
|
||||
|
||||
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
||||
|
||||
if (avatar.expressionsMenu == null)
|
||||
if (avatar.expressionsMenu == null)
|
||||
{
|
||||
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath());
|
||||
_context.SaveAsset(menu);
|
||||
avatar.expressionsMenu = menu;
|
||||
_clonedMenus[menu] = menu;
|
||||
}
|
||||
@ -48,20 +54,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_rootMenu = avatar.expressionsMenu;
|
||||
_menuTree = new MenuTree(avatar);
|
||||
_menuTree.TraverseAvatarMenu();
|
||||
|
||||
|
||||
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
||||
|
||||
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
|
||||
|
||||
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
|
||||
{
|
||||
_menuTree.TraverseMenuInstaller(installer);
|
||||
}
|
||||
|
||||
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
|
||||
|
||||
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
|
||||
{
|
||||
InstallMenu(childElement.installer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null)
|
||||
{
|
||||
if (!installer.enabled) return;
|
||||
@ -71,22 +77,22 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
installer.installTargetMenu = _rootMenu;
|
||||
}
|
||||
|
||||
if (installTarget == null)
|
||||
if (installTarget == null)
|
||||
{
|
||||
installTarget = installer.installTargetMenu;
|
||||
}
|
||||
|
||||
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
|
||||
if (!_clonedMenus.TryGetValue(installTarget, out var targetMenu)) return;
|
||||
|
||||
|
||||
// Clone before appending to sanitize menu icons
|
||||
targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls);
|
||||
|
||||
SplitMenu(installer, targetMenu);
|
||||
|
||||
|
||||
if (_visitedInstallerStack.Contains(installer)) return;
|
||||
_visitedInstallerStack.Push(installer);
|
||||
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer))
|
||||
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer))
|
||||
{
|
||||
InstallMenu(childElement.installer, childElement.parent);
|
||||
}
|
||||
@ -94,13 +100,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_visitedInstallerStack.Pop();
|
||||
}
|
||||
|
||||
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
|
||||
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
|
||||
{
|
||||
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
|
||||
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
|
||||
{
|
||||
// Split target menu
|
||||
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||
_context.SaveAsset(newMenu);
|
||||
const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1;
|
||||
newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount));
|
||||
targetMenu.controls.RemoveRange(keepCount,
|
||||
@ -125,20 +131,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
targetMenu = newMenu;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
if (menu == null) return null;
|
||||
if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu;
|
||||
newMenu = Object.Instantiate(menu);
|
||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||
_context.SaveAsset(newMenu);
|
||||
_clonedMenus[menu] = newMenu;
|
||||
|
||||
|
||||
foreach (var control in newMenu.controls)
|
||||
{
|
||||
if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success)
|
||||
control.icon = null;
|
||||
|
||||
|
||||
for (int i = 0; i < control.labels.Length; i++)
|
||||
{
|
||||
var label = control.labels[i];
|
||||
@ -149,7 +155,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
control.labels[i] = label;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
control.subMenu = CloneMenu(control.subMenu);
|
||||
|
@ -39,6 +39,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
|
||||
|
||||
private BuildContext _context;
|
||||
|
||||
private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
|
||||
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
|
||||
|
||||
@ -48,8 +50,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
|
||||
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
|
||||
|
||||
internal void OnPreprocessAvatar(GameObject avatarGameObject)
|
||||
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
defaultControllers_.Clear();
|
||||
mergeSessions.Clear();
|
||||
|
||||
@ -77,7 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
if (!mergeSessions.TryGetValue(merge.layerType, out var session))
|
||||
{
|
||||
session = new AnimatorCombiner();
|
||||
session = new AnimatorCombiner(context);
|
||||
mergeSessions[merge.layerType] = session;
|
||||
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
|
||||
// processing phases
|
||||
mergeSessions[layer.type] = new AnimatorCombiner();
|
||||
mergeSessions[layer.type] = new AnimatorCombiner(_context);
|
||||
mergeSessions[layer.type].AddController("", controller, null);
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
RetainBoneReferences(c as Component);
|
||||
}
|
||||
|
||||
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject);
|
||||
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, context);
|
||||
}
|
||||
|
||||
private void RetainBoneReferences(Component c)
|
||||
|
@ -77,8 +77,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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))
|
||||
{
|
||||
bool isRetargetable = false;
|
||||
@ -93,7 +97,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void Retarget()
|
||||
public Mesh Retarget()
|
||||
{
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform);
|
||||
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.localScale = avScale;
|
||||
|
||||
AssetDatabase.CreateAsset(dst, Util.GenerateAssetPath());
|
||||
return dst;
|
||||
}
|
||||
|
||||
private void AdjustShapeKeys()
|
||||
|
@ -17,13 +17,17 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7";
|
||||
|
||||
private BuildContext _context;
|
||||
|
||||
private int internalParamIndex = 0;
|
||||
|
||||
private Dictionary<string, VRCExpressionParameters.Parameter> _syncedParams =
|
||||
new Dictionary<string, VRCExpressionParameters.Parameter>();
|
||||
|
||||
public void OnPreprocessAvatar(GameObject avatar)
|
||||
public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
_syncedParams.Clear();
|
||||
|
||||
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);
|
||||
AssetDatabase.CreateAsset(expParams, Util.GenerateAssetPath());
|
||||
_context.SaveAsset(expParams);
|
||||
|
||||
var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet();
|
||||
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
|
||||
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
|
||||
{
|
||||
anim.runtimeAnimatorController = Util.ConvertAnimatorController(overrideController);
|
||||
anim.runtimeAnimatorController = _context.ConvertAnimatorController(overrideController);
|
||||
}
|
||||
|
||||
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
|
||||
if (merger.animator is AnimatorOverrideController overrideController)
|
||||
{
|
||||
merger.animator = Util.ConvertAnimatorController(overrideController);
|
||||
merger.animator = _context.ConvertAnimatorController(overrideController);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
newMenu = Object.Instantiate(menu);
|
||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||
_context.SaveAsset(newMenu);
|
||||
remapped[menu] = newMenu;
|
||||
ClonedMenuMappings.Add(menu, newMenu);
|
||||
|
||||
@ -222,7 +226,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
// Deep clone the animator
|
||||
if (!Util.IsTemporaryAsset(controller))
|
||||
{
|
||||
controller = Util.DeepCloneAnimator(controller);
|
||||
controller = _context.DeepCloneAnimator(controller);
|
||||
}
|
||||
|
||||
var parameters = controller.parameters;
|
||||
|
@ -60,23 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(obj);
|
||||
|
Loading…
Reference in New Issue
Block a user