mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-16 19:25:01 +08:00
Merge branch 'bdunderscore:main' into main
This commit is contained in:
commit
3251dd6cf2
Editor
EasySetupOutfit.cs
HarmonyPatches
HandleUtilityPatches.csHandleUtilityPatches.cs.metaHideScaleAdjusterFromPrefabOverrideView.csHierarchyViewPatches.csHierarchyViewPatches.cs.metaPatchLoader.csPickingObjectPatch.csPickingObjectPatch.cs.metaSnoopHeaderRendering.csSnoopHeaderRendering.cs.meta
HeuristicBoneMapper.csLocalization
MergeArmatureHook.csRenameParametersHook.csScaleAdjuster.metaScaleAdjuster
ScaleAdjusterPass.csRuntime
ArmatureAwase
ArmatureLock.csArmatureLock.cs.metaArmatureLockController.csArmatureLockJob.csArmatureLockJob.cs.metaArmatureLockJobAccessor.csArmatureLockJobAccessor.cs.metaArmatureLockOperator.csArmatureLockOperator.cs.metaBidirectionalArmatureLock.csDeferDestroy.csLockResult.csLockResult.cs.metaOnewayArmatureLock.csReadBone.csReadBone.cs.metaTransformState.csUnity2019Compat.csUnity2019Compat.cs.metaUpdateLoopController.cs
ModularAvatarMergeArmature.csScaleAdjuster
Util
nadena.dev.modular-avatar.core.asmdefUnitTests~
BlendshapeSyncTests
RenameParametersTests
@ -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;
|
||||
}
|
||||
|
55
Editor/HarmonyPatches/HandleUtilityPatches.cs
Normal file
55
Editor/HarmonyPatches/HandleUtilityPatches.cs
Normal file
@ -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<GameObject> gameObjects,
|
||||
out int[] parentInstanceIDs,
|
||||
out int[] childInstanceIDs
|
||||
)
|
||||
{
|
||||
gameObjects = RemapObjects(gameObjects);
|
||||
parentInstanceIDs = childInstanceIDs = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> RemapObjects(IEnumerable<GameObject> 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();
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/HarmonyPatches/HandleUtilityPatches.cs.meta
Normal file
3
Editor/HarmonyPatches/HandleUtilityPatches.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 807736f252df4b1b8402827257dcbea3
|
||||
timeCreated: 1709354699
|
@ -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<ScaleAdjusterRenderer>()
|
||||
.Select(sar => sar.gameObject)
|
||||
.ToImmutableHashSet();
|
||||
List<AddedGameObject> added = p_AddedGameObjects.GetValue(__result) as List<AddedGameObject>;
|
||||
|
||||
if (added == null) return;
|
||||
added.RemoveAll(obj => ignoredObjects.Contains(obj.instanceGameObject));
|
||||
added.RemoveAll(obj => ScaleAdjusterRenderer.proxyObjects.ContainsKey(obj.instanceGameObject));
|
||||
|
||||
List<ObjectOverride> objectOverrides = p_ObjectOverrides.GetValue(__result) as List<ObjectOverride>;
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
175
Editor/HarmonyPatches/HierarchyViewPatches.cs
Normal file
175
Editor/HarmonyPatches/HierarchyViewPatches.cs
Normal file
@ -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<TreeViewItem>
|
||||
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<TreeViewItem>)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<CodeInstruction> Transpile_InitializeRows(IEnumerable<CodeInstruction> instructions,
|
||||
ILGenerator generator)
|
||||
{
|
||||
foreach (var c in Transpile_InitializeRows0(instructions, generator))
|
||||
{
|
||||
//Debug.Log(c);
|
||||
yield return c;
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private static IEnumerable<CodeInstruction> Transpile_InitializeRows0(IEnumerable<CodeInstruction> 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;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/HarmonyPatches/HierarchyViewPatches.cs.meta
Normal file
3
Editor/HarmonyPatches/HierarchyViewPatches.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42f70698a5df48c0908400c425a2f6ee
|
||||
timeCreated: 1709356304
|
@ -13,9 +13,12 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
||||
{
|
||||
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
|
||||
{
|
||||
SnoopHeaderRendering.Patch1,
|
||||
SnoopHeaderRendering.Patch2,
|
||||
HideScaleAdjusterFromPrefabOverrideView.Patch
|
||||
HideScaleAdjusterFromPrefabOverrideView.Patch,
|
||||
HierarchyViewPatches.Patch,
|
||||
#if UNITY_2022_3_OR_NEWER
|
||||
HandleUtilityPatches.Patch_FilterInstanceIDs,
|
||||
PickingObjectPatch.Patch,
|
||||
#endif
|
||||
};
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
|
78
Editor/HarmonyPatches/PickingObjectPatch.cs
Normal file
78
Editor/HarmonyPatches/PickingObjectPatch.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/HarmonyPatches/PickingObjectPatch.cs.meta
Normal file
3
Editor/HarmonyPatches/PickingObjectPatch.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf06818f1c0c436fbae7f755d7110aba
|
||||
timeCreated: 1709359553
|
@ -1,45 +0,0 @@
|
||||
#region
|
||||
|
||||
using HarmonyLib;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cafb5b7e681644cbbeafbeb12d833f6e
|
||||
timeCreated: 1708235926
|
@ -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);
|
||||
|
@ -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.",
|
||||
|
@ -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はヒューマノイドアバター以外には対応していません。",
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
3
Editor/ScaleAdjuster.meta
Normal file
3
Editor/ScaleAdjuster.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45b2b4957674444fa96bc0b6e221425e
|
||||
timeCreated: 1709361970
|
28
Editor/ScaleAdjuster/SelectionHack.cs
Normal file
28
Editor/ScaleAdjuster/SelectionHack.cs
Normal file
@ -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<ScaleAdjusterRenderer>() != null)
|
||||
{
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
Selection.activeGameObject = gameObject.transform.parent.gameObject;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
3
Editor/ScaleAdjuster/SelectionHack.cs.meta
Normal file
3
Editor/ScaleAdjuster/SelectionHack.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfa3ba0c82bc4439aa86228715f61831
|
||||
timeCreated: 1709376243
|
@ -10,8 +10,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
protected override void Execute(ndmf.BuildContext context)
|
||||
{
|
||||
ScaleAdjusterRenderer.ClearAllOverrides();
|
||||
|
||||
Dictionary<Transform, Transform> boneMappings = new Dictionary<Transform, Transform>();
|
||||
foreach (var component in context.AvatarRootObject.GetComponentsInChildren<ScaleProxy>())
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b4b88c94c2144128ffbe7f271b28ba2
|
||||
timeCreated: 1693712261
|
@ -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<ArmatureLockConfig>
|
||||
: ScriptableSingleton<ArmatureLockConfig>
|
||||
#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<ModularAvatarMergeArmature, ArmatureLockController>
|
||||
_controllers = new Dictionary<ModularAvatarMergeArmature, ArmatureLockController>();
|
||||
|
||||
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,
|
||||
|
100
Runtime/ArmatureAwase/ArmatureLockJob.cs
Normal file
100
Runtime/ArmatureAwase/ArmatureLockJob.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
3
Runtime/ArmatureAwase/ArmatureLockJob.cs.meta
Normal file
3
Runtime/ArmatureAwase/ArmatureLockJob.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38a41bc7e55d4f7c8efeafd6107487da
|
||||
timeCreated: 1709207112
|
96
Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs
Normal file
96
Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal struct ArmatureLockJobAccessor
|
||||
{
|
||||
internal void Allocate(int nBones, int nWords)
|
||||
{
|
||||
_in_baseBone = new NativeArray<TransformState>(nBones, Allocator.Persistent);
|
||||
_in_targetBone = new NativeArray<TransformState>(nBones, Allocator.Persistent);
|
||||
_out_baseBone = new NativeArray<TransformState>(nBones, Allocator.Persistent);
|
||||
_out_targetBone = new NativeArray<TransformState>(nBones, Allocator.Persistent);
|
||||
_out_dirty_baseBone = new NativeArray<int>(nBones, Allocator.Persistent);
|
||||
_out_dirty_targetBone = new NativeArray<int>(nBones, Allocator.Persistent);
|
||||
_boneToJobIndex = new NativeArray<int>(nBones, Allocator.Persistent);
|
||||
_abortFlag = new NativeArray<int>(nWords, Allocator.Persistent);
|
||||
_didAnyWriteFlag = new NativeArray<int>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initial transform states
|
||||
/// </summary>
|
||||
public NativeArray<TransformState> _in_baseBone, _in_targetBone;
|
||||
|
||||
/// <summary>
|
||||
/// Transform states to write out (if _out_dirty is set)
|
||||
/// </summary>
|
||||
public NativeArray<TransformState> _out_baseBone, _out_targetBone;
|
||||
|
||||
/// <summary>
|
||||
/// Flags indicating whether the given bone should be written back to its transform
|
||||
/// </summary>
|
||||
public NativeArray<int> _out_dirty_baseBone, _out_dirty_targetBone;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction]
|
||||
public NativeArray<int> _abortFlag;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction]
|
||||
public NativeArray<int> _didAnyWriteFlag;
|
||||
|
||||
/// <summary>
|
||||
/// Maps from bone index to job index.
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("_statusWordIndex")]
|
||||
public NativeArray<int> _boneToJobIndex;
|
||||
}
|
||||
}
|
3
Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs.meta
Normal file
3
Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73bef51200bd478c9e22761598e22d16
|
||||
timeCreated: 1709116462
|
445
Runtime/ArmatureAwase/ArmatureLockOperator.cs
Normal file
445
Runtime/ArmatureAwase/ArmatureLockOperator.cs
Normal file
@ -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<T> : IDisposable where T : ArmatureLockOperator<T>, 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<ArmatureLockJob> _jobs = ImmutableList<ArmatureLockJob>.Empty;
|
||||
private JobHandle _lastJob;
|
||||
private List<ArmatureLockJob> _requestedJobs = new List<ArmatureLockJob>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the lock operator with a particular list of transforms.
|
||||
/// </summary>
|
||||
/// <param name="transforms"></param>
|
||||
protected abstract void Reinit(List<(Transform, Transform)> transforms, List<int> problems);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the new positions and status words for a given range of bones.
|
||||
/// </summary>
|
||||
/// <param name="accessor"></param>
|
||||
/// <param name="startBone"></param>
|
||||
/// <param name="endBone"></param>
|
||||
/// <returns></returns>
|
||||
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<int> problems = new List<int>();
|
||||
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<TransformState> _bone;
|
||||
public NativeArray<TransformState> _bone2;
|
||||
|
||||
[ReadOnly] public NativeArray<int> _boneToJobIndex;
|
||||
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction]
|
||||
public NativeArray<int> _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<TransformState> _boneState;
|
||||
[ReadOnly] public NativeArray<int> _dirtyBoneFlag;
|
||||
[ReadOnly] public NativeArray<int> _boneToJobIndex;
|
||||
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [ReadOnly]
|
||||
public NativeArray<int> _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
|
||||
}
|
||||
}
|
3
Runtime/ArmatureAwase/ArmatureLockOperator.cs.meta
Normal file
3
Runtime/ArmatureAwase/ArmatureLockOperator.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad2138add6244aa19ae24ccc42389efb
|
||||
timeCreated: 1709207125
|
@ -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<BidirectionalArmatureLockOperator>
|
||||
{
|
||||
private bool _disposed;
|
||||
private TransformAccessArray _baseBoneAccess, _mergeBoneAccess;
|
||||
private readonly Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones;
|
||||
private NativeArray<TransformState> SavedState;
|
||||
protected override bool WritesBaseBones => true;
|
||||
|
||||
private NativeArray<TransformState> BaseBones, MergeBones, SavedMerge;
|
||||
private NativeArray<bool> 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<int> 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<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
MergeBones = new NativeArray<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
SavedMerge = new NativeArray<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
SavedState = new NativeArray<TransformState>(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<bool>(_baseBones.Length, Allocator.Persistent);
|
||||
ShouldWriteMerge = new NativeArray<bool>(_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<TransformState> BaseBones, SavedMerge;
|
||||
public int singleJobIndex;
|
||||
|
||||
[WriteOnly] public NativeArray<TransformState> MergeBones;
|
||||
public NativeArray<TransformState> base_in, merge_in, base_out, merge_out;
|
||||
|
||||
[WriteOnly] public NativeArray<bool> ShouldWriteBase, ShouldWriteMerge;
|
||||
public NativeArray<TransformState> SavedState;
|
||||
|
||||
[WriteOnly] public NativeIntPtr.Parallel WroteAny;
|
||||
[WriteOnly] public NativeArray<int> baseDirty, mergeDirty;
|
||||
[ReadOnly] public NativeArray<int> boneToJobIndex;
|
||||
|
||||
public void Execute(int index, TransformAccess mergeTransform)
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [WriteOnly]
|
||||
public NativeArray<int> 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<TransformState> BoneState;
|
||||
[ReadOnly] public NativeArray<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal enum LockResult
|
||||
{
|
||||
Failed,
|
||||
Success,
|
||||
NoOp,
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 361faa0a05e34f7b8fbd1b2ae73d27bf
|
||||
timeCreated: 1693713933
|
@ -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<OnewayArmatureLockOperator>
|
||||
{
|
||||
private Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones;
|
||||
private NativeArray<BoneStaticData> _boneStaticData;
|
||||
public NativeArray<TransformState> _mergeSavedState;
|
||||
|
||||
private List<(Transform, Transform)> _transforms;
|
||||
protected override bool WritesBaseBones => false;
|
||||
|
||||
protected override void Reinit(List<(Transform, Transform)> transforms, List<int> problems)
|
||||
{
|
||||
if (_boneStaticData.IsCreated) _boneStaticData.Dispose();
|
||||
if (_mergeSavedState.IsCreated) _mergeSavedState.Dispose();
|
||||
|
||||
_transforms = transforms;
|
||||
|
||||
_boneStaticData = new NativeArray<BoneStaticData>(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<TransformState>(_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> _boneStaticData;
|
||||
private NativeArray<TransformState> _mergeSavedState;
|
||||
private NativeArray<TransformState> _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<TransformState> _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<TransformState> _baseState;
|
||||
|
||||
public NativeArray<TransformState> _mergeSavedState;
|
||||
public NativeArray<TransformState> _outputState;
|
||||
public NativeArray<int> _wroteBone;
|
||||
|
||||
public NativeIntPtr.Parallel _fault, _wroteAny;
|
||||
public int jobIndexLimit;
|
||||
|
||||
[ReadOnly] public NativeArray<int> _boneToJobIndex;
|
||||
|
||||
// job indexed
|
||||
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction]
|
||||
public NativeArray<int> _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<BoneStaticData>(mergeToBase.Count, Allocator.Persistent);
|
||||
_mergeSavedState = new NativeArray<TransformState>(mergeToBase.Count, Allocator.Persistent);
|
||||
_baseState = new NativeArray<TransformState>(mergeToBase.Count, Allocator.Persistent);
|
||||
_mergeState = new NativeArray<TransformState>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the armature lock job.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false if cached data was invalidated and needs recreating</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<TransformState> _state;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
_state[index] = new TransformState
|
||||
{
|
||||
localPosition = transform.localPosition,
|
||||
localRotation = transform.localRotation,
|
||||
localScale = transform.localScale
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df590c12d16249608a9d8a8204b154bf
|
||||
timeCreated: 1693712551
|
@ -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);
|
||||
|
20
Runtime/ArmatureAwase/Unity2019Compat.cs
Normal file
20
Runtime/ArmatureAwase/Unity2019Compat.cs
Normal file
@ -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<T>(this T task, TransformAccessArray transforms, int batchCount,
|
||||
JobHandle dependsOn = default)
|
||||
where T : struct, IJobParallelForTransform
|
||||
{
|
||||
return task.Schedule(transforms, dependsOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
3
Runtime/ArmatureAwase/Unity2019Compat.cs.meta
Normal file
3
Runtime/ArmatureAwase/Unity2019Compat.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dda11d07446e441d8313a99f53903d99
|
||||
timeCreated: 1709287006
|
@ -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<JobHandle> jobs = new List<JobHandle>();
|
||||
|
||||
private static void Update()
|
||||
{
|
||||
if (ArmatureLockConfig.instance.GlobalEnable)
|
||||
{
|
||||
UpdateCallbacks?.Invoke();
|
||||
}
|
||||
|
||||
OnMoveIndependentlyUpdate?.Invoke();
|
||||
}
|
||||
#endif
|
||||
|
||||
internal static void InvokeArmatureLockPrepare()
|
||||
{
|
||||
OnArmatureLockPrepare?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
86
Runtime/ScaleAdjuster/CameraHooks.cs
Normal file
86
Runtime/ScaleAdjuster/CameraHooks.cs
Normal file
@ -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<SkinnedMeshRenderer, SkinnedMeshRenderer> originalToProxy
|
||||
= new Dictionary<SkinnedMeshRenderer, SkinnedMeshRenderer>(
|
||||
new ObjectIdentityComparer<SkinnedMeshRenderer>()
|
||||
);
|
||||
|
||||
#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<SkinnedMeshRenderer> toDeregister = new List<SkinnedMeshRenderer>();
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
3
Runtime/ScaleAdjuster/CameraHooks.cs.meta
Normal file
3
Runtime/ScaleAdjuster/CameraHooks.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 117b3ad981cb487aa5029043f7482a94
|
||||
timeCreated: 1709447257
|
@ -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<SkinnedMeshRenderer>();
|
||||
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<ScaleAdjusterRenderer>();
|
||||
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;
|
||||
|
@ -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<ScaleAdjusterRenderer, GameObject> originalParent =
|
||||
new Dictionary<ScaleAdjusterRenderer, GameObject>(new ObjectIdentityComparer<ScaleAdjusterRenderer>());
|
||||
|
||||
internal static Dictionary<GameObject, GameObject> proxyObjects = new Dictionary<GameObject, GameObject>(
|
||||
new ObjectIdentityComparer<GameObject>());
|
||||
|
||||
internal static Dictionary<GameObject, ScaleAdjusterRenderer> originalObjects =
|
||||
new Dictionary<GameObject, ScaleAdjusterRenderer>(
|
||||
new ObjectIdentityComparer<GameObject>()
|
||||
);
|
||||
|
||||
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<Transform, Transform> BoneMappings = new Dictionary<Transform, Transform>();
|
||||
internal Dictionary<Transform, Transform> BoneMappings = new Dictionary<Transform, Transform>(
|
||||
new ObjectIdentityComparer<Transform>()
|
||||
);
|
||||
|
||||
|
||||
#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<Transform, Transform>();
|
||||
}
|
||||
|
||||
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<SkinnedMeshRenderer>();
|
||||
@ -90,27 +188,34 @@ namespace nadena.dev.modular_avatar.core
|
||||
parentRenderer = transform.parent.GetComponent<SkinnedMeshRenderer>();
|
||||
}
|
||||
|
||||
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<K, V>(IDictionary<K, V> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
Runtime/Util/ObjectIdentityComparer.cs
Normal file
19
Runtime/Util/ObjectIdentityComparer.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace nadena.dev.modular_avatar.JacksonDunstan.NativeCollections
|
||||
{
|
||||
internal class ObjectIdentityComparer<T> : IEqualityComparer<T>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
3
Runtime/Util/ObjectIdentityComparer.cs.meta
Normal file
3
Runtime/Util/ObjectIdentityComparer.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e674cbd75db24fb2b238674cd7010edb
|
||||
timeCreated: 1709448428
|
@ -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": [],
|
||||
|
@ -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
|
||||
|
@ -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<VRCExpressionsMenu>();
|
||||
var paramsAsset = ScriptableObject.CreateInstance<VRCExpressionParameters>();
|
||||
|
||||
var desc = av.GetComponent<VRCAvatarDescriptor>();
|
||||
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<ModularAvatarParameters>();
|
||||
param.parameters.Add(new ParameterConfig()
|
||||
{
|
||||
nameOrPrefix = name,
|
||||
syncType = ParameterSyncType.Float
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user