diff --git a/Editor/EasySetupOutfit.cs b/Editor/EasySetupOutfit.cs index 4898bb60..28fbe56c 100644 --- a/Editor/EasySetupOutfit.cs +++ b/Editor/EasySetupOutfit.cs @@ -308,10 +308,19 @@ namespace nadena.dev.modular_avatar.core.editor // refusing to run if we detect multiple avatar descriptors above the current object (or if we're run on // the avdesc object itself) var nearestAvatarTransform = RuntimeUtil.FindAvatarTransformInParents(xform); - if (nearestAvatarTransform == null || nearestAvatarTransform == xform) + if (nearestAvatarTransform == null) { errorMessageGroups = new string[] - { S_f("setup_outfit.err.multiple_avatar_descriptors", xform.gameObject.name) }; + { + S_f("setup_outfit.err.no_avatar_descriptor", xform.gameObject.name) + }; + return false; + } + + if (nearestAvatarTransform == xform) + { + errorMessageGroups = new string[] + { S_f("setup_outfit.err.run_on_avatar_itself", xform.gameObject.name) }; return false; } @@ -320,7 +329,7 @@ namespace nadena.dev.modular_avatar.core.editor { errorMessageGroups = new string[] { - S_f("setup_outfit.err.no_avatar_descriptor", xform.gameObject.name) + S_f("setup_outfit.err.multiple_avatar_descriptors", xform.gameObject.name) }; return false; } diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs b/Editor/HarmonyPatches/HandleUtilityPatches.cs new file mode 100644 index 00000000..6d735313 --- /dev/null +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs @@ -0,0 +1,55 @@ +#region + +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class HandleUtilityPatches + { + internal static void Patch_FilterInstanceIDs(Harmony h) + { + var t_HandleUtility = AccessTools.TypeByName("UnityEditor.HandleUtility"); + var m_orig = AccessTools.Method(t_HandleUtility, "FilterInstanceIDs"); + + var m_prefix = AccessTools.Method(typeof(HandleUtilityPatches), "Prefix_FilterInstanceIDs"); + + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); + } + + [UsedImplicitly] + private static bool Prefix_FilterInstanceIDs( + ref IEnumerable gameObjects, + out int[] parentInstanceIDs, + out int[] childInstanceIDs + ) + { + gameObjects = RemapObjects(gameObjects); + parentInstanceIDs = childInstanceIDs = null; + return true; + } + + private static IEnumerable RemapObjects(IEnumerable objs) + { + return objs.Select( + obj => + { + if (obj == null) return obj; + if (ScaleAdjusterRenderer.originalObjects.TryGetValue(obj, out var proxy) && proxy != null) + { + return proxy.gameObject; + } + else + { + return obj; + } + } + ).ToArray(); + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta b/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta new file mode 100644 index 00000000..82224e59 --- /dev/null +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 807736f252df4b1b8402827257dcbea3 +timeCreated: 1709354699 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs index 6a601d53..3069107c 100644 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs +++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs @@ -39,20 +39,17 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches [UsedImplicitly] private static void Postfix(GameObject prefabInstance, object __result) { - var ignoredObjects = prefabInstance.GetComponentsInChildren() - .Select(sar => sar.gameObject) - .ToImmutableHashSet(); List added = p_AddedGameObjects.GetValue(__result) as List; if (added == null) return; - added.RemoveAll(obj => ignoredObjects.Contains(obj.instanceGameObject)); + added.RemoveAll(obj => ScaleAdjusterRenderer.proxyObjects.ContainsKey(obj.instanceGameObject)); List objectOverrides = p_ObjectOverrides.GetValue(__result) as List; if (objectOverrides == null) return; objectOverrides.RemoveAll(oo => { var c = oo.instanceObject as Component; - return c != null && ignoredObjects.Contains(c.gameObject); + return c != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(c.gameObject); }); } } diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs b/Editor/HarmonyPatches/HierarchyViewPatches.cs new file mode 100644 index 00000000..c769d016 --- /dev/null +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs @@ -0,0 +1,175 @@ +#region + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using JetBrains.Annotations; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class HierarchyViewPatches + { + private static readonly Type t_HierarchyProperty = AccessTools.TypeByName("UnityEditor.HierarchyProperty"); + private static readonly PropertyInfo p_pptrValue = AccessTools.Property(t_HierarchyProperty, "pptrValue"); + + private static FieldInfo f_m_Rows; // List + private static FieldInfo f_m_RowCount; // int + private static PropertyInfo p_objectPPTR; + + internal static void Patch(Harmony h) + { +#if MODULAR_AVATAR_DEBUG_HIDDEN + return; +#endif + var t_GameObjectTreeViewDataSource = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewDataSource"); + var t_GameObjectTreeViewItem = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewItem"); + + f_m_Rows = t_GameObjectTreeViewDataSource.GetField("m_Rows", + BindingFlags.NonPublic | BindingFlags.Instance); + f_m_RowCount = + t_GameObjectTreeViewDataSource.GetField("m_RowCount", BindingFlags.NonPublic | BindingFlags.Instance); + p_objectPPTR = t_GameObjectTreeViewItem.GetProperty("objectPPTR"); + + var m_orig = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitTreeViewItem", + new[] + { + t_GameObjectTreeViewItem, + typeof(int), + typeof(Scene), + typeof(bool), + typeof(int), + typeof(Object), + typeof(bool), + typeof(int) + }); + var m_patch = AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitTreeViewItem"); + + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_patch)); + + var m_InitRows = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitializeRows"); + var m_transpiler = AccessTools.Method(typeof(HierarchyViewPatches), "Transpile_InitializeRows"); + + h.Patch(original: m_InitRows, + transpiler: new HarmonyMethod(m_transpiler), + postfix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Postfix_InitializeRows")), + prefix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitializeRows")) + ); + } + + private static int skipped = 0; + + private static void Prefix_InitializeRows() + { + skipped = 0; + } + + [UsedImplicitly] + private static void Postfix_InitializeRows(object __instance) + { + var rows = (IList)f_m_Rows.GetValue(__instance); + + var rowCount = (int)f_m_RowCount.GetValue(__instance); + + f_m_RowCount.SetValue(__instance, rowCount - skipped); + + for (int i = 0; i < skipped; i++) + { + rows.RemoveAt(rows.Count - 1); + } + } + + [UsedImplicitly] + private static IEnumerable Transpile_InitializeRows(IEnumerable instructions, + ILGenerator generator) + { + foreach (var c in Transpile_InitializeRows0(instructions, generator)) + { + //Debug.Log(c); + yield return c; + } + } + + [UsedImplicitly] + private static IEnumerable Transpile_InitializeRows0(IEnumerable instructions, + ILGenerator generator) + { + var m_shouldLoop = AccessTools.Method(typeof(HierarchyViewPatches), "ShouldLoop"); + + var m_Next = AccessTools.Method(t_HierarchyProperty, "Next", new[] { typeof(int[]) }); + + foreach (var c in instructions) + { + if (c.Is(OpCodes.Callvirt, m_Next)) + { + var loopLabel = generator.DefineLabel(); + var stash_arg = generator.DeclareLocal(typeof(int[])); + var stash_obj = generator.DeclareLocal(t_HierarchyProperty); + + yield return new CodeInstruction(OpCodes.Stloc, stash_arg); + yield return new CodeInstruction(OpCodes.Stloc, stash_obj); + + var tmp = new CodeInstruction(OpCodes.Ldloc, stash_obj); + tmp.labels.Add(loopLabel); + yield return tmp; + + yield return new CodeInstruction(OpCodes.Ldloc, stash_arg); + yield return new CodeInstruction(OpCodes.Call, m_Next); + + // Check if this item should be ignored. + yield return new CodeInstruction(OpCodes.Ldloc, stash_obj); + yield return new CodeInstruction(OpCodes.Call, m_shouldLoop); + yield return new CodeInstruction(OpCodes.Brtrue_S, loopLabel); + } + else + { + yield return c; + } + } + } + + [UsedImplicitly] + private static bool ShouldLoop(object hierarchyProperty) + { + if (hierarchyProperty == null) return false; + + var pptrValue = p_pptrValue.GetValue(hierarchyProperty); + if (pptrValue == null) return false; + + var skip = ScaleAdjusterRenderer.proxyObjects.ContainsKey((GameObject)pptrValue); + if (skip) skipped++; + + return skip; + } + + private static bool Prefix_InitTreeViewItem( + object __instance, + ref object item, + int itemID, + Scene scene, + bool isSceneHeader, + int colorCode, + Object pptrObject, + ref bool hasChildren, + int depth + ) + { + if (pptrObject == null || isSceneHeader) return true; + + if (hasChildren && ScaleAdjusterRenderer.originalObjects.ContainsKey((GameObject)pptrObject)) + { + // See if there are any other children... + hasChildren = ((GameObject)pptrObject).transform.childCount > 1; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta b/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta new file mode 100644 index 00000000..06d79c30 --- /dev/null +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42f70698a5df48c0908400c425a2f6ee +timeCreated: 1709356304 \ No newline at end of file diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs index 72e71618..0e290761 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -13,9 +13,12 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { private static readonly Action[] patches = new Action[] { - SnoopHeaderRendering.Patch1, - SnoopHeaderRendering.Patch2, - HideScaleAdjusterFromPrefabOverrideView.Patch + HideScaleAdjusterFromPrefabOverrideView.Patch, + HierarchyViewPatches.Patch, + #if UNITY_2022_3_OR_NEWER + HandleUtilityPatches.Patch_FilterInstanceIDs, + PickingObjectPatch.Patch, + #endif }; [InitializeOnLoadMethod] diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs b/Editor/HarmonyPatches/PickingObjectPatch.cs new file mode 100644 index 00000000..3b64a2c3 --- /dev/null +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs @@ -0,0 +1,78 @@ +#region + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using UnityEngine; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class PickingObjectPatch + { + private static Type t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject"); + + private static Type l_PickingObject = + typeof(List<>).MakeGenericType(new[] { t_PickingObject }); + + private static ConstructorInfo ctor_l = AccessTools.Constructor(l_PickingObject); + + private static ConstructorInfo ctor_PickingObject = + AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) }); + + private static PropertyInfo p_materialIndex = AccessTools.Property(t_PickingObject, "materialIndex"); + + private static MethodInfo m_TryGetGameObject = AccessTools.Method(t_PickingObject, "TryGetGameObject"); + + internal static void Patch(Harmony h) + { + var t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject"); + var ctor_PickingObject = AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) }); + + var t_SceneViewPicking = AccessTools.TypeByName("UnityEditor.SceneViewPicking"); + var m_GetAllOverlapping = AccessTools.Method(t_SceneViewPicking, "GetAllOverlapping"); + + var m_postfix = AccessTools.Method(typeof(PickingObjectPatch), nameof(Postfix_GetAllOverlapping)); + + h.Patch(original: m_GetAllOverlapping, postfix: new HarmonyMethod(m_postfix)); + } + + private static void Postfix_GetAllOverlapping(ref object __result) + { + var erased = (IEnumerable)__result; + var list = (IList)ctor_l.Invoke(new object[0]); + + foreach (var obj in erased) + { + if (obj == null) + { + list.Add(obj); + continue; + } + + var args = new object[] { null }; + if ((bool)m_TryGetGameObject.Invoke(obj, args)) + { + var go = args[0] as GameObject; + if (go != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(go)) + { + list.Add(ctor_PickingObject.Invoke(new[] + { + go.transform.parent.gameObject, + p_materialIndex.GetValue(obj) + })); + continue; + } + } + + list.Add(obj); + } + + __result = list; + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs.meta b/Editor/HarmonyPatches/PickingObjectPatch.cs.meta new file mode 100644 index 00000000..acafbc99 --- /dev/null +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cf06818f1c0c436fbae7f755d7110aba +timeCreated: 1709359553 \ No newline at end of file diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs b/Editor/HarmonyPatches/SnoopHeaderRendering.cs deleted file mode 100644 index 39b91c7b..00000000 --- a/Editor/HarmonyPatches/SnoopHeaderRendering.cs +++ /dev/null @@ -1,45 +0,0 @@ -#region - -using HarmonyLib; -using JetBrains.Annotations; -using UnityEngine; - -#endregion - -namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches -{ - /// - /// ScaleAdjusterRenderer toggles off the enabled state of the original mesh just before rendering, - /// in order to allow us to effectively replace it at rendering time. We restore this in OnPostRender, - /// but GUI rendering can happen before this; as such, snoop GUI events and re-enable the original - /// at that time. - /// - internal class SnoopHeaderRendering - { - internal static void Patch1(Harmony harmony) - { - var t_orig = AccessTools.TypeByName("UnityEditor.UIElements.EditorElement"); - var m_orig = AccessTools.Method(t_orig, "HeaderOnGUI"); - - var m_prefix = AccessTools.Method(typeof(SnoopHeaderRendering), "Prefix"); - - harmony.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); - } - - internal static void Patch2(Harmony harmony) - { - var t_GUIUtility = typeof(GUIUtility); - var m_ProcessEvent = AccessTools.Method(t_GUIUtility, "ProcessEvent"); - - var m_prefix = AccessTools.Method(typeof(SnoopHeaderRendering), "Prefix"); - - harmony.Patch(original: m_ProcessEvent, prefix: new HarmonyMethod(m_prefix)); - } - - [UsedImplicitly] - private static void Prefix() - { - ScaleAdjusterRenderer.ClearAllOverrides(); - } - } -} \ No newline at end of file diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta b/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta deleted file mode 100644 index bf645cc0..00000000 --- a/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: cafb5b7e681644cbbeafbeb12d833f6e -timeCreated: 1708235926 \ No newline at end of file diff --git a/Editor/HeuristicBoneMapper.cs b/Editor/HeuristicBoneMapper.cs index 99dcee15..4493d659 100644 --- a/Editor/HeuristicBoneMapper.cs +++ b/Editor/HeuristicBoneMapper.cs @@ -1,10 +1,14 @@ -using System.Collections.Generic; +#region + +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; +#endregion + namespace nadena.dev.modular_avatar.core.editor { internal class HeuristicBoneMapper @@ -320,7 +324,9 @@ namespace nadena.dev.modular_avatar.core.editor foreach (Transform child in src.transform) { var childName = child.gameObject.name; - if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix)) + if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix) + && childName.Length > + config.prefix.Length + config.suffix.Length) { var targetObjectName = childName.Substring(config.prefix.Length, childName.Length - config.prefix.Length - config.suffix.Length); diff --git a/Editor/Localization/en-us.json b/Editor/Localization/en-us.json index 9735b4bd..8154297f 100644 --- a/Editor/Localization/en-us.json +++ b/Editor/Localization/en-us.json @@ -259,7 +259,8 @@ "setup_outfit.err.header": "Setup Outfit failed to process {0}", "setup_outfit.err.unknown": "Unknown error", "setup_outfit.err.no_selection": "No object selected.", - "setup_outfit.err.multiple_avatar_descriptors": "Multiple avatar descriptors found in {0} and its parents.", + "setup_outfit.err.run_on_avatar_itself": "Setup outfit needs to be run on the outfit object, not on the avatar itself.\n\nAre you trying to make a hybrid avatar? If so, remove the avatar descriptor from the inner avatar, and run setup outfit on that.", + "setup_outfit.err.multiple_avatar_descriptors": "Multiple avatar descriptors found in {0} and its parents.\n\nAre you trying to make a hybrid avatar? If so, remove the avatar descriptor from the inner avatar, and run setup outfit on that.", "setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0}'s parents. Make sure your outfit is placed inside your avatar.", "setup_outfit.err.no_animator": "Your avatar does not have an Animator component.", "setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.", diff --git a/Editor/Localization/ja-jp.json b/Editor/Localization/ja-jp.json index 0134b7b8..c5e2c695 100644 --- a/Editor/Localization/ja-jp.json +++ b/Editor/Localization/ja-jp.json @@ -213,7 +213,8 @@ "setup_outfit.err.header": "Setup outfit が「{0}」を処理中に失敗しました。", "setup_outfit.err.unknown": "原因不明のエラーが発生しました。", "setup_outfit.err.no_selection": "オブジェクトが選択されていません。", - "setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親に、複数のavatar descriptorを発見しました。", + "setup_outfit.err.run_on_avatar_itself": "Setup Outfitはアバター自体ではなく、衣装のほうで実行してください。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。", + "setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親に、複数のavatar descriptorを発見しました。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。", "setup_outfit.err.no_avatar_descriptor": "「{0}」の親に、avatar descriptorが見つかりませんでした。衣装のオブジェクトをアバターの中に配置してください。", "setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。", "setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup Outfitはヒューマノイドアバター以外には対応していません。", diff --git a/Editor/MergeArmatureHook.cs b/Editor/MergeArmatureHook.cs index db8e4f2a..e5be6ec1 100644 --- a/Editor/MergeArmatureHook.cs +++ b/Editor/MergeArmatureHook.cs @@ -22,6 +22,12 @@ * SOFTWARE. */ +#region + +#if MA_VRCSDK3_AVATARS +using VRC.Dynamics; +using VRC.SDK3.Dynamics.PhysBone.Components; +#endif using System; using System.Collections.Generic; using System.Linq; @@ -30,17 +36,14 @@ using nadena.dev.modular_avatar.editor.ErrorReporting; using UnityEditor; using UnityEngine; using UnityEngine.Animations; - -#if MA_VRCSDK3_AVATARS -using VRC.Dynamics; -using VRC.SDK3.Dynamics.PhysBone.Components; -#endif - using Object = UnityEngine.Object; +#endregion + namespace nadena.dev.modular_avatar.core.editor { - internal class MergeArmatureHook + internal class + MergeArmatureHook { private const float DuplicatedBoneMaxSqrDistance = 0.001f * 0.001f; @@ -149,7 +152,7 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var next in mergeArmatures) { - UnityEngine.Object.DestroyImmediate(next); + Object.DestroyImmediate(next); } void TopoLoop(ModularAvatarMergeArmature config) @@ -372,7 +375,9 @@ namespace nadena.dev.modular_avatar.core.editor GameObject childNewParent = mergedSrcBone; bool shouldZip = false; - if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix)) + if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix) + && childName.Length > config.prefix.Length + + config.suffix.Length) { var targetObjectName = childName.Substring(config.prefix.Length, childName.Length - config.prefix.Length - config.suffix.Length); diff --git a/Editor/RenameParametersHook.cs b/Editor/RenameParametersHook.cs index bc38d4d8..b1a57e5d 100644 --- a/Editor/RenameParametersHook.cs +++ b/Editor/RenameParametersHook.cs @@ -166,7 +166,7 @@ namespace nadena.dev.modular_avatar.core.editor .Select(p => ResolveParameter(p, syncParams)) .ToList(); - foreach (var kvp in syncParams) + foreach (var kvp in syncParams.OrderBy(kvp => kvp.Value.encounterOrder)) { var name = kvp.Key; var param = kvp.Value; diff --git a/Editor/ScaleAdjuster.meta b/Editor/ScaleAdjuster.meta new file mode 100644 index 00000000..b92ab1cb --- /dev/null +++ b/Editor/ScaleAdjuster.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 45b2b4957674444fa96bc0b6e221425e +timeCreated: 1709361970 \ No newline at end of file diff --git a/Editor/ScaleAdjuster/SelectionHack.cs b/Editor/ScaleAdjuster/SelectionHack.cs new file mode 100644 index 00000000..a5334f2e --- /dev/null +++ b/Editor/ScaleAdjuster/SelectionHack.cs @@ -0,0 +1,28 @@ +using UnityEditor; + +namespace nadena.dev.modular_avatar.core.editor.ScaleAdjuster +{ + #if !UNITY_2022_3_OR_NEWER + internal static class SelectionHack + { + [InitializeOnLoadMethod] + static void Init() + { + Selection.selectionChanged += OnSelectionChanged; + + } + + static void OnSelectionChanged() + { + var gameObject = Selection.activeGameObject; + if (gameObject != null && gameObject.GetComponent() != null) + { + EditorApplication.delayCall += () => + { + Selection.activeGameObject = gameObject.transform.parent.gameObject; + }; + } + } + } + #endif +} \ No newline at end of file diff --git a/Editor/ScaleAdjuster/SelectionHack.cs.meta b/Editor/ScaleAdjuster/SelectionHack.cs.meta new file mode 100644 index 00000000..737bed39 --- /dev/null +++ b/Editor/ScaleAdjuster/SelectionHack.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfa3ba0c82bc4439aa86228715f61831 +timeCreated: 1709376243 \ No newline at end of file diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs index 7ffbd0a0..5b1fa799 100644 --- a/Editor/ScaleAdjusterPass.cs +++ b/Editor/ScaleAdjusterPass.cs @@ -10,8 +10,6 @@ namespace nadena.dev.modular_avatar.core.editor { protected override void Execute(ndmf.BuildContext context) { - ScaleAdjusterRenderer.ClearAllOverrides(); - Dictionary boneMappings = new Dictionary(); foreach (var component in context.AvatarRootObject.GetComponentsInChildren()) { diff --git a/Runtime/ArmatureAwase/ArmatureLock.cs b/Runtime/ArmatureAwase/ArmatureLock.cs deleted file mode 100644 index daed42cb..00000000 --- a/Runtime/ArmatureAwase/ArmatureLock.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace nadena.dev.modular_avatar.core.armature_lock -{ - internal abstract class ArmatureLock : IDisposable - { - private bool _enableAssemblyReloadCallback; - - protected bool EnableAssemblyReloadCallback - { - get => _enableAssemblyReloadCallback; - set - { - if (_enableAssemblyReloadCallback == value) return; - _enableAssemblyReloadCallback = value; -#if UNITY_EDITOR - if (value) - { - UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload; - } - else - { - UnityEditor.AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload; - } -#endif - } - } - - public abstract void Prepare(); - public abstract LockResult Execute(); - public abstract bool IsStable(); - public abstract void Dispose(); - - private void OnDomainUnload() - { - // Unity 2019 does not call deferred callbacks before domain unload completes, - // so we need to make sure to immediately destroy all our TransformAccessArrays. - DeferDestroy.DestroyImmediate(this); - } - } -} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLock.cs.meta b/Runtime/ArmatureAwase/ArmatureLock.cs.meta deleted file mode 100644 index 6aa2aaef..00000000 --- a/Runtime/ArmatureAwase/ArmatureLock.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 7b4b88c94c2144128ffbe7f271b28ba2 -timeCreated: 1693712261 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockController.cs b/Runtime/ArmatureAwase/ArmatureLockController.cs index 877bb5c3..fc2b74bf 100644 --- a/Runtime/ArmatureAwase/ArmatureLockController.cs +++ b/Runtime/ArmatureAwase/ArmatureLockController.cs @@ -1,4 +1,6 @@ -using System; +#region + +using System; using System.Collections.Generic; using nadena.dev.modular_avatar.ui; using UnityEngine; @@ -6,12 +8,14 @@ using UnityEngine; using UnityEditor; #endif +#endregion + namespace nadena.dev.modular_avatar.core.armature_lock { internal class ArmatureLockConfig #if UNITY_EDITOR - : UnityEditor.ScriptableSingleton + : ScriptableSingleton #endif { #if !UNITY_EDITOR @@ -19,6 +23,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock #endif [SerializeField] private bool _globalEnable = true; + internal event Action OnGlobalEnableChange; internal bool GlobalEnable { @@ -34,11 +39,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock _globalEnable = value; - if (!value) - { - // Run prepare one last time to dispose of lock structures - UpdateLoopController.InvokeArmatureLockPrepare(); - } + OnGlobalEnableChange?.Invoke(); } } @@ -60,65 +61,54 @@ namespace nadena.dev.modular_avatar.core.armature_lock #endif } + internal class ArmatureLockController : IDisposable { + public delegate IReadOnlyList<(Transform, Transform)> GetTransformsDelegate(); + private static long lastMovedFrame = 0; - public static bool MovedThisFrame => Time.frameCount == lastMovedFrame; // Undo operations can reinitialize the MAMA component, which destroys critical lock controller state. // Avoid this issue by keeping a static reference to the controller for each MAMA component. private static Dictionary _controllers = new Dictionary(); - public delegate IReadOnlyList<(Transform, Transform)> GetTransformsDelegate(); + private readonly GetTransformsDelegate _getTransforms; private readonly ModularAvatarMergeArmature _mama; - private readonly GetTransformsDelegate _getTransforms; - private ArmatureLock _lock; - - private bool GlobalEnable => ArmatureLockConfig.instance.GlobalEnable; - private bool _updateActive; - - private bool UpdateActive - { - get => _updateActive; - set - { - if (UpdateActive == value) return; -#if UNITY_EDITOR - if (value) - { - UpdateLoopController.OnArmatureLockPrepare += UpdateLoopPrepare; - UpdateLoopController.OnArmatureLockUpdate += UpdateLoopFinish; - } - else - { - UpdateLoopController.OnArmatureLockPrepare -= UpdateLoopPrepare; - UpdateLoopController.OnArmatureLockUpdate -= UpdateLoopFinish; - } - - _updateActive = value; -#endif - } - } private ArmatureLockMode _curMode, _mode; + private bool _enabled; + private ArmatureLockJob _job; + + public ArmatureLockController(ModularAvatarMergeArmature mama, GetTransformsDelegate getTransforms) + { +#if UNITY_EDITOR + AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload; +#endif + + _mama = mama; + _getTransforms = getTransforms; + } + + public static bool MovedThisFrame => Time.frameCount == lastMovedFrame; + + private bool GlobalEnable => ArmatureLockConfig.instance.GlobalEnable; + public ArmatureLockMode Mode { get => _mode; set { - if (value == _mode) return; + if (value == _mode && _job != null) return; _mode = value; - UpdateActive = true; + RebuildLock(); } } - private bool _enabled; - public bool Enabled { get => _enabled; @@ -127,20 +117,25 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (Enabled == value) return; _enabled = value; - if (_enabled) UpdateActive = true; + + RebuildLock(); } } - public ArmatureLockController(ModularAvatarMergeArmature mama, GetTransformsDelegate getTransforms) + public void Dispose() { + _job?.Dispose(); + _job = null; + #if UNITY_EDITOR - AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload; + AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload; #endif - this._mama = mama; - this._getTransforms = getTransforms; + _controllers.Remove(_mama); } + internal event Action WhenUnstable; + public static ArmatureLockController ForMerge(ModularAvatarMergeArmature mama, GetTransformsDelegate getTransforms) { @@ -153,102 +148,32 @@ namespace nadena.dev.modular_avatar.core.armature_lock return controller; } - public bool IsStable() + internal void CheckLockJob() { - if (Mode == ArmatureLockMode.NotLocked) return true; - - if (_curMode == _mode && _lock?.IsStable() == true) return true; - return RebuildLock() && (_lock?.IsStable() ?? false); - } - - private void VoidPrepare() - { - UpdateLoopPrepare(); - } - - private void UpdateLoopFinish() - { - DoFinish(); - } - - internal bool Update() - { - UpdateLoopPrepare(); - return DoFinish(); - } - - private bool IsPrepared = false; - - private void UpdateLoopPrepare() - { - if (_mama == null || !_mama.gameObject.scene.IsValid()) + if (_mama == null || !_mama.gameObject.scene.IsValid() || !Enabled) { - UpdateActive = false; + _job?.Dispose(); return; } - if (!Enabled) + if (_curMode != _mode || _job == null || !_job.IsValid) { - UpdateActive = false; - _lock?.Dispose(); - _lock = null; - return; - } - - if (!GlobalEnable) - { - _lock?.Dispose(); - _lock = null; - return; - } - - if (_curMode == _mode) - { - _lock?.Prepare(); - IsPrepared = _lock != null; - } - } - - private bool DoFinish() - { - LockResult result; - - if (!GlobalEnable) - { - _lock?.Dispose(); - _lock = null; - return true; - } - - var wasPrepared = IsPrepared; - IsPrepared = false; - - if (!Enabled) return true; - - if (_curMode == _mode) - { - if (!wasPrepared) _lock?.Prepare(); - result = _lock?.Execute() ?? LockResult.Failed; - if (result == LockResult.Success) + if (_job != null && _job.FailedOnStartup) { - lastMovedFrame = Time.frameCount; + WhenUnstable?.Invoke(); + Enabled = false; + _job?.Dispose(); + return; } - if (result != LockResult.Failed) return true; + RebuildLock(); } - - if (!RebuildLock()) return false; - - _lock?.Prepare(); - result = (_lock?.Execute() ?? LockResult.Failed); - - return result != LockResult.Failed; } private bool RebuildLock() { - _lock?.Dispose(); - _lock = null; + _job?.Dispose(); + _job = null; var xforms = _getTransforms(); if (xforms == null) @@ -261,40 +186,34 @@ namespace nadena.dev.modular_avatar.core.armature_lock switch (Mode) { case ArmatureLockMode.BidirectionalExact: - _lock = new BidirectionalArmatureLock(_getTransforms()); + _job = BidirectionalArmatureLockOperator.Instance.RegisterLock(xforms); break; case ArmatureLockMode.BaseToMerge: - _lock = new OnewayArmatureLock(_getTransforms()); + _job = OnewayArmatureLockOperator.Instance.RegisterLock(xforms); break; default: - UpdateActive = false; + Enabled = false; break; } } catch (Exception) { - _lock = null; + _job = null; return false; } + if (_job != null) + { +#if UNITY_EDITOR + _job.OnInvalidation += () => { EditorApplication.delayCall += CheckLockJob; }; +#endif + } + _curMode = _mode; return true; } - public void Dispose() - { - _lock?.Dispose(); - _lock = null; - - #if UNITY_EDITOR - AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload; - #endif - - _controllers.Remove(_mama); - UpdateActive = false; - } - private void OnDomainUnload() { // Unity 2019 does not call deferred callbacks before domain unload completes, diff --git a/Runtime/ArmatureAwase/ArmatureLockJob.cs b/Runtime/ArmatureAwase/ArmatureLockJob.cs new file mode 100644 index 00000000..cedcbad1 --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockJob.cs @@ -0,0 +1,100 @@ +#region + +using System; +using System.Collections.Immutable; +using System.Linq; +using UnityEditor; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + internal sealed class ArmatureLockJob : IDisposable + { + private bool _didLoop = false; + + private Action _dispose; + + private bool _isValid = true; + private long _lastHierarchyCheck = -1; + private Action _update; + + internal ImmutableList<(Transform, Transform)> RecordedParents; + internal ImmutableList<(Transform, Transform)> Transforms; + + internal ArmatureLockJob(ImmutableList<(Transform, Transform)> transforms, Action dispose, Action update) + { + Transforms = transforms; + RecordedParents = transforms.Select(((tuple, _) => (tuple.Item1.parent, tuple.Item2.parent))) + .ToImmutableList(); + _dispose = dispose; + _update = update; + } + + internal bool FailedOnStartup => !_isValid && !_didLoop; + + internal bool HierarchyChanged + { + get + { + var unchanged = RecordedParents.Zip(Transforms, + (p, t) => + { + return t.Item1 != null && t.Item2 != null && t.Item1.parent == p.Item1 && + t.Item2.parent == p.Item2; + }).All(b => b); + + return !unchanged; + } + } + + internal bool IsValid + { + get => _isValid; + set + { + var transitioned = (_isValid && !value); + _isValid = value; + + if (transitioned) + { + Debug.Log("Invalidated job!"); +#if UNITY_EDITOR + EditorApplication.delayCall += () => OnInvalidation?.Invoke(); +#endif + } + } + } + + internal bool WroteAny { get; set; } + + public void Dispose() + { + _dispose?.Invoke(); + _dispose = null; + _update = null; + } + + internal event Action OnInvalidation; + + internal void MarkLoop() + { + _didLoop = _didLoop || _isValid; + } + + internal bool BoneChanged(int boneIndex) + { + return Transforms[boneIndex].Item1 == null || Transforms[boneIndex].Item2 == null + || Transforms[boneIndex].Item1.parent != + RecordedParents[boneIndex].Item1 + || Transforms[boneIndex].Item2.parent != + RecordedParents[boneIndex].Item2; + } + + public void UpdateNow() + { + _update?.Invoke(); + } + } +} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockJob.cs.meta b/Runtime/ArmatureAwase/ArmatureLockJob.cs.meta new file mode 100644 index 00000000..6577aff8 --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockJob.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 38a41bc7e55d4f7c8efeafd6107487da +timeCreated: 1709207112 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs new file mode 100644 index 00000000..df282bdd --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs @@ -0,0 +1,96 @@ +#region + +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Serialization; + +#endregion + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + /// + /// Abstractly, an armature lock job works by taking the local transforms of the base armature and target armature, + /// deciding whether to abort updates, and if not, what the transforms should be set to, and writing out the + /// results. + /// + /// This struct handles these common inputs and outputs for different armature lock types. + /// + internal struct ArmatureLockJobAccessor + { + internal void Allocate(int nBones, int nWords) + { + _in_baseBone = new NativeArray(nBones, Allocator.Persistent); + _in_targetBone = new NativeArray(nBones, Allocator.Persistent); + _out_baseBone = new NativeArray(nBones, Allocator.Persistent); + _out_targetBone = new NativeArray(nBones, Allocator.Persistent); + _out_dirty_baseBone = new NativeArray(nBones, Allocator.Persistent); + _out_dirty_targetBone = new NativeArray(nBones, Allocator.Persistent); + _boneToJobIndex = new NativeArray(nBones, Allocator.Persistent); + _abortFlag = new NativeArray(nWords, Allocator.Persistent); + _didAnyWriteFlag = new NativeArray(nWords, Allocator.Persistent); + } + + internal void Destroy() + { + if (_in_baseBone.IsCreated) _in_baseBone.Dispose(); + _in_baseBone = default; + if (_in_targetBone.IsCreated) _in_targetBone.Dispose(); + _in_targetBone = default; + if (_out_baseBone.IsCreated) _out_baseBone.Dispose(); + _out_baseBone = default; + if (_out_targetBone.IsCreated) _out_targetBone.Dispose(); + _out_targetBone = default; + if (_out_dirty_baseBone.IsCreated) _out_dirty_baseBone.Dispose(); + _out_dirty_baseBone = default; + if (_out_dirty_targetBone.IsCreated) _out_dirty_targetBone.Dispose(); + _out_dirty_targetBone = default; + if (_boneToJobIndex.IsCreated) _boneToJobIndex.Dispose(); + _boneToJobIndex = default; + if (_abortFlag.IsCreated) _abortFlag.Dispose(); + _abortFlag = default; + if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); + _didAnyWriteFlag = default; + } + + /// + /// Initial transform states + /// + public NativeArray _in_baseBone, _in_targetBone; + + /// + /// Transform states to write out (if _out_dirty is set) + /// + public NativeArray _out_baseBone, _out_targetBone; + + /// + /// Flags indicating whether the given bone should be written back to its transform + /// + public NativeArray _out_dirty_baseBone, _out_dirty_targetBone; + + /// + /// Indexed by the job index (via _boneToJobIndex). If set to a nonzero value, none of the bones in this + /// particular job (e.g. a single MergeArmature component) will be committed. + /// + /// Note: This array is written simultaneously from multiple threads. Jobs may set this to 1, but otherwise + /// shouldn't read this value. + /// + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] + public NativeArray _abortFlag; + + /// + /// Indexed by the job index (via _boneToJobIndex). Should be set to a nonzero value when any bone in the job + /// has changes that need to be written out. + /// + /// Note: This array is written simultaneously from multiple threads. Jobs may set this to 1, but otherwise + /// shouldn't read this value. + /// + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] + public NativeArray _didAnyWriteFlag; + + /// + /// Maps from bone index to job index. + /// + [FormerlySerializedAs("_statusWordIndex")] + public NativeArray _boneToJobIndex; + } +} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs.meta b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs.meta new file mode 100644 index 00000000..87b4ea87 --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73bef51200bd478c9e22761598e22d16 +timeCreated: 1709116462 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockOperator.cs b/Runtime/ArmatureAwase/ArmatureLockOperator.cs new file mode 100644 index 00000000..ab41d13b --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockOperator.cs @@ -0,0 +1,445 @@ +#region + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using UnityEditor; +using UnityEngine; +using UnityEngine.Jobs; +using UnityEngine.Profiling; + +#endregion + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + internal abstract class ArmatureLockOperator : IDisposable where T : ArmatureLockOperator, new() + { + internal static readonly T Instance = new T(); + + private static long LastHierarchyChange = 0; + private ArmatureLockJobAccessor _accessor; + + private TransformAccessArray _baseBones, _targetBones; + + private int _commitFilter; + + private bool _isDisposed = false; + private bool _isInit = false, _isValid = false; + + private ImmutableList _jobs = ImmutableList.Empty; + private JobHandle _lastJob; + private List _requestedJobs = new List(); + private long LastCheckedHierarchy = -1; + + static ArmatureLockOperator() + { + Instance = new T(); +#if UNITY_EDITOR + EditorApplication.delayCall += StaticInit; +#endif + } + + protected ArmatureLockOperator() + { +#if UNITY_EDITOR + AssemblyReloadEvents.beforeAssemblyReload += () => DeferDestroy.DestroyImmediate(this); +#endif + } + + protected abstract bool WritesBaseBones { get; } + + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + + if (!_isInit) return; + + _lastJob.Complete(); + DeferDestroy.DeferDestroyObj(_baseBones); + DeferDestroy.DeferDestroyObj(_targetBones); + DerivedDispose(); + _accessor.Destroy(); + } + +#if UNITY_EDITOR + protected static void StaticInit() + { + EditorApplication.hierarchyChanged += () => { LastHierarchyChange += 1; }; + UpdateLoopController.UpdateCallbacks += Instance.Update; + ArmatureLockConfig.instance.OnGlobalEnableChange += Instance.Invalidate; + + EditorApplication.playModeStateChanged += (change) => + { + // If we allow ourselves to simply enter play mode without a final update, any movement applied by + // automatically leaving animation preview mode won't be applied, leaving any outfits in the wrong pose. + if (change == PlayModeStateChange.ExitingEditMode) + { + Instance.Update(); + } + }; + } +#endif + + /// + /// Initialize the lock operator with a particular list of transforms. + /// + /// + protected abstract void Reinit(List<(Transform, Transform)> transforms, List problems); + + /// + /// Computes the new positions and status words for a given range of bones. + /// + /// + /// + /// + /// + protected abstract JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency); + + public ArmatureLockJob RegisterLock(IEnumerable<(Transform, Transform)> transforms) + { + ArmatureLockJob job = null; + job = new ArmatureLockJob( + transforms.ToImmutableList(), + () => RemoveJob(job), + () => UpdateSingle(job) + ); + + _requestedJobs.Add(job); + Invalidate(); + + return job; + } + + private void Invalidate() + { + _isValid = false; + } + + private void MaybeRevalidate() + { + if (!_isValid) + { + // Do an update to make sure all the old jobs are in sync first, before we reset our state. + if (_isInit) SingleUpdate(null); + Reset(); + } + } + + private void Reset() + { + if (_isDisposed) return; + + _lastJob.Complete(); + + if (_isInit) + { + _accessor.Destroy(); + _baseBones.Dispose(); + _targetBones.Dispose(); + } + + _isInit = true; + + // TODO: toposort? + int[] boneToJobIndex = null; + + List problems = new List(); + do + { + var failed = problems.Select(p => _jobs[boneToJobIndex[p]]).Distinct().ToList(); + foreach (var job in failed) + { + job.IsValid = false; + _requestedJobs.Remove(job); + } + + problems.Clear(); + + _jobs = _requestedJobs.ToImmutableList(); + + _accessor.Destroy(); + if (_baseBones.isCreated) _baseBones.Dispose(); + if (_targetBones.isCreated) _targetBones.Dispose(); + + _baseBones = _targetBones = default; + + var bones = _jobs.SelectMany(j => j.Transforms).ToList(); + boneToJobIndex = _jobs.SelectMany((i, j) => Enumerable.Repeat(j, i.Transforms.Count)).ToArray(); + + var baseBones = bones.Select(t => t.Item1).ToArray(); + var targetBones = bones.Select(t => t.Item2).ToArray(); + + _accessor.Allocate( + bones.Count, + _jobs.Count + ); + + _baseBones = new TransformAccessArray(baseBones); + _targetBones = new TransformAccessArray(targetBones); + + Reinit(_jobs.SelectMany(j => j.Transforms).ToList(), problems); + } while (problems.Count > 0); + + _isValid = true; + } + + public void Update() + { + InternalUpdate(); + } + + private void UpdateSingle(ArmatureLockJob job) + { + var index = _jobs.IndexOf(job); + if (index < 0) return; + + InternalUpdate(index); + } + + private void InternalUpdate(int? jobIndex = null) + { + if (_isDisposed) return; + + MaybeRevalidate(); + + SingleUpdate(jobIndex); + } + + private long CycleStartHierarchyIndex = -1; + private int _nextCheckIndex = 0; + + private void SingleUpdate(int? jobIndex) + { + if (!_isInit || _jobs.Count == 0) return; + + Profiler.BeginSample("InternalUpdate"); + _lastJob.Complete(); + + for (int i = 0; i < _jobs.Count; i++) + { + _accessor._abortFlag[i] = 0; + _accessor._didAnyWriteFlag[i] = 0; + } + + _lastJob = ReadTransforms(jobIndex); + _lastJob = Compute(_accessor, jobIndex, _lastJob); + + if (LastCheckedHierarchy != LastHierarchyChange) + { + Profiler.BeginSample("Recheck"); + + int startCheckIndex = _nextCheckIndex; + do + { + if (_nextCheckIndex == 0) + { + CycleStartHierarchyIndex = LastHierarchyChange; + } + + var job = _jobs[_nextCheckIndex % _jobs.Count]; + _nextCheckIndex = (1 + _nextCheckIndex) % _jobs.Count; + + if (job.HierarchyChanged) + { + job.IsValid = false; + Invalidate(); + } + } while (_nextCheckIndex != startCheckIndex && !_lastJob.IsCompleted); + + if (_nextCheckIndex == 0) + { + LastCheckedHierarchy = CycleStartHierarchyIndex; + } + + Profiler.EndSample(); + } + + // Before committing, do a spot check of any bones that moved, to see if their parents changed. + // This is needed because the hierarchyChanged event fires after Update ... + + _lastJob.Complete(); + Profiler.BeginSample("Revalidate dirty bones"); + int boneBase = 0; + bool anyDirty = false; + for (int job = 0; job < _jobs.Count; job++) + { + int curBoneBase = boneBase; + boneBase += _jobs[job].Transforms.Count; + if (_accessor._didAnyWriteFlag[job] == 0) continue; + + for (int b = curBoneBase; b < boneBase; b++) + { + if (_accessor._out_dirty_targetBone[b] != 0 || _accessor._out_dirty_baseBone[b] != 0) + { + anyDirty = true; + + if (_jobs[job].BoneChanged(b - curBoneBase)) + { + _accessor._abortFlag[job] = 1; + _jobs[job].IsValid = false; + break; + } + } + } + } + + Profiler.EndSample(); + + if (anyDirty) + { + _lastJob = CommitTransforms(jobIndex, _lastJob); + _lastJob.Complete(); + } + + for (int i = 0; i < _jobs.Count; i++) + { + if (_accessor._abortFlag[i] != 0) + { + Invalidate(); + } + else + { + _jobs[i].MarkLoop(); + } + + _jobs[i].WroteAny = _accessor._didAnyWriteFlag[i] != 0; + } + + if (!_isValid) + { + Reset(); + } + + Profiler.EndSample(); + } + + private void RemoveJob(ArmatureLockJob job) + { + if (_requestedJobs.Remove(job)) Invalidate(); + } + + protected abstract void DerivedDispose(); + + #region Job logic + + [BurstCompile] + struct ReadTransformsJob : IJobParallelForTransform + { + public NativeArray _bone; + public NativeArray _bone2; + + [ReadOnly] public NativeArray _boneToJobIndex; + + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] + public NativeArray _abortFlag; + + [BurstCompile] + public void Execute(int index, TransformAccess transform) + { +#if UNITY_2021_1_OR_NEWER + if (!transform.isValid) + { + _abortFlag[_boneToJobIndex[index]] = 1; + return; + } +#endif + + _bone[index] = _bone2[index] = new TransformState + { + localPosition = transform.localPosition, + localRotation = transform.localRotation, + localScale = transform.localScale + }; + } + } + + JobHandle ReadTransforms(int? jobIndex) + { + var baseRead = new ReadTransformsJob() + { + _bone = _accessor._in_baseBone, + _bone2 = _accessor._out_baseBone, + _boneToJobIndex = _accessor._boneToJobIndex, + _abortFlag = _accessor._abortFlag + }.ScheduleReadOnly(_baseBones, 32); + + var targetRead = new ReadTransformsJob() + { + _bone = _accessor._in_targetBone, + _bone2 = _accessor._out_targetBone, + _boneToJobIndex = _accessor._boneToJobIndex, + _abortFlag = _accessor._abortFlag + }.ScheduleReadOnly(_targetBones, 32, baseRead); + + return JobHandle.CombineDependencies(baseRead, targetRead); + } + + [BurstCompile] + struct CommitTransformsJob : IJobParallelForTransform + { + [ReadOnly] public NativeArray _boneState; + [ReadOnly] public NativeArray _dirtyBoneFlag; + [ReadOnly] public NativeArray _boneToJobIndex; + + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [ReadOnly] + public NativeArray _abortFlag; + + public int jobIndexFilter; + + [BurstCompile] + public void Execute(int index, TransformAccess transform) + { +#if UNITY_2021_1_OR_NEWER + if (!transform.isValid) return; +#endif + + var jobIndex = _boneToJobIndex[index]; + if (jobIndexFilter >= 0 && jobIndex != jobIndexFilter) return; + if (_abortFlag[jobIndex] != 0) return; + if (_dirtyBoneFlag[index] == 0) return; + + transform.localPosition = _boneState[index].localPosition; + transform.localRotation = _boneState[index].localRotation; + transform.localScale = _boneState[index].localScale; + } + } + + JobHandle CommitTransforms(int? jobIndex, JobHandle prior) + { + JobHandle job = new CommitTransformsJob() + { + _boneState = _accessor._out_targetBone, + _dirtyBoneFlag = _accessor._out_dirty_targetBone, + _boneToJobIndex = _accessor._boneToJobIndex, + _abortFlag = _accessor._abortFlag, + jobIndexFilter = jobIndex ?? -1 + }.Schedule(_targetBones, prior); + + if (WritesBaseBones) + { + var job2 = new CommitTransformsJob() + { + _boneState = _accessor._out_baseBone, + _dirtyBoneFlag = _accessor._out_dirty_baseBone, + _boneToJobIndex = _accessor._boneToJobIndex, + _abortFlag = _accessor._abortFlag, + jobIndexFilter = jobIndex ?? -1 + }.Schedule(_baseBones, prior); + + return JobHandle.CombineDependencies(job, job2); + } + else + { + return job; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockOperator.cs.meta b/Runtime/ArmatureAwase/ArmatureLockOperator.cs.meta new file mode 100644 index 00000000..2a23d87d --- /dev/null +++ b/Runtime/ArmatureAwase/ArmatureLockOperator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ad2138add6244aa19ae24ccc42389efb +timeCreated: 1709207125 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs b/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs index 68e07533..cf6462db 100644 --- a/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs +++ b/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs @@ -1,233 +1,107 @@ #region -using System; using System.Collections.Generic; -using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; using Unity.Burst; using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEngine; -using UnityEngine.Jobs; #endregion namespace nadena.dev.modular_avatar.core.armature_lock { - internal class BidirectionalArmatureLock : ArmatureLock, IDisposable + internal class BidirectionalArmatureLockOperator : ArmatureLockOperator { - private bool _disposed; - private TransformAccessArray _baseBoneAccess, _mergeBoneAccess; - private readonly Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones; + private NativeArray SavedState; + protected override bool WritesBaseBones => true; - private NativeArray BaseBones, MergeBones, SavedMerge; - private NativeArray ShouldWriteBase, ShouldWriteMerge; - private NativeIntPtr WroteAny; - - private JobHandle LastOp; - private JobHandle LastPrepare; - - public BidirectionalArmatureLock(IReadOnlyList<(Transform, Transform)> bones) + protected override void Reinit(List<(Transform, Transform)> transforms, List problems) { - _baseBones = new Transform[bones.Count]; - _mergeBones = new Transform[bones.Count]; - _baseParentBones = new Transform[bones.Count]; - _mergeParentBones = new Transform[bones.Count]; + if (SavedState.IsCreated) SavedState.Dispose(); - BaseBones = new NativeArray(_baseBones.Length, Allocator.Persistent); - MergeBones = new NativeArray(_baseBones.Length, Allocator.Persistent); - SavedMerge = new NativeArray(_baseBones.Length, Allocator.Persistent); + SavedState = new NativeArray(transforms.Count, Allocator.Persistent); - for (int i = 0; i < _baseBones.Length; i++) + for (int i = 0; i < transforms.Count; i++) { - var (mergeBone, baseBone) = bones[i]; - _baseBones[i] = baseBone; - _mergeBones[i] = mergeBone; - _baseParentBones[i] = baseBone.parent; - _mergeParentBones[i] = mergeBone.parent; + var (baseBone, mergeBone) = transforms[i]; + SavedState[i] = TransformState.FromTransform(mergeBone); - var mergeState = TransformState.FromTransform(mergeBone); - SavedMerge[i] = mergeState; - MergeBones[i] = mergeState; - BaseBones[i] = TransformState.FromTransform(baseBone); + if (TransformState.Differs(TransformState.FromTransform(baseBone), SavedState[i])) + { + problems.Add(i); + } } + } - _baseBoneAccess = new TransformAccessArray(_baseBones); - _mergeBoneAccess = new TransformAccessArray(_mergeBones); + protected override JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency) + { + return new ComputeOperator() + { + base_in = accessor._in_baseBone, + merge_in = accessor._in_targetBone, + base_out = accessor._out_baseBone, + merge_out = accessor._out_targetBone, - ShouldWriteBase = new NativeArray(_baseBones.Length, Allocator.Persistent); - ShouldWriteMerge = new NativeArray(_baseBones.Length, Allocator.Persistent); - WroteAny = new NativeIntPtr(Allocator.Persistent); + SavedState = SavedState, + baseDirty = accessor._out_dirty_baseBone, + mergeDirty = accessor._out_dirty_targetBone, + boneToJobIndex = accessor._boneToJobIndex, + wroteAny = accessor._didAnyWriteFlag, + + singleJobIndex = jobIndex ?? -1 + }.Schedule(accessor._in_baseBone.Length, 16, dependency); + } + + protected override void DerivedDispose() + { + SavedState.Dispose(); } [BurstCompile] - struct Compute : IJobParallelForTransform + private struct ComputeOperator : IJobParallelFor { - public NativeArray BaseBones, SavedMerge; + public int singleJobIndex; - [WriteOnly] public NativeArray MergeBones; + public NativeArray base_in, merge_in, base_out, merge_out; - [WriteOnly] public NativeArray ShouldWriteBase, ShouldWriteMerge; + public NativeArray SavedState; - [WriteOnly] public NativeIntPtr.Parallel WroteAny; + [WriteOnly] public NativeArray baseDirty, mergeDirty; + [ReadOnly] public NativeArray boneToJobIndex; - public void Execute(int index, TransformAccess mergeTransform) + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [WriteOnly] + public NativeArray wroteAny; + + [BurstCompile] + public void Execute(int index) { - var baseBone = BaseBones[index]; - var mergeBone = new TransformState() - { - localPosition = mergeTransform.localPosition, - localRotation = mergeTransform.localRotation, - localScale = mergeTransform.localScale, - }; - MergeBones[index] = mergeBone; + var jobIndex = boneToJobIndex[index]; - var saved = SavedMerge[index]; + if (singleJobIndex != -1 && jobIndex != singleJobIndex) return; + + var baseBone = base_in[index]; + var mergeBone = merge_in[index]; + var saved = SavedState[index]; if (TransformState.Differs(saved, mergeBone)) { - ShouldWriteBase[index] = true; - ShouldWriteMerge[index] = false; + baseDirty[index] = 1; + mergeDirty[index] = 0; - var mergeToBase = mergeBone; - BaseBones[index] = mergeToBase; - SavedMerge[index] = mergeBone; - WroteAny.SetOne(); + SavedState[index] = base_out[index] = merge_in[index]; + + wroteAny[jobIndex] = 1; } else if (TransformState.Differs(saved, baseBone)) { - ShouldWriteMerge[index] = true; - ShouldWriteBase[index] = false; + mergeDirty[index] = 1; + baseDirty[index] = 0; - MergeBones[index] = baseBone; - SavedMerge[index] = baseBone; - WroteAny.SetOne(); + SavedState[index] = merge_out[index] = base_in[index]; + + wroteAny[jobIndex] = 1; } - else - { - ShouldWriteBase[index] = false; - ShouldWriteMerge[index] = false; - } - } - } - - [BurstCompile] - struct Commit : IJobParallelForTransform - { - [ReadOnly] public NativeArray BoneState; - [ReadOnly] public NativeArray ShouldWrite; - - public void Execute(int index, TransformAccess transform) - { - if (ShouldWrite[index]) - { - var boneState = BoneState[index]; - - transform.localPosition = boneState.localPosition; - transform.localRotation = boneState.localRotation; - transform.localScale = boneState.localScale; - } - } - } - - public override void Dispose() - { - if (_disposed) return; - - LastOp.Complete(); - - // work around crashes caused by destroying TransformAccessArray from within Undo processing - DeferDestroy.DeferDestroyObj(_baseBoneAccess); - DeferDestroy.DeferDestroyObj(_mergeBoneAccess); - BaseBones.Dispose(); - MergeBones.Dispose(); - SavedMerge.Dispose(); - ShouldWriteBase.Dispose(); - ShouldWriteMerge.Dispose(); - WroteAny.Dispose(); - - _disposed = true; - } - - public override void Prepare() - { - if (_disposed) return; - - LastOp.Complete(); - - WroteAny.Value = 0; - - var readBase = new ReadBone() - { - _state = BaseBones, - }.Schedule(_baseBoneAccess); - - LastOp = LastPrepare = new Compute() - { - BaseBones = BaseBones, - MergeBones = MergeBones, - SavedMerge = SavedMerge, - ShouldWriteBase = ShouldWriteBase, - ShouldWriteMerge = ShouldWriteMerge, - WroteAny = WroteAny.GetParallel(), - }.Schedule(_mergeBoneAccess, readBase); - } - - private bool CheckConsistency() - { - if (_disposed) return false; - - // Check parents haven't changed - for (int i = 0; i < _baseBones.Length; i++) - { - if (_baseBones[i] == null || _mergeBones[i] == null || _baseParentBones[i] == null || - _mergeParentBones[i] == null) - { - return false; - } - - if (_baseBones[i].parent != _baseParentBones[i] || _mergeBones[i].parent != _mergeParentBones[i]) - { - return false; - } - } - - return true; - } - - public override bool IsStable() - { - Prepare(); - if (!CheckConsistency()) return false; - LastPrepare.Complete(); - - return WroteAny.Value == 0; - } - - public override LockResult Execute() - { - if (!CheckConsistency()) return LockResult.Failed; - - var commitBase = new Commit() - { - BoneState = BaseBones, - ShouldWrite = ShouldWriteBase, - }.Schedule(_baseBoneAccess, LastPrepare); - var commitMerge = new Commit() - { - BoneState = MergeBones, - ShouldWrite = ShouldWriteMerge, - }.Schedule(_mergeBoneAccess, LastPrepare); - - commitBase.Complete(); - commitMerge.Complete(); - - if (WroteAny.Value == 0) - { - return LockResult.NoOp; - } - else - { - return LockResult.Success; } } } diff --git a/Runtime/ArmatureAwase/DeferDestroy.cs b/Runtime/ArmatureAwase/DeferDestroy.cs index 853c5cfe..2fa738cd 100644 --- a/Runtime/ArmatureAwase/DeferDestroy.cs +++ b/Runtime/ArmatureAwase/DeferDestroy.cs @@ -1,7 +1,7 @@ #region using System; -using UnityEngine; +using UnityEditor; #endregion @@ -33,7 +33,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock return; } #if UNITY_EDITOR - UnityEditor.EditorApplication.delayCall += () => obj.Dispose(); + EditorApplication.delayCall += () => obj.Dispose(); #endif } } diff --git a/Runtime/ArmatureAwase/LockResult.cs b/Runtime/ArmatureAwase/LockResult.cs deleted file mode 100644 index cc6d0246..00000000 --- a/Runtime/ArmatureAwase/LockResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace nadena.dev.modular_avatar.core.armature_lock -{ - internal enum LockResult - { - Failed, - Success, - NoOp, - } -} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/LockResult.cs.meta b/Runtime/ArmatureAwase/LockResult.cs.meta deleted file mode 100644 index 1df01b91..00000000 --- a/Runtime/ArmatureAwase/LockResult.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 361faa0a05e34f7b8fbd1b2ae73d27bf -timeCreated: 1693713933 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/OnewayArmatureLock.cs b/Runtime/ArmatureAwase/OnewayArmatureLock.cs index 63fbc9a4..0e279ff3 100644 --- a/Runtime/ArmatureAwase/OnewayArmatureLock.cs +++ b/Runtime/ArmatureAwase/OnewayArmatureLock.cs @@ -1,58 +1,132 @@ #region -using System; using System.Collections.Generic; -using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; using Unity.Burst; using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEngine; -using UnityEngine.Jobs; #if UNITY_EDITOR -using UnityEditor; #endif #endregion namespace nadena.dev.modular_avatar.core.armature_lock { - internal class OnewayArmatureLock : ArmatureLock, IDisposable + internal class OnewayArmatureLockOperator : ArmatureLockOperator { + private Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones; + private NativeArray _boneStaticData; + public NativeArray _mergeSavedState; + + private List<(Transform, Transform)> _transforms; + protected override bool WritesBaseBones => false; + + protected override void Reinit(List<(Transform, Transform)> transforms, List problems) + { + if (_boneStaticData.IsCreated) _boneStaticData.Dispose(); + if (_mergeSavedState.IsCreated) _mergeSavedState.Dispose(); + + _transforms = transforms; + + _boneStaticData = new NativeArray(transforms.Count, Allocator.Persistent); + + _baseBones = new Transform[_transforms.Count]; + _mergeBones = new Transform[_transforms.Count]; + _baseParentBones = new Transform[_transforms.Count]; + _mergeParentBones = new Transform[_transforms.Count]; + _mergeSavedState = new NativeArray(_transforms.Count, Allocator.Persistent); + + for (int i = 0; i < transforms.Count; i++) + { + var (baseBone, mergeBone) = transforms[i]; + var mergeParent = mergeBone.parent; + var baseParent = baseBone.parent; + + if (mergeParent == null || baseParent == null) + { + problems.Add(i); + continue; + } + + if (SmallScale(mergeParent.localScale) || SmallScale(mergeBone.localScale) || + SmallScale(baseBone.localScale)) + { + problems.Add(i); + continue; + } + + _baseBones[i] = baseBone; + _mergeBones[i] = mergeBone; + _baseParentBones[i] = baseParent; + _mergeParentBones[i] = mergeParent; + + _mergeSavedState[i] = TransformState.FromTransform(mergeBone); + + // We want to emulate the hierarchy: + // baseParent + // - baseBone + // - v_mergeBone + // + // However our hierarchy actually is: + // mergeParent + // - mergeBone + // + // Our question is: What is the local affine transform of mergeBone -> mergeParent space, given a new + // baseBone -> baseParent affine transform? + + // First, relative to baseBone, what is the local affine transform of mergeBone? + var mat_l = baseBone.worldToLocalMatrix * mergeBone.localToWorldMatrix; + // We also find parent -> mergeParent + var mat_r = mergeParent.worldToLocalMatrix * baseParent.localToWorldMatrix; + // Now we can multiply: + // (baseParent -> mergeParent) * (baseBone -> baseParent) * (mergeBone -> baseBone) + // = (baseParent -> mergeParent) * (mergeBone -> baseParent) + // = (mergeBone -> mergeParent) + + _boneStaticData[i] = new BoneStaticData() + { + _mat_l = mat_r, + _mat_r = mat_l + }; + } + } + + private bool SmallScale(Vector3 scale) + { + var epsilon = 0.000001f; + + return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon); + } + + protected override JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency) + { + return new ComputePosition() + { + _baseState = accessor._in_baseBone, + _mergeState = accessor._in_targetBone, + _mergeSavedState = _mergeSavedState, + _boneStatic = _boneStaticData, + _fault = accessor._abortFlag, + _wroteAny = accessor._didAnyWriteFlag, + _wroteBone = accessor._out_dirty_targetBone, + jobIndexLimit = jobIndex ?? -1, + _boneToJobIndex = accessor._boneToJobIndex, + _outputState = accessor._out_targetBone, + }.Schedule(accessor._in_baseBone.Length, 32, dependency); + } + + protected override void DerivedDispose() + { + if (_boneStaticData.IsCreated) _boneStaticData.Dispose(); + if (_mergeSavedState.IsCreated) _mergeSavedState.Dispose(); + } + struct BoneStaticData { public Matrix4x4 _mat_l, _mat_r; } - private NativeArray _boneStaticData; - private NativeArray _mergeSavedState; - private NativeArray _baseState, _mergeState; - - private NativeIntPtr _fault, _wroteAny; - - private readonly Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones; - private TransformAccessArray _baseBonesAccessor, _mergeBonesAccessor; - - private bool _disposed; - private JobHandle LastOp, LastPrepare; - - [BurstCompile] - struct WriteBone : IJobParallelForTransform - { - [ReadOnly] public NativeIntPtr _fault, _shouldWrite; - - [ReadOnly] public NativeArray _values; - - public void Execute(int index, TransformAccess transform) - { - if (_fault.Value == 0 && _shouldWrite.Value != 0) - { - var val = _values[index]; - transform.localPosition = val.localPosition; - transform.localRotation = val.localRotation; - transform.localScale = val.localScale; - } - } - } [BurstCompile] struct ComputePosition : IJobParallelFor @@ -63,11 +137,23 @@ namespace nadena.dev.modular_avatar.core.armature_lock [ReadOnly] public NativeArray _baseState; public NativeArray _mergeSavedState; + public NativeArray _outputState; + public NativeArray _wroteBone; - public NativeIntPtr.Parallel _fault, _wroteAny; + public int jobIndexLimit; + + [ReadOnly] public NativeArray _boneToJobIndex; + + // job indexed + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] + public NativeArray _fault, _wroteAny; public void Execute(int index) { + var jobIndex = _boneToJobIndex[index]; + + if (jobIndexLimit >= 0 && jobIndex >= jobIndexLimit) return; + var boneStatic = _boneStatic[index]; var mergeState = _mergeState[index]; var baseState = _baseState[index]; @@ -79,8 +165,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (TransformState.Differs(mergeSaved, mergeState)) { - TransformState.Differs(mergeSaved, mergeState); - _fault.Increment(); + _fault[jobIndex] = 1; } var relTransform = boneStatic._mat_l * Matrix4x4.TRS(basePos, baseRot, baseScale) * boneStatic._mat_r; @@ -98,219 +183,12 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (TransformState.Differs(mergeSaved, newState)) { - _wroteAny.SetOne(); + _wroteAny[jobIndex] = 1; + _wroteBone[index] = 1; _mergeSavedState[index] = newState; + _outputState[index] = newState; } } } - - public OnewayArmatureLock(IReadOnlyList<(Transform, Transform)> mergeToBase) - { - _boneStaticData = new NativeArray(mergeToBase.Count, Allocator.Persistent); - _mergeSavedState = new NativeArray(mergeToBase.Count, Allocator.Persistent); - _baseState = new NativeArray(mergeToBase.Count, Allocator.Persistent); - _mergeState = new NativeArray(mergeToBase.Count, Allocator.Persistent); - - _fault = new NativeIntPtr(Allocator.Persistent); - _wroteAny = new NativeIntPtr(Allocator.Persistent); - - _baseBones = new Transform[mergeToBase.Count]; - _mergeBones = new Transform[mergeToBase.Count]; - _baseParentBones = new Transform[mergeToBase.Count]; - _mergeParentBones = new Transform[mergeToBase.Count]; - - try - { - for (int i = 0; i < mergeToBase.Count; i++) - { - var (mergeBone, baseBone) = mergeToBase[i]; - var mergeParent = mergeBone.parent; - var baseParent = baseBone.parent; - - if (mergeParent == null || baseParent == null) - { - throw new Exception("Can't handle root objects"); - } - - if (SmallScale(mergeParent.localScale) || SmallScale(mergeBone.localScale) || - SmallScale(baseBone.localScale)) - { - throw new Exception("Can't handle near-zero scale bones"); - } - - _baseBones[i] = baseBone; - _mergeBones[i] = mergeBone; - _baseParentBones[i] = baseParent; - _mergeParentBones[i] = mergeParent; - - _baseState[i] = TransformState.FromTransform(baseBone); - _mergeSavedState[i] = _mergeState[i] = TransformState.FromTransform(mergeBone); - - // We want to emulate the hierarchy: - // baseParent - // - baseBone - // - v_mergeBone - // - // However our hierarchy actually is: - // mergeParent - // - mergeBone - // - // Our question is: What is the local affine transform of mergeBone -> mergeParent space, given a new - // baseBone -> baseParent affine transform? - - // First, relative to baseBone, what is the local affine transform of mergeBone? - var mat_l = baseBone.worldToLocalMatrix * mergeBone.localToWorldMatrix; - // We also find parent -> mergeParent - var mat_r = mergeParent.worldToLocalMatrix * baseParent.localToWorldMatrix; - // Now we can multiply: - // (baseParent -> mergeParent) * (baseBone -> baseParent) * (mergeBone -> baseBone) - // = (baseParent -> mergeParent) * (mergeBone -> baseParent) - // = (mergeBone -> mergeParent) - - _boneStaticData[i] = new BoneStaticData() - { - _mat_l = mat_r, - _mat_r = mat_l - }; - } - } - catch (Exception e) - { - _boneStaticData.Dispose(); - _mergeSavedState.Dispose(); - _baseState.Dispose(); - _mergeState.Dispose(); - _fault.Dispose(); - _wroteAny.Dispose(); - - throw e; - } - - _baseBonesAccessor = new TransformAccessArray(_baseBones); - _mergeBonesAccessor = new TransformAccessArray(_mergeBones); - - EnableAssemblyReloadCallback = true; - } - - private bool SmallScale(Vector3 scale) - { - var epsilon = 0.000001f; - - return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon); - } - - public override void Prepare() - { - if (_disposed) return; - - LastOp.Complete(); - - _baseBonesAccessor.SetTransforms(_baseBones); - _mergeBonesAccessor.SetTransforms(_mergeBones); - - _fault.Value = 0; - _wroteAny.Value = 0; - - var jobReadBase = new ReadBone - { - _state = _baseState - }.Schedule(_baseBonesAccessor); - var jobReadMerged = new ReadBone - { - _state = _mergeState - }.Schedule(_mergeBonesAccessor); - var readAll = JobHandle.CombineDependencies(jobReadBase, jobReadMerged); - LastOp = LastPrepare = new ComputePosition - { - _boneStatic = _boneStaticData, - _mergeState = _mergeState, - _baseState = _baseState, - _mergeSavedState = _mergeSavedState, - _fault = _fault.GetParallel(), - _wroteAny = _wroteAny.GetParallel(), - }.Schedule(_baseBones.Length, 32, readAll); - } - - private bool CheckConsistency() - { - if (_disposed) return false; - - // Validate parents while that job is running - for (int i = 0; i < _baseBones.Length; i++) - { - if (_baseBones[i] == null || _mergeBones[i] == null || _baseParentBones[i] == null || - _mergeParentBones[i] == null) - { - return false; - } - - if (_baseBones[i].parent != _baseParentBones[i] || _mergeBones[i].parent != _mergeParentBones[i]) - { - return false; - } - } - - return true; - } - - public override bool IsStable() - { - Prepare(); - if (!CheckConsistency()) return false; - - LastPrepare.Complete(); - - return _fault.Value == 0 && _wroteAny.Value == 0; - } - - /// - /// Executes the armature lock job. - /// - /// True if successful, false if cached data was invalidated and needs recreating - public override LockResult Execute() - { - if (!CheckConsistency()) return LockResult.Failed; - - var commit = new WriteBone() - { - _fault = _fault, - _values = _mergeSavedState, - _shouldWrite = _wroteAny - }.Schedule(_mergeBonesAccessor, LastPrepare); - - commit.Complete(); - - if (_fault.Value != 0) - { - return LockResult.Failed; - } - else if (_wroteAny.Value == 0) - { - return LockResult.NoOp; - } - else - { - return LockResult.Success; - } - } - - public override void Dispose() - { - if (_disposed) return; - - LastOp.Complete(); - _boneStaticData.Dispose(); - _mergeSavedState.Dispose(); - _baseState.Dispose(); - _mergeState.Dispose(); - _fault.Dispose(); - _wroteAny.Dispose(); - // work around crashes caused by destroying TransformAccessArray from within Undo processing - DeferDestroy.DeferDestroyObj(_baseBonesAccessor); - DeferDestroy.DeferDestroyObj(_mergeBonesAccessor); - _disposed = true; - - EnableAssemblyReloadCallback = false; - } } } \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ReadBone.cs b/Runtime/ArmatureAwase/ReadBone.cs deleted file mode 100644 index 719404e5..00000000 --- a/Runtime/ArmatureAwase/ReadBone.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Unity.Burst; -using Unity.Collections; -using UnityEngine.Jobs; - -namespace nadena.dev.modular_avatar.core.armature_lock -{ - [BurstCompile] - internal struct ReadBone : IJobParallelForTransform - { - public NativeArray _state; - - public void Execute(int index, TransformAccess transform) - { - _state[index] = new TransformState - { - localPosition = transform.localPosition, - localRotation = transform.localRotation, - localScale = transform.localScale - }; - } - } -} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ReadBone.cs.meta b/Runtime/ArmatureAwase/ReadBone.cs.meta deleted file mode 100644 index e653c21e..00000000 --- a/Runtime/ArmatureAwase/ReadBone.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: df590c12d16249608a9d8a8204b154bf -timeCreated: 1693712551 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/TransformState.cs b/Runtime/ArmatureAwase/TransformState.cs index 1cc928ed..0f340f18 100644 --- a/Runtime/ArmatureAwase/TransformState.cs +++ b/Runtime/ArmatureAwase/TransformState.cs @@ -1,7 +1,12 @@ -using System.Runtime.CompilerServices; +#region + +using System.Runtime.CompilerServices; using Unity.Burst; +using UnityEditor; using UnityEngine; +#endregion + namespace nadena.dev.modular_avatar.core.armature_lock { internal struct TransformState @@ -14,7 +19,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock public Quaternion localRotation; public Vector3 localScale; - public static TransformState FromTransform(Transform mergeBone) + internal static TransformState FromTransform(Transform mergeBone) { return new TransformState { @@ -24,10 +29,10 @@ namespace nadena.dev.modular_avatar.core.armature_lock }; } - public void ToTransform(Transform bone) + internal void ToTransform(Transform bone) { #if UNITY_EDITOR - UnityEditor.Undo.RecordObject(bone, UnityEditor.Undo.GetCurrentGroupName()); + Undo.RecordObject(bone, Undo.GetCurrentGroupName()); #endif bone.localPosition = localPosition; bone.localRotation = localRotation; @@ -36,7 +41,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock [BurstCompile] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Differs(TransformState self, TransformState other) + internal static bool Differs(TransformState self, TransformState other) { var deltaMergePos = (self.localPosition - other.localPosition).sqrMagnitude; var deltaMergeRot = self.localRotation * Quaternion.Inverse(other.localRotation); diff --git a/Runtime/ArmatureAwase/Unity2019Compat.cs b/Runtime/ArmatureAwase/Unity2019Compat.cs new file mode 100644 index 00000000..fdb9afbc --- /dev/null +++ b/Runtime/ArmatureAwase/Unity2019Compat.cs @@ -0,0 +1,20 @@ +#if !UNITY_2021_1_OR_NEWER + +using Unity.Jobs; +using UnityEngine.Jobs; + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + internal static class Unity2019Compat + { + internal static JobHandle ScheduleReadOnly(this T task, TransformAccessArray transforms, int batchCount, + JobHandle dependsOn = default) + where T : struct, IJobParallelForTransform + { + return task.Schedule(transforms, dependsOn); + } + } +} + + +#endif \ No newline at end of file diff --git a/Runtime/ArmatureAwase/Unity2019Compat.cs.meta b/Runtime/ArmatureAwase/Unity2019Compat.cs.meta new file mode 100644 index 00000000..8c102fc0 --- /dev/null +++ b/Runtime/ArmatureAwase/Unity2019Compat.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dda11d07446e441d8313a99f53903d99 +timeCreated: 1709287006 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/UpdateLoopController.cs b/Runtime/ArmatureAwase/UpdateLoopController.cs index fc98f167..77df55cd 100644 --- a/Runtime/ArmatureAwase/UpdateLoopController.cs +++ b/Runtime/ArmatureAwase/UpdateLoopController.cs @@ -1,33 +1,37 @@ -using System; +#region + +using System; +using System.Collections.Generic; +using Unity.Jobs; +using UnityEditor; + +#endregion namespace nadena.dev.modular_avatar.core.armature_lock { internal static class UpdateLoopController { - internal static event Action OnArmatureLockPrepare; - internal static event Action OnArmatureLockUpdate; + internal static event Action UpdateCallbacks; internal static event Action OnMoveIndependentlyUpdate; #if UNITY_EDITOR - [UnityEditor.InitializeOnLoadMethod] + [InitializeOnLoadMethod] private static void Init() { - UnityEditor.EditorApplication.update += () => - { - if (ArmatureLockConfig.instance.GlobalEnable) - { - OnArmatureLockPrepare?.Invoke(); - OnArmatureLockUpdate?.Invoke(); - } + EditorApplication.update += Update; + } - OnMoveIndependentlyUpdate?.Invoke(); - }; + private static List jobs = new List(); + + private static void Update() + { + if (ArmatureLockConfig.instance.GlobalEnable) + { + UpdateCallbacks?.Invoke(); + } + + OnMoveIndependentlyUpdate?.Invoke(); } #endif - - internal static void InvokeArmatureLockPrepare() - { - OnArmatureLockPrepare?.Invoke(); - } } } \ No newline at end of file diff --git a/Runtime/ModularAvatarMergeArmature.cs b/Runtime/ModularAvatarMergeArmature.cs index 746c8408..489130f3 100644 --- a/Runtime/ModularAvatarMergeArmature.cs +++ b/Runtime/ModularAvatarMergeArmature.cs @@ -22,13 +22,16 @@ * SOFTWARE. */ +#region + using System; using System.Collections.Generic; using nadena.dev.modular_avatar.core.armature_lock; using UnityEngine; -using UnityEngine.Analytics; using UnityEngine.Serialization; +#endregion + namespace nadena.dev.modular_avatar.core { [Serializable] @@ -72,7 +75,8 @@ namespace nadena.dev.modular_avatar.core var pointer = mergeTarget.Get(this).transform; foreach (var segment in segments) { - if (!segment.StartsWith(prefix) || !segment.EndsWith(suffix)) return null; + if (!segment.StartsWith(prefix) || !segment.EndsWith(suffix) + || segment.Length == prefix.Length + suffix.Length) return null; var targetObjectName = segment.Substring(prefix.Length, segment.Length - prefix.Length - suffix.Length); pointer = pointer.Find(targetObjectName); @@ -85,7 +89,8 @@ namespace nadena.dev.modular_avatar.core { var childName = bone.gameObject.name; - if (!childName.StartsWith(prefix) || !childName.EndsWith(suffix)) return null; + if (!childName.StartsWith(prefix) || !childName.EndsWith(suffix) + || childName.Length == prefix.Length + suffix.Length) return null; var targetObjectName = childName.Substring(prefix.Length, childName.Length - prefix.Length - suffix.Length); return baseParent.Find(targetObjectName); @@ -109,28 +114,26 @@ namespace nadena.dev.modular_avatar.core SetLockMode(); } - private void SetLockMode() + internal void SetLockMode() { if (this == null) return; if (_lockController == null) { _lockController = ArmatureLockController.ForMerge(this, GetBonesForLock); + _lockController.WhenUnstable += OnUnstableLock; } - if (_lockController.Mode != LockMode) - { - _lockController.Mode = LockMode; - - if (!_lockController.IsStable()) - { - _lockController.Mode = LockMode = ArmatureLockMode.NotLocked; - } - } + _lockController.Mode = LockMode; _lockController.Enabled = enabled; } + private void OnUnstableLock() + { + _lockController.Mode = LockMode = ArmatureLockMode.NotLocked; + } + private void MigrateLockConfig() { if (LockMode == ArmatureLockMode.Legacy) @@ -190,7 +193,7 @@ namespace nadena.dev.modular_avatar.core var baseChild = FindCorrespondingBone(t, baseBone); if (baseChild != null) { - mergeBones.Add((t, baseChild)); + mergeBones.Add((baseChild, t)); ScanHierarchy(t, baseChild); } } diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs b/Runtime/ScaleAdjuster/CameraHooks.cs new file mode 100644 index 00000000..e9da8699 --- /dev/null +++ b/Runtime/ScaleAdjuster/CameraHooks.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; +using UnityEngine; + +namespace nadena.dev.modular_avatar.core +{ + internal static class CameraHooks + { + private static Dictionary originalToProxy + = new Dictionary( + new ObjectIdentityComparer() + ); + + #if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] + private static void Init() + { + Camera.onPreCull += OnPreCull; + Camera.onPostRender += OnPostRender; + UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearStates; + UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearStates(); + } + #endif + + internal static void RegisterProxy(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy) + { + originalToProxy[original] = proxy; + } + + internal static void UnregisterProxy(SkinnedMeshRenderer original) + { + originalToProxy.Remove(original); + } + + private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>(); + + private static List toDeregister = new List(); + + + private static void OnPreCull(Camera camera) + { + ClearStates(); + toDeregister.Clear(); + + foreach (var kvp in originalToProxy) + { + var original = kvp.Key; + var proxy = kvp.Value; + + if (original == null || proxy == null) + { + toDeregister.Add(original); + continue; + } + + proxy.enabled = original.enabled; + if (original.enabled && original.gameObject.activeInHierarchy) + { + statesToRestore.Add((original, original.enabled)); + original.enabled = false; + } + } + + foreach (var original in toDeregister) + { + originalToProxy.Remove(original); + } + } + + private static void OnPostRender(Camera camera) + { + ClearStates(); + } + + + private static void ClearStates() + { + foreach (var (original, state) in statesToRestore) + { + original.enabled = state; + } + + statesToRestore.Clear(); + } + } +} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs.meta b/Runtime/ScaleAdjuster/CameraHooks.cs.meta new file mode 100644 index 00000000..9ee0bfc6 --- /dev/null +++ b/Runtime/ScaleAdjuster/CameraHooks.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 117b3ad981cb487aa5029043f7482a94 +timeCreated: 1709447257 \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs index 9171ad21..235cb938 100644 --- a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -101,15 +101,17 @@ namespace nadena.dev.modular_avatar.core if (child == null) { var childObj = new GameObject(ADJUSTER_OBJECT); + Undo.RegisterCreatedObjectUndo(childObj, ""); var childSmr = childObj.AddComponent(); EditorUtility.CopySerialized(smr, childSmr); + childObj.transform.SetParent(smr.transform, false); + childObj.transform.localPosition = Vector3.zero; + childObj.transform.localRotation = Quaternion.identity; + childObj.transform.localScale = Vector3.one; + child = childObj.AddComponent(); - child.transform.SetParent(smr.transform, false); - child.transform.localPosition = Vector3.zero; - child.transform.localRotation = Quaternion.identity; - child.transform.localScale = Vector3.one; } child.BoneMappings[transform] = scaleProxy; diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs index 824cb627..f53655ac 100644 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs @@ -1,10 +1,14 @@ #region -using System; +#if UNITY_EDITOR +using UnityEditor; +#endif using System.Collections.Generic; using System.Linq; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; using UnityEngine; using VRC.SDKBase; +using Object = System.Object; #endregion @@ -15,16 +19,24 @@ namespace nadena.dev.modular_avatar.core [RequireComponent(typeof(SkinnedMeshRenderer))] internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly { - private static event Action OnClearAllOverrides; + internal static Dictionary originalParent = + new Dictionary(new ObjectIdentityComparer()); + + internal static Dictionary proxyObjects = new Dictionary( + new ObjectIdentityComparer()); + + internal static Dictionary originalObjects = + new Dictionary( + new ObjectIdentityComparer() + ); + private static int RecreateHierarchyIndexCount = 0; #if UNITY_EDITOR - [UnityEditor.InitializeOnLoadMethod] + [InitializeOnLoadMethod] static void Setup() { - UnityEditor.EditorApplication.hierarchyChanged += InvalidateAll; - UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearAllOverrides; - UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearAllOverrides(); + EditorApplication.hierarchyChanged += InvalidateAll; } #endif @@ -32,36 +44,51 @@ namespace nadena.dev.modular_avatar.core { RecreateHierarchyIndexCount++; } - + private SkinnedMeshRenderer myRenderer; private SkinnedMeshRenderer parentRenderer; private bool wasActive = false; private bool redoBoneMappings = true; + private bool hasRelevantBones = false; private int lastRecreateHierarchyIndex = -1; - internal Dictionary BoneMappings = new Dictionary(); + internal Dictionary BoneMappings = new Dictionary( + new ObjectIdentityComparer() + ); + #if UNITY_EDITOR private void OnValidate() { - if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return; + if (PrefabUtility.IsPartOfPrefabAsset(this)) return; redoBoneMappings = true; - UnityEditor.EditorApplication.delayCall += () => + EditorApplication.delayCall += () => { if (this == null) return; -#if MODULAR_AVATAR_DEBUG_HIDDEN - gameObject.hideFlags = HideFlags.None; -#else - gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSaveInBuild; -#endif + // We hide this in Harmony, not here, so it is eligible for click-to-select. + gameObject.hideFlags = HideFlags.DontSaveInBuild; + if (BoneMappings == null) { BoneMappings = new Dictionary(); } + + Configure(); }; + + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + redoBoneMappings = true; + } + + private void OnPlayModeStateChanged(PlayModeStateChange change) + { + if (change == PlayModeStateChange.ExitingEditMode) + { + ClearHooks(); + } } #endif @@ -74,12 +101,83 @@ namespace nadena.dev.modular_avatar.core private void OnDestroy() { - ClearAllOverrides(); + ClearHooks(); + #if UNITY_EDITOR + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + #endif } + private void Configure() + { + if (originalParent.TryGetValue(this, out var prevParent) && transform.parent?.gameObject == prevParent) + { + return; + } + + if (prevParent != null) + { + ClearHooks(); + } + + if (!hasRelevantBones) + { + return; + } + + if (transform.parent == null) + { + return; + } + +#if UNITY_EDITOR + AssemblyReloadEvents.beforeAssemblyReload += ClearHooks; +#endif + + var parent = transform.parent.gameObject; + + proxyObjects[gameObject] = parent; + originalObjects[parent] = this; + originalParent[this] = parent; + + CheckBoneUsage(); // register the proxy if needed + } + + private void ClearHooks() + { + if (originalParent.TryGetValue(this, out var prevParent)) + { + if (parentRenderer != null) + { + CameraHooks.UnregisterProxy(parentRenderer); + } + + if ((Object)prevParent != null) + { + originalObjects.Remove(prevParent); + } + + originalParent.Remove(this); + if (gameObject != null) + { + proxyObjects.Remove(gameObject); + } + else + { + CleanDeadObjects(); + } + } + } + #if UNITY_EDITOR private void Update() { + if (EditorApplication.isPlayingOrWillChangePlaymode) return; + if (transform.parent == null) + { + DestroyImmediate(gameObject); + return; + } + if (myRenderer == null) { myRenderer = GetComponent(); @@ -90,27 +188,34 @@ namespace nadena.dev.modular_avatar.core parentRenderer = transform.parent.GetComponent(); } + Configure(); + myRenderer.sharedMaterials = parentRenderer.sharedMaterials; - myRenderer.sharedMesh = parentRenderer.sharedMesh; myRenderer.localBounds = parentRenderer.localBounds; - if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount) + if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount + || myRenderer.sharedMesh != parentRenderer.sharedMesh) { - var deadBones = BoneMappings.Keys.Where(k => BoneMappings[k] == null) - .ToList(); - deadBones.ForEach(k => { BoneMappings.Remove(k); }); + CleanDeadObjects(BoneMappings); if (BoneMappings.Count == 0) { + #if UNITY_2022_3_OR_NEWER DestroyImmediate(gameObject); return; + #endif } + myRenderer.sharedMesh = parentRenderer.sharedMesh; myRenderer.rootBone = MapBone(parentRenderer.rootBone); myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray(); redoBoneMappings = false; lastRecreateHierarchyIndex = RecreateHierarchyIndexCount; + + CheckBoneUsage(); } + if (!hasRelevantBones) return; + myRenderer.quality = parentRenderer.quality; myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode; myRenderer.receiveShadows = parentRenderer.receiveShadows; @@ -129,47 +234,38 @@ namespace nadena.dev.modular_avatar.core myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); } } - - ClearAllOverrides(); - - myRenderer.enabled = parentRenderer.enabled; - } - - public void OnWillRenderObject() - { - if (myRenderer == null || parentRenderer == null) - { - return; - } - - ClearAllOverrides(); - - if (!parentRenderer.enabled || !parentRenderer.gameObject.activeInHierarchy) - { - return; - } - - parentRenderer.enabled = false; - wasActive = true; - var objName = parentRenderer.gameObject.name; - OnClearAllOverrides += ClearLocalOverride; - // Sometimes - e.g. around domain reloads or undo operations - the parent renderer's enabled field might get - // re-disabled; re-enabler it in delayCall in this case. - UnityEditor.EditorApplication.delayCall += ClearLocalOverride; } #endif - - private void ClearLocalOverride() + private void CheckBoneUsage() { - if (parentRenderer != null) + hasRelevantBones = false; + if (myRenderer.sharedMesh != null) { - parentRenderer.enabled = true; - } - } + var weights = myRenderer.sharedMesh.GetAllBoneWeights(); + var parentBones = parentRenderer.bones; + foreach (var weight in weights) + { + if (weight.weight < 0.0001f) continue; + if (weight.boneIndex < 0 || weight.boneIndex >= parentBones.Length) continue; - private void OnPostRender() - { - ClearAllOverrides(); + var bone = parentBones[weight.boneIndex]; + if (BoneMappings.ContainsKey(bone)) + { + hasRelevantBones = true; + break; + } + } + } + + if (hasRelevantBones) + { + CameraHooks.RegisterProxy(parentRenderer, myRenderer); + } + else + { + CameraHooks.UnregisterProxy(parentRenderer); + myRenderer.enabled = false; + } } public void ClearBoneCache() @@ -177,10 +273,28 @@ namespace nadena.dev.modular_avatar.core redoBoneMappings = true; } - internal static void ClearAllOverrides() + private static void CleanDeadObjects() { - OnClearAllOverrides?.Invoke(); - OnClearAllOverrides = null; + CleanDeadObjects(originalParent); + CleanDeadObjects(originalObjects); + CleanDeadObjects(proxyObjects); + } + + private static int lastCleanedFrame = 0; + private static void CleanDeadObjects(IDictionary dict) + where K: UnityEngine.Object + where V: UnityEngine.Object + { + // Avoid any O(n^2) behavior if we have lots of cleanup calls happening at the same instant + if (Time.frameCount == lastCleanedFrame) return; + lastCleanedFrame = Time.frameCount; + + var dead = dict.Where(kvp => kvp.Key == null || kvp.Value == null).ToList(); + + foreach (var kvp in dead) + { + dict.Remove(kvp.Key); + } } } } \ No newline at end of file diff --git a/Runtime/Util/ObjectIdentityComparer.cs b/Runtime/Util/ObjectIdentityComparer.cs new file mode 100644 index 00000000..002f6395 --- /dev/null +++ b/Runtime/Util/ObjectIdentityComparer.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace nadena.dev.modular_avatar.JacksonDunstan.NativeCollections +{ + internal class ObjectIdentityComparer : IEqualityComparer + { + public bool Equals(T x, T y) + { + return (object)x == (object)y; + } + + public int GetHashCode(T obj) + { + if (obj == null) return 0; + return RuntimeHelpers.GetHashCode(obj); + } + } +} \ No newline at end of file diff --git a/Runtime/Util/ObjectIdentityComparer.cs.meta b/Runtime/Util/ObjectIdentityComparer.cs.meta new file mode 100644 index 00000000..a1ec9a3e --- /dev/null +++ b/Runtime/Util/ObjectIdentityComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e674cbd75db24fb2b238674cd7010edb +timeCreated: 1709448428 \ No newline at end of file diff --git a/Runtime/nadena.dev.modular-avatar.core.asmdef b/Runtime/nadena.dev.modular-avatar.core.asmdef index ac932af5..6e529bce 100644 --- a/Runtime/nadena.dev.modular-avatar.core.asmdef +++ b/Runtime/nadena.dev.modular-avatar.core.asmdef @@ -1,5 +1,6 @@ { "name": "nadena.dev.modular-avatar.core", + "rootNamespace": "", "references": [ "Unity.Burst", "nadena.dev.ndmf.runtime" @@ -13,7 +14,8 @@ "VRCSDK3A.dll", "VRC.Dynamics.dll", "VRC.SDK3.Dynamics.Contact.dll", - "VRC.SDK3.Dynamics.PhysBone.dll" + "VRC.SDK3.Dynamics.PhysBone.dll", + "System.Collections.Immutable.dll" ], "autoReferenced": true, "defineConstraints": [], diff --git a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab index 55b1bb32..46e1bb93 100644 --- a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab +++ b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab @@ -26,18 +26,19 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3825275463613500755} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0.023681391, y: 1.0559628, z: -0.6872994} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3646968714803193661} - {fileID: 3646968713996568948} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!95 &3825275463613500750 Animator: - serializedVersion: 3 + serializedVersion: 5 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -50,10 +51,12 @@ Animator: m_UpdateMode: 0 m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 - m_KeepAnimatorControllerStateOnDisable: 0 + m_KeepAnimatorStateOnDisable: 0 + m_WriteDefaultValuesOnDisable: 0 --- !u!114 &3825275463613500753 MonoBehaviour: m_ObjectHideFlags: 0 @@ -322,40 +325,12 @@ MonoBehaviour: contentType: 0 assetBundleUnityVersion: fallbackStatus: 0 ---- !u!114 &3825275463971368602 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 4167925416990528462} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 6fd7cab7d93b403280f2f9da978d8a4f, type: 3} - m_Name: - m_EditorClassIdentifier: - Bindings: - - ReferenceMesh: - referencePath: BaseMesh - Blendshape: shape_0 - LocalBlendshape: shape_0_local - - ReferenceMesh: - referencePath: BaseMesh - Blendshape: shape_1 - LocalBlendshape: shape_1 - - ReferenceMesh: - referencePath: MissingMesh - Blendshape: missing_mesh_shape - LocalBlendshape: missing_mesh_shape - - ReferenceMesh: - referencePath: - Blendshape: missing_mesh_shape_2 - LocalBlendshape: missing_mesh_shape_2 --- !u!1001 &3825275463173128406 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 3825275463613500751} m_Modifications: - target: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, @@ -454,6 +429,9 @@ PrefabInstance: value: BaseMesh objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} --- !u!4 &3646968714803193661 stripped Transform: @@ -466,6 +444,7 @@ PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 3825275463613500751} m_Modifications: - target: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, @@ -569,16 +548,52 @@ PrefabInstance: value: SyncedMesh objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, + type: 3} + insertIndex: -1 + addedObject: {fileID: 3825275463971368602} m_SourcePrefab: {fileID: 100100000, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} ---- !u!1 &4167925416990528462 stripped -GameObject: - m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, - type: 3} - m_PrefabInstance: {fileID: 3825275463971368607} - m_PrefabAsset: {fileID: 0} --- !u!4 &3646968713996568948 stripped Transform: m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 14ac2ad30c5d3444ca37f76cea5a7047, type: 3} m_PrefabInstance: {fileID: 3825275463971368607} m_PrefabAsset: {fileID: 0} +--- !u!1 &4167925416990528462 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 14ac2ad30c5d3444ca37f76cea5a7047, + type: 3} + m_PrefabInstance: {fileID: 3825275463971368607} + m_PrefabAsset: {fileID: 0} +--- !u!114 &3825275463971368602 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4167925416990528462} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6fd7cab7d93b403280f2f9da978d8a4f, type: 3} + m_Name: + m_EditorClassIdentifier: + Bindings: + - ReferenceMesh: + referencePath: BaseMesh + Blendshape: shape_0 + LocalBlendshape: shape_0_local + - ReferenceMesh: + referencePath: BaseMesh + Blendshape: shape_1 + LocalBlendshape: shape_1 + - ReferenceMesh: + referencePath: MissingMesh + Blendshape: missing_mesh_shape + LocalBlendshape: missing_mesh_shape + - ReferenceMesh: + referencePath: + Blendshape: missing_mesh_shape_2 + LocalBlendshape: missing_mesh_shape_2 diff --git a/UnitTests~/RenameParametersTests/RenameParametersTests.cs b/UnitTests~/RenameParametersTests/RenameParametersTests.cs index 8a0f6005..d72db7ae 100644 --- a/UnitTests~/RenameParametersTests/RenameParametersTests.cs +++ b/UnitTests~/RenameParametersTests/RenameParametersTests.cs @@ -6,7 +6,6 @@ using System.Collections.Immutable; using System.Linq; using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor; -using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; using NUnit.Framework; using UnityEditor.Animations; @@ -249,6 +248,48 @@ namespace modular_avatar_tests.RenameParametersTests Assert.IsNotEmpty(errors); } + + [Test] + public void ParameterOrderTest() + { + var av = CreateRoot("avatar"); + + var rootMenu = ScriptableObject.CreateInstance(); + var paramsAsset = ScriptableObject.CreateInstance(); + + var desc = av.GetComponent(); + desc.expressionsMenu = rootMenu; + desc.expressionParameters = paramsAsset; + + var c1 = CreateChild(av, "a"); + var c2 = CreateChild(av, "b"); + var c3 = CreateChild(av, "c"); + var c4 = CreateChild(av, "d"); + + AddParam(c1, "A"); + AddParam(c2, "B"); + AddParam(c3, "C"); + AddParam(c4, "D"); + + AvatarProcessor.ProcessAvatar(av); + + paramsAsset = desc.expressionParameters; + + Assert.AreEqual("A", paramsAsset.parameters[0].name); + Assert.AreEqual("B", paramsAsset.parameters[1].name); + Assert.AreEqual("C", paramsAsset.parameters[2].name); + Assert.AreEqual("D", paramsAsset.parameters[3].name); + + void AddParam(GameObject child, String name) + { + var param = child.AddComponent(); + param.parameters.Add(new ParameterConfig() + { + nameOrPrefix = name, + syncType = ParameterSyncType.Float + }); + } + } } } diff --git a/package.json b/package.json index b82cbb79..52cd87c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nadena.dev.modular-avatar", "displayName": "Modular Avatar", - "version": "1.9.4", + "version": "1.9.5-rc.0", "unity": "2019.4", "description": "A suite of tools for assembling your avatar out of reusable components", "author": {