From fedf07c5c799dd823d15d7f816605cd9223cab2f Mon Sep 17 00:00:00 2001 From: bd_ Date: Sun, 6 Aug 2023 19:24:53 +0900 Subject: [PATCH] feat: add a feature to unpack generated assets to separate files (#376) --- .../Editor/Inspector/MAAssetBundleEditor.cs | 275 ++++++++++++++++++ .../Inspector/MAAssetBundleEditor.cs.meta | 3 + .../Runtime/MAAssetBundle.cs | 2 +- 3 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs create mode 100644 Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs.meta diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs new file mode 100644 index 00000000..9f55071d --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; +using Object = UnityEngine.Object; + +namespace nadena.dev.modular_avatar.core.editor +{ + [CustomEditor(typeof(MAAssetBundle))] + class MAAssetBundleEditor : MAEditorBase + { + protected override void OnInnerInspectorGUI() + { + if (GUILayout.Button("Unpack")) + { + foreach (var target in targets) + { + MAAssetBundle bundle = (MAAssetBundle) target; + MAAssetBundleExtractor.Unpack(bundle); + } + } + } + } + + public class MAAssetBundleExtractor + { + private static readonly ISet RootAssets = new HashSet() + { + typeof(Mesh), + typeof(AnimationClip), + typeof(RuntimeAnimatorController), + typeof(VRCExpressionParameters), + typeof(VRCExpressionsMenu), + }; + + private Dictionary _assets; + private MAAssetBundle Bundle; + private HashSet _unassigned; + + private MAAssetBundleExtractor(MAAssetBundle bundle) + { + _assets = GetContainedAssets(bundle); + this.Bundle = bundle; + } + + class AssetInfo + { + public readonly UnityEngine.Object Asset; + public readonly HashSet IncomingReferences = new HashSet(); + public readonly HashSet OutgoingReferences = new HashSet(); + + public AssetInfo Root; + + public AssetInfo(UnityEngine.Object obj) + { + this.Asset = obj; + } + + public void PopulateReferences(Dictionary assets) + { + if (Asset is Mesh || Asset is AnimationClip || Asset is VRCExpressionsMenu || + Asset is VRCExpressionsMenu) + { + return; // No child objects + } + + var so = new SerializedObject(Asset); + var prop = so.GetIterator(); + + // TODO extract to common code + bool enterChildren = true; + while (prop.Next(enterChildren)) + { + enterChildren = true; + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + var value = prop.objectReferenceValue; + if (value != null && assets.TryGetValue(value, out var target)) + { + OutgoingReferences.Add(target); + target.IncomingReferences.Add(this); + } + } + else if (prop.propertyType == SerializedPropertyType.String) + { + enterChildren = false; + } + } + } + + public void ForceAssignRoot() + { + // First, see if we're reachable only from one root. + HashSet visited = new HashSet(); + HashSet roots = new HashSet(); + Queue queue = new Queue(); + visited.Add(this); + queue.Enqueue(this); + + while (queue.Count > 0 && roots.Count < 2) + { + var next = queue.Dequeue(); + if (next.Root != null) + { + roots.Add(next.Root); + } + + foreach (var outgoingReference in next.IncomingReferences) + { + if (visited.Add(outgoingReference)) + { + queue.Enqueue(outgoingReference); + } + } + } + + if (roots.Count == 1) + { + this.Root = roots.First(); + } + else + { + this.Root = this; + } + } + } + + public static void Unpack(MAAssetBundle bundle) + { + try + { + AssetDatabase.StartAssetEditing(); + new MAAssetBundleExtractor(bundle).Extract(); + } + finally + { + AssetDatabase.StopAssetEditing(); + } + } + + + private bool TryAssignRoot(AssetInfo info) + { + if (info.Root != null) + { + return true; + } + + if (RootAssets.Any(t => t.IsInstanceOfType(info.Asset)) || info.IncomingReferences.Count == 0) + { + info.Root = info; + return true; + } + + var firstRoot = info.IncomingReferences.First().Root; + if (firstRoot != null && !_unassigned.Contains(firstRoot.Asset) + && info.IncomingReferences.All(t => t.Root == firstRoot)) + { + info.Root = firstRoot; + return true; + } + + return false; + } + + private void Extract() + { + string path = AssetDatabase.GetAssetPath(Bundle); + var directory = System.IO.Path.GetDirectoryName(path); + _unassigned = new HashSet(_assets.Keys); + + foreach (var info in _assets.Values) + { + info.PopulateReferences(_assets); + } + + var queue = new Queue(); + while (_unassigned.Count > 0) + { + // Bootstrap + if (queue.Count == 0) + { + _unassigned.Where(o => TryAssignRoot(_assets[o])).ToList().ForEach(o => { queue.Enqueue(o); }); + + if (queue.Count == 0) + { + _assets[_unassigned.First()].ForceAssignRoot(); + queue.Enqueue(_unassigned.First()); + } + } + + while (queue.Count > 0) + { + var next = queue.Dequeue(); + ProcessSingleAsset(directory, next); + _unassigned.Remove(next); + + foreach (var outgoingReference in _assets[next].OutgoingReferences) + { + if (_unassigned.Contains(outgoingReference.Asset) && TryAssignRoot(outgoingReference)) + { + queue.Enqueue(outgoingReference.Asset); + } + } + } + } + + AssetDatabase.DeleteAsset(path); + } + + private string AssignAssetFilename(string directory, Object next) + { + string assetName = next.name; + if (string.IsNullOrEmpty(assetName)) + { + next.name = next.GetType().Name + " " + GUID.Generate().ToString(); + assetName = next.name; + } + + string assetFile; + for (int extension = 0;; extension++) + { + assetFile = assetName + (extension == 0 ? "" : $" ({extension})") + ".asset"; + assetFile = System.IO.Path.Combine(directory, assetFile); + if (!System.IO.File.Exists(assetFile)) + { + break; + } + } + + return assetFile; + } + + private void ProcessSingleAsset(string directory, Object next) + { + AssetDatabase.RemoveObjectFromAsset(next); + + var info = _assets[next]; + if (info.Root != info) + { + if (!AssetDatabase.IsMainAsset(info.Root.Asset)) + { + throw new Exception( + $"Desired root {info.Root.Asset.name} for asset {next.name} is not a root asset"); + } + + AssetDatabase.AddObjectToAsset(next, info.Root.Asset); + } + else + { + AssetDatabase.CreateAsset(next, AssignAssetFilename(directory, next)); + } + } + + private static Dictionary GetContainedAssets(MAAssetBundle bundle) + { + string path = AssetDatabase.GetAssetPath(bundle); + var rawAssets = AssetDatabase.LoadAllAssetsAtPath(path); + Dictionary infos = new Dictionary(rawAssets.Length); + foreach (var asset in rawAssets) + { + if (!(asset is MAAssetBundle)) + { + infos.Add(asset, new AssetInfo(asset)); + } + } + + + return infos; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs.meta new file mode 100644 index 00000000..28b5326b --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 888db038dcb64c9486cdbe230d29bb72 +timeCreated: 1691168129 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/MAAssetBundle.cs b/Packages/nadena.dev.modular-avatar/Runtime/MAAssetBundle.cs index bec03ef7..a7d554d5 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/MAAssetBundle.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/MAAssetBundle.cs @@ -3,7 +3,7 @@ namespace nadena.dev.modular_avatar.core { [PreferBinarySerialization] - class MAAssetBundle : ScriptableObject + public class MAAssetBundle : ScriptableObject { } } \ No newline at end of file