From f077d24d4871b7c13cddd60d00beea9d3c71f425 Mon Sep 17 00:00:00 2001 From: bd_ Date: Tue, 5 Mar 2024 00:19:54 -0800 Subject: [PATCH] fix: improved Scale Adjuster implementation (#730) This new implementation avoids creating any internal objects within the avatar itself, instead creating them as hidden scene root objects. We also move all update processing to be driven by camera callbacks as well. These changes help improve compatibility with tools such as FaceEmo, as well as being less likely to break when other tools manipulate the avatar's hierarchy directly. --- Editor/HarmonyPatches/HandleUtilityPatches.cs | 33 +- ...HideScaleAdjusterFromPrefabOverrideView.cs | 56 --- ...caleAdjusterFromPrefabOverrideView.cs.meta | 3 - Editor/HarmonyPatches/HierarchyViewPatches.cs | 4 +- Editor/HarmonyPatches/PatchLoader.cs | 1 - Editor/HarmonyPatches/PickingObjectPatch.cs | 4 +- Editor/ScaleAdjusterPass.cs | 43 ++- Runtime/ScaleAdjuster/CameraHooks.cs | 86 ----- .../ModularAvatarScaleAdjuster.cs | 117 +++--- Runtime/ScaleAdjuster/ProxyManager.cs | 338 ++++++++++++++++++ ...meraHooks.cs.meta => ProxyManager.cs.meta} | 0 .../ScaleAdjuster/ScaleAdjusterRenderer.cs | 281 +-------------- Runtime/ScaleAdjuster/ScaleProxy.cs | 48 +-- 13 files changed, 469 insertions(+), 545 deletions(-) delete mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs delete mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta delete mode 100644 Runtime/ScaleAdjuster/CameraHooks.cs create mode 100644 Runtime/ScaleAdjuster/ProxyManager.cs rename Runtime/ScaleAdjuster/{CameraHooks.cs.meta => ProxyManager.cs.meta} (100%) diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs b/Editor/HarmonyPatches/HandleUtilityPatches.cs index 6d735313..7560cf37 100644 --- a/Editor/HarmonyPatches/HandleUtilityPatches.cs +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs @@ -18,8 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches var m_orig = AccessTools.Method(t_HandleUtility, "FilterInstanceIDs"); var m_prefix = AccessTools.Method(typeof(HandleUtilityPatches), "Prefix_FilterInstanceIDs"); + var m_postfix = AccessTools.Method(typeof(HandleUtilityPatches), "Postfix_FilterInstanceIDs"); - h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix), postfix: new HarmonyMethod(m_postfix)); } [UsedImplicitly] @@ -34,13 +35,41 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches return true; } + private static void Postfix_FilterInstanceIDs( + ref IEnumerable gameObjects, + ref int[] parentInstanceIDs, + ref int[] childInstanceIDs + ) + { + HashSet newChildInstanceIDs = null; + + foreach (var parent in gameObjects) + { + foreach (var renderer in parent.GetComponentsInChildren()) + { + if (renderer is SkinnedMeshRenderer smr && + ProxyManager.OriginalToProxyRenderer.TryGetValue(smr, out var proxy) && + proxy != null) + { + if (newChildInstanceIDs == null) newChildInstanceIDs = new HashSet(childInstanceIDs); + newChildInstanceIDs.Add(proxy.GetInstanceID()); + } + } + } + + if (newChildInstanceIDs != null) + { + childInstanceIDs = newChildInstanceIDs.ToArray(); + } + } + 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) + if (ProxyManager.OriginalToProxyObject.TryGetValue(obj, out var proxy) && proxy != null) { return proxy.gameObject; } diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs deleted file mode 100644 index 3069107c..00000000 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using JetBrains.Annotations; -using UnityEditor.SceneManagement; -using UnityEngine; - -#endregion - -namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches -{ - /// - /// Try to prevent various internal objects from showing up in the Prefab Overrides window... - /// - internal class HideScaleAdjusterFromPrefabOverrideView - { - internal static Type t_PrefabOverrides; - internal static PropertyInfo p_AddedGameObjects, p_ObjectOverrides; - - internal static void Patch(Harmony harmony) - { - var t_PrefabOverridesTreeView = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView"); - var m_GetPrefabOverrides = AccessTools.Method(t_PrefabOverridesTreeView, "GetPrefabOverrides"); - - var m_postfix = AccessTools.Method(typeof(HideScaleAdjusterFromPrefabOverrideView), "Postfix"); - - t_PrefabOverrides = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView+PrefabOverrides"); - p_AddedGameObjects = AccessTools.Property(t_PrefabOverrides, "addedGameObjects"); - p_ObjectOverrides = AccessTools.Property(t_PrefabOverrides, "objectOverrides"); - - harmony.Patch(original: m_GetPrefabOverrides, postfix: new HarmonyMethod(m_postfix)); - } - - [UsedImplicitly] - private static void Postfix(GameObject prefabInstance, object __result) - { - List added = p_AddedGameObjects.GetValue(__result) as List; - - if (added == null) return; - 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 && ScaleAdjusterRenderer.proxyObjects.ContainsKey(c.gameObject); - }); - } - } -} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta deleted file mode 100644 index 79b92b42..00000000 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 0893522c012a46358e5ecf1df6628b2e -timeCreated: 1708237029 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs b/Editor/HarmonyPatches/HierarchyViewPatches.cs index c769d016..bdb4b01f 100644 --- a/Editor/HarmonyPatches/HierarchyViewPatches.cs +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs @@ -143,7 +143,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches var pptrValue = p_pptrValue.GetValue(hierarchyProperty); if (pptrValue == null) return false; - var skip = ScaleAdjusterRenderer.proxyObjects.ContainsKey((GameObject)pptrValue); + var skip = ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrValue); if (skip) skipped++; return skip; @@ -163,7 +163,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { if (pptrObject == null || isSceneHeader) return true; - if (hasChildren && ScaleAdjusterRenderer.originalObjects.ContainsKey((GameObject)pptrObject)) + if (hasChildren && ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrObject)) { // See if there are any other children... hasChildren = ((GameObject)pptrObject).transform.childCount > 1; diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs index 0e290761..a2c1bf60 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -13,7 +13,6 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { private static readonly Action[] patches = new Action[] { - HideScaleAdjusterFromPrefabOverrideView.Patch, HierarchyViewPatches.Patch, #if UNITY_2022_3_OR_NEWER HandleUtilityPatches.Patch_FilterInstanceIDs, diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs b/Editor/HarmonyPatches/PickingObjectPatch.cs index 3b64a2c3..a7e5f029 100644 --- a/Editor/HarmonyPatches/PickingObjectPatch.cs +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs @@ -58,11 +58,11 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches if ((bool)m_TryGetGameObject.Invoke(obj, args)) { var go = args[0] as GameObject; - if (go != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(go)) + if (go != null && ProxyManager.ProxyToOriginalObject.TryGetValue(go, out var original)) { list.Add(ctor_PickingObject.Invoke(new[] { - go.transform.parent.gameObject, + original, p_materialIndex.GetValue(obj) })); continue; diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs index 5b1fa799..239eebb5 100644 --- a/Editor/ScaleAdjusterPass.cs +++ b/Editor/ScaleAdjusterPass.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; -using System.Linq; +#region + +using System.Collections.Generic; using nadena.dev.ndmf; -using UnityEditor.EditorTools; using UnityEngine; +#endregion + namespace nadena.dev.modular_avatar.core.editor { internal class ScaleAdjusterPass : Pass @@ -11,25 +13,30 @@ namespace nadena.dev.modular_avatar.core.editor protected override void Execute(ndmf.BuildContext context) { Dictionary boneMappings = new Dictionary(); - foreach (var component in context.AvatarRootObject.GetComponentsInChildren()) + + foreach (var adjuster in context.AvatarRootObject.GetComponentsInChildren(true)) { - var proxyTransform = component.transform; - var parentAdjuster = component.transform.parent?.GetComponent(); - if (parentAdjuster != null) - { - UnityEngine.Object.DestroyImmediate(component); - - proxyTransform.localScale = parentAdjuster.Scale; - parentAdjuster.scaleProxy = null; // prevent destruction of the ScaleProxy itself - UnityEngine.Object.DestroyImmediate(parentAdjuster); - - boneMappings.Add(proxyTransform.parent, proxyTransform); - } + var proxyObject = new GameObject("ScaleProxy"); + var proxyTransform = proxyObject.transform; + + proxyTransform.SetParent(adjuster.transform, false); + proxyTransform.localPosition = Vector3.zero; + proxyTransform.localRotation = Quaternion.identity; + proxyTransform.localScale = adjuster.Scale; + + boneMappings.Add(adjuster.transform, proxyTransform); + + Object.DestroyImmediate(adjuster); } - + + // Legacy cleanup foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) { - UnityEngine.Object.DestroyImmediate(sar.gameObject); + Object.DestroyImmediate(sar.gameObject); + } + foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) + { + Object.DestroyImmediate(sar.gameObject); } if (boneMappings.Count == 0) diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs b/Runtime/ScaleAdjuster/CameraHooks.cs deleted file mode 100644 index e9da8699..00000000 --- a/Runtime/ScaleAdjuster/CameraHooks.cs +++ /dev/null @@ -1,86 +0,0 @@ -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/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs index 235cb938..33f90a5b 100644 --- a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -1,6 +1,8 @@ #region +using System; using UnityEngine; +using UnityEngine.SceneManagement; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; @@ -25,105 +27,100 @@ namespace nadena.dev.modular_avatar.core set { m_Scale = value; - Update(); + PreCull(); } } [SerializeField] [FormerlySerializedAs("scaleProxy")] internal Transform legacyScaleProxy; - internal Transform scaleProxy; + internal Transform scaleProxyParent, scaleProxyChild; + [NonSerialized] private bool initialized = false; #if UNITY_EDITOR - void OnValidate() + void Awake() { - base.OnValidate(); + ProxyManager.RegisterAdjuster(this); initialized = false; } - - private void Update() + + void OnValidate() { - if (scaleProxy == null || initialized == false) + ProxyManager.RegisterAdjuster(this); + initialized = false; + } + + internal void PreCull() + { + if (PrefabUtility.IsPartOfPrefabAsset(this)) return; + + if (scaleProxyParent == null || initialized == false) { InitializeProxy(); } + var xform = transform; + scaleProxyParent.position = transform.position; + scaleProxyParent.rotation = transform.rotation; + scaleProxyParent.localScale = transform.localScale; + scaleProxyChild.localScale = m_Scale; + + ProxyManager.RegisterBone(xform, scaleProxyChild); + if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy)) { DestroyImmediate(legacyScaleProxy.gameObject); legacyScaleProxy = null; } - - scaleProxy.localScale = m_Scale; } private void InitializeProxy() { - if (scaleProxy == null) + if (scaleProxyParent == null) { - scaleProxy = new GameObject(gameObject.name + " (Scale Proxy)").transform; - scaleProxy.SetParent(transform, false); - scaleProxy.localPosition = Vector3.zero; - scaleProxy.localRotation = Quaternion.identity; - scaleProxy.localScale = m_Scale; - scaleProxy.gameObject.AddComponent(); - PrefabUtility.RecordPrefabInstancePropertyModifications(this); + scaleProxyParent = new GameObject(gameObject.name + " (Scale Proxy)").transform; + scaleProxyChild = new GameObject("Child").transform; + + scaleProxyChild.transform.SetParent(scaleProxyParent, false); + +#if MODULAR_AVATAR_DEBUG_HIDDEN + scaleProxyParent.gameObject.hideFlags = HideFlags.DontSave; + scaleProxyChild.gameObject.hideFlags = HideFlags.DontSave; +#else + scaleProxyParent.gameObject.hideFlags = HideFlags.HideAndDontSave; + scaleProxyChild.gameObject.hideFlags = HideFlags.HideAndDontSave; +#endif + + if (scaleProxyParent.gameObject.scene != gameObject.scene && gameObject.scene.IsValid()) + { + SceneManager.MoveGameObjectToScene(scaleProxyParent.gameObject, gameObject.scene); + } } - - ConfigureRenderers(); - + initialized = true; } private void OnDestroy() { - if (scaleProxy != null) + ProxyManager.UnregisterAdjuster(this); + + if (scaleProxyParent != null) { - DestroyImmediate(scaleProxy.gameObject); + DestroyImmediate(scaleProxyParent.gameObject); + } + + if (transform != null) + { + ProxyManager.UnregisterBone(transform); } - ScaleAdjusterRenderer.InvalidateAll(); base.OnDestroy(); } - - - private void ConfigureRenderers() - { - var avatar = RuntimeUtil.FindAvatarInParents(transform); - if (avatar == null) return; - foreach (var smr in avatar.GetComponentsInChildren(true)) - { - if (smr.GetComponent() != null) continue; - - var child = smr.transform.Find(ADJUSTER_OBJECT)?.GetComponent(); - 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.BoneMappings[transform] = scaleProxy; - child.ClearBoneCache(); - } - } -#endif - -#if !UNITY_EDITOR - private void Update() - { - // placeholder to make builds work +#else + internal void PreCull() { + // build time stub } #endif } diff --git a/Runtime/ScaleAdjuster/ProxyManager.cs b/Runtime/ScaleAdjuster/ProxyManager.cs new file mode 100644 index 00000000..6c22f55e --- /dev/null +++ b/Runtime/ScaleAdjuster/ProxyManager.cs @@ -0,0 +1,338 @@ +#region + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; +#if UNITY_EDITOR +using UnityEditor; +using UnityEditor.SceneManagement; +#endif +using UnityEngine; +using UnityEngine.SceneManagement; + +#endregion + +namespace nadena.dev.modular_avatar.core +{ + internal static class ProxyManager + { + #region Accessible from multiple threads + + private static bool _dirty = false; + private static readonly object _lock = new object(); + + private static ImmutableHashSet _adjusters + = ImmutableHashSet.Empty; + + private static ImmutableDictionary _originalToReplacementBone + = ImmutableDictionary.Empty.WithComparers(new ObjectIdentityComparer()); + + internal static void RegisterAdjuster(ModularAvatarScaleAdjuster adjuster) + { + lock (_lock) + { + _adjusters = _adjusters.Add(adjuster); + _dirty = true; + } + } + + internal static void UnregisterAdjuster(ModularAvatarScaleAdjuster adjuster) + { + lock (_lock) + { + _adjusters = _adjusters.Remove(adjuster); + _dirty = true; + } + } + + internal static void RegisterBone(Transform original, Transform replacement) + { + lock (_lock) + { + if (_originalToReplacementBone.TryGetValue(original, out var val) && val == replacement) + { + return; + } + + _originalToReplacementBone = _originalToReplacementBone.Add(original, replacement); + _dirty = true; + } + } + + internal static void UnregisterBone(Transform original) + { + lock (_lock) + { + _originalToReplacementBone = _originalToReplacementBone.Remove(original); + _dirty = true; + } + } + + #endregion + + private static ImmutableHashSet _capturedAdjusters = + ImmutableHashSet.Empty; + + private static ImmutableDictionary _capturedBones = + ImmutableDictionary.Empty; + + private static ImmutableDictionary _originalToReplacementRenderer + = ImmutableDictionary.Empty.WithComparers( + new ObjectIdentityComparer()); + + internal static ImmutableDictionary ProxyToOriginalObject { get; private set; } = + ImmutableDictionary.Empty; + + internal static ImmutableDictionary OriginalToProxyObject { get; private set; } = + ImmutableDictionary.Empty; + + internal static ImmutableDictionary OriginalToProxyRenderer => + _originalToReplacementRenderer; + + internal static ImmutableHashSet RetainedObjects; + + internal static bool ShouldRetain(GameObject obj) => RetainedObjects.Contains(obj); + + private static void BuildRenderers() + { + lock (_lock) + { + _capturedAdjusters = _adjusters; + + // Give each adjuster a chance to initialize the bone mappings first + foreach (var adj in _capturedAdjusters) + { + adj.PreCull(); + } + + foreach (var kvp in _originalToReplacementBone) + { + if (kvp.Key == null || kvp.Value == null) + { + _originalToReplacementBone = _originalToReplacementBone.Remove(kvp.Key); + } + } + + _capturedBones = _originalToReplacementBone; + _dirty = false; + } + + var avatarRoots = _capturedBones.Keys.Select(RuntimeUtil.FindAvatarTransformInParents).ToImmutableHashSet(); + var potentialRenderers = avatarRoots.SelectMany(r => r.GetComponentsInChildren(true)) + .ToList(); + + ImmutableDictionary.Builder renderers = + ImmutableDictionary.CreateBuilder( + new ObjectIdentityComparer() + ); + + foreach (var originalRenderer in potentialRenderers) + { + SkinnedMeshRenderer replacement; + + if (!NeedsReplacement(originalRenderer)) + { + if (_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) && + replacement != null) + { + Object.DestroyImmediate(replacement.gameObject); + } + + continue; + } + + if (!_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) || + replacement == null) + { + replacement = CreateReplacement(originalRenderer); + } + + SetupBoneMappings(originalRenderer, replacement); + + renderers.Add(originalRenderer, replacement); + } + + foreach (var kvp in _originalToReplacementRenderer) + { + if (!renderers.ContainsKey(kvp.Key)) + { + if (kvp.Value != null) + { + Object.DestroyImmediate(kvp.Value.gameObject); + } + } + } + + _originalToReplacementRenderer = renderers.ToImmutable(); + ProxyToOriginalObject = _originalToReplacementRenderer.ToImmutableDictionary( + kvp => kvp.Value.gameObject, + kvp => kvp.Key.gameObject + ); + + OriginalToProxyObject = _originalToReplacementRenderer.ToImmutableDictionary( + kvp => kvp.Key.gameObject, + kvp => kvp.Value.gameObject + ); + + RetainedObjects = ProxyToOriginalObject.Keys.Concat( + _capturedBones.Values.Where(b => b != null).Select(b => b.gameObject) + ).ToImmutableHashSet(new ObjectIdentityComparer()); + } + + private static void SetupBoneMappings(SkinnedMeshRenderer originalRenderer, SkinnedMeshRenderer replacement) + { + replacement.sharedMesh = originalRenderer.sharedMesh; + replacement.bones = originalRenderer.bones.Select(MapBone).ToArray(); + } + + private static Transform MapBone(Transform srcBone) + { + if (_capturedBones.TryGetValue(srcBone, out var newBone) && newBone != null) + { + return newBone; + } + else + { + return srcBone; + } + } + + private static SkinnedMeshRenderer CreateReplacement(SkinnedMeshRenderer originalRenderer) + { + var obj = new GameObject("MA Proxy Renderer for " + originalRenderer.gameObject.name); + // We can't use HideAndDontSave as this would break scene view click-to-pick handling + // (so instead this is hidden via the HierarchyViewPatches harmony hack) + obj.hideFlags = HideFlags.DontSave; + + var renderer = obj.AddComponent(); + + return renderer; + } + + private static bool NeedsReplacement(SkinnedMeshRenderer originalRenderer) + { + if (originalRenderer.sharedMesh == null) return false; + + var bones = originalRenderer.bones; + var weights = originalRenderer.sharedMesh.GetAllBoneWeights(); + + for (var i = 0; i < weights.Length; i++) + { + var bone = bones[weights[i].boneIndex]; + if (_capturedBones.ContainsKey(bone)) return true; + } + + return false; + } + +#if UNITY_EDITOR + [InitializeOnLoadMethod] + private static void Init() + { + Camera.onPreCull += OnPreCull; + Camera.onPostRender += OnPostRender; + AssemblyReloadEvents.beforeAssemblyReload += () => + { + ClearStates(); + foreach (var renderer in _originalToReplacementRenderer.Values) + { + Object.DestroyImmediate(renderer.gameObject); + } + }; + EditorSceneManager.sceneSaving += (scene, path) => ClearStates(); + } + + private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>(); + + private static void OnPreCull(Camera camera) + { + if (_dirty) + { + BuildRenderers(); + } + + ClearStates(); + + foreach (var adj in _capturedAdjusters) + { + adj.PreCull(); // update scale + } + + foreach (var kvp in _originalToReplacementRenderer) + { + var original = kvp.Key; + var proxy = kvp.Value; + + if (original == null || proxy == null) + { + _dirty = true; + continue; + } + + var originalGameObject = original.gameObject; + var proxyActive = original.enabled && originalGameObject.activeInHierarchy && + !SceneVisibilityManager.instance.IsHidden(originalGameObject, false); + + proxy.enabled = proxyActive; + if (original.enabled && originalGameObject.activeInHierarchy) + { + CopyRendererStates(original, proxy); + + statesToRestore.Add((original, original.enabled)); + original.enabled = false; + } + } + } + + private static void CopyRendererStates(SkinnedMeshRenderer parentRenderer, SkinnedMeshRenderer myRenderer) + { + myRenderer.transform.position = parentRenderer.transform.position; + myRenderer.transform.rotation = parentRenderer.transform.rotation; + + myRenderer.sharedMaterials = parentRenderer.sharedMaterials; + myRenderer.localBounds = parentRenderer.localBounds; + myRenderer.rootBone = MapBone(parentRenderer.rootBone); + myRenderer.quality = parentRenderer.quality; + myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode; + myRenderer.receiveShadows = parentRenderer.receiveShadows; + myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage; + myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage; + myRenderer.probeAnchor = parentRenderer.probeAnchor; + myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode; + myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic; + + if (myRenderer.gameObject.scene != parentRenderer.gameObject.scene && + parentRenderer.gameObject.scene.IsValid()) + { + SceneManager.MoveGameObjectToScene(myRenderer.gameObject, parentRenderer.gameObject.scene); + } + + if (myRenderer.sharedMesh != null) + { + var blendShapeCount = myRenderer.sharedMesh.blendShapeCount; + + for (int i = 0; i < blendShapeCount; i++) + { + myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); + } + } + } + + private static void OnPostRender(Camera camera) + { + ClearStates(); + } + + private static void ClearStates() + { + foreach (var (original, state) in statesToRestore) + { + original.enabled = state; + } + + statesToRestore.Clear(); + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs.meta b/Runtime/ScaleAdjuster/ProxyManager.cs.meta similarity index 100% rename from Runtime/ScaleAdjuster/CameraHooks.cs.meta rename to Runtime/ScaleAdjuster/ProxyManager.cs.meta diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs index f53655ac..e4ab0e57 100644 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs @@ -1,300 +1,33 @@ #region +using UnityEngine; +using VRC.SDKBase; #if UNITY_EDITOR using UnityEditor; #endif -using System.Collections.Generic; -using System.Linq; -using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; -using UnityEngine; -using VRC.SDKBase; -using Object = System.Object; #endregion namespace nadena.dev.modular_avatar.core { + /// + /// Legacy component from early 1.9.x builds. + /// [ExecuteInEditMode] [AddComponentMenu("")] [RequireComponent(typeof(SkinnedMeshRenderer))] internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly { - 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 - [InitializeOnLoadMethod] - static void Setup() - { - EditorApplication.hierarchyChanged += InvalidateAll; - } -#endif - - internal static void InvalidateAll() - { - RecreateHierarchyIndexCount++; - } - - private SkinnedMeshRenderer myRenderer; - private SkinnedMeshRenderer parentRenderer; - - private bool wasActive = false; - private bool redoBoneMappings = true; - private bool hasRelevantBones = false; - private int lastRecreateHierarchyIndex = -1; - - internal Dictionary BoneMappings = new Dictionary( - new ObjectIdentityComparer() - ); - - #if UNITY_EDITOR private void OnValidate() { if (PrefabUtility.IsPartOfPrefabAsset(this)) return; - redoBoneMappings = true; - + EditorApplication.delayCall += () => { - if (this == null) return; - - // 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(); + if (this != null) DestroyImmediate(gameObject); }; - - EditorApplication.playModeStateChanged += OnPlayModeStateChanged; - redoBoneMappings = true; - } - - private void OnPlayModeStateChanged(PlayModeStateChange change) - { - if (change == PlayModeStateChange.ExitingEditMode) - { - ClearHooks(); - } } #endif - - private Transform MapBone(Transform bone) - { - if (bone == null) return null; - if (BoneMappings.TryGetValue(bone, out var newBone) && newBone != null) return newBone; - return bone; - } - - private void OnDestroy() - { - 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 (!hasRelevantBones) - { - return; - } - - 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; - - CheckBoneUsage(); // register the proxy if needed - } - - 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(); - } - - if (parentRenderer == null) - { - parentRenderer = transform.parent.GetComponent(); - } - - Configure(); - - myRenderer.sharedMaterials = parentRenderer.sharedMaterials; - myRenderer.localBounds = parentRenderer.localBounds; - if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount - || myRenderer.sharedMesh != parentRenderer.sharedMesh) - { - CleanDeadObjects(BoneMappings); - - if (BoneMappings.Count == 0) - { - #if UNITY_2022_3_OR_NEWER - DestroyImmediate(gameObject); - return; - #endif - } - - myRenderer.sharedMesh = parentRenderer.sharedMesh; - myRenderer.rootBone = MapBone(parentRenderer.rootBone); - myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray(); - redoBoneMappings = false; - lastRecreateHierarchyIndex = RecreateHierarchyIndexCount; - - CheckBoneUsage(); - } - - if (!hasRelevantBones) return; - - myRenderer.quality = parentRenderer.quality; - myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode; - myRenderer.receiveShadows = parentRenderer.receiveShadows; - myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage; - myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage; - myRenderer.probeAnchor = parentRenderer.probeAnchor; - myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode; - myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic; - - if (myRenderer.sharedMesh != null) - { - var blendShapeCount = myRenderer.sharedMesh.blendShapeCount; - - for (int i = 0; i < blendShapeCount; i++) - { - myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); - } - } - } -#endif - private void CheckBoneUsage() - { - hasRelevantBones = false; - if (myRenderer.sharedMesh != null) - { - var weights = myRenderer.sharedMesh.GetAllBoneWeights(); - var parentBones = parentRenderer.bones; - foreach (var weight in weights) - { - if (weight.weight < 0.0001f) continue; - if (weight.boneIndex < 0 || weight.boneIndex >= parentBones.Length) continue; - - var bone = parentBones[weight.boneIndex]; - if (BoneMappings.ContainsKey(bone)) - { - hasRelevantBones = true; - break; - } - } - } - - if (hasRelevantBones) - { - CameraHooks.RegisterProxy(parentRenderer, myRenderer); - } - else - { - CameraHooks.UnregisterProxy(parentRenderer); - myRenderer.enabled = false; - } - } - - public void ClearBoneCache() - { - redoBoneMappings = true; - } - - private static void CleanDeadObjects() - { - 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/ScaleAdjuster/ScaleProxy.cs b/Runtime/ScaleAdjuster/ScaleProxy.cs index 25cac20b..27dca2ef 100644 --- a/Runtime/ScaleAdjuster/ScaleProxy.cs +++ b/Runtime/ScaleAdjuster/ScaleProxy.cs @@ -16,13 +16,12 @@ namespace nadena.dev.modular_avatar.core #if UNITY_EDITOR void OnValidate() { - base.OnDestroy(); + base.OnValidate(); EditorApplication.delayCall += DeferredValidate; } void OnDestroy() { - ScaleAdjusterRenderer.InvalidateAll(); base.OnDestroy(); } @@ -30,44 +29,12 @@ namespace nadena.dev.modular_avatar.core { if (this == null) return; - if (GetComponent() == null) + // Avoid logspam on Unity 2019 + if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; + + if (!ProxyManager.ShouldRetain(gameObject)) { - gameObject.AddComponent(); - } - - var avatar = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform); - ClearOverrides(avatar); - - gameObject.hideFlags = HideFlags.HideInHierarchy; - -#if MODULAR_AVATAR_DEBUG_HIDDEN - gameObject.hideFlags = HideFlags.None; -#endif - hideFlags = HideFlags.None; - - var parentObject = transform.parent; - var parentScaleAdjuster = - parentObject != null ? parentObject.GetComponent() : null; - - if (parentScaleAdjuster == null || parentScaleAdjuster.scaleProxy != transform) - { - if (PrefabUtility.IsPartOfPrefabAsset(this)) - { - var path = AssetDatabase.GetAssetPath(this); - var root = PrefabUtility.LoadPrefabContents(path); - - foreach (var obj in root.GetComponentsInChildren()) - { - obj.DeferredValidate(); - } - - PrefabUtility.SaveAsPrefabAsset(root, path); - PrefabUtility.UnloadPrefabContents(root); - } - else - { - SelfDestruct(); - } + SelfDestruct(); } } @@ -79,10 +46,9 @@ namespace nadena.dev.modular_avatar.core root = transform; while (root.parent != null) root = root.parent; } - + ClearOverrides(root); - // Avoid logspam on Unity 2019 if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; DestroyImmediate(gameObject);