From a5d985a3976c1acd21a0e015f39d62dd878faab3 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sat, 15 Oct 2022 19:18:58 -0700 Subject: [PATCH] Add support for automatically installing expression menus Closes: #3 --- .../Editor/AvatarProcessor.cs | 1 + .../Editor/MenuInstallHook.cs | 83 ++++++++++++ .../Editor/MenuInstallHook.cs.meta | 3 + .../Editor/RenameParametersHook.cs | 125 ++++++++++++++++-- .../Samples/Clap/ClapMenu.asset | 25 ++++ .../Samples/Clap/ClapMenu.asset.meta | 8 ++ .../Samples/Clap/ClapSample.prefab | 67 +++++++++- .../Samples/Clap/clap_controller.controller | 21 ++- 8 files changed, 312 insertions(+), 21 deletions(-) create mode 100644 Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs create mode 100644 Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs.meta create mode 100644 Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset create mode 100644 Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset.meta diff --git a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs index 4558236e..f68e8be8 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs @@ -108,6 +108,7 @@ namespace net.fushizen.modular_avatar.core.editor PathMappings.Clear(); new RenameParametersHook().OnPreprocessAvatar(avatarGameObject); + new MenuInstallHook().OnPreprocessAvatar(avatarGameObject); new MergeArmatureHook().OnPreprocessAvatar(avatarGameObject); new RetargetMeshes().OnPreprocessAvatar(avatarGameObject); diff --git a/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs b/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs new file mode 100644 index 00000000..0c956895 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace net.fushizen.modular_avatar.core.editor +{ + public class MenuInstallHook + { + private Dictionary _clonedMenus; + private Dictionary _installTargets; + + public void OnPreprocessAvatar(GameObject avatarRoot) + { + var menuInstallers = avatarRoot.GetComponentsInChildren(true); + if (menuInstallers.Length == 0) return; + + _clonedMenus = new Dictionary(); + + var avatar = avatarRoot.GetComponent(); + + avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu); + _installTargets = new Dictionary(_clonedMenus); + + foreach (var install in menuInstallers) + { + InstallMenu(install); + } + } + + private void InstallMenu(ModularAvatarMenuInstaller installer) + { + if (installer.installTargetMenu == null || installer.menuToAppend == null) return; + if (!_installTargets.TryGetValue(installer.installTargetMenu, out var targetMenu)) return; + if (_installTargets.ContainsKey(installer.menuToAppend)) return; + + targetMenu.controls.AddRange(installer.menuToAppend.controls); + + while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) + { + // Split target menu + var newMenu = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); + var keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; + newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); + targetMenu.controls.RemoveRange(keepCount, + targetMenu.controls.Count - keepCount + ); + + targetMenu.controls.Add(new VRCExpressionsMenu.Control() + { + name = "More", + type = VRCExpressionsMenu.Control.ControlType.SubMenu, + subMenu = newMenu + }); + + _installTargets[installer.installTargetMenu] = newMenu; + targetMenu = newMenu; + } + } + + private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) + { + if (menu == null) return null; + if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu; + newMenu = Object.Instantiate(menu); + AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); + _clonedMenus[menu] = newMenu; + + foreach (var control in newMenu.controls) + { + if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) + { + control.subMenu = CloneMenu(control.subMenu); + } + } + + return newMenu; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs.meta new file mode 100644 index 00000000..05bd531a --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/MenuInstallHook.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 66c78a48b75b499c99eb8d0fee0a097b +timeCreated: 1665885951 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs index e16482df..919dc7ba 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -7,16 +9,71 @@ using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Dynamics.Contact.Components; using VRC.SDK3.Dynamics.PhysBone.Components; +using Object = UnityEngine.Object; namespace net.fushizen.modular_avatar.core.editor { public class RenameParametersHook { + private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7"; + private int internalParamIndex = 0; + private Dictionary _syncedParams = + new Dictionary(); + public void OnPreprocessAvatar(GameObject avatar) { + _syncedParams.Clear(); + WalkTree(avatar, ImmutableDictionary.Empty, ImmutableDictionary.Empty); + + SetExpressionParameters(avatar); + } + + private void SetExpressionParameters(GameObject avatarRoot) + { + var avatar = avatarRoot.GetComponent(); + var expParams = avatar.expressionParameters; + + if (expParams == null) + { + var path = AssetDatabase.GUIDToAssetPath(DEFAULT_EXP_PARAMS_ASSET_GUID); + expParams = AssetDatabase.LoadAssetAtPath(path); + } + + if (expParams == null) + { + // Can't find the defaults??? + expParams = ScriptableObject.CreateInstance(); + } + + expParams = Object.Instantiate(expParams); + AssetDatabase.CreateAsset(expParams, Util.GenerateAssetPath()); + + var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet(); + var parameters = expParams.parameters.ToList(); + + foreach (var kvp in _syncedParams) + { + var name = kvp.Key; + var param = kvp.Value; + if (!knownParams.Contains(name)) + { + parameters.Add(param); + } + } + + expParams.parameters = parameters.ToArray(); + if (expParams.CalcTotalCost() > VRCExpressionParameters.MAX_PARAMETER_COST) + { + throw new Exception("Too many synced parameters: " + + "Cost " + expParams.CalcTotalCost() + " > " + + VRCExpressionParameters.MAX_PARAMETER_COST + ); + } + + avatar.expressionParameters = expParams; } private void WalkTree( @@ -261,20 +318,25 @@ namespace net.fushizen.modular_avatar.core.editor t.conditions = conditions; } - private void ApplyRemappings( - ModularAvatarParameters p, + private void ApplyRemappings(ModularAvatarParameters p, ref ImmutableDictionary remaps, ref ImmutableDictionary prefixRemaps ) { foreach (var param in p.parameters) { + bool doRemap = true; + var remapTo = param.remapTo; if (param.internalParameter) { remapTo = param.nameOrPrefix + "$$Internal_" + internalParamIndex++; } - else if (string.IsNullOrWhiteSpace(remapTo)) continue; + else if (string.IsNullOrWhiteSpace(remapTo)) + { + doRemap = false; + remapTo = param.nameOrPrefix; + } // 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)) @@ -286,23 +348,62 @@ namespace net.fushizen.modular_avatar.core.editor remapTo = outerScope; } - if (param.isPrefix) + if (doRemap) { - prefixRemaps = prefixRemaps.Add(param.nameOrPrefix, remapTo); - foreach (var suffix in ParameterPolicy.PhysBoneSuffixes) + if (param.isPrefix) { - var suffixKey = param.nameOrPrefix + suffix; - var suffixValue = remapTo + suffix; - remaps = remaps.Add(suffixKey, suffixValue); + 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); } } - else + + if (!param.internalParameter && !param.isPrefix && param.syncType != ParameterSyncType.NotSynced) { - remaps = remaps.Add(param.nameOrPrefix, remapTo); + AddSyncParam(param, remapTo); } } } + private void AddSyncParam( + ParameterConfig parameterConfig, + string remapTo + ) + { + if (_syncedParams.ContainsKey(remapTo)) return; + + VRCExpressionParameters.ValueType type; + switch (parameterConfig.syncType) + { + case ParameterSyncType.Bool: + type = VRCExpressionParameters.ValueType.Bool; + break; + case ParameterSyncType.Float: + type = VRCExpressionParameters.ValueType.Float; + break; + case ParameterSyncType.Int: + type = VRCExpressionParameters.ValueType.Int; + break; + default: throw new Exception("Unknown sync type " + parameterConfig.syncType); + } + + _syncedParams[remapTo] = new VRCExpressionParameters.Parameter + { + name = remapTo, + valueType = type, + defaultValue = parameterConfig.defaultValue, + saved = parameterConfig.saved, + }; + } + // 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 diff --git a/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset new file mode 100644 index 00000000..769c3f1f --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -340790334, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3} + m_Name: ClapMenu + m_EditorClassIdentifier: + controls: + - name: Clap + icon: {fileID: 2800000, guid: 5acca5d9b1a37724880f1a1dc1bc54d3, type: 3} + type: 102 + parameter: + name: ClapEnable + value: 1 + style: 0 + subMenu: {fileID: 0} + subParameters: [] + labels: [] diff --git a/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset.meta b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset.meta new file mode 100644 index 00000000..7c231353 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapMenu.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9ace57384de9f5a4d8add2b79bf1115f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapSample.prefab b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapSample.prefab index 492be529..497f2904 100644 --- a/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapSample.prefab +++ b/Packages/net.fushizen.modular-avatar/Samples/Clap/ClapSample.prefab @@ -5002,7 +5002,7 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8484779175115796483} - m_LocalRotation: {x: -0, y: -0, z: 0.7071068, w: 0.7071068} + m_LocalRotation: {x: 0.000000010510536, y: -0.000000010044875, z: 0.7071068, w: 0.7071068} m_LocalPosition: {x: -0.45912412, y: 1.04774, z: -0.0050687576} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: @@ -5022,10 +5022,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 42581d8044b64899834d3d515ab3a144, type: 3} m_Name: m_EditorClassIdentifier: - target: {fileID: 0} boneReference: 17 subPath: - constraint: {fileID: 0} --- !u!1 &8484779175182551488 GameObject: m_ObjectHideFlags: 0 @@ -10035,7 +10033,7 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8484779175388350922} - m_LocalRotation: {x: -0, y: 0, z: -0.7071068, w: 0.7071068} + m_LocalRotation: {x: 0.000000010510536, y: 0.000000010976198, z: -0.7071068, w: 0.7071068} m_LocalPosition: {x: 0.45912412, y: 1.04774, z: -0.0050687576} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: @@ -10055,10 +10053,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 42581d8044b64899834d3d515ab3a144, type: 3} m_Name: m_EditorClassIdentifier: - target: {fileID: 0} boneReference: 18 subPath: - constraint: {fileID: 0} --- !u!1 &8484779175587995429 GameObject: m_ObjectHideFlags: 0 @@ -10128,6 +10124,8 @@ GameObject: - component: {fileID: 8484779176005096009} - component: {fileID: 8484779176005096011} - component: {fileID: 8484779176005096010} + - component: {fileID: 3174898267815582933} + - component: {fileID: 1950333307926010754} m_Layer: 0 m_Name: ClapSample m_TagString: Untagged @@ -10189,6 +10187,63 @@ MonoBehaviour: animator: {fileID: 9100000, guid: c5ebfa99229e64f4986175c32f717990, type: 2} layerType: 5 deleteAttachedAnimator: 1 + pathMode: 0 + matchAvatarWriteDefaults: 0 +--- !u!114 &3174898267815582933 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8484779176005096008} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 71a96d4ea0c344f39e277d82035bf9bd, type: 3} + m_Name: + m_EditorClassIdentifier: + parameters: + - nameOrPrefix: ClapSensorSelf + remapTo: + internalParameter: 1 + isPrefix: 0 + syncType: 0 + defaultValue: 0 + saved: 0 + - nameOrPrefix: ClapSensorOtherR + remapTo: + internalParameter: 1 + isPrefix: 0 + syncType: 0 + defaultValue: 0 + saved: 0 + - nameOrPrefix: ClapSensorOtherL + remapTo: + internalParameter: 1 + isPrefix: 0 + syncType: 0 + defaultValue: 0 + saved: 0 + - nameOrPrefix: ClapEnable + remapTo: + internalParameter: 0 + isPrefix: 0 + syncType: 3 + defaultValue: 0 + saved: 0 +--- !u!114 &1950333307926010754 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8484779176005096008} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7ef83cb0c23d4d7c9d41021e544a1978, type: 3} + m_Name: + m_EditorClassIdentifier: + menuToAppend: {fileID: 11400000, guid: 9ace57384de9f5a4d8add2b79bf1115f, type: 2} + installTargetMenu: {fileID: 0} --- !u!1 &8484779176112939649 GameObject: m_ObjectHideFlags: 0 diff --git a/Packages/net.fushizen.modular-avatar/Samples/Clap/clap_controller.controller b/Packages/net.fushizen.modular-avatar/Samples/Clap/clap_controller.controller index e730ae92..523fa043 100644 --- a/Packages/net.fushizen.modular-avatar/Samples/Clap/clap_controller.controller +++ b/Packages/net.fushizen.modular-avatar/Samples/Clap/clap_controller.controller @@ -11,6 +11,9 @@ AnimatorStateTransition: - m_ConditionMode: 1 m_ConditionEvent: ClapSensorSelf m_EventTreshold: 0 + - m_ConditionMode: 1 + m_ConditionEvent: ClapEnable + m_EventTreshold: 0 m_DstStateMachine: {fileID: 0} m_DstState: {fileID: 6108923960830049082} m_Solo: 0 @@ -90,19 +93,25 @@ AnimatorController: m_DefaultFloat: 0 m_DefaultInt: 0 m_DefaultBool: 0 - m_Controller: {fileID: 9100000} + m_Controller: {fileID: 0} - m_Name: ClapSensorOtherR m_Type: 4 m_DefaultFloat: 0 m_DefaultInt: 0 m_DefaultBool: 0 - m_Controller: {fileID: 9100000} + m_Controller: {fileID: 0} - m_Name: ClapSensorSelf m_Type: 4 m_DefaultFloat: 0 m_DefaultInt: 0 m_DefaultBool: 0 - m_Controller: {fileID: 9100000} + m_Controller: {fileID: 0} + - m_Name: ClapEnable + m_Type: 4 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 0} m_AnimatorLayers: - serializedVersion: 5 m_Name: Base Layer @@ -127,6 +136,9 @@ AnimatorStateTransition: - m_ConditionMode: 1 m_ConditionEvent: ClapSensorOtherL m_EventTreshold: 0 + - m_ConditionMode: 1 + m_ConditionEvent: ClapEnable + m_EventTreshold: 0 m_DstStateMachine: {fileID: 0} m_DstState: {fileID: 4753965434546290636} m_Solo: 0 @@ -256,6 +268,9 @@ AnimatorStateTransition: - m_ConditionMode: 1 m_ConditionEvent: ClapSensorOtherR m_EventTreshold: 0 + - m_ConditionMode: 1 + m_ConditionEvent: ClapEnable + m_EventTreshold: 0 m_DstStateMachine: {fileID: 0} m_DstState: {fileID: 6108923960830049082} m_Solo: 0