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