From 8d3da50b37e2d90ba0411802cb5496520db10abd Mon Sep 17 00:00:00 2001 From: bd_ Date: Mon, 3 Jun 2024 09:52:08 +0900 Subject: [PATCH] feat: MA Shape Changer (#863) * chore: migrate Scale Adjuster to NDMF preview system * feat: MA Shape Changer * chore: update NDMF dependency * fix: unit test failure --- .github/ProjectRoot/vpm-manifest-2022.json | 2 +- Editor/Animation/AnimationDatabase.cs | 18 +- Editor/Animation/AnimationServicesContext.cs | 106 ++- Editor/HarmonyPatches/HandleUtilityPatches.cs | 84 --- .../HandleUtilityPatches.cs.meta | 3 - Editor/HarmonyPatches/HierarchyViewPatches.cs | 175 ----- .../HierarchyViewPatches.cs.meta | 3 - Editor/HarmonyPatches/PatchLoader.cs | 6 +- Editor/HarmonyPatches/PickingObjectPatch.cs | 78 --- .../HarmonyPatches/PickingObjectPatch.cs.meta | 3 - Editor/Inspector/ShapeChanger.meta | 3 + .../Inspector/ShapeChanger/AddShapePopup.cs | 99 +++ .../ShapeChanger/AddShapePopup.cs.meta | 3 + .../Inspector/ShapeChanger/AddShapePopup.uxml | 11 + .../ShapeChanger/AddShapePopup.uxml.meta | 3 + .../ShapeChanger/ChangedShapeEditor.cs | 44 ++ .../ShapeChanger/ChangedShapeEditor.cs.meta | 3 + .../ShapeChanger/ChangedShapeEditor.uxml | 8 + .../ShapeChanger/ChangedShapeEditor.uxml.meta | 3 + .../Inspector/ShapeChanger/ShapeChanger.uxml | 24 + .../ShapeChanger/ShapeChanger.uxml.meta | 3 + .../ShapeChanger/ShapeChangerEditor.cs | 52 ++ .../ShapeChanger/ShapeChangerEditor.cs.meta | 13 + .../ShapeChanger/ShapeChangerStyles.uss | 112 ++++ .../ShapeChanger/ShapeChangerStyles.uss.meta | 3 + Editor/MergeArmatureHook.cs | 4 +- Editor/PluginDefinition/PluginDefinition.cs | 4 +- Editor/ScaleAdjuster/ScaleAdjustedBones.cs | 155 +++++ .../ScaleAdjuster/ScaleAdjustedBones.cs.meta | 3 + Editor/ScaleAdjuster/ScaleAdjusterPreview.cs | 83 +++ .../ScaleAdjusterPreview.cs.meta | 3 + Editor/ScaleAdjuster/SelectionHack.cs | 28 - Editor/ScaleAdjuster/SelectionHack.cs.meta | 3 - Editor/ScaleAdjusterPass.cs | 4 +- Editor/ShapeChanger.meta | 3 + Editor/ShapeChanger/DESIGN.md | 33 + Editor/ShapeChanger/DESIGN.md.meta | 3 + .../ShapeChanger/RemoveBlendShapeFromMesh.cs | 320 +++++++++ .../RemoveBlendShapeFromMesh.cs.meta | 3 + Editor/ShapeChanger/ShapeChangerPass.cs | 616 ++++++++++++++++++ Editor/ShapeChanger/ShapeChangerPass.cs.meta | 3 + Editor/ShapeChanger/ShapeChangerPreview.cs | 198 ++++++ .../ShapeChanger/ShapeChangerPreview.cs.meta | 3 + ...dena.dev.modular-avatar.core.editor.asmdef | 4 +- Runtime/MAMoveIndependently.cs | 2 - Runtime/ModularAvatarShapeChanger.cs | 49 ++ Runtime/ModularAvatarShapeChanger.cs.meta | 3 + .../ModularAvatarScaleAdjuster.cs | 141 +--- Runtime/ScaleAdjuster/ProxyManager.cs | 359 ---------- Runtime/ScaleAdjuster/ProxyManager.cs.meta | 3 - .../ScaleAdjuster/ScaleAdjusterRenderer.cs | 33 - .../ScaleAdjusterRenderer.cs.meta | 3 - Runtime/ScaleAdjuster/ScaleProxy.cs | 80 --- Runtime/ScaleAdjuster/ScaleProxy.cs.meta | 3 - UnitTests~/ComponentSettingsTest.cs | 5 +- 55 files changed, 1993 insertions(+), 1023 deletions(-) delete mode 100644 Editor/HarmonyPatches/HandleUtilityPatches.cs delete mode 100644 Editor/HarmonyPatches/HandleUtilityPatches.cs.meta delete mode 100644 Editor/HarmonyPatches/HierarchyViewPatches.cs delete mode 100644 Editor/HarmonyPatches/HierarchyViewPatches.cs.meta delete mode 100644 Editor/HarmonyPatches/PickingObjectPatch.cs delete mode 100644 Editor/HarmonyPatches/PickingObjectPatch.cs.meta create mode 100644 Editor/Inspector/ShapeChanger.meta create mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.cs create mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta create mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.uxml create mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta create mode 100644 Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs create mode 100644 Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta create mode 100644 Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml create mode 100644 Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml.meta create mode 100644 Editor/Inspector/ShapeChanger/ShapeChanger.uxml create mode 100644 Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta create mode 100644 Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs create mode 100644 Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta create mode 100644 Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss create mode 100644 Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss.meta create mode 100644 Editor/ScaleAdjuster/ScaleAdjustedBones.cs create mode 100644 Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta create mode 100644 Editor/ScaleAdjuster/ScaleAdjusterPreview.cs create mode 100644 Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta delete mode 100644 Editor/ScaleAdjuster/SelectionHack.cs delete mode 100644 Editor/ScaleAdjuster/SelectionHack.cs.meta create mode 100644 Editor/ShapeChanger.meta create mode 100644 Editor/ShapeChanger/DESIGN.md create mode 100644 Editor/ShapeChanger/DESIGN.md.meta create mode 100644 Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs create mode 100644 Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta create mode 100644 Editor/ShapeChanger/ShapeChangerPass.cs create mode 100644 Editor/ShapeChanger/ShapeChangerPass.cs.meta create mode 100644 Editor/ShapeChanger/ShapeChangerPreview.cs create mode 100644 Editor/ShapeChanger/ShapeChangerPreview.cs.meta create mode 100644 Runtime/ModularAvatarShapeChanger.cs create mode 100644 Runtime/ModularAvatarShapeChanger.cs.meta delete mode 100644 Runtime/ScaleAdjuster/ProxyManager.cs delete mode 100644 Runtime/ScaleAdjuster/ProxyManager.cs.meta delete mode 100644 Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs delete mode 100644 Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta delete mode 100644 Runtime/ScaleAdjuster/ScaleProxy.cs delete mode 100644 Runtime/ScaleAdjuster/ScaleProxy.cs.meta diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index 82e8a102..b277f729 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.4.0" + "version": "1.5.0-alpha.0" } } } \ No newline at end of file diff --git a/Editor/Animation/AnimationDatabase.cs b/Editor/Animation/AnimationDatabase.cs index 728d1afe..4f5c8647 100644 --- a/Editor/Animation/AnimationDatabase.cs +++ b/Editor/Animation/AnimationDatabase.cs @@ -1,19 +1,20 @@ -using System; +#region + +using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Data.Odbc; using nadena.dev.modular_avatar.core.editor; using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; using UnityEditor; using UnityEditor.Animations; using UnityEngine; - +using BuildContext = nadena.dev.ndmf.BuildContext; #if MA_VRCSDK3_AVATARS using VRC.SDK3.Avatars.Components; #endif -using Object = UnityEngine.Object; +#endregion namespace nadena.dev.modular_avatar.animation { @@ -72,9 +73,14 @@ namespace nadena.dev.modular_avatar.animation { return _currentClip; } + + public void SetCurrentNoInvalidate(Motion newMotion) + { + _currentClip = newMotion; + } } - private ndmf.BuildContext _context; + private BuildContext _context; private List _clipCommitActions = new List(); private List _clips = new List(); @@ -119,7 +125,7 @@ namespace nadena.dev.modular_avatar.animation } } - internal void OnActivate(ndmf.BuildContext context) + internal void OnActivate(BuildContext context) { _context = context; diff --git a/Editor/Animation/AnimationServicesContext.cs b/Editor/Animation/AnimationServicesContext.cs index 961f4260..33229e21 100644 --- a/Editor/Animation/AnimationServicesContext.cs +++ b/Editor/Animation/AnimationServicesContext.cs @@ -1,5 +1,14 @@ -using System; +#region + +using System; +using System.Collections.Generic; +using System.Linq; using nadena.dev.ndmf; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; + +#endregion namespace nadena.dev.modular_avatar.animation { @@ -17,11 +26,15 @@ namespace nadena.dev.modular_avatar.animation /// internal sealed class AnimationServicesContext : IExtensionContext { + private BuildContext _context; private AnimationDatabase _animationDatabase; private PathMappings _pathMappings; + private Dictionary _selfProxies = new(); public void OnActivate(BuildContext context) { + _context = context; + _animationDatabase = new AnimationDatabase(); _animationDatabase.OnActivate(context); @@ -65,5 +78,96 @@ namespace nadena.dev.modular_avatar.animation return _pathMappings; } } + + /// + /// Returns a parameter which proxies the "activeSelf" state of the specified GameObject. + /// + /// + /// + /// + /// + public bool TryGetActiveSelfProxy(GameObject obj, out string paramName) + { + if (_selfProxies.TryGetValue(obj, out paramName)) return !string.IsNullOrEmpty(paramName); + + var path = PathMappings.GetObjectIdentifier(obj); + var clips = AnimationDatabase.ClipsForPath(path); + if (clips == null || clips.IsEmpty) + { + _selfProxies[obj] = ""; + return false; + } + + var iid = obj.GetInstanceID(); + paramName = $"_MA/ActiveSelf/{iid}"; + + var binding = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"); + + bool hadAnyClip = false; + foreach (var clip in clips) + { + Motion newMotion = ProcessActiveSelf(clip.CurrentClip, paramName, binding); + if (newMotion != clip.CurrentClip) + { + clip.SetCurrentNoInvalidate(newMotion); + hadAnyClip = true; + } + } + + if (hadAnyClip) + { + _selfProxies[obj] = paramName; + return true; + } + else + { + _selfProxies[obj] = ""; + return false; + } + } + + private Motion ProcessActiveSelf(Motion motion, string paramName, EditorCurveBinding binding) + { + if (motion is AnimationClip clip) + { + var curve = AnimationUtility.GetEditorCurve(clip, binding); + if (curve == null) return motion; + + var newClip = new AnimationClip(); + EditorUtility.CopySerialized(motion, newClip); + + newClip.SetCurve("", typeof(Animator), paramName, curve); + return newClip; + } + else if (motion is BlendTree bt) + { + bool anyChanged = false; + + var motions = bt.children.Select(c => // c is struct ChildMotion + { + var newMotion = ProcessActiveSelf(c.motion, paramName, binding); + anyChanged |= newMotion != c.motion; + c.motion = newMotion; + return c; + }).ToArray(); + + if (anyChanged) + { + var newBt = new BlendTree(); + EditorUtility.CopySerialized(bt, newBt); + + newBt.children = motions; + return newBt; + } + else + { + return bt; + } + } + else + { + return motion; + } + } } } \ No newline at end of file diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs b/Editor/HarmonyPatches/HandleUtilityPatches.cs deleted file mode 100644 index 7560cf37..00000000 --- a/Editor/HarmonyPatches/HandleUtilityPatches.cs +++ /dev/null @@ -1,84 +0,0 @@ -#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"); - var m_postfix = AccessTools.Method(typeof(HandleUtilityPatches), "Postfix_FilterInstanceIDs"); - - h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix), postfix: new HarmonyMethod(m_postfix)); - } - - [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 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 (ProxyManager.OriginalToProxyObject.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 deleted file mode 100644 index 82224e59..00000000 --- a/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 807736f252df4b1b8402827257dcbea3 -timeCreated: 1709354699 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs b/Editor/HarmonyPatches/HierarchyViewPatches.cs deleted file mode 100644 index bdb4b01f..00000000 --- a/Editor/HarmonyPatches/HierarchyViewPatches.cs +++ /dev/null @@ -1,175 +0,0 @@ -#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 = ProxyManager.ProxyToOriginalObject.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 && ProxyManager.ProxyToOriginalObject.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 deleted file mode 100644 index 06d79c30..00000000 --- a/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 092b79a4..0ce7ffa4 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -13,11 +13,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { private static readonly Action[] patches = new Action[] { - HierarchyViewPatches.Patch, - #if UNITY_2022_3_OR_NEWER - HandleUtilityPatches.Patch_FilterInstanceIDs, - PickingObjectPatch.Patch, - #endif + //HierarchyViewPatches.Patch, }; [InitializeOnLoadMethod] diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs b/Editor/HarmonyPatches/PickingObjectPatch.cs deleted file mode 100644 index a7e5f029..00000000 --- a/Editor/HarmonyPatches/PickingObjectPatch.cs +++ /dev/null @@ -1,78 +0,0 @@ -#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 && ProxyManager.ProxyToOriginalObject.TryGetValue(go, out var original)) - { - list.Add(ctor_PickingObject.Invoke(new[] - { - original, - 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 deleted file mode 100644 index acafbc99..00000000 --- a/Editor/HarmonyPatches/PickingObjectPatch.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: cf06818f1c0c436fbae7f755d7110aba -timeCreated: 1709359553 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger.meta b/Editor/Inspector/ShapeChanger.meta new file mode 100644 index 00000000..c1173cf4 --- /dev/null +++ b/Editor/Inspector/ShapeChanger.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2400605596824043a59b55dcb2a8e89a +timeCreated: 1717196942 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.cs b/Editor/Inspector/ShapeChanger/AddShapePopup.cs new file mode 100644 index 00000000..ec0e32c5 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/AddShapePopup.cs @@ -0,0 +1,99 @@ +#region + +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.ShapeChanger +{ + public class AddShapePopup : PopupWindowContent + { + private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/"; + const string UxmlPath = Root + "AddShapePopup.uxml"; + const string UssPath = Root + "ShapeChangerStyles.uss"; + + private VisualElement _elem; + private ScrollView _scrollView; + + public AddShapePopup(ModularAvatarShapeChanger changer) + { + if (changer == null) return; + var target = changer.targetRenderer.Get(changer)?.GetComponent(); + if (target == null || target.sharedMesh == null) return; + + var alreadyRegistered = changer.Shapes.Select(c => c.ShapeName).ToHashSet(); + + var keys = new List(); + for (int i = 0; i < target.sharedMesh.blendShapeCount; i++) + { + var name = target.sharedMesh.GetBlendShapeName(i); + if (alreadyRegistered.Contains(name)) continue; + + keys.Add(name); + } + + var uss = AssetDatabase.LoadAssetAtPath(UssPath); + var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath); + + _elem = uxml.CloneTree(); + _elem.styleSheets.Add(uss); + Localization.UI.Localize(_elem); + + _scrollView = _elem.Q("scroll-view"); + + if (keys.Count > 0) + { + _scrollView.contentContainer.Clear(); + + foreach (var key in keys) + { + var container = new VisualElement(); + container.AddToClassList("add-shape-row"); + + Button btn = default; + btn = new Button(() => + { + AddShape(changer, key); + container.RemoveFromHierarchy(); + }); + btn.text = "+"; + container.Add(btn); + + var label = new Label(key); + container.Add(label); + + _scrollView.contentContainer.Add(container); + } + } + } + + private void AddShape(ModularAvatarShapeChanger changer, string key) + { + Undo.RecordObject(changer, "Add Shape"); + + changer.Shapes.Add(new ChangedShape() + { + ShapeName = key, + ChangeType = ShapeChangeType.Delete, + Value = 100 + }); + } + + public override void OnGUI(Rect rect) + { + } + + public override void OnOpen() + { + editorWindow.rootVisualElement.Clear(); + editorWindow.rootVisualElement.Add(_elem); + //editorWindow.rootVisualElement.Clear(); + + //editorWindow.rootVisualElement.Add(new Label("Hello, World!")); + } + } +} \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta b/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta new file mode 100644 index 00000000..daad933e --- /dev/null +++ b/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a8351fafb3740918363f60365adfeda +timeCreated: 1717205112 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml new file mode 100644 index 00000000..85ec5d1e --- /dev/null +++ b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta new file mode 100644 index 00000000..bbd7c309 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6753a7b3eae1416cb04786cf53778c33 +timeCreated: 1717205258 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs new file mode 100644 index 00000000..d8588535 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs @@ -0,0 +1,44 @@ +#region + +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.ShapeChanger +{ + [CustomPropertyDrawer(typeof(ChangedShape))] + public class ChangedShapeEditor : PropertyDrawer + { + private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/"; + const string UxmlPath = Root + "ChangedShapeEditor.uxml"; + const string UssPath = Root + "ShapeChangerStyles.uss"; + + public override VisualElement CreatePropertyGUI(SerializedProperty property) + { + var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath).CloneTree(); + var uss = AssetDatabase.LoadAssetAtPath(UssPath); + + Localization.UI.Localize(uxml); + uxml.styleSheets.Add(uss); + uxml.BindProperty(property); + + uxml.Q("f-change-type").RegisterCallback>( + e => + { + if (e.newValue == "Delete") + { + uxml.AddToClassList("change-type-delete"); + } + else + { + uxml.RemoveFromClassList("change-type-delete"); + } + } + ); + + return uxml; + } + } +} \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta new file mode 100644 index 00000000..e1362a3c --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c0778099a393468c9775fd9a99b321af +timeCreated: 1717198244 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml new file mode 100644 index 00000000..e2cf8875 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml.meta b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml.meta new file mode 100644 index 00000000..7036fc68 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d8131b91fff04f9e94b71d70105ae05b +timeCreated: 1717198707 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml new file mode 100644 index 00000000..69c03b33 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta new file mode 100644 index 00000000..311f9e06 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c8267295b2dc4d90b92dcc289f6d31c4 +timeCreated: 1717197307 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs new file mode 100644 index 00000000..8ade9ee4 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs @@ -0,0 +1,52 @@ +#region + +using System; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using static System.Reflection.BindingFlags; +using PopupWindow = UnityEditor.PopupWindow; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.ShapeChanger +{ + [CustomEditor(typeof(ModularAvatarShapeChanger))] + public class ShapeChangerEditor : MAEditorBase + { + [SerializeField] private StyleSheet uss; + [SerializeField] private VisualTreeAsset uxml; + + + protected override void OnInnerInspectorGUI() + { + throw new NotImplementedException(); + } + + protected override VisualElement CreateInnerInspectorGUI() + { + var root = uxml.CloneTree(); + Localization.UI.Localize(root); + root.styleSheets.Add(uss); + + root.Bind(serializedObject); + + var listView = root.Q("Shapes"); + + listView.showBoundCollectionSize = false; + listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; + + // The Add button callback isn't exposed publicly for some reason... + var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance); + var addButton = (Button)field_addButton.GetValue(listView); + + addButton.clickable = new Clickable(() => + { + PopupWindow.Show(addButton.worldBound, new AddShapePopup(target as ModularAvatarShapeChanger)); + }); + + return root; + } + } +} \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta new file mode 100644 index 00000000..263eba36 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 419e9c8f5ec74da9a4cdd702f7e63902 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - uss: {fileID: 7433441132597879392, guid: 8b52b9cfc8514c55af39aeb11de7f279, type: 3} + - uxml: {fileID: 9197481963319205126, guid: c8267295b2dc4d90b92dcc289f6d31c4, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss new file mode 100644 index 00000000..ebd9a7ef --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss @@ -0,0 +1,112 @@ +VisualElement { +} + +#group-box { + margin-top: 4px; + margin-bottom: 4px; + padding: 4px; + border-width: 3px; + border-left-color: rgba(0, 1, 0, 0.2); + border-top-color: rgba(0, 1, 0, 0.2); + border-right-color: rgba(0, 1, 0, 0.2); + border-bottom-color: rgba(0, 1, 0, 0.2); + border-radius: 4px; + /* background-color: rgba(0, 0, 0, 0.1); */ +} + +#ListViewContainer { + margin-top: 4px; +} + +#group-box > Label { + -unity-font-style: bold; +} + +.group-root { + margin-top: 4px; +} + +.group-root Toggle { + margin-left: 0; +} + +.group-children { + padding-left: 10px; +} + +.left-toggle { + display: flex; + flex-direction: row; +} + +.changed-shape-editor { + flex-direction: row; +} + +.changed-shape-editor #f-name { + flex-grow: 1; +} + +.changed-shape-editor #f-change-type { + flex-grow: 0; +} + +.changed-shape-editor #f-value { + flex-grow: 0; +} + +.changed-shape-editor PropertyField Label { + display: none; +} + +#f-change-type { + width: 75px; +} + +.f-value { + width: 40px; +} + +#f-value-delete { + display: none; +} + +.change-type-delete #f-value { + display: none; +} + +.change-type-delete #f-value-delete { + display: flex; +} + +/* Add shape window */ + +.add-shape-popup { + margin: 2px; +} + +.vline { + width: 100%; + height: 4px; + border-top-width: 4px; + margin-top: 2px; + margin-bottom: 2px; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.add-shape-row { + flex-direction: row; +} + +.add-shape-row Button { + flex-grow: 0; +} + +.add-shape-popup ScrollView Label.placeholder { + -unity-text-align: middle-center; +} + +.add-shape-row Label { + flex-grow: 1; + -unity-text-align: middle-left; +} diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss.meta b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss.meta new file mode 100644 index 00000000..b7c5c662 --- /dev/null +++ b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8b52b9cfc8514c55af39aeb11de7f279 +timeCreated: 1717197307 \ No newline at end of file diff --git a/Editor/MergeArmatureHook.cs b/Editor/MergeArmatureHook.cs index e5be6ec1..42942fb7 100644 --- a/Editor/MergeArmatureHook.cs +++ b/Editor/MergeArmatureHook.cs @@ -85,10 +85,10 @@ namespace nadena.dev.modular_avatar.core.editor TopoProcessMergeArmatures(mergeArmatures); #if MA_VRCSDK3_AVATARS - foreach (var c in avatarGameObject.transform.GetComponentsInChildren(true)) + /*foreach (var c in avatarGameObject.transform.GetComponentsInChildren(true)) { BoneDatabase.AddMergedBone(c.transform); - } + }*/ foreach (var c in avatarGameObject.transform.GetComponentsInChildren(true)) { diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index ee7f63d1..8aec1f80 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -46,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin { seq.Run(ClearEditorOnlyTags.Instance); seq.Run(MeshSettingsPluginPass.Instance); - seq.Run(ScaleAdjusterPass.Instance); + seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview()); #if MA_VRCSDK3_AVATARS seq.Run(RenameParametersPluginPass.Instance); seq.Run(MergeBlendTreePass.Instance); @@ -56,6 +56,8 @@ namespace nadena.dev.modular_avatar.core.editor.plugin #endif seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 => { + seq.Run("Shape Changer", ctx => new ShapeChangerPass(ctx).Execute()) + .PreviewingWith(new ShapeChangerPreview()); seq.Run(MergeArmaturePluginPass.Instance); seq.Run(BoneProxyPluginPass.Instance); seq.Run(VisibleHeadAccessoryPluginPass.Instance); diff --git a/Editor/ScaleAdjuster/ScaleAdjustedBones.cs b/Editor/ScaleAdjuster/ScaleAdjustedBones.cs new file mode 100644 index 00000000..0d786098 --- /dev/null +++ b/Editor/ScaleAdjuster/ScaleAdjustedBones.cs @@ -0,0 +1,155 @@ +#region + +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.SceneManagement; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.ScaleAdjuster +{ + internal class ScaleAdjustedBones + { + private static int editorFrameCount = 0; + private static int lastMutatingUpdate = 0; + private static int mutatingUpdateCount = 0; + + [InitializeOnLoadMethod] + static void Init() + { + EditorApplication.update += () => editorFrameCount++; + } + + internal class BoneState + { + public Component original; + public Transform proxy; + public int lastUsedFrame; + public BoneState parentHint; + } + + private Dictionary _bones = new(new ObjectIdentityComparer()); + //private List _states = new List(); + + public BoneState GetBone(Component src, bool force = true) + { + if (src == null) return null; + + if (_bones.TryGetValue(src, out var state)) + { + state.lastUsedFrame = mutatingUpdateCount; + return state; + } + + if (!force) return null; + + var proxyObj = new GameObject(src.name); + proxyObj.hideFlags = HideFlags.DontSave; + proxyObj.AddComponent().KeepAlive = this; + +#if MODULAR_AVATAR_DEBUG_HIDDEN + proxyObj.hideFlags = HideFlags.DontSave; +#endif + + var boneState = new BoneState(); + boneState.original = src; + boneState.proxy = proxyObj.transform; + boneState.parentHint = null; + boneState.lastUsedFrame = Time.frameCount; + + _bones[src] = boneState; + + CheckParent(CopyState(boneState), boneState); + + return boneState; + } + + private List toRemove = new List(); + private List stateList = new List(); + + public void Update() + { + if (lastMutatingUpdate != editorFrameCount) + { + mutatingUpdateCount++; + lastMutatingUpdate = editorFrameCount; + } + + toRemove.Clear(); + + stateList.Clear(); + stateList.AddRange(_bones.Values); + + foreach (var entry in stateList) + { + if (entry.original == null || entry.proxy == null) + { + if (entry.proxy != null) + { + Object.DestroyImmediate(entry.proxy.gameObject); + } + + toRemove.Add(entry.original); + continue; + } + + if (mutatingUpdateCount - entry.lastUsedFrame > 5 && entry.proxy.childCount == 0) + { + Object.DestroyImmediate(entry.proxy.gameObject); + toRemove.Add(entry.original); + continue; + } + + if (entry.original.gameObject.scene != entry.proxy.gameObject.scene) + { + SceneManager.MoveGameObjectToScene(entry.proxy.gameObject, entry.original.gameObject.scene); + } + + Transform parent = CopyState(entry); + + CheckParent(parent, entry); + } + + foreach (var remove in toRemove) + { + _bones.Remove(remove); + } + } + + private void CheckParent(Transform parent, BoneState entry) + { + if (parent != entry.parentHint?.original) + { + entry.parentHint = GetBone(parent); + entry.proxy.SetParent(entry.parentHint?.proxy, false); + } + } + + private static Transform CopyState(BoneState entry) + { + Transform parent; + if (entry.original is Transform t) + { + parent = t.parent; + + entry.proxy.localPosition = t.localPosition; + entry.proxy.localRotation = t.localRotation; + entry.proxy.localScale = t.localScale; + } + else + { + parent = entry.original.transform; + + if (entry.original is ModularAvatarScaleAdjuster sa) + { + entry.proxy.localPosition = Vector3.zero; + entry.proxy.localRotation = Quaternion.identity; + entry.proxy.localScale = sa.Scale; + } + } + + return parent; + } + } +} \ No newline at end of file diff --git a/Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta b/Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta new file mode 100644 index 00000000..f3c42595 --- /dev/null +++ b/Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b62de718b16e4f47b48e2354235ee158 +timeCreated: 1716607885 \ No newline at end of file diff --git a/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs b/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs new file mode 100644 index 00000000..d19964eb --- /dev/null +++ b/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs @@ -0,0 +1,83 @@ +#region + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.core.editor.ScaleAdjuster; +using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.rq; +using nadena.dev.ndmf.rq.unity.editor; +using UnityEditor; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ScaleAdjusterPreview : IRenderFilter + { + private ScaleAdjustedBones _bones = new ScaleAdjustedBones(); + + [InitializeOnLoadMethod] + private static void StaticInit() + { + } + + private static GameObject FindAvatarRootObserving(ComputeContext ctx, GameObject ptr) + { + while (ptr != null) + { + ctx.Observe(ptr); + var xform = ptr.transform; + if (RuntimeUtil.IsAvatarRoot(xform)) return ptr; + + ptr = xform.parent?.gameObject; + } + + return null; + } + + public ReactiveValue>> TargetGroups { get; } = + ReactiveValue>>.Create( + "Scale Adjuster: Find targets", + async ctx => + { + var scaleAdjusters = await ctx.Observe(CommonQueries.GetComponentsByType()); + + HashSet targets = new HashSet(); + + foreach (var adjuster in scaleAdjusters) + { + // Find parent object + // TODO: Reactive helper + var root = FindAvatarRootObserving(ctx, adjuster.gameObject); + if (root == null) continue; + + var renderers = ctx.GetComponentsInChildren(root, true); + + foreach (var renderer in renderers) + { + targets.Add(renderer); + } + } + + return targets.Select(r => (IImmutableList)ImmutableList.Create(r)).ToImmutableList(); + }); + + public void OnFrame(Renderer original, Renderer proxy) + { + if (proxy is SkinnedMeshRenderer p_smr && original is SkinnedMeshRenderer o_smr) + { + p_smr.rootBone = _bones.GetBone(o_smr.rootBone)?.proxy ?? o_smr.rootBone; + p_smr.bones = o_smr.bones.Select(b => + { + var sa = (Component)b?.GetComponent(); + return _bones.GetBone(sa ?? b, true)?.proxy ?? b; + }).ToArray(); + } + + _bones.Update(); + } + } + +} \ No newline at end of file diff --git a/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta b/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta new file mode 100644 index 00000000..40d09131 --- /dev/null +++ b/Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 54077f1844e74fa6a344b94d83a2044f +timeCreated: 1716174270 \ No newline at end of file diff --git a/Editor/ScaleAdjuster/SelectionHack.cs b/Editor/ScaleAdjuster/SelectionHack.cs deleted file mode 100644 index a5334f2e..00000000 --- a/Editor/ScaleAdjuster/SelectionHack.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 737bed39..00000000 --- a/Editor/ScaleAdjuster/SelectionHack.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: cfa3ba0c82bc4439aa86228715f61831 -timeCreated: 1709376243 \ No newline at end of file diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs index d6c5487a..58a5c2bc 100644 --- a/Editor/ScaleAdjusterPass.cs +++ b/Editor/ScaleAdjusterPass.cs @@ -30,14 +30,14 @@ namespace nadena.dev.modular_avatar.core.editor } // Legacy cleanup - foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) + /*foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) { Object.DestroyImmediate(sar.gameObject); } foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) { Object.DestroyImmediate(sar.gameObject); - } + }*/ if (boneMappings.Count == 0) { diff --git a/Editor/ShapeChanger.meta b/Editor/ShapeChanger.meta new file mode 100644 index 00000000..5a38a710 --- /dev/null +++ b/Editor/ShapeChanger.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 57bb6d1a371c4993bf7cfed797b2eb65 +timeCreated: 1717105136 \ No newline at end of file diff --git a/Editor/ShapeChanger/DESIGN.md b/Editor/ShapeChanger/DESIGN.md new file mode 100644 index 00000000..da014323 --- /dev/null +++ b/Editor/ShapeChanger/DESIGN.md @@ -0,0 +1,33 @@ +# Semantics + +Shape Changer will set the specified shape keys when the GameObject it is on is active and enabled. +This analysis is based on the original hierarchy position of the GameObject in question, not the position after any +Merge Armature or Bone Proxy operations. + +## Shape Key Modes + +Shape keys can be set to either + +* Set - Set the shape key to the given value +* Delete - If the GameObject is always active, deletes the associated triangles and vertices. Otherwise, sets the shape + key to 100%. + +## Conflict resolution + +First, if any Delete action is active, we will delete the affected vertices (if static) or set to 100 (if dynamic). +Otherwise, the last object in hierarchy traversal order wins. + +Implementation-wise, we first determine if the result is static (ie, if the last object - or multiple objects with +consistent settings - is non-animated). If so, we take that result. + +Otherwise, we: + +* Create animator parameters to proxy game object active states. These are injected into any animation clips which + manipulate relevant game objects. +* Build a blend tree to determine the final state. + * For delete actions, we create a blend tree which effectively adds together all the active objects. + * For set actions, we create a blend tree which selects the last active object. + * This blend tree works by exploiting the FP32 exponent field. Each object is assigned a floating point value with a + unique exponent and a zero mantissa; we skip every other exponent here, in order to provide a "guard band" between + values. We then sum these using a direct blend tree. + * We finally use a two-level 1D blend tree to evaluate the delete action and then any set actions. diff --git a/Editor/ShapeChanger/DESIGN.md.meta b/Editor/ShapeChanger/DESIGN.md.meta new file mode 100644 index 00000000..c4d21495 --- /dev/null +++ b/Editor/ShapeChanger/DESIGN.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ca1da54f230b498fa21d1a70d196a12a +timeCreated: 1717123475 \ No newline at end of file diff --git a/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs b/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs new file mode 100644 index 00000000..8baf80cb --- /dev/null +++ b/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs @@ -0,0 +1,320 @@ +#region + +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Rendering; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + internal static class RemoveBlendShapeFromMesh + { + private const float THRESHOLD = 0.001f; + + public static Mesh RemoveBlendshapes(Mesh original, IEnumerable targets) + { + var mesh = new Mesh(); + mesh.indexFormat = original.indexFormat; + mesh.bounds = original.bounds; + + bool[] toDeleteVertices = new bool[original.vertexCount]; + bool[] toRetainVertices = new bool[original.vertexCount]; + + ProbeBlendshapes(original, toDeleteVertices, targets); + ProbeRetainedVertices(original, toDeleteVertices, toRetainVertices); + + RemapVerts(toRetainVertices, out var origToNewVertIndex, out var newToOrigVertIndex); + + TransferVertexData(mesh, original, toRetainVertices); + TransferBoneData(mesh, original, toRetainVertices); + mesh.bindposes = original.bindposes; + TransferShapes(mesh, original, newToOrigVertIndex); + UpdateTriangles(mesh, original, toRetainVertices, origToNewVertIndex); + + return mesh; + } + + + private static VertexAttribute[] uvAttrs = new[] + { + VertexAttribute.TexCoord0, + VertexAttribute.TexCoord1, + VertexAttribute.TexCoord2, + VertexAttribute.TexCoord3, + VertexAttribute.TexCoord4, + VertexAttribute.TexCoord5, + VertexAttribute.TexCoord6, + VertexAttribute.TexCoord7, + }; + + private static void TransferVertexData(Mesh mesh, Mesh original, bool[] toRetain) + { + List tmpVec2 = new(); + List tmpVec3 = new(); + List tmpVec4 = new(); + + TransferData(tmpVec3, mesh.SetVertices, original.GetVertices); + TransferData(tmpVec3, mesh.SetNormals, original.GetNormals); + TransferData(tmpVec4, mesh.SetTangents, original.GetTangents); + + for (int uv = 0; uv < 8; uv++) + { + if (!original.HasVertexAttribute(uvAttrs[uv])) continue; + switch (original.GetVertexAttributeDimension(uvAttrs[uv])) + { + case 2: + TransferData(tmpVec2, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l)); + break; + case 3: + TransferData(tmpVec3, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l)); + break; + case 4: + TransferData(tmpVec4, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l)); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + void TransferData(List tmp, Action> setter, Action> getter) + { + getter(tmp); + int index = 0; + + tmp.RemoveAll(_t => !toRetain[index++]); + + setter(tmp); + } + } + + private static void TransferShapes(Mesh mesh, Mesh original, int[] newToOrigVertIndex) + { + Vector3[] o_pos = new Vector3[original.vertexCount]; + Vector3[] n_pos = new Vector3[mesh.vertexCount]; + + Vector3[] o_nrm = new Vector3[original.vertexCount]; + Vector3[] n_nrm = new Vector3[mesh.vertexCount]; + + Vector3[] o_tan = new Vector3[original.vertexCount]; + Vector3[] n_tan = new Vector3[mesh.vertexCount]; + + int blendshapeCount = original.blendShapeCount; + for (int s = 0; s < blendshapeCount; s++) + { + int frameCount = original.GetBlendShapeFrameCount(s); + var shapeName = original.GetBlendShapeName(s); + + for (int f = 0; f < frameCount; f++) + { + original.GetBlendShapeFrameVertices(s, f, o_pos, o_nrm, o_tan); + Remap(); + + var frameWeight = original.GetBlendShapeFrameWeight(s, f); + mesh.AddBlendShapeFrame(shapeName, frameWeight, n_pos, n_nrm, n_tan); + } + } + + void Remap() + { + for (int i = 0; i < n_pos.Length; i++) + { + try + { + n_pos[i] = o_pos[newToOrigVertIndex[i]]; + n_nrm[i] = o_nrm[newToOrigVertIndex[i]]; + n_tan[i] = o_tan[newToOrigVertIndex[i]]; + } + catch (IndexOutOfRangeException e) + { + throw; + } + } + } + } + + private static void TransferBoneData(Mesh mesh, Mesh original, bool[] toRetain) + { + var origBoneWeights = original.GetAllBoneWeights(); + var origBonesPerVertex = original.GetBonesPerVertex(); + + List boneWeights = new(origBoneWeights.Length); + List bonesPerVertex = new(origBonesPerVertex.Length); + + int ptr = 0; + for (int i = 0; i < toRetain.Length; i++) + { + byte n_weight = origBonesPerVertex[i]; + + if (toRetain[i]) + { + for (int j = 0; j < n_weight; j++) + { + boneWeights.Add(origBoneWeights[ptr + j]); + } + + bonesPerVertex.Add(n_weight); + } + + ptr += n_weight; + } + + var native_boneWeights = new NativeArray(boneWeights.ToArray(), Allocator.Temp); + var native_bonesPerVertex = new NativeArray(bonesPerVertex.ToArray(), Allocator.Temp); + + mesh.SetBoneWeights(native_bonesPerVertex, native_boneWeights); + + native_boneWeights.Dispose(); + native_bonesPerVertex.Dispose(); + } + + private static void UpdateTriangles(Mesh mesh, Mesh original, bool[] toRetainVertices, int[] origToNewVertIndex) + { + int submeshCount = original.subMeshCount; + + List orig_tris = new List(); + List new_tris = new List(); + + List orig_tris_16 = new List(); + List new_tris_16 = new List(); + + mesh.subMeshCount = submeshCount; + + for (int sm = 0; sm < submeshCount; sm++) + { + if (original.indexFormat == IndexFormat.UInt32) + { + original.GetTriangles(orig_tris, sm, true); + ProcessSubmesh(orig_tris, new_tris, i => i, i => i); + + int min = Math.Max(0, new_tris.Min()); + for (int i = 0; i < new_tris.Count; i++) + { + new_tris[i] -= min; + } + + mesh.SetTriangles(new_tris, sm, true, min); + } + else + { + original.GetTriangles(orig_tris_16, sm, true); + ProcessSubmesh(orig_tris_16, new_tris_16, i => i, i => (ushort)i); + + ushort min = new_tris_16.Min(); + for (int i = 0; i < new_tris_16.Count; i++) + { + new_tris_16[i] -= min; + } + + mesh.SetTriangles(new_tris_16, sm, true, min); + } + } + + void ProcessSubmesh(List orig_tri, List new_tri, Func toInt, Func fromInt) + { + int limit = orig_tri.Count - 2; + + new_tri.Clear(); + + for (int i = 0; i < limit; i += 3) + { + if (!toRetainVertices[toInt(orig_tri[i])] + || !toRetainVertices[toInt(orig_tri[i + 1])] + || !toRetainVertices[toInt(orig_tri[i + 2])] + ) + { + continue; + } + + new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i])])); + new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i + 1])])); + new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i + 2])])); + } + + if (new_tri.Count == 0) + { + new_tri.Add(default); + new_tri.Add(default); + new_tri.Add(default); + } + } + } + + private static void RemapVerts(bool[] toRetainVertices, out int[] origToNewVertIndex, + out int[] newToOrigVertIndex) + { + List n2o = new List(toRetainVertices.Length); + List o2n = new List(toRetainVertices.Length); + int i = 0; + + for (int j = 0; j < toRetainVertices.Length; j++) + { + if (toRetainVertices[j]) + { + o2n.Add(n2o.Count); + n2o.Add(j); + } + else + { + o2n.Add(-1); + } + } + + newToOrigVertIndex = n2o.ToArray(); + origToNewVertIndex = o2n.ToArray(); + } + + private static void ProbeBlendshapes(Mesh mesh, bool[] toDeleteVertices, IEnumerable shapes) + { + var bsPos = new Vector3[mesh.vertexCount]; + + foreach (var index in shapes) + { + int frames = mesh.GetBlendShapeFrameCount(index); + + for (int f = 0; f < frames; f++) + { + mesh.GetBlendShapeFrameVertices(index, f, bsPos, null, null); + + for (int i = 0; i < bsPos.Length; i++) + { + if (bsPos[i].sqrMagnitude > 0.0001f) + { + toDeleteVertices[i] = true; + } + } + } + } + } + + private static void ProbeRetainedVertices(Mesh mesh, bool[] toDeleteVertices, bool[] toRetainVertices) + { + List tris = new List(); + for (int subMesh = 0; subMesh < mesh.subMeshCount; subMesh++) + { + tris.Clear(); + + var baseVertex = (int)mesh.GetBaseVertex(subMesh); + mesh.GetTriangles(tris, subMesh, false); + + for (int i = 0; i < tris.Count; i += 3) + { + if (toDeleteVertices[tris[i] + baseVertex] || toDeleteVertices[tris[i + 1] + baseVertex] || + toDeleteVertices[tris[i + 2] + baseVertex]) + { + continue; + } + else + { + toRetainVertices[tris[i] + baseVertex] = true; + toRetainVertices[tris[i + 1] + baseVertex] = true; + toRetainVertices[tris[i + 2] + baseVertex] = true; + } + } + } + } + } +} \ No newline at end of file diff --git a/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta b/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta new file mode 100644 index 00000000..0d735b76 --- /dev/null +++ b/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4e45c8fdd80a4a37bd7f2bd3616886bb +timeCreated: 1717209652 \ No newline at end of file diff --git a/Editor/ShapeChanger/ShapeChangerPass.cs b/Editor/ShapeChanger/ShapeChangerPass.cs new file mode 100644 index 00000000..eb555cdf --- /dev/null +++ b/Editor/ShapeChanger/ShapeChangerPass.cs @@ -0,0 +1,616 @@ +#region + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.animation; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using VRC.SDK3.Avatars.Components; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ShapeChangerPass + { + struct ShapeKey + { + public int RendererInstanceId; + public int ShapeIndex; + public string ShapeKeyName; // not equated + + public bool Equals(ShapeKey other) + { + return RendererInstanceId == other.RendererInstanceId && ShapeIndex == other.ShapeIndex; + } + + public override bool Equals(object obj) + { + return obj is ShapeKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (RendererInstanceId * 397) ^ ShapeIndex; + } + } + } + + class ShapeKeyInfo + { + public ShapeKey ShapeKey { get; } + public string SetParam { get; set; } + public string DeleteParam { get; set; } + + public bool alwaysDeleted; + + // Objects which trigger deletion of this shape key. + public List deletionObjects = new List(); + public List setObjects = new List(); + + public ShapeKeyInfo(ShapeKey key) + { + ShapeKey = key; + } + } + + class ActionGroupKey + { + public ActionGroupKey(AnimationServicesContext asc, ShapeKey key, GameObject controllingObject, + ChangedShape shape) + { + ShapeKey = key; + InitiallyActive = controllingObject?.activeInHierarchy == true; + + var origControlling = controllingObject?.name ?? ""; + while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _)) + { + controllingObject = controllingObject.transform.parent?.gameObject; + } + + var newControlling = controllingObject?.name ?? ""; + Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling); + + ControllingObject = controllingObject; + IsDelete = shape.ChangeType == ShapeChangeType.Delete; + Value = IsDelete ? 100 : shape.Value; + } + + public ShapeKey ShapeKey; + public bool IsDelete; + public float Value; + + public float ConditionKey; + // When constructing the 1D blend tree to interpret the sum-of-condition-keys value, we need to ensure that + // all valid values are solidly between two control points with the same animation clip, to avoid undesired + // interpolation. This is done by constructing a "guard band": + // [ valid range ] [ guard band ] [ valid range ] + // + // The valid range must contain all values that could be created by valid summations. We therefore reserve + // a "guard band" in between; by reserving the exponent below each valid stop, we can put our guard bands + // in there. + // [ valid ] [ guard ] [ valid ] + // ^-r0 ^-g0 ^-g1 + // ^- r1 + // g0 = r1 / 2 = r0 * 2 + // g1 = BitDecrement(r1) (we don't actually use this currently as r0-g0 is enough) + + public float Guard => ConditionKey * 2; + + public bool ConditionKeyIsValid => float.IsFinite(ConditionKey) + && float.IsFinite(Guard) + && ConditionKey > 0; + + public GameObject ControllingObject; + public bool InitiallyActive; + + public override string ToString() + { + var obj = ControllingObject?.name ?? ""; + + return $"AGK: {ShapeKey.RendererInstanceId}:{ShapeKey.ShapeIndex} ({ShapeKey.ShapeKeyName})={Value} " + + $"range={ConditionKey}/{Guard} controlling object={obj}"; + } + + public bool TryMerge(ActionGroupKey other) + { + if (!ShapeKey.Equals(other.ShapeKey)) return false; + if (Mathf.Abs(Value - other.Value) > 0.001f) return false; + if (ControllingObject != other.ControllingObject) return false; + + IsDelete |= other.IsDelete; + + return true; + } + } + + private readonly ndmf.BuildContext context; + private Dictionary initialValues = new(); + + public ShapeChangerPass(ndmf.BuildContext context) + { + this.context = context; + } + + internal void Execute() + { + Dictionary shapes = FindShapes(context); + + foreach (var groups in shapes.Values) + { + ProcessShapeKey(groups); + } + + ProcessMeshDeletion(shapes); + } + + #region Mesh processing + + private void ProcessMeshDeletion(Dictionary shapes) + { + ImmutableDictionary> renderers = shapes.Values.GroupBy( + v => v.ShapeKey.RendererInstanceId + ).ToImmutableDictionary( + g => g.Key, + g => g.ToList() + ); + + foreach (var (rendererId, infos) in renderers) + { + var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(rendererId); + if (renderer == null) continue; + + var mesh = renderer.sharedMesh; + if (mesh == null) continue; + + renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes( + mesh, + infos.Where(i => i.alwaysDeleted).Select(i => i.ShapeKey.ShapeIndex) + ); + } + } + + #endregion + + private void ProcessShapeKey(ShapeKeyInfo info) + { + // TODO: prune non-animated keys + + // Check if this is non-animated and skip most processing if so + if (info.alwaysDeleted) return; + if (info.setObjects[^1].ControllingObject == null) + { + var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(info.ShapeKey.RendererInstanceId); + renderer.SetBlendShapeWeight(info.ShapeKey.ShapeIndex, info.setObjects[0].Value); + return; + } + + // This value is the first non-subnormal float32 + float shift = BitConverter.Int32BitsToSingle(0x00800000); + + foreach (var group in info.setObjects) + { + group.ConditionKey = shift; + shift *= 4; + + if (!group.ConditionKeyIsValid) + { + throw new ArithmeticException("Floating point overflow - too many shape key controls"); + } + } + + info.SetParam = + $"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/set"; + info.DeleteParam = $"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/delete"; + + var summationTree = BuildSummationTree(info); + var applyTree = BuildApplyTree(info); + var merged = BuildMergeTree(summationTree, applyTree); + + ApplyController(merged, "ShapeChanger Apply: " + info.ShapeKey.ShapeKeyName); + } + + private BlendTree BuildMergeTree(BlendTree summationTree, BlendTree applyTree) + { + var bt = new BlendTree(); + bt.blendType = BlendTreeType.Direct; + bt.blendParameter = MergeBlendTreePass.ALWAYS_ONE; + bt.useAutomaticThresholds = false; + + bt.children = new[] + { + new ChildMotion() + { + motion = summationTree, + directBlendParameter = MergeBlendTreePass.ALWAYS_ONE, + timeScale = 1, + }, + new ChildMotion() + { + motion = applyTree, + directBlendParameter = MergeBlendTreePass.ALWAYS_ONE, + timeScale = 1, + }, + }; + + return bt; + } + + private BlendTree BuildApplyTree(ShapeKeyInfo info) + { + var groups = info.setObjects; + + var setTree = new BlendTree(); + setTree.blendType = BlendTreeType.Simple1D; + setTree.blendParameter = info.SetParam; + setTree.useAutomaticThresholds = false; + + var childMotions = new List(); + + childMotions.Add(new ChildMotion() + { + motion = AnimResult(groups.First().ShapeKey, 0), + timeScale = 1, + threshold = 0, + }); + + foreach (var group in groups) + { + var lo = group.ConditionKey; + var hi = group.Guard; + + Debug.Log("Threshold: [" + lo + ", " + hi + "]: " + group); + + childMotions.Add(new ChildMotion() + { + motion = AnimResult(group.ShapeKey, group.Value), + timeScale = 1, + threshold = lo, + }); + childMotions.Add(new ChildMotion() + { + motion = AnimResult(group.ShapeKey, group.Value), + timeScale = 1, + threshold = hi, + }); + } + + setTree.children = childMotions.ToArray(); + + var delTree = new BlendTree(); + delTree.blendType = BlendTreeType.Simple1D; + delTree.blendParameter = info.DeleteParam; + delTree.useAutomaticThresholds = false; + + delTree.children = new[] + { + new ChildMotion() + { + motion = setTree, + timeScale = 1, + threshold = 0 + }, + new ChildMotion() + { + motion = setTree, + timeScale = 1, + threshold = 0.4f + }, + new ChildMotion() + { + motion = delTree, + timeScale = 1, + threshold = 0.6f + }, + new ChildMotion() + { + motion = delTree, + timeScale = 1, + threshold = 1 + }, + }; + + return delTree; + } + + private Motion AnimResult(ShapeKey key, float value) + { + var renderer = EditorUtility.InstanceIDToObject(key.RendererInstanceId) as SkinnedMeshRenderer; + if (renderer == null) throw new InvalidOperationException("Failed to get object"); + + var obj = renderer.gameObject; + var path = RuntimeUtil.RelativePath(context.AvatarRootObject, obj); + + var clip = new AnimationClip(); + clip.name = $"Set {obj.name}:{key.ShapeKeyName}={value}"; + + var curve = new AnimationCurve(); + curve.AddKey(0, value); + curve.AddKey(1, value); + + var binding = + EditorCurveBinding.FloatCurve(path, typeof(SkinnedMeshRenderer), $"blendShape.{key.ShapeKeyName}"); + AnimationUtility.SetEditorCurve(clip, binding, curve); + + return clip; + } + + private BlendTree BuildSummationTree(ShapeKeyInfo info) + { + var groups = info.setObjects; + + var setParam = info.SetParam; + var delParam = info.DeleteParam; + + var asc = context.Extension(); + + BlendTree bt = new BlendTree(); + bt.blendType = BlendTreeType.Direct; + + HashSet paramNames = new HashSet(); + + var childMotions = new List(); + + // TODO eliminate excess motion field + var initMotion = new ChildMotion() + { + motion = AnimParam((setParam, 0), (delParam, 0)), + directBlendParameter = MergeBlendTreePass.ALWAYS_ONE, + timeScale = 1, + }; + childMotions.Add(initMotion); + paramNames.Add(MergeBlendTreePass.ALWAYS_ONE); + initialValues[MergeBlendTreePass.ALWAYS_ONE] = 1; + + foreach (var group in groups) + { + Debug.Log("Group: " + group); + string controllingParam; + if (group.ControllingObject == null) + { + controllingParam = MergeBlendTreePass.ALWAYS_ONE; + } + else + { + // TODO: path evaluation + if (!asc.TryGetActiveSelfProxy(group.ControllingObject, out controllingParam)) + { + throw new InvalidOperationException("Failed to get active self proxy"); + } + + initialValues[controllingParam] = group.ControllingObject.activeSelf ? 1 : 0; + } + + var childMotion = new ChildMotion() + { + motion = AnimParam(setParam, group.ConditionKey), + directBlendParameter = controllingParam, + timeScale = 1, + }; + childMotions.Add(childMotion); + paramNames.Add(controllingParam); + } + + foreach (var delController in info.deletionObjects) + { + if (!asc.TryGetActiveSelfProxy(delController, out var controllingParam)) + { + throw new InvalidOperationException("Failed to get active self proxy"); + } + + initialValues[controllingParam] = delController.activeSelf ? 1 : 0; + + var childMotion = new ChildMotion() + { + motion = AnimParam(delParam, 1), + directBlendParameter = controllingParam, + timeScale = 1, + }; + + childMotions.Add(childMotion); + paramNames.Add(controllingParam); + } + + bt.children = childMotions.ToArray(); + + return bt; + } + + private void ApplyController(BlendTree bt, string stateName) + { + var fx = context.AvatarDescriptor.baseAnimationLayers + .FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX); + if (fx.animatorController == null) + { + throw new InvalidOperationException("No FX layer found"); + } + + if (!context.IsTemporaryAsset(fx.animatorController)) + { + throw new InvalidOperationException("FX layer is not a temporary asset"); + } + + if (!(fx.animatorController is AnimatorController animController)) + { + throw new InvalidOperationException("FX layer is not an animator controller"); + } + + var stateMachine = new AnimatorStateMachine(); + var layers = animController.layers.ToList(); + layers.Add( + new AnimatorControllerLayer() { defaultWeight = 1, name = stateName, stateMachine = stateMachine } + ); + var state = new AnimatorState(); + state.name = stateName; + state.motion = bt; + state.writeDefaultValues = true; + stateMachine.states = new[] { new ChildAnimatorState() { state = state } }; + stateMachine.defaultState = state; + + var paramList = animController.parameters.ToList(); + var paramSet = paramList.Select(p => p.name).ToHashSet(); + + foreach (var paramName in FindParams(bt)) + { + if (!paramSet.Contains(paramName)) + { + paramList.Add(new AnimatorControllerParameter() + { + name = paramName, + type = AnimatorControllerParameterType.Float, + defaultFloat = 0, // TODO + }); + paramSet.Add(paramName); + } + } + + animController.parameters = paramList.ToArray(); + + animController.layers = layers.ToArray(); + } + + private static IEnumerable FindParams(BlendTree bt) + { + if (bt == null) yield break; + + if (bt.blendType == BlendTreeType.Direct) + { + foreach (var child in bt.children) + { + yield return child.directBlendParameter; + } + } + else + { + yield return bt.blendParameter; + } + + foreach (var child in bt.children) + { + foreach (var param in FindParams(child.motion as BlendTree)) + { + yield return param; + } + } + } + + + private AnimationClip AnimParam(string param, float val) + { + return AnimParam((param, val)); + } + + private AnimationClip AnimParam(params (string param, float val)[] pairs) + { + AnimationClip clip = new AnimationClip(); + clip.name = "Set " + string.Join(", ", pairs.Select(p => $"{p.param}={p.val}")); + + // TODO - check property syntax + foreach (var (param, val) in pairs) + { + var curve = new AnimationCurve(); + curve.AddKey(0, val); + curve.AddKey(1, val); + clip.SetCurve("", typeof(Animator), "" + param, curve); + } + + return clip; + } + + private Dictionary FindShapes(ndmf.BuildContext context) + { + var asc = context.Extension(); + + var changers = context.AvatarRootObject.GetComponentsInChildren(true); + + Dictionary shapeKeys = new(); + + foreach (var changer in changers) + { + var renderer = changer.targetRenderer.Get(changer)?.GetComponent(); + if (renderer == null) continue; + + int rendererInstanceId = renderer.GetInstanceID(); + var mesh = renderer.sharedMesh; + + if (mesh == null) continue; + + foreach (var shape in changer.Shapes) + { + var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName); + if (shapeId < 0) continue; + + var key = new ShapeKey + { + RendererInstanceId = rendererInstanceId, + ShapeIndex = shapeId, + ShapeKeyName = shape.ShapeName, + }; + + if (!shapeKeys.TryGetValue(key, out var info)) + { + info = new ShapeKeyInfo(key); + shapeKeys[key] = info; + + // Add initial state + info.setObjects.Add(new ActionGroupKey(asc, key, null, shape)); + } + + var action = new ActionGroupKey(asc, key, changer.gameObject, shape); + + if (action.IsDelete) + { + if (action.ControllingObject == null) + { + // always active? + info.alwaysDeleted |= changer.gameObject.activeInHierarchy; + } + else + { + info.deletionObjects.Add(action.ControllingObject); + } + + continue; + } + + // TODO: lift controlling object resolution out of loop? + if (action.ControllingObject == null) + { + if (action.InitiallyActive) + { + // always active control + info.setObjects.Clear(); + } + else + { + // never active control + continue; + } + } + + Debug.Log("Trying merge: " + action); + if (info.setObjects.Count == 0) + { + info.setObjects.Add(action); + } + else if (!info.setObjects[^1].TryMerge(action)) + { + Debug.Log("Failed merge"); + info.setObjects.Add(action); + } + else + { + Debug.Log("Post merge: " + info.setObjects[^1]); + } + } + } + + return shapeKeys; + } + } +} \ No newline at end of file diff --git a/Editor/ShapeChanger/ShapeChangerPass.cs.meta b/Editor/ShapeChanger/ShapeChangerPass.cs.meta new file mode 100644 index 00000000..49ae3542 --- /dev/null +++ b/Editor/ShapeChanger/ShapeChangerPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 740a387c9d934623a6b06d945b38d8d0 +timeCreated: 1717123900 \ No newline at end of file diff --git a/Editor/ShapeChanger/ShapeChangerPreview.cs b/Editor/ShapeChanger/ShapeChangerPreview.cs new file mode 100644 index 00000000..9d8a1452 --- /dev/null +++ b/Editor/ShapeChanger/ShapeChangerPreview.cs @@ -0,0 +1,198 @@ +#region + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.rq; +using nadena.dev.ndmf.rq.unity.editor; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + public class ShapeChangerPreview : IRenderFilter + { + private static ReactiveValue>> + InternalTargetGroups + = ReactiveValue>>.Create( + "ShapeChangerPreview.TargetGroups", async ctx => + { + var allChangers = + await ctx.Observe(CommonQueries.GetComponentsByType()); + + Dictionary.Builder> groups = + new Dictionary.Builder>( + new ObjectIdentityComparer()); + + foreach (var changer in allChangers) + { + // TODO: observe avatar root + ctx.Observe(changer); + var target = ctx.Observe(changer.targetRenderer.Get(changer)); + var renderer = ctx.GetComponent(target); + + if (renderer == null) continue; + + if (!groups.TryGetValue(renderer, out var group)) + { + group = ImmutableList.CreateBuilder(); + groups[renderer] = group; + } + + group.Add(changer); + } + + return groups.ToImmutableDictionary(p => p.Key, p => p.Value.ToImmutable()); + }); + + public ReactiveValue>> TargetGroups { get; } = + ReactiveValue>>.Create( + "ShapeChangerPreview.TargetGroups", async ctx => + { + var targetGroups = await ctx.Observe(InternalTargetGroups); + + return targetGroups.Keys + .Select(v => (IImmutableList)ImmutableList.Create(v)) + .ToImmutableList(); + }); + + private bool IsChangerActive(ModularAvatarShapeChanger changer, ComputeContext context) + { + if (context != null) + { + if (!context.ActiveAndEnabled(changer)) return false; + } + else + { + if (!changer.isActiveAndEnabled) return false; + } + + var changerRenderer = context != null + ? context.GetComponent(changer.gameObject) + : changer.GetComponent(); + context?.Observe(changerRenderer); + + if (changerRenderer == null) return false; + return changerRenderer.enabled && (context?.ActiveAndEnabled(changer) ?? changer.isActiveAndEnabled); + } + + + public async Task MutateMeshData(IList states, ComputeContext context) + { + var targetGroups = await context.Observe(InternalTargetGroups); + + var state = states[0]; + var renderer = state.Original; + if (renderer == null) return; + if (!targetGroups.TryGetValue(renderer, out var changers)) return; + if (!(renderer is SkinnedMeshRenderer smr)) return; + + HashSet toDelete = new HashSet(); + + foreach (var changer in changers) + { + if (!IsChangerActive(changer, context)) continue; + + foreach (var shape in changer.Shapes) + { + if (shape.ChangeType == ShapeChangeType.Delete) + { + var index = state.Mesh.GetBlendShapeIndex(shape.ShapeName); + if (index < 0) continue; + toDelete.Add(index); + } + } + } + + if (toDelete.Count > 0) + { + var mesh = Object.Instantiate(state.Mesh); + + var bsPos = new Vector3[mesh.vertexCount]; + bool[] targetVertex = new bool[mesh.vertexCount]; + foreach (var bs in toDelete) + { + int frames = mesh.GetBlendShapeFrameCount(bs); + for (int f = 0; f < frames; f++) + { + mesh.GetBlendShapeFrameVertices(bs, f, bsPos, null, null); + + for (int i = 0; i < bsPos.Length; i++) + { + if (bsPos[i].sqrMagnitude > 0.0001f) + { + targetVertex[i] = true; + } + } + } + } + + List tris = new List(); + for (int subMesh = 0; subMesh < mesh.subMeshCount; subMesh++) + { + tris.Clear(); + + var baseVertex = (int)mesh.GetBaseVertex(subMesh); + mesh.GetTriangles(tris, subMesh, false); + + for (int i = 0; i < tris.Count; i += 3) + { + if (targetVertex[tris[i] + baseVertex] || targetVertex[tris[i + 1] + baseVertex] || + targetVertex[tris[i + 2] + baseVertex]) + { + tris.RemoveRange(i, 3); + i -= 3; + } + } + + mesh.SetTriangles(tris, subMesh, false, baseVertex: baseVertex); + } + + state.Mesh = mesh; + state.OnDispose += () => + { + if (mesh != null) Object.Destroy(mesh); + }; + } + } + + + public void OnFrame(Renderer original, Renderer proxy) + { + if (!InternalTargetGroups.TryGetValue(out var targetGroups)) return; + if (!targetGroups.TryGetValue(original, out var changers)) return; + if (!(proxy is SkinnedMeshRenderer smr)) return; + + var mesh = smr.sharedMesh; + if (mesh == null) return; + + foreach (var changer in changers) + { + if (!IsChangerActive(changer, null)) continue; + + foreach (var shape in changer.Shapes) + { + var index = mesh.GetBlendShapeIndex(shape.ShapeName); + if (index < 0) continue; + + float setToValue = -1; + + switch (shape.ChangeType) + { + case ShapeChangeType.Delete: + setToValue = 100; + break; + case ShapeChangeType.Set: + setToValue = shape.Value; + break; + } + + smr.SetBlendShapeWeight(index, setToValue); + } + } + } + } +} \ No newline at end of file diff --git a/Editor/ShapeChanger/ShapeChangerPreview.cs.meta b/Editor/ShapeChanger/ShapeChangerPreview.cs.meta new file mode 100644 index 00000000..3573038e --- /dev/null +++ b/Editor/ShapeChanger/ShapeChangerPreview.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 01cd76f7b31543b9bba5690ba478f865 +timeCreated: 1717105143 \ No newline at end of file diff --git a/Editor/nadena.dev.modular-avatar.core.editor.asmdef b/Editor/nadena.dev.modular-avatar.core.editor.asmdef index 70e69df1..d84a270d 100644 --- a/Editor/nadena.dev.modular-avatar.core.editor.asmdef +++ b/Editor/nadena.dev.modular-avatar.core.editor.asmdef @@ -6,7 +6,9 @@ "VRC.SDK3A", "VRC.SDKBase", "nadena.dev.ndmf", - "nadena.dev.ndmf.vrchat" + "nadena.dev.ndmf.vrchat", + "nadena.dev.ndmf.reactive-query.core", + "nadena.dev.ndmf.runtime" ], "includePlatforms": [ "Editor" diff --git a/Runtime/MAMoveIndependently.cs b/Runtime/MAMoveIndependently.cs index 6a343807..360c4898 100644 --- a/Runtime/MAMoveIndependently.cs +++ b/Runtime/MAMoveIndependently.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using nadena.dev.modular_avatar.core.armature_lock; using UnityEngine; - #if MA_VRCSDK3_AVATARS using VRC.SDKBase; #endif @@ -128,7 +127,6 @@ namespace nadena.dev.modular_avatar.core.ArmatureAwase foreach (Transform child in parent) { if (_excluded.Contains(child)) continue; - if (child.GetComponent() != null) continue; _observed.Add(child); diff --git a/Runtime/ModularAvatarShapeChanger.cs b/Runtime/ModularAvatarShapeChanger.cs new file mode 100644 index 00000000..c4f7ffdb --- /dev/null +++ b/Runtime/ModularAvatarShapeChanger.cs @@ -0,0 +1,49 @@ +#region + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Serialization; + +#endregion + +namespace nadena.dev.modular_avatar.core +{ + [Serializable] + public enum ShapeChangeType + { + Delete, + Set + } + + [Serializable] + public struct ChangedShape + { + public string ShapeName; + public ShapeChangeType ChangeType; + public float Value; + } + + [AddComponentMenu("Modular Avatar/MA Shape Changer")] + [HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")] + public class ModularAvatarShapeChanger : AvatarTagComponent + { + [SerializeField] [FormerlySerializedAs("targetRenderer")] + private AvatarObjectReference m_targetRenderer; + + public AvatarObjectReference targetRenderer + { + get => m_targetRenderer; + set => m_targetRenderer = value; + } + + [SerializeField] [FormerlySerializedAs("Shapes")] + private List m_shapes = new(); + + public List Shapes + { + get => m_shapes; + set => m_shapes = value; + } + } +} \ No newline at end of file diff --git a/Runtime/ModularAvatarShapeChanger.cs.meta b/Runtime/ModularAvatarShapeChanger.cs.meta new file mode 100644 index 00000000..9cc0d258 --- /dev/null +++ b/Runtime/ModularAvatarShapeChanger.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2db441f589c3407bb6fb5f02ff8ab541 +timeCreated: 1717105020 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs index a995c900..8b70b744 100644 --- a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -1,11 +1,8 @@ #region -using System; using UnityEngine; -using UnityEngine.SceneManagement; using UnityEngine.Serialization; #if UNITY_EDITOR -using UnityEditor; #endif #endregion @@ -24,146 +21,10 @@ namespace nadena.dev.modular_avatar.core public Vector3 Scale { get => m_Scale; - set - { - m_Scale = value; - PreCull(); - } + set => m_Scale = value; } [SerializeField] [FormerlySerializedAs("scaleProxy")] internal Transform legacyScaleProxy; - - internal Transform scaleProxyChild; - - [NonSerialized] - private bool initialized = false; - -#if UNITY_EDITOR - void Awake() - { - ProxyManager.RegisterAdjuster(this); - initialized = false; - } - - void OnValidate() - { - ProxyManager.RegisterAdjuster(this); - initialized = false; - } - - internal void PreCull() - { - if (this == null) - { - EditorApplication.delayCall += () => ProxyManager.UnregisterAdjuster(this); - return; - } - - if (PrefabUtility.IsPartOfPrefabAsset(this)) return; - - if (scaleProxyChild == null || initialized == false) - { - InitializeProxy(); - } - - UpdateProxyParent(scaleProxyChild, transform); - - var xform = transform; - scaleProxyChild.localScale = m_Scale; - - ProxyManager.RegisterBone(xform, scaleProxyChild); - - if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy)) - { - DestroyImmediate(legacyScaleProxy.gameObject); - legacyScaleProxy = null; - } - } - - private void UpdateProxyParent(Transform proxyChild, Transform trueParent) - { - while (trueParent != null) - { - Transform parent = proxyChild.parent; - if (parent == null) - { - GameObject obj = new GameObject(); - proxyChild.transform.SetParent(obj.transform, false); - #if MODULAR_AVATAR_DEBUG_HIDDEN - obj.hideFlags = HideFlags.DontSave; - #else - obj.hideFlags = HideFlags.HideAndDontSave; - #endif - parent = obj.transform; - - if (obj.scene != gameObject.scene && gameObject.scene.IsValid()) - { - SceneManager.MoveGameObjectToScene(obj, gameObject.scene); - } - } - - parent.gameObject.name = "Proxy object for " + trueParent.gameObject.name; - parent.localPosition = trueParent.localPosition; - parent.localRotation = trueParent.localRotation; - parent.localScale = trueParent.localScale; - - proxyChild = parent; - trueParent = trueParent.parent; - } - - // Clean up any additional parentage we might not want - if (proxyChild.parent != null) - { - Transform parent = proxyChild.parent; - - // Reparent to root - proxyChild.SetParent(null, false); - - // Destroy old hierarchy - while (parent.parent != null) parent = parent.parent; - DestroyImmediate(parent.gameObject); - } - } - - private void InitializeProxy() - { - if (scaleProxyChild == null) - { - scaleProxyChild = new GameObject("Child").transform; - -#if MODULAR_AVATAR_DEBUG_HIDDEN - scaleProxyChild.gameObject.hideFlags = HideFlags.DontSave; -#else - scaleProxyChild.gameObject.hideFlags = HideFlags.HideAndDontSave; -#endif - } - - initialized = true; - } - - private void OnDestroy() - { - ProxyManager.UnregisterAdjuster(this); - - if (scaleProxyChild != null) - { - Transform parent = scaleProxyChild.parent; - while (parent.parent != null) parent = parent.parent; - DestroyImmediate(parent.gameObject); - } - - if (transform != null) - { - ProxyManager.UnregisterBone(transform); - } - - base.OnDestroy(); - } -#else - internal void PreCull() { - // build time stub - } -#endif } } \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ProxyManager.cs b/Runtime/ScaleAdjuster/ProxyManager.cs deleted file mode 100644 index 49d6560d..00000000 --- a/Runtime/ScaleAdjuster/ProxyManager.cs +++ /dev/null @@ -1,359 +0,0 @@ -#region - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using UnityEngine; -using UnityEngine.SceneManagement; -#if UNITY_EDITOR -using UnityEditor; -using UnityEditor.SceneManagement; -#endif - -#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) - { - if (_adjusters.Contains(adjuster)) return; - _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 = ImmutableHashSet.Empty; - - 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) - { - if (adj == null) - { - _capturedAdjusters = _adjusters = _adjusters.Remove(adj); - } - 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(bone => - { - var root = RuntimeUtil.FindAvatarTransformInParents(bone); - if (root == null) - { - root = bone; - while (root.parent != null) root = root.parent; - } - - return root; - }).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 (srcBone == null) - { - return null; - } else 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 (bone != null && _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) - { - if (adj == null) - { - _capturedAdjusters = _capturedAdjusters.Remove(adj); - } - 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.forceRenderingOff = true; - } - } - } - - 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.forceRenderingOff = false; - } - - statesToRestore.Clear(); - } -#endif - } -} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ProxyManager.cs.meta b/Runtime/ScaleAdjuster/ProxyManager.cs.meta deleted file mode 100644 index 9ee0bfc6..00000000 --- a/Runtime/ScaleAdjuster/ProxyManager.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 117b3ad981cb487aa5029043f7482a94 -timeCreated: 1709447257 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs deleted file mode 100644 index e4ab0e57..00000000 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs +++ /dev/null @@ -1,33 +0,0 @@ -#region - -using UnityEngine; -using VRC.SDKBase; -#if UNITY_EDITOR -using UnityEditor; -#endif - -#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 - { -#if UNITY_EDITOR - private void OnValidate() - { - if (PrefabUtility.IsPartOfPrefabAsset(this)) return; - - EditorApplication.delayCall += () => - { - if (this != null) DestroyImmediate(gameObject); - }; - } -#endif - } -} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta deleted file mode 100644 index 870af8e4..00000000 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c8bc16baa6c345eea5edf47232ee4069 -timeCreated: 1708232586 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleProxy.cs b/Runtime/ScaleAdjuster/ScaleProxy.cs deleted file mode 100644 index 1b1957ac..00000000 --- a/Runtime/ScaleAdjuster/ScaleProxy.cs +++ /dev/null @@ -1,80 +0,0 @@ - -#region - -using UnityEngine; -#if UNITY_EDITOR -using UnityEditor; -#endif - -#endregion - -namespace nadena.dev.modular_avatar.core -{ - [AddComponentMenu("")] - internal sealed class ScaleProxy : AvatarTagComponent - { -#if UNITY_EDITOR - protected override void OnValidate() - { - base.OnValidate(); - EditorApplication.delayCall += DeferredValidate; - } - - private void DeferredValidate() - { - if (this == null) return; - - // Avoid logspam on Unity 2019 - if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; - - if (!ProxyManager.ShouldRetain(gameObject)) - { - SelfDestruct(); - } - } - - private void SelfDestruct() - { - var root = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform); - if (root == null) - { - root = transform; - while (root.parent != null) root = root.parent; - } - - ClearOverrides(root); - - if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; - - 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; - - for (var i = 0; i < bones.Length; i++) - { - if (bones[i] == transform) - { - bones[i] = transform.parent; - changed = true; - } - } - - if (changed) - { - smr.bones = bones; - } - } - } -#endif - } -} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleProxy.cs.meta b/Runtime/ScaleAdjuster/ScaleProxy.cs.meta deleted file mode 100644 index 5fcb9766..00000000 --- a/Runtime/ScaleAdjuster/ScaleProxy.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3f0c19b32ba845a2a84f37f48e4ec4d5 -timeCreated: 1703659053 \ No newline at end of file diff --git a/UnitTests~/ComponentSettingsTest.cs b/UnitTests~/ComponentSettingsTest.cs index 90aa6262..6cd825e8 100644 --- a/UnitTests~/ComponentSettingsTest.cs +++ b/UnitTests~/ComponentSettingsTest.cs @@ -39,8 +39,7 @@ namespace modular_avatar_tests if (type == typeof(Activator)) return; if (type == typeof(AvatarActivator)) return; if (type == typeof(TestComponent)) return; - if (type == typeof(ScaleProxy)) return; - if (type == typeof(ScaleAdjusterRenderer)) return; + if (type == typeof(ModularAvatarShapeChanger)) return; // get icon var component = (MonoBehaviour) _gameObject.AddComponent(type); @@ -65,8 +64,6 @@ namespace modular_avatar_tests if (type == typeof(Activator)) return; 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();