From 197d847514ef1f087aba8cabd5063bc714ee4021 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sun, 18 Feb 2024 20:21:26 +0900 Subject: [PATCH] fix/reimplement: scale adjuster results in infinite loops sometimes (#677) Closes: #676 --- Editor/HarmonyPatches.meta | 3 + ...HideScaleAdjusterFromPrefabOverrideView.cs | 59 ++++ ...caleAdjusterFromPrefabOverrideView.cs.meta | 3 + Editor/HarmonyPatches/PatchLoader.cs | 21 ++ Editor/HarmonyPatches/PatchLoader.cs.meta | 3 + Editor/HarmonyPatches/SnoopHeaderRendering.cs | 40 +++ .../SnoopHeaderRendering.cs.meta | 3 + ....dev.modular-avatar.harmony-patches.asmdef | 28 ++ ...modular-avatar.harmony-patches.asmdef.meta | 7 + Editor/PluginDefinition/PluginDefinition.cs | 1 + Editor/ScaleAdjusterPass.cs | 51 ++++ Editor/ScaleAdjusterPass.cs.meta | 3 + Editor/assembly-info.cs | 9 +- Runtime/ModularAvatarScaleAdjuster.cs | 268 ------------------ Runtime/ScaleAdjuster.meta | 3 + .../ModularAvatarScaleAdjuster.cs | 128 +++++++++ .../ModularAvatarScaleAdjuster.cs.meta | 0 .../ScaleAdjuster/ScaleAdjusterRenderer.cs | 178 ++++++++++++ .../ScaleAdjusterRenderer.cs.meta | 3 + Runtime/{ => ScaleAdjuster}/ScaleProxy.cs | 37 ++- .../{ => ScaleAdjuster}/ScaleProxy.cs.meta | 0 Runtime/TestBehavior.cs.meta | 3 + Runtime/assembly-info.cs | 7 +- UnitTests~/ComponentSettingsTest.cs | 2 + 24 files changed, 585 insertions(+), 275 deletions(-) create mode 100644 Editor/HarmonyPatches.meta create mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs create mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta create mode 100644 Editor/HarmonyPatches/PatchLoader.cs create mode 100644 Editor/HarmonyPatches/PatchLoader.cs.meta create mode 100644 Editor/HarmonyPatches/SnoopHeaderRendering.cs create mode 100644 Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta create mode 100644 Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef create mode 100644 Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta create mode 100644 Editor/ScaleAdjusterPass.cs create mode 100644 Editor/ScaleAdjusterPass.cs.meta delete mode 100644 Runtime/ModularAvatarScaleAdjuster.cs create mode 100644 Runtime/ScaleAdjuster.meta create mode 100644 Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs rename Runtime/{ => ScaleAdjuster}/ModularAvatarScaleAdjuster.cs.meta (100%) create mode 100644 Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs create mode 100644 Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta rename Runtime/{ => ScaleAdjuster}/ScaleProxy.cs (75%) rename Runtime/{ => ScaleAdjuster}/ScaleProxy.cs.meta (100%) create mode 100644 Runtime/TestBehavior.cs.meta diff --git a/Editor/HarmonyPatches.meta b/Editor/HarmonyPatches.meta new file mode 100644 index 00000000..0ee1c19c --- /dev/null +++ b/Editor/HarmonyPatches.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cf056e4454ad43ababbfff1dd06ec1d0 +timeCreated: 1708235917 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs new file mode 100644 index 00000000..6a601d53 --- /dev/null +++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs @@ -0,0 +1,59 @@ +#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) + { + 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)); + + 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); + }); + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta new file mode 100644 index 00000000..79b92b42 --- /dev/null +++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0893522c012a46358e5ecf1df6628b2e +timeCreated: 1708237029 \ No newline at end of file diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs new file mode 100644 index 00000000..7d0a6f22 --- /dev/null +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -0,0 +1,21 @@ +#region + +using HarmonyLib; +using UnityEditor; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal class PatchLoader + { + [InitializeOnLoadMethod] + static void ApplyPatches() + { + var harmony = new Harmony("nadena.dev.modular_avatar"); + + SnoopHeaderRendering.Patch(harmony); + HideScaleAdjusterFromPrefabOverrideView.Patch(harmony); + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/PatchLoader.cs.meta b/Editor/HarmonyPatches/PatchLoader.cs.meta new file mode 100644 index 00000000..d82b39f6 --- /dev/null +++ b/Editor/HarmonyPatches/PatchLoader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e92aedf0fa324b8fb1b425a7fc2b1771 +timeCreated: 1708235934 \ No newline at end of file diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs b/Editor/HarmonyPatches/SnoopHeaderRendering.cs new file mode 100644 index 00000000..894cd10c --- /dev/null +++ b/Editor/HarmonyPatches/SnoopHeaderRendering.cs @@ -0,0 +1,40 @@ +#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 Patch(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)); + + var t_GUIUtility = typeof(GUIUtility); + var m_ProcessEvent = AccessTools.Method(t_GUIUtility, "ProcessEvent"); + + 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 new file mode 100644 index 00000000..bf645cc0 --- /dev/null +++ b/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cafb5b7e681644cbbeafbeb12d833f6e +timeCreated: 1708235926 \ No newline at end of file diff --git a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef new file mode 100644 index 00000000..e3e391fc --- /dev/null +++ b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef @@ -0,0 +1,28 @@ +{ + "name": "nadena.dev.modular-avatar.harmony-patches", + "rootNamespace": "", + "references": [ + "nadena.dev.modular-avatar.core", + "nadena.dev.modular-avatar.core.editor", + "VRC.SDKBase.Editor" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [ + "VRCSDK_HAS_HARMONY" + ], + "versionDefines": [ + { + "name": "com.vrchat.base", + "expression": "(3.4.999,)", + "define": "VRCSDK_HAS_HARMONY" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta new file mode 100644 index 00000000..58c24e87 --- /dev/null +++ b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0f9cfa335019051479cc80ce30c95a68 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index c8e31e5d..8f99db5a 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -36,6 +36,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin { seq.Run(ClearEditorOnlyTags.Instance); seq.Run(MeshSettingsPluginPass.Instance); + seq.Run(ScaleAdjusterPass.Instance); #if MA_VRCSDK3_AVATARS seq.Run(RenameParametersPluginPass.Instance); seq.Run(MergeBlendTreePass.Instance); diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs new file mode 100644 index 00000000..76dd924d --- /dev/null +++ b/Editor/ScaleAdjusterPass.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using nadena.dev.ndmf; +using UnityEditor.EditorTools; +using UnityEngine; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ScaleAdjusterPass : Pass + { + protected override void Execute(ndmf.BuildContext context) + { + ScaleAdjusterRenderer.ClearAllOverrides(); + + Dictionary boneMappings = new Dictionary(); + foreach (var component in context.AvatarRootObject.GetComponentsInChildren()) + { + var proxyTransform = component.transform; + var parentAdjuster = component.transform.parent?.GetComponent(); + if (parentAdjuster != null) + { + UnityEngine.Object.DestroyImmediate(component); + + proxyTransform.localScale = parentAdjuster.Scale; + UnityEngine.Object.DestroyImmediate(parentAdjuster); + + boneMappings.Add(proxyTransform.parent, proxyTransform); + } + } + + foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) + { + UnityEngine.Object.DestroyImmediate(sar.gameObject); + } + + foreach (var smr in context.AvatarRootObject.GetComponentsInChildren()) + { + var bones = smr.bones; + for (int i = 0; i < bones.Length; i++) + { + if (boneMappings.TryGetValue(bones[i], out var newBone)) + { + bones[i] = newBone; + } + } + smr.bones = bones; + } + + } + } +} \ No newline at end of file diff --git a/Editor/ScaleAdjusterPass.cs.meta b/Editor/ScaleAdjusterPass.cs.meta new file mode 100644 index 00000000..efbc8ddd --- /dev/null +++ b/Editor/ScaleAdjusterPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 62b78456069047e0949dd1d8c4b25edc +timeCreated: 1708253304 \ No newline at end of file diff --git a/Editor/assembly-info.cs b/Editor/assembly-info.cs index 0502fef3..467a811f 100644 --- a/Editor/assembly-info.cs +++ b/Editor/assembly-info.cs @@ -1,4 +1,9 @@ -using System.Runtime.CompilerServices; +#region + +using System.Runtime.CompilerServices; + +#endregion [assembly: InternalsVisibleTo("net.fushizen.xdress")] -[assembly: InternalsVisibleTo("net.fushizen.xdress.editor")] \ No newline at end of file +[assembly: InternalsVisibleTo("net.fushizen.xdress.editor")] +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] \ No newline at end of file diff --git a/Runtime/ModularAvatarScaleAdjuster.cs b/Runtime/ModularAvatarScaleAdjuster.cs deleted file mode 100644 index be09a58b..00000000 --- a/Runtime/ModularAvatarScaleAdjuster.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -#if UNITY_EDITOR -using UnityEditor; -#endif -using UnityEngine; - -namespace nadena.dev.modular_avatar.core -{ - [Serializable] - internal struct ScalePatch - { - public SkinnedMeshRenderer smr; - public int boneIndex; - - public ScalePatch(SkinnedMeshRenderer smr, int boneIndex) - { - this.smr = smr; - this.boneIndex = boneIndex; - } - - public bool Equals(ScalePatch other) - { - return smr.Equals(other.smr) && boneIndex == other.boneIndex; - } - - public override bool Equals(object obj) - { - return obj is ScalePatch other && Equals(other); - } - - public override int GetHashCode() - { - unchecked - { - return (smr.GetHashCode() * 397) ^ boneIndex; - } - } - } - - [ExecuteInEditMode] - [DisallowMultipleComponent] - [AddComponentMenu("Modular Avatar/MA Scale Adjuster")] - [HelpURL("https://modular-avatar.nadena.dev/docs/reference/scale-adjuster?lang=auto")] - public sealed class ModularAvatarScaleAdjuster : AvatarTagComponent - { - [SerializeField] private Vector3 m_Scale = Vector3.one; - - public Vector3 Scale - { - get => m_Scale; - set - { - m_Scale = value; - Update(); - } - } - - [SerializeField] internal Transform scaleProxy; - - [SerializeField] private List patches = new List(); - - private bool initialized = false; - -#if UNITY_EDITOR - private void Update() - { - if (this == null) return; - - PatchRenderers(); - - scaleProxy.localScale = m_Scale; - } - - void OnValidate() - { - initialized = false; - EditorApplication.delayCall += Update; - } - - private void PatchRenderers() - { - if (initialized || this == null) return; - - if (PrefabUtility.IsPartOfPrefabInstance(this)) - { - // Ensure we're using the same ScaleProxy as the corresponding prefab asset. - var prefab = PrefabUtility.GetCorrespondingObjectFromSource(this); - if (this.scaleProxy == null || prefab.scaleProxy == null || prefab.scaleProxy != - PrefabUtility.GetCorrespondingObjectFromSource(this.scaleProxy)) - { - if (prefab.scaleProxy == null && scaleProxy != null) - { - // Push our ScaleProxy down into the prefab (this happens after applying the ScaleAdjuster - // component to a prefab) - var assetPath = AssetDatabase.GetAssetPath(prefab); - PrefabUtility.ApplyAddedGameObject(scaleProxy.gameObject, assetPath, - InteractionMode.AutomatedAction); - prefab.scaleProxy = PrefabUtility.GetCorrespondingObjectFromSource(this.scaleProxy); - } - else - { - // Clear any duplicate scaleProxy we have - - if (scaleProxy != null) DestroyImmediate(scaleProxy.gameObject); - } - - var so = new SerializedObject(this); - var sp = so.FindProperty(nameof(scaleProxy)); - PrefabUtility.RevertPropertyOverride(sp, InteractionMode.AutomatedAction); - so.ApplyModifiedPropertiesWithoutUndo(); - - // Find the corresponding child - foreach (Transform t in transform) - { - if (PrefabUtility.GetCorrespondingObjectFromSource(t) == prefab.scaleProxy) - { - scaleProxy = t; - break; - } - } - } - } - - if (scaleProxy == null && !PrefabUtility.IsPartOfPrefabAsset(this)) - { - 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); - } - - if (scaleProxy != null) - { - scaleProxy.hideFlags = HideFlags.HideInHierarchy; - - RewriteBoneReferences(transform, scaleProxy); - } - - initialized = true; - } - - private void RewriteBoneReferences(Transform oldBone, Transform newBone, Transform selfTransform = null) - { - if (selfTransform == null) selfTransform = transform; - - var prefabNewBone = PrefabUtility.GetCorrespondingObjectFromSource(newBone); - - var oldPatches = new HashSet(this.patches); - var newPatches = new HashSet(); - var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selfTransform); - - if (avatarRoot != null) - { - foreach (var smr in avatarRoot.GetComponentsInChildren(true)) - { - var serializedObject = new SerializedObject(smr); - var bonesArray = serializedObject.FindProperty("m_Bones"); - int boneCount = bonesArray.arraySize; - - var parentSmr = PrefabUtility.GetCorrespondingObjectFromSource(smr); - var parentBones = parentSmr != null ? parentSmr.bones : null; - var propMods = PrefabUtility.GetPropertyModifications(smr); - - bool changed = false; - - for (int i = 0; i < boneCount; i++) - { - var boneProp = bonesArray.GetArrayElementAtIndex(i); - var bone = boneProp.objectReferenceValue as Transform; - if (bone == oldBone || bone == newBone || - (bone == null && oldPatches.Contains(new ScalePatch(smr, i)))) - { - if (parentBones != null && parentBones[i] == prefabNewBone) - { - // Remove any prefab overrides for this bone entry - changed = boneProp.objectReferenceValue != newBone; - boneProp.objectReferenceValue = newBone; - serializedObject.ApplyModifiedPropertiesWithoutUndo(); - PrefabUtility.RevertPropertyOverride(boneProp, InteractionMode.AutomatedAction); - } - else - { - boneProp.objectReferenceValue = newBone; - changed = true; - } - - newPatches.Add(new ScalePatch(smr, i)); - } - } - - if (changed) - { - serializedObject.ApplyModifiedPropertiesWithoutUndo(); - - ConfigurePrefab(); - } - } - - if (this != null && newPatches != oldPatches) - { - this.patches = newPatches.ToList(); - PrefabUtility.RecordPrefabInstancePropertyModifications(this); - } - } - } - - private void ConfigurePrefab() - { - if (this == null || !PrefabUtility.IsPartOfPrefabInstance(this)) return; - var source = PrefabUtility.GetCorrespondingObjectFromSource(this); - var path = AssetDatabase.GetAssetPath(source); - var root = PrefabUtility.LoadPrefabContents(path); - - foreach (var obj in root.GetComponentsInChildren()) - { - obj.PatchRenderers(); - } - - PrefabUtility.SaveAsPrefabAsset(root, path); - PrefabUtility.UnloadPrefabContents(root); - - initialized = false; - } - - protected override void OnDestroy() - { - base.OnDestroy(); - - UnpatchRenderers(); - } - - private void UnpatchRenderers() - { - var scaleProxy2 = this.scaleProxy; - var transform2 = this.transform; - - EditorApplication.delayCall += () => - { - if (scaleProxy2 == null) return; - - if (transform2 != null) - { - RewriteBoneReferences(scaleProxy2, transform2, transform2); - } - - try - { - DestroyImmediate(scaleProxy2.gameObject); - } - catch (InvalidOperationException e) - { - // not supported in Unity 2019... - } - }; - } -#else - private void Update() - { - // placeholder to make builds work - } -#endif - } -} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster.meta b/Runtime/ScaleAdjuster.meta new file mode 100644 index 00000000..15b1d22e --- /dev/null +++ b/Runtime/ScaleAdjuster.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 96f10f3195df42bba58b9efb61948b9c +timeCreated: 1708232523 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs new file mode 100644 index 00000000..9171ad21 --- /dev/null +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -0,0 +1,128 @@ +#region + +using UnityEngine; +using UnityEngine.Serialization; +#if UNITY_EDITOR +using UnityEditor; +#endif + +#endregion + +namespace nadena.dev.modular_avatar.core +{ + [ExecuteInEditMode] + [DisallowMultipleComponent] + [AddComponentMenu("Modular Avatar/MA Scale Adjuster")] + [HelpURL("https://modular-avatar.nadena.dev/docs/reference/scale-adjuster?lang=auto")] + public sealed class ModularAvatarScaleAdjuster : AvatarTagComponent + { + private const string ADJUSTER_OBJECT = "MA Scale Adjuster Proxy Renderer"; + [SerializeField] private Vector3 m_Scale = Vector3.one; + + public Vector3 Scale + { + get => m_Scale; + set + { + m_Scale = value; + Update(); + } + } + + [SerializeField] [FormerlySerializedAs("scaleProxy")] + internal Transform legacyScaleProxy; + + internal Transform scaleProxy; + + private bool initialized = false; + +#if UNITY_EDITOR + void OnValidate() + { + base.OnValidate(); + initialized = false; + } + + private void Update() + { + if (scaleProxy == null || initialized == false) + { + InitializeProxy(); + } + + if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy)) + { + DestroyImmediate(legacyScaleProxy.gameObject); + legacyScaleProxy = null; + } + + scaleProxy.localScale = m_Scale; + } + + private void InitializeProxy() + { + if (scaleProxy == 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); + } + + ConfigureRenderers(); + + initialized = true; + } + + private void OnDestroy() + { + if (scaleProxy != null) + { + DestroyImmediate(scaleProxy.gameObject); + } + + 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); + + var childSmr = childObj.AddComponent(); + EditorUtility.CopySerialized(smr, childSmr); + + 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; + child.ClearBoneCache(); + } + } +#endif + +#if !UNITY_EDITOR + private void Update() + { + // placeholder to make builds work + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/ModularAvatarScaleAdjuster.cs.meta b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs.meta similarity index 100% rename from Runtime/ModularAvatarScaleAdjuster.cs.meta rename to Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs.meta diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs new file mode 100644 index 00000000..0716b5de --- /dev/null +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs @@ -0,0 +1,178 @@ +#region + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VRC.SDKBase; + +#endregion + +namespace nadena.dev.modular_avatar.core +{ + [ExecuteInEditMode] + //[AddComponentMenu("")] + [RequireComponent(typeof(SkinnedMeshRenderer))] + internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly + { + private static event Action OnPreInspector; + private static int RecreateHierarchyIndexCount = 0; + + #if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] + static void Setup() + { + UnityEditor.EditorApplication.hierarchyChanged += InvalidateAll; + } + #endif + + internal static void InvalidateAll() + { + RecreateHierarchyIndexCount++; + } + + private SkinnedMeshRenderer myRenderer; + private SkinnedMeshRenderer parentRenderer; + + private bool wasActive = false; + private bool redoBoneMappings = true; + private int lastRecreateHierarchyIndex = -1; + + internal Dictionary BoneMappings = new Dictionary(); + + #if UNITY_EDITOR + private void OnValidate() + { + if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return; + redoBoneMappings = true; + + UnityEditor.EditorApplication.delayCall += () => + { + if (this == null) return; + +#if MODULAR_AVATAR_DEBUG_HIDDEN + gameObject.hideFlags = HideFlags.None; +#else + gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSaveInBuild; +#endif + if (BoneMappings == null) + { + BoneMappings = new Dictionary(); + } + }; + } + #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() + { + ClearOverrides(); + } + + private void Update() + { + if (myRenderer == null) + { + myRenderer = GetComponent(); + } + + if (parentRenderer == null) + { + parentRenderer = transform.parent.GetComponent(); + } + + 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); }); + + if (BoneMappings.Count == 0) + { + DestroyImmediate(gameObject); + return; + } + + myRenderer.rootBone = MapBone(parentRenderer.rootBone); + myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray(); + redoBoneMappings = false; + lastRecreateHierarchyIndex = RecreateHierarchyIndexCount; + } + + 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; + + var blendShapeCount = myRenderer.sharedMesh.blendShapeCount; + + for (int i = 0; i < blendShapeCount; i++) + { + myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); + } + + ClearOverrides(); + + myRenderer.enabled = parentRenderer.enabled; + } + + public void OnWillRenderObject() + { + if (myRenderer == null || parentRenderer == null) + { + return; + } + + ClearOverrides(); + + if (!parentRenderer.enabled || !parentRenderer.gameObject.activeInHierarchy) + { + return; + } + + parentRenderer.enabled = false; + wasActive = true; + OnPreInspector += ClearOverrides; + } + + private void OnPostRender() + { + ClearOverrides(); + } + + private void ClearOverrides() + { + if (this == null) return; + + if (wasActive && parentRenderer != null) + { + parentRenderer.enabled = true; + wasActive = false; + } + } + + public void ClearBoneCache() + { + redoBoneMappings = true; + } + + internal static void ClearAllOverrides() + { + OnPreInspector?.Invoke(); + OnPreInspector = null; + } + } +} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta new file mode 100644 index 00000000..870af8e4 --- /dev/null +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c8bc16baa6c345eea5edf47232ee4069 +timeCreated: 1708232586 \ No newline at end of file diff --git a/Runtime/ScaleProxy.cs b/Runtime/ScaleAdjuster/ScaleProxy.cs similarity index 75% rename from Runtime/ScaleProxy.cs rename to Runtime/ScaleAdjuster/ScaleProxy.cs index ca972524..40d75685 100644 --- a/Runtime/ScaleProxy.cs +++ b/Runtime/ScaleAdjuster/ScaleProxy.cs @@ -1,7 +1,12 @@ -#if UNITY_EDITOR + +#region + +using UnityEngine; +#if UNITY_EDITOR using UnityEditor; #endif -using UnityEngine; + +#endregion namespace nadena.dev.modular_avatar.core { @@ -11,9 +16,16 @@ namespace nadena.dev.modular_avatar.core #if UNITY_EDITOR void OnValidate() { + base.OnDestroy(); EditorApplication.delayCall += DeferredValidate; } + void OnDestroy() + { + ScaleAdjusterRenderer.InvalidateAll(); + base.OnDestroy(); + } + private void DeferredValidate() { if (this == null) return; @@ -23,8 +35,16 @@ namespace nadena.dev.modular_avatar.core 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; @@ -60,8 +80,19 @@ namespace nadena.dev.modular_avatar.core while (root.parent != null) root = root.parent; } + ClearOverrides(root); + + DestroyImmediate(gameObject); + } + + private void ClearOverrides(Transform root) + { + // This clears bone overrides that date back to the 1.9.0-rc.2 implementation, to ease rc.2 -> rc.3 + // migrations. It'll be removed in 1.10. foreach (var smr in root.GetComponentsInChildren(true)) { + if (smr.GetComponent()) continue; + var bones = smr.bones; bool changed = false; @@ -79,8 +110,6 @@ namespace nadena.dev.modular_avatar.core smr.bones = bones; } } - - DestroyImmediate(gameObject); } #endif } diff --git a/Runtime/ScaleProxy.cs.meta b/Runtime/ScaleAdjuster/ScaleProxy.cs.meta similarity index 100% rename from Runtime/ScaleProxy.cs.meta rename to Runtime/ScaleAdjuster/ScaleProxy.cs.meta diff --git a/Runtime/TestBehavior.cs.meta b/Runtime/TestBehavior.cs.meta new file mode 100644 index 00000000..bf95728b --- /dev/null +++ b/Runtime/TestBehavior.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f2c964d1229a4ce698db85bec9043608 +timeCreated: 1708232304 \ No newline at end of file diff --git a/Runtime/assembly-info.cs b/Runtime/assembly-info.cs index 5e7b5f6f..0386f743 100644 --- a/Runtime/assembly-info.cs +++ b/Runtime/assembly-info.cs @@ -1,6 +1,11 @@ +#region + using System.Runtime.CompilerServices; +#endregion + [assembly: InternalsVisibleTo("nadena.dev.modular-avatar.core.editor")] [assembly: InternalsVisibleTo("net.fushizen.xdress")] [assembly: InternalsVisibleTo("net.fushizen.xdress.editor")] -[assembly: InternalsVisibleTo("Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Tests")] +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] \ No newline at end of file diff --git a/UnitTests~/ComponentSettingsTest.cs b/UnitTests~/ComponentSettingsTest.cs index 38c05392..90aa6262 100644 --- a/UnitTests~/ComponentSettingsTest.cs +++ b/UnitTests~/ComponentSettingsTest.cs @@ -40,6 +40,7 @@ namespace modular_avatar_tests if (type == typeof(AvatarActivator)) return; if (type == typeof(TestComponent)) return; if (type == typeof(ScaleProxy)) return; + if (type == typeof(ScaleAdjusterRenderer)) return; // get icon var component = (MonoBehaviour) _gameObject.AddComponent(type); @@ -65,6 +66,7 @@ namespace modular_avatar_tests if (type == typeof(AvatarActivator)) return; if (type == typeof(TestComponent)) return; if (type == typeof(ScaleProxy)) return; + if (type == typeof(ScaleAdjusterRenderer)) return; // get icon var helpUrl = type.GetCustomAttribute();