diff --git a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs index 6417e2fd..66cc997d 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs @@ -188,6 +188,8 @@ namespace nadena.dev.modular_avatar.core.editor context.AnimationDatabase.Commit(); + new GCGameObjectsPass(context, avatarGameObject).OnPreprocessAvatar(); + AfterProcessing?.Invoke(avatarGameObject); } finally diff --git a/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses.meta b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses.meta new file mode 100644 index 00000000..bd8c2f7e --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 2f3bf44d18c6489ab4425ec3cdee360d +timeCreated: 1677488644 +folderAsset: yes \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs new file mode 100644 index 00000000..dc0a19a9 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Dynamics.PhysBone.Components; + +namespace nadena.dev.modular_avatar.core.editor +{ + /// + /// Remove all GameObjects which have no influence on the avatar. + /// + internal class GCGameObjectsPass + { + private readonly BuildContext _context; + private readonly GameObject _root; + private readonly HashSet referencedGameObjects = new HashSet(); + + internal GCGameObjectsPass(BuildContext context, GameObject root) + { + _context = context; + _root = root; + } + + internal void OnPreprocessAvatar() + { + MarkAll(); + Sweep(); + } + + private void MarkAll() + { + foreach (var obj in GameObjects(_root, + node => + { + if (node.CompareTag("EditorOnly")) + { + if (EditorApplication.isPlayingOrWillChangePlaymode) + { + // Retain EditorOnly objects (in case they contain camera fixtures or something), + // but ignore references _from_ them. (TODO: should we mark from them as well?) + MarkObject(node); + } + + return false; + } + + return true; + } + )) + { + foreach (var component in obj.GetComponents()) + { + switch (component) + { + case Transform t: break; + + case VRCPhysBone pb: + MarkObject(obj); + MarkPhysBone(pb); + break; + + case AvatarTagComponent _: + // Tag components will not be retained at runtime, so pretend they're not there. + break; + + default: + MarkObject(obj); + MarkAllReferencedObjects(component); + break; + } + } + } + } + + private void MarkPhysBone(VRCPhysBone pb) + { + var rootTransform = pb.GetRootTransform(); + var ignoreTransforms = pb.ignoreTransforms ?? new List(); + + foreach (var obj in GameObjects(rootTransform.gameObject, + obj => !obj.CompareTag("EditorOnly") && !ignoreTransforms.Contains(obj.transform))) + { + MarkObject(obj); + } + + // Mark colliders, etc + MarkAllReferencedObjects(pb); + } + + private void MarkAllReferencedObjects(Component component) + { + var so = new SerializedObject(component); + var sp = so.GetIterator(); + + bool enterChildren = true; + while (sp.Next(enterChildren)) + { + enterChildren = true; + + switch (sp.propertyType) + { + case SerializedPropertyType.String: + enterChildren = false; + continue; + case SerializedPropertyType.ObjectReference: + if (sp.objectReferenceValue != null) + { + if (sp.objectReferenceValue is GameObject refObj) + { + MarkObject(refObj); + } + else if (sp.objectReferenceValue is Component comp) + { + MarkObject(comp.gameObject); + } + } + + break; + } + } + } + + private void MarkObject(GameObject go) + { + while (go != null && referencedGameObjects.Add(go) && go != _root) + { + go = go.transform.parent?.gameObject; + } + } + + private void Sweep() + { + foreach (var go in GameObjects()) + { + if (!referencedGameObjects.Contains(go)) + { + Debug.Log("Purging object: " + RuntimeUtil.AvatarRootPath(go)); + UnityEngine.Object.DestroyImmediate(go); + } + } + } + + private IEnumerable GameObjects(GameObject node = null, + Func shouldTraverse = null) + { + if (node == null) node = _root; + if (shouldTraverse == null) shouldTraverse = obj => !obj.CompareTag("EditorOnly"); + + if (!shouldTraverse(node)) yield break; + + yield return node; + if (node == null) yield break; + + // Guard against object deletion mid-traversal + List children = new List(); + foreach (Transform t in node.transform) + { + children.Add(t); + } + + foreach (var child in children) + { + foreach (var grandchild in GameObjects(child.gameObject, shouldTraverse)) + { + yield return grandchild; + } + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs.meta new file mode 100644 index 00000000..db4e3c5d --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/OptimizationPasses/GCGameObjectsPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3c155e44d9874f2ba44e67f129112d63 +timeCreated: 1677488653 \ No newline at end of file