diff --git a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs index 95f2a077..4558236e 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs @@ -107,6 +107,8 @@ namespace net.fushizen.modular_avatar.core.editor BoneDatabase.ResetBones(); PathMappings.Clear(); + new RenameParametersHook().OnPreprocessAvatar(avatarGameObject); + new MergeArmatureHook().OnPreprocessAvatar(avatarGameObject); new RetargetMeshes().OnPreprocessAvatar(avatarGameObject); new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject); diff --git a/Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs b/Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs index c9423794..f985dc82 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs @@ -123,6 +123,12 @@ namespace net.fushizen.modular_avatar.core.editor WalkMenu(parameters, installer.menuToAppend, new HashSet()); break; } + + case ModularAvatarMergeAnimator merger: + { + WalkAnimator(parameters, merger.animator as AnimatorController); + break; + } } } diff --git a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs new file mode 100644 index 00000000..e16482df --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs @@ -0,0 +1,318 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDK3.Dynamics.Contact.Components; +using VRC.SDK3.Dynamics.PhysBone.Components; + +namespace net.fushizen.modular_avatar.core.editor +{ + public class RenameParametersHook + { + private int internalParamIndex = 0; + + public void OnPreprocessAvatar(GameObject avatar) + { + WalkTree(avatar, ImmutableDictionary.Empty, ImmutableDictionary.Empty); + } + + private void WalkTree( + GameObject obj, + ImmutableDictionary remaps, + ImmutableDictionary prefixRemaps + ) + { + var p = obj.GetComponent(); + if (p != null) + { + ApplyRemappings(p, ref remaps, ref prefixRemaps); + } + + var willPurgeAnimators = false; + foreach (var merger in obj.GetComponents()) + { + if (merger.deleteAttachedAnimator) + { + willPurgeAnimators = true; + break; + } + } + + foreach (var component in obj.GetComponents()) + { + switch (component) + { + case VRCPhysBone bone: + { + if (bone.parameter != null && prefixRemaps.TryGetValue(bone.parameter, out var newVal)) + { + bone.parameter = newVal; + } + + break; + } + + case VRCContactReceiver contact: + { + if (contact.parameter != null && remaps.TryGetValue(contact.parameter, out var newVal)) + { + contact.parameter = newVal; + } + + break; + } + + case Animator anim: + { + if (willPurgeAnimators) break; // animator will be deleted in subsequent processing + + var controller = anim.runtimeAnimatorController as AnimatorController; + if (controller != null) + { + ProcessAnimator(ref controller, remaps); + anim.runtimeAnimatorController = controller; + } + + break; + } + + case ModularAvatarMergeAnimator merger: + { + var controller = merger.animator as AnimatorController; + if (controller != null) + { + ProcessAnimator(ref controller, remaps); + merger.animator = controller; + } + + break; + } + + case ModularAvatarMenuInstaller installer: + { + if (installer.menuToAppend != null && installer.installTargetMenu != null) + { + ProcessMenu(ref installer.menuToAppend, remaps); + } + + break; + } + } + } + + foreach (Transform child in obj.transform) + { + WalkTree(child.gameObject, remaps, prefixRemaps); + } + } + + private void ProcessMenu(ref VRCExpressionsMenu rootMenu, ImmutableDictionary remaps) + { + Dictionary remapped = + new Dictionary(); + + rootMenu = Transform(rootMenu); + + VRCExpressionsMenu Transform(VRCExpressionsMenu menu) + { + if (remapped.TryGetValue(menu, out var newMenu)) return newMenu; + + newMenu = Object.Instantiate(menu); + AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); + remapped[menu] = newMenu; + + foreach (var control in newMenu.controls) + { + control.parameter.name = remap(remaps, control.parameter.name); + foreach (var subParam in control.subParameters) + { + subParam.name = remap(remaps, subParam.name); + } + + if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) + { + control.subMenu = Transform(control.subMenu); + } + } + + return newMenu; + } + } + + private void ProcessAnimator(ref AnimatorController controller, ImmutableDictionary remaps) + { + var visited = new HashSet(); + var queue = new Queue(); + + // Deep clone the animator + var merger = new AnimatorCombiner(); + merger.AddController("", controller, null); + controller = merger.Finish(); + + var parameters = controller.parameters; + for (int i = 0; i < parameters.Length; i++) + { + if (remaps.TryGetValue(parameters[i].name, out var newName)) + { + parameters[i].name = newName; + } + } + + controller.parameters = parameters; + + foreach (var layer in controller.layers) + { + if (layer.stateMachine != null) + { + queue.Enqueue(layer.stateMachine); + } + } + + while (queue.Count > 0) + { + var sm = queue.Dequeue(); + if (visited.Contains(sm)) continue; + visited.Add(sm); + + foreach (var t in sm.anyStateTransitions) + { + ProcessTransition(t, remaps); + } + + foreach (var t in sm.entryTransitions) + { + ProcessTransition(t, remaps); + } + + foreach (var sub in sm.stateMachines) + { + queue.Enqueue(sub.stateMachine); + } + + foreach (var st in sm.states) + { + ProcessState(st.state, remaps); + } + } + } + + private void ProcessState(AnimatorState state, ImmutableDictionary remaps) + { + state.mirrorParameter = remap(remaps, state.mirrorParameter); + state.timeParameter = remap(remaps, state.timeParameter); + state.speedParameter = remap(remaps, state.speedParameter); + state.cycleOffsetParameter = remap(remaps, state.cycleOffsetParameter); + + foreach (var t in state.transitions) + { + ProcessTransition(t, remaps); + } + + foreach (var behavior in state.behaviours) + { + if (behavior is VRCAvatarParameterDriver driver) + { + ProcessDriver(driver, remaps); + } + } + } + + private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary remaps) + { + var parameters = driver.parameters; + for (int i = 0; i < parameters.Count; i++) + { + var p = parameters[i]; + p.name = remap(remaps, p.name); + p.source = remap(remaps, p.source); + p.destParam = remap(remaps, p.destParam); + p.sourceParam = remap(remaps, p.sourceParam); + } + } + + private void ProcessTransition(AnimatorStateTransition t, ImmutableDictionary remaps) + { + var conditions = t.conditions; + + for (int i = 0; i < conditions.Length; i++) + { + var cond = conditions[i]; + cond.parameter = remap(remaps, cond.parameter); + conditions[i] = cond; + } + + t.conditions = conditions; + } + + private void ProcessTransition(AnimatorTransition t, ImmutableDictionary remaps) + { + var conditions = t.conditions; + + for (int i = 0; i < conditions.Length; i++) + { + var cond = conditions[i]; + cond.parameter = remap(remaps, cond.parameter); + conditions[i] = cond; + } + + t.conditions = conditions; + } + + private void ApplyRemappings( + ModularAvatarParameters p, + ref ImmutableDictionary remaps, + ref ImmutableDictionary prefixRemaps + ) + { + foreach (var param in p.parameters) + { + var remapTo = param.remapTo; + if (param.internalParameter) + { + remapTo = param.nameOrPrefix + "$$Internal_" + internalParamIndex++; + } + else if (string.IsNullOrWhiteSpace(remapTo)) continue; + // Apply outer scope remaps (only if not an internal parameter) + // Note that this continues the else chain above. + else if (param.isPrefix && prefixRemaps.TryGetValue(remapTo, out var outerScope)) + { + remapTo = outerScope; + } + else if (remaps.TryGetValue(remapTo, out outerScope)) + { + remapTo = outerScope; + } + + if (param.isPrefix) + { + prefixRemaps = prefixRemaps.Add(param.nameOrPrefix, remapTo); + foreach (var suffix in ParameterPolicy.PhysBoneSuffixes) + { + var suffixKey = param.nameOrPrefix + suffix; + var suffixValue = remapTo + suffix; + remaps = remaps.Add(suffixKey, suffixValue); + } + } + else + { + remaps = remaps.Add(param.nameOrPrefix, remapTo); + } + } + } + + // This is generic to simplify remapping parameter driver fields, some of which are 'object's. + private T remap(ImmutableDictionary remaps, T x) + where T : class + { + if (x is string s && remaps.TryGetValue(s, out var newS)) + { + return (T) (object) newS; + } + + return x; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs.meta new file mode 100644 index 00000000..fad2b296 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0ff12183ae434c55a3c717170b9e0864 +timeCreated: 1665874651 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs index ddfcea10..8313813f 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs @@ -12,6 +12,7 @@ namespace net.fushizen.modular_avatar.core public bool internalParameter, isPrefix; } + [DisallowMultipleComponent] public class ModularAvatarParameters : AvatarTagComponent { public List parameters = new List();