diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs b/Editor/HarmonyPatches/HandleUtilityPatches.cs new file mode 100644 index 00000000..6d735313 --- /dev/null +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs @@ -0,0 +1,55 @@ +#region + +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class HandleUtilityPatches + { + internal static void Patch_FilterInstanceIDs(Harmony h) + { + var t_HandleUtility = AccessTools.TypeByName("UnityEditor.HandleUtility"); + var m_orig = AccessTools.Method(t_HandleUtility, "FilterInstanceIDs"); + + var m_prefix = AccessTools.Method(typeof(HandleUtilityPatches), "Prefix_FilterInstanceIDs"); + + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); + } + + [UsedImplicitly] + private static bool Prefix_FilterInstanceIDs( + ref IEnumerable gameObjects, + out int[] parentInstanceIDs, + out int[] childInstanceIDs + ) + { + gameObjects = RemapObjects(gameObjects); + parentInstanceIDs = childInstanceIDs = null; + return true; + } + + private static IEnumerable RemapObjects(IEnumerable objs) + { + return objs.Select( + obj => + { + if (obj == null) return obj; + if (ScaleAdjusterRenderer.originalObjects.TryGetValue(obj, out var proxy) && proxy != null) + { + return proxy.gameObject; + } + else + { + return obj; + } + } + ).ToArray(); + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta b/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta new file mode 100644 index 00000000..82224e59 --- /dev/null +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 807736f252df4b1b8402827257dcbea3 +timeCreated: 1709354699 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs index 6a601d53..3069107c 100644 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs +++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs @@ -39,20 +39,17 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches [UsedImplicitly] private static void Postfix(GameObject prefabInstance, object __result) { - var ignoredObjects = prefabInstance.GetComponentsInChildren() - .Select(sar => sar.gameObject) - .ToImmutableHashSet(); List added = p_AddedGameObjects.GetValue(__result) as List; if (added == null) return; - added.RemoveAll(obj => ignoredObjects.Contains(obj.instanceGameObject)); + added.RemoveAll(obj => ScaleAdjusterRenderer.proxyObjects.ContainsKey(obj.instanceGameObject)); List objectOverrides = p_ObjectOverrides.GetValue(__result) as List; if (objectOverrides == null) return; objectOverrides.RemoveAll(oo => { var c = oo.instanceObject as Component; - return c != null && ignoredObjects.Contains(c.gameObject); + return c != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(c.gameObject); }); } } diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs b/Editor/HarmonyPatches/HierarchyViewPatches.cs new file mode 100644 index 00000000..c769d016 --- /dev/null +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs @@ -0,0 +1,175 @@ +#region + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using JetBrains.Annotations; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class HierarchyViewPatches + { + private static readonly Type t_HierarchyProperty = AccessTools.TypeByName("UnityEditor.HierarchyProperty"); + private static readonly PropertyInfo p_pptrValue = AccessTools.Property(t_HierarchyProperty, "pptrValue"); + + private static FieldInfo f_m_Rows; // List + private static FieldInfo f_m_RowCount; // int + private static PropertyInfo p_objectPPTR; + + internal static void Patch(Harmony h) + { +#if MODULAR_AVATAR_DEBUG_HIDDEN + return; +#endif + var t_GameObjectTreeViewDataSource = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewDataSource"); + var t_GameObjectTreeViewItem = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewItem"); + + f_m_Rows = t_GameObjectTreeViewDataSource.GetField("m_Rows", + BindingFlags.NonPublic | BindingFlags.Instance); + f_m_RowCount = + t_GameObjectTreeViewDataSource.GetField("m_RowCount", BindingFlags.NonPublic | BindingFlags.Instance); + p_objectPPTR = t_GameObjectTreeViewItem.GetProperty("objectPPTR"); + + var m_orig = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitTreeViewItem", + new[] + { + t_GameObjectTreeViewItem, + typeof(int), + typeof(Scene), + typeof(bool), + typeof(int), + typeof(Object), + typeof(bool), + typeof(int) + }); + var m_patch = AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitTreeViewItem"); + + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_patch)); + + var m_InitRows = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitializeRows"); + var m_transpiler = AccessTools.Method(typeof(HierarchyViewPatches), "Transpile_InitializeRows"); + + h.Patch(original: m_InitRows, + transpiler: new HarmonyMethod(m_transpiler), + postfix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Postfix_InitializeRows")), + prefix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitializeRows")) + ); + } + + private static int skipped = 0; + + private static void Prefix_InitializeRows() + { + skipped = 0; + } + + [UsedImplicitly] + private static void Postfix_InitializeRows(object __instance) + { + var rows = (IList)f_m_Rows.GetValue(__instance); + + var rowCount = (int)f_m_RowCount.GetValue(__instance); + + f_m_RowCount.SetValue(__instance, rowCount - skipped); + + for (int i = 0; i < skipped; i++) + { + rows.RemoveAt(rows.Count - 1); + } + } + + [UsedImplicitly] + private static IEnumerable Transpile_InitializeRows(IEnumerable instructions, + ILGenerator generator) + { + foreach (var c in Transpile_InitializeRows0(instructions, generator)) + { + //Debug.Log(c); + yield return c; + } + } + + [UsedImplicitly] + private static IEnumerable Transpile_InitializeRows0(IEnumerable instructions, + ILGenerator generator) + { + var m_shouldLoop = AccessTools.Method(typeof(HierarchyViewPatches), "ShouldLoop"); + + var m_Next = AccessTools.Method(t_HierarchyProperty, "Next", new[] { typeof(int[]) }); + + foreach (var c in instructions) + { + if (c.Is(OpCodes.Callvirt, m_Next)) + { + var loopLabel = generator.DefineLabel(); + var stash_arg = generator.DeclareLocal(typeof(int[])); + var stash_obj = generator.DeclareLocal(t_HierarchyProperty); + + yield return new CodeInstruction(OpCodes.Stloc, stash_arg); + yield return new CodeInstruction(OpCodes.Stloc, stash_obj); + + var tmp = new CodeInstruction(OpCodes.Ldloc, stash_obj); + tmp.labels.Add(loopLabel); + yield return tmp; + + yield return new CodeInstruction(OpCodes.Ldloc, stash_arg); + yield return new CodeInstruction(OpCodes.Call, m_Next); + + // Check if this item should be ignored. + yield return new CodeInstruction(OpCodes.Ldloc, stash_obj); + yield return new CodeInstruction(OpCodes.Call, m_shouldLoop); + yield return new CodeInstruction(OpCodes.Brtrue_S, loopLabel); + } + else + { + yield return c; + } + } + } + + [UsedImplicitly] + private static bool ShouldLoop(object hierarchyProperty) + { + if (hierarchyProperty == null) return false; + + var pptrValue = p_pptrValue.GetValue(hierarchyProperty); + if (pptrValue == null) return false; + + var skip = ScaleAdjusterRenderer.proxyObjects.ContainsKey((GameObject)pptrValue); + if (skip) skipped++; + + return skip; + } + + private static bool Prefix_InitTreeViewItem( + object __instance, + ref object item, + int itemID, + Scene scene, + bool isSceneHeader, + int colorCode, + Object pptrObject, + ref bool hasChildren, + int depth + ) + { + if (pptrObject == null || isSceneHeader) return true; + + if (hasChildren && ScaleAdjusterRenderer.originalObjects.ContainsKey((GameObject)pptrObject)) + { + // See if there are any other children... + hasChildren = ((GameObject)pptrObject).transform.childCount > 1; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta b/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta new file mode 100644 index 00000000..06d79c30 --- /dev/null +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42f70698a5df48c0908400c425a2f6ee +timeCreated: 1709356304 \ No newline at end of file diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs index 72e71618..0e290761 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -13,9 +13,12 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { private static readonly Action[] patches = new Action[] { - SnoopHeaderRendering.Patch1, - SnoopHeaderRendering.Patch2, - HideScaleAdjusterFromPrefabOverrideView.Patch + HideScaleAdjusterFromPrefabOverrideView.Patch, + HierarchyViewPatches.Patch, + #if UNITY_2022_3_OR_NEWER + HandleUtilityPatches.Patch_FilterInstanceIDs, + PickingObjectPatch.Patch, + #endif }; [InitializeOnLoadMethod] diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs b/Editor/HarmonyPatches/PickingObjectPatch.cs new file mode 100644 index 00000000..3b64a2c3 --- /dev/null +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs @@ -0,0 +1,78 @@ +#region + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using UnityEngine; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class PickingObjectPatch + { + private static Type t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject"); + + private static Type l_PickingObject = + typeof(List<>).MakeGenericType(new[] { t_PickingObject }); + + private static ConstructorInfo ctor_l = AccessTools.Constructor(l_PickingObject); + + private static ConstructorInfo ctor_PickingObject = + AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) }); + + private static PropertyInfo p_materialIndex = AccessTools.Property(t_PickingObject, "materialIndex"); + + private static MethodInfo m_TryGetGameObject = AccessTools.Method(t_PickingObject, "TryGetGameObject"); + + internal static void Patch(Harmony h) + { + var t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject"); + var ctor_PickingObject = AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) }); + + var t_SceneViewPicking = AccessTools.TypeByName("UnityEditor.SceneViewPicking"); + var m_GetAllOverlapping = AccessTools.Method(t_SceneViewPicking, "GetAllOverlapping"); + + var m_postfix = AccessTools.Method(typeof(PickingObjectPatch), nameof(Postfix_GetAllOverlapping)); + + h.Patch(original: m_GetAllOverlapping, postfix: new HarmonyMethod(m_postfix)); + } + + private static void Postfix_GetAllOverlapping(ref object __result) + { + var erased = (IEnumerable)__result; + var list = (IList)ctor_l.Invoke(new object[0]); + + foreach (var obj in erased) + { + if (obj == null) + { + list.Add(obj); + continue; + } + + var args = new object[] { null }; + if ((bool)m_TryGetGameObject.Invoke(obj, args)) + { + var go = args[0] as GameObject; + if (go != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(go)) + { + list.Add(ctor_PickingObject.Invoke(new[] + { + go.transform.parent.gameObject, + p_materialIndex.GetValue(obj) + })); + continue; + } + } + + list.Add(obj); + } + + __result = list; + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs.meta b/Editor/HarmonyPatches/PickingObjectPatch.cs.meta new file mode 100644 index 00000000..acafbc99 --- /dev/null +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cf06818f1c0c436fbae7f755d7110aba +timeCreated: 1709359553 \ No newline at end of file diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs b/Editor/HarmonyPatches/SnoopHeaderRendering.cs deleted file mode 100644 index 39b91c7b..00000000 --- a/Editor/HarmonyPatches/SnoopHeaderRendering.cs +++ /dev/null @@ -1,45 +0,0 @@ -#region - -using HarmonyLib; -using JetBrains.Annotations; -using UnityEngine; - -#endregion - -namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches -{ - /// - /// ScaleAdjusterRenderer toggles off the enabled state of the original mesh just before rendering, - /// in order to allow us to effectively replace it at rendering time. We restore this in OnPostRender, - /// but GUI rendering can happen before this; as such, snoop GUI events and re-enable the original - /// at that time. - /// - internal class SnoopHeaderRendering - { - internal static void Patch1(Harmony harmony) - { - var t_orig = AccessTools.TypeByName("UnityEditor.UIElements.EditorElement"); - var m_orig = AccessTools.Method(t_orig, "HeaderOnGUI"); - - var m_prefix = AccessTools.Method(typeof(SnoopHeaderRendering), "Prefix"); - - harmony.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); - } - - internal static void Patch2(Harmony harmony) - { - var t_GUIUtility = typeof(GUIUtility); - var m_ProcessEvent = AccessTools.Method(t_GUIUtility, "ProcessEvent"); - - var m_prefix = AccessTools.Method(typeof(SnoopHeaderRendering), "Prefix"); - - harmony.Patch(original: m_ProcessEvent, prefix: new HarmonyMethod(m_prefix)); - } - - [UsedImplicitly] - private static void Prefix() - { - ScaleAdjusterRenderer.ClearAllOverrides(); - } - } -} \ No newline at end of file diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta b/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta deleted file mode 100644 index bf645cc0..00000000 --- a/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: cafb5b7e681644cbbeafbeb12d833f6e -timeCreated: 1708235926 \ No newline at end of file diff --git a/Editor/ScaleAdjuster.meta b/Editor/ScaleAdjuster.meta new file mode 100644 index 00000000..b92ab1cb --- /dev/null +++ b/Editor/ScaleAdjuster.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 45b2b4957674444fa96bc0b6e221425e +timeCreated: 1709361970 \ No newline at end of file diff --git a/Editor/ScaleAdjuster/SelectionHack.cs b/Editor/ScaleAdjuster/SelectionHack.cs new file mode 100644 index 00000000..a5334f2e --- /dev/null +++ b/Editor/ScaleAdjuster/SelectionHack.cs @@ -0,0 +1,28 @@ +using UnityEditor; + +namespace nadena.dev.modular_avatar.core.editor.ScaleAdjuster +{ + #if !UNITY_2022_3_OR_NEWER + internal static class SelectionHack + { + [InitializeOnLoadMethod] + static void Init() + { + Selection.selectionChanged += OnSelectionChanged; + + } + + static void OnSelectionChanged() + { + var gameObject = Selection.activeGameObject; + if (gameObject != null && gameObject.GetComponent() != null) + { + EditorApplication.delayCall += () => + { + Selection.activeGameObject = gameObject.transform.parent.gameObject; + }; + } + } + } + #endif +} \ No newline at end of file diff --git a/Editor/ScaleAdjuster/SelectionHack.cs.meta b/Editor/ScaleAdjuster/SelectionHack.cs.meta new file mode 100644 index 00000000..737bed39 --- /dev/null +++ b/Editor/ScaleAdjuster/SelectionHack.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfa3ba0c82bc4439aa86228715f61831 +timeCreated: 1709376243 \ No newline at end of file diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs index 7ffbd0a0..5b1fa799 100644 --- a/Editor/ScaleAdjusterPass.cs +++ b/Editor/ScaleAdjusterPass.cs @@ -10,8 +10,6 @@ namespace nadena.dev.modular_avatar.core.editor { protected override void Execute(ndmf.BuildContext context) { - ScaleAdjusterRenderer.ClearAllOverrides(); - Dictionary boneMappings = new Dictionary(); foreach (var component in context.AvatarRootObject.GetComponentsInChildren()) { diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs b/Runtime/ScaleAdjuster/CameraHooks.cs new file mode 100644 index 00000000..e9da8699 --- /dev/null +++ b/Runtime/ScaleAdjuster/CameraHooks.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; +using UnityEngine; + +namespace nadena.dev.modular_avatar.core +{ + internal static class CameraHooks + { + private static Dictionary originalToProxy + = new Dictionary( + new ObjectIdentityComparer() + ); + + #if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] + private static void Init() + { + Camera.onPreCull += OnPreCull; + Camera.onPostRender += OnPostRender; + UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearStates; + UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearStates(); + } + #endif + + internal static void RegisterProxy(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy) + { + originalToProxy[original] = proxy; + } + + internal static void UnregisterProxy(SkinnedMeshRenderer original) + { + originalToProxy.Remove(original); + } + + private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>(); + + private static List toDeregister = new List(); + + + private static void OnPreCull(Camera camera) + { + ClearStates(); + toDeregister.Clear(); + + foreach (var kvp in originalToProxy) + { + var original = kvp.Key; + var proxy = kvp.Value; + + if (original == null || proxy == null) + { + toDeregister.Add(original); + continue; + } + + proxy.enabled = original.enabled; + if (original.enabled && original.gameObject.activeInHierarchy) + { + statesToRestore.Add((original, original.enabled)); + original.enabled = false; + } + } + + foreach (var original in toDeregister) + { + originalToProxy.Remove(original); + } + } + + private static void OnPostRender(Camera camera) + { + ClearStates(); + } + + + private static void ClearStates() + { + foreach (var (original, state) in statesToRestore) + { + original.enabled = state; + } + + statesToRestore.Clear(); + } + } +} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs.meta b/Runtime/ScaleAdjuster/CameraHooks.cs.meta new file mode 100644 index 00000000..9ee0bfc6 --- /dev/null +++ b/Runtime/ScaleAdjuster/CameraHooks.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 117b3ad981cb487aa5029043f7482a94 +timeCreated: 1709447257 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs index 9171ad21..235cb938 100644 --- a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -101,15 +101,17 @@ namespace nadena.dev.modular_avatar.core if (child == null) { var childObj = new GameObject(ADJUSTER_OBJECT); + Undo.RegisterCreatedObjectUndo(childObj, ""); var childSmr = childObj.AddComponent(); EditorUtility.CopySerialized(smr, childSmr); + childObj.transform.SetParent(smr.transform, false); + childObj.transform.localPosition = Vector3.zero; + childObj.transform.localRotation = Quaternion.identity; + childObj.transform.localScale = Vector3.one; + child = childObj.AddComponent(); - child.transform.SetParent(smr.transform, false); - child.transform.localPosition = Vector3.zero; - child.transform.localRotation = Quaternion.identity; - child.transform.localScale = Vector3.one; } child.BoneMappings[transform] = scaleProxy; diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs index 824cb627..142a40b0 100644 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs @@ -3,8 +3,13 @@ using System; using System.Collections.Generic; using System.Linq; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; +#if UNITY_EDITOR +using UnityEditor; +#endif using UnityEngine; using VRC.SDKBase; +using Object = System.Object; #endregion @@ -15,16 +20,24 @@ namespace nadena.dev.modular_avatar.core [RequireComponent(typeof(SkinnedMeshRenderer))] internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly { - private static event Action OnClearAllOverrides; + internal static Dictionary originalParent = + new Dictionary(new ObjectIdentityComparer()); + + internal static Dictionary proxyObjects = new Dictionary( + new ObjectIdentityComparer()); + + internal static Dictionary originalObjects = + new Dictionary( + new ObjectIdentityComparer() + ); + private static int RecreateHierarchyIndexCount = 0; #if UNITY_EDITOR - [UnityEditor.InitializeOnLoadMethod] + [InitializeOnLoadMethod] static void Setup() { - UnityEditor.EditorApplication.hierarchyChanged += InvalidateAll; - UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearAllOverrides; - UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearAllOverrides(); + EditorApplication.hierarchyChanged += InvalidateAll; } #endif @@ -32,7 +45,7 @@ namespace nadena.dev.modular_avatar.core { RecreateHierarchyIndexCount++; } - + private SkinnedMeshRenderer myRenderer; private SkinnedMeshRenderer parentRenderer; @@ -40,28 +53,42 @@ namespace nadena.dev.modular_avatar.core private bool redoBoneMappings = true; private int lastRecreateHierarchyIndex = -1; - internal Dictionary BoneMappings = new Dictionary(); + internal Dictionary BoneMappings = new Dictionary( + new ObjectIdentityComparer() + ); + #if UNITY_EDITOR private void OnValidate() { - if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return; + if (PrefabUtility.IsPartOfPrefabAsset(this)) return; redoBoneMappings = true; - UnityEditor.EditorApplication.delayCall += () => + EditorApplication.delayCall += () => { if (this == null) return; -#if MODULAR_AVATAR_DEBUG_HIDDEN - gameObject.hideFlags = HideFlags.None; -#else - gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSaveInBuild; -#endif + // We hide this in Harmony, not here, so it is eligible for click-to-select. + gameObject.hideFlags = HideFlags.DontSaveInBuild; + if (BoneMappings == null) { BoneMappings = new Dictionary(); } + + Configure(); }; + + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + redoBoneMappings = true; + } + + private void OnPlayModeStateChanged(PlayModeStateChange change) + { + if (change == PlayModeStateChange.ExitingEditMode) + { + ClearHooks(); + } } #endif @@ -74,12 +101,76 @@ namespace nadena.dev.modular_avatar.core private void OnDestroy() { - ClearAllOverrides(); + ClearHooks(); + #if UNITY_EDITOR + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + #endif } + private void Configure() + { + if (originalParent.TryGetValue(this, out var prevParent) && transform.parent?.gameObject == prevParent) + { + return; + } + + if (prevParent != null) + { + ClearHooks(); + } + + if (transform.parent == null) + { + return; + } + +#if UNITY_EDITOR + AssemblyReloadEvents.beforeAssemblyReload += ClearHooks; +#endif + + var parent = transform.parent.gameObject; + + proxyObjects[gameObject] = parent; + originalObjects[parent] = this; + originalParent[this] = parent; + } + + private void ClearHooks() + { + if (originalParent.TryGetValue(this, out var prevParent)) + { + if (parentRenderer != null) + { + CameraHooks.UnregisterProxy(parentRenderer); + } + + if ((Object)prevParent != null) + { + originalObjects.Remove(prevParent); + } + + originalParent.Remove(this); + if (gameObject != null) + { + proxyObjects.Remove(gameObject); + } + else + { + CleanDeadObjects(); + } + } + } + #if UNITY_EDITOR private void Update() { + if (EditorApplication.isPlayingOrWillChangePlaymode) return; + if (transform.parent == null) + { + DestroyImmediate(gameObject); + return; + } + if (myRenderer == null) { myRenderer = GetComponent(); @@ -89,20 +180,24 @@ namespace nadena.dev.modular_avatar.core { parentRenderer = transform.parent.GetComponent(); } + + CameraHooks.RegisterProxy(parentRenderer, myRenderer); + + Configure(); myRenderer.sharedMaterials = parentRenderer.sharedMaterials; myRenderer.sharedMesh = parentRenderer.sharedMesh; myRenderer.localBounds = parentRenderer.localBounds; if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount) { - var deadBones = BoneMappings.Keys.Where(k => BoneMappings[k] == null) - .ToList(); - deadBones.ForEach(k => { BoneMappings.Remove(k); }); + CleanDeadObjects(BoneMappings); if (BoneMappings.Count == 0) { + #if UNITY_2022_3_OR_NEWER DestroyImmediate(gameObject); return; + #endif } myRenderer.rootBone = MapBone(parentRenderer.rootBone); @@ -129,58 +224,37 @@ namespace nadena.dev.modular_avatar.core myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); } } - - ClearAllOverrides(); - - myRenderer.enabled = parentRenderer.enabled; } - public void OnWillRenderObject() - { - if (myRenderer == null || parentRenderer == null) - { - return; - } - - ClearAllOverrides(); - - if (!parentRenderer.enabled || !parentRenderer.gameObject.activeInHierarchy) - { - return; - } - - parentRenderer.enabled = false; - wasActive = true; - var objName = parentRenderer.gameObject.name; - OnClearAllOverrides += ClearLocalOverride; - // Sometimes - e.g. around domain reloads or undo operations - the parent renderer's enabled field might get - // re-disabled; re-enabler it in delayCall in this case. - UnityEditor.EditorApplication.delayCall += ClearLocalOverride; - } #endif - private void ClearLocalOverride() - { - if (parentRenderer != null) - { - parentRenderer.enabled = true; - } - } - - private void OnPostRender() - { - ClearAllOverrides(); - } - public void ClearBoneCache() { redoBoneMappings = true; } - internal static void ClearAllOverrides() + private static void CleanDeadObjects() { - OnClearAllOverrides?.Invoke(); - OnClearAllOverrides = null; + CleanDeadObjects(originalParent); + CleanDeadObjects(originalObjects); + CleanDeadObjects(proxyObjects); + } + + private static int lastCleanedFrame = 0; + private static void CleanDeadObjects(IDictionary dict) + where K: UnityEngine.Object + where V: UnityEngine.Object + { + // Avoid any O(n^2) behavior if we have lots of cleanup calls happening at the same instant + if (Time.frameCount == lastCleanedFrame) return; + lastCleanedFrame = Time.frameCount; + + var dead = dict.Where(kvp => kvp.Key == null || kvp.Value == null).ToList(); + + foreach (var kvp in dead) + { + dict.Remove(kvp.Key); + } } } } \ No newline at end of file diff --git a/Runtime/Util/ObjectIdentityComparer.cs b/Runtime/Util/ObjectIdentityComparer.cs new file mode 100644 index 00000000..002f6395 --- /dev/null +++ b/Runtime/Util/ObjectIdentityComparer.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace nadena.dev.modular_avatar.JacksonDunstan.NativeCollections +{ + internal class ObjectIdentityComparer : IEqualityComparer + { + public bool Equals(T x, T y) + { + return (object)x == (object)y; + } + + public int GetHashCode(T obj) + { + if (obj == null) return 0; + return RuntimeHelpers.GetHashCode(obj); + } + } +} \ No newline at end of file diff --git a/Runtime/Util/ObjectIdentityComparer.cs.meta b/Runtime/Util/ObjectIdentityComparer.cs.meta new file mode 100644 index 00000000..a1ec9a3e --- /dev/null +++ b/Runtime/Util/ObjectIdentityComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e674cbd75db24fb2b238674cd7010edb +timeCreated: 1709448428 \ No newline at end of file diff --git a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab index 55b1bb32..46e1bb93 100644 --- a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab +++ b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab @@ -26,18 +26,19 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3825275463613500755} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0.023681391, y: 1.0559628, z: -0.6872994} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3646968714803193661} - {fileID: 3646968713996568948} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!95 &3825275463613500750 Animator: - serializedVersion: 3 + serializedVersion: 5 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -50,10 +51,12 @@ Animator: m_UpdateMode: 0 m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 - m_KeepAnimatorControllerStateOnDisable: 0 + m_KeepAnimatorStateOnDisable: 0 + m_WriteDefaultValuesOnDisable: 0 --- !u!114 &3825275463613500753 MonoBehaviour: m_ObjectHideFlags: 0 @@ -322,40 +325,12 @@ MonoBehaviour: contentType: 0 assetBundleUnityVersion: fallbackStatus: 0 ---- !u!114 &3825275463971368602 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 4167925416990528462} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 6fd7cab7d93b403280f2f9da978d8a4f, type: 3} - m_Name: - m_EditorClassIdentifier: - Bindings: - - ReferenceMesh: - referencePath: BaseMesh - Blendshape: shape_0 - LocalBlendshape: shape_0_local - - ReferenceMesh: - referencePath: BaseMesh - Blendshape: shape_1 - LocalBlendshape: shape_1 - - ReferenceMesh: - referencePath: MissingMesh - Blendshape: missing_mesh_shape - LocalBlendshape: missing_mesh_shape - - ReferenceMesh: - referencePath: - Blendshape: missing_mesh_shape_2 - LocalBlendshape: missing_mesh_shape_2 --- !u!1001 &3825275463173128406 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 3825275463613500751} m_Modifications: - target: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, @@ -454,6 +429,9 @@ PrefabInstance: value: BaseMesh objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} --- !u!4 &3646968714803193661 stripped Transform: @@ -466,6 +444,7 @@ PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 3825275463613500751} m_Modifications: - target: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, @@ -569,16 +548,52 @@ PrefabInstance: value: SyncedMesh objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, + type: 3} + insertIndex: -1 + addedObject: {fileID: 3825275463971368602} m_SourcePrefab: {fileID: 100100000, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} ---- !u!1 &4167925416990528462 stripped -GameObject: - m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, - type: 3} - m_PrefabInstance: {fileID: 3825275463971368607} - m_PrefabAsset: {fileID: 0} --- !u!4 &3646968713996568948 stripped Transform: m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} m_PrefabInstance: {fileID: 3825275463971368607} m_PrefabAsset: {fileID: 0} +--- !u!1 &4167925416990528462 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, + type: 3} + m_PrefabInstance: {fileID: 3825275463971368607} + m_PrefabAsset: {fileID: 0} +--- !u!114 &3825275463971368602 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4167925416990528462} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6fd7cab7d93b403280f2f9da978d8a4f, type: 3} + m_Name: + m_EditorClassIdentifier: + Bindings: + - ReferenceMesh: + referencePath: BaseMesh + Blendshape: shape_0 + LocalBlendshape: shape_0_local + - ReferenceMesh: + referencePath: BaseMesh + Blendshape: shape_1 + LocalBlendshape: shape_1 + - ReferenceMesh: + referencePath: MissingMesh + Blendshape: missing_mesh_shape + LocalBlendshape: missing_mesh_shape + - ReferenceMesh: + referencePath: + Blendshape: missing_mesh_shape_2 + LocalBlendshape: missing_mesh_shape_2