From f9ed511add6c714f6fa3639460b6930836d374a6 Mon Sep 17 00:00:00 2001 From: poi <77053052+poi-vrc@users.noreply.github.com> Date: Mon, 28 Aug 2023 22:28:30 +0800 Subject: [PATCH] feat: implement customizable and related vrc integrations (#176) --- Packages/com.chocopoi.vrc.avatarlib | 2 +- .../Editor/Animations/AnimationGenerator.cs | 58 ++++++-- .../Editor/DTEditorUtils.cs | 14 +- .../VRChat/VRChatIntegrationWearableModule.cs | 135 +++++++++++++++--- .../Editor/UI/Presenters/CabinetPresenter.cs | 2 +- ...GenerationWearableModuleEditorPresenter.cs | 118 ++++++++++++--- ...AnimationGenerationWearableModuleEditor.cs | 98 +++++++++++-- ...ationGenerationWearableModuleEditorView.cs | 59 +++++++- .../Modules/ArmatureMappingWearableModule.cs | 12 +- .../Lib/Editor/Cabinet/CabinetConfig.cs | 5 - .../Wearable/AnimationBlendshapeValue.cs | 1 + .../Editor/Wearable/WearableCustomizable.cs | 11 +- 12 files changed, 424 insertions(+), 91 deletions(-) diff --git a/Packages/com.chocopoi.vrc.avatarlib b/Packages/com.chocopoi.vrc.avatarlib index 339713f5..6da88b8a 160000 --- a/Packages/com.chocopoi.vrc.avatarlib +++ b/Packages/com.chocopoi.vrc.avatarlib @@ -1 +1 @@ -Subproject commit 339713f54138944eddcb1599f31a74a0bdf56511 +Subproject commit 6da88b8aa56ae282c62d42b57399ce17cf1b67a3 diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/Animations/AnimationGenerator.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/Animations/AnimationGenerator.cs index 5f5ab4db..76485e30 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/Animations/AnimationGenerator.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/Animations/AnimationGenerator.cs @@ -165,7 +165,8 @@ private void GenerateWearableBlendshapeAnimations(AnimationClip enableClip, Anim var obj = _wearableObject.transform.Find(blendshape.path); if (obj == null) { - DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredAvatarBlendshapeObjectNotFound, _wearableObject.name, blendshape.path); + DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredWearableBlendshapeObjectNotFound, _wearableObject.name, blendshape.path); + continue; } if (!TryGetBlendshapeValue(obj.gameObject, blendshape.blendshapeName, out var originalValue)) @@ -235,7 +236,7 @@ public System.Tuple GenerateWearAnimations() return new System.Tuple(enableClip, disableClip); } - public Dictionary> GenerateCustomizableAnimations() + public Dictionary> GenerateCustomizableToggleAnimations() { // prevent unexpected behaviour if (!DTEditorUtils.IsGrandParent(_avatarObject.transform, _wearableObject.transform)) @@ -250,14 +251,14 @@ public System.Tuple GenerateWearAnimations() var enableClip = new AnimationClip(); var disableClip = new AnimationClip(); - if (customizable.type == WearableCustomizableType.Toggle) - { - // avatar required toggles - GenerateAvatarToggleAnimations(enableClip, disableClip, customizable.avatarRequiredToggles.ToArray()); + // avatar required toggles + GenerateAvatarToggleAnimations(enableClip, disableClip, customizable.avatarToggles.ToArray()); - // avatar required blendshapes - GenerateAvatarBlendshapeAnimations(enableClip, disableClip, customizable.avatarRequiredBlendshapes.ToArray()); + // avatar required blendshapes + GenerateAvatarBlendshapeAnimations(enableClip, disableClip, customizable.avatarBlendshapes.ToArray()); + if (customizable.type == WearableCustomizableType.Toggle) + { // wearable required blendshapes GenerateWearableBlendshapeAnimations(enableClip, disableClip, customizable.wearableBlendshapes.ToArray()); @@ -266,8 +267,10 @@ public System.Tuple GenerateWearAnimations() } else if (customizable.type == WearableCustomizableType.Blendshape) { - // TODO: we need to create a curve from 0.0f to 100.0f to handle this type of customizable - throw new System.NotImplementedException(); + // wearable required toggle + GenerateWearableToggleAnimations(enableClip, disableClip, customizable.wearableToggles.ToArray()); + + // we only the toggles here, for radial blendshapes, we need to separate a layer to do that } dict.Add(customizable, new System.Tuple(enableClip, disableClip)); @@ -275,5 +278,40 @@ public System.Tuple GenerateWearAnimations() return dict; } + + public Dictionary GenerateCustomizableBlendshapeAnimations() + { + // prevent unexpected behaviour + if (!DTEditorUtils.IsGrandParent(_avatarObject.transform, _wearableObject.transform)) + { + throw new System.Exception("Wearable object is not inside avatar! Cannot proceed animation generation."); + } + + var dict = new Dictionary(); + + foreach (var customizable in _module.wearableCustomizables) + { + if (customizable.type == WearableCustomizableType.Blendshape) + { + var clip = new AnimationClip(); + + foreach (var wearableBlendshape in customizable.wearableBlendshapes) + { + var obj = _wearableObject.transform.Find(wearableBlendshape.path); + if (obj == null) + { + DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredWearableBlendshapeObjectNotFound, _wearableObject.name, wearableBlendshape.path); + continue; + } + + AnimationUtils.SetLinearZeroToHundredBlendshapeCurve(clip, AnimationUtils.GetRelativePath(obj.transform, _avatarObject.transform), wearableBlendshape.blendshapeName); + } + + dict.Add(customizable, clip); + } + } + + return dict; + } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs index c5412114..c6ff78bd 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using Chocopoi.DressingTools.Lib.Cabinet; using Chocopoi.DressingTools.Lib.Cabinet.Modules; using Chocopoi.DressingTools.Lib.Extensibility.Providers; @@ -36,6 +37,8 @@ internal class DTEditorUtils private static Dictionary s_reflectionTypeCache = new Dictionary(); + private static readonly System.Random Random = new System.Random(); + private static List> s_boneNameMappings = null; //Reference: https://forum.unity.com/threads/horizontal-line-in-editor-window.520812/#post-3416790 @@ -77,7 +80,7 @@ public static DTCabinet GetAvatarCabinet(GameObject avatar, bool createIfNotExis // TODO: read default config, scan for armature names? comp.avatarGameObject = avatar; var config = new CabinetConfig(); - comp.configJson = config.ToString(); + comp.configJson = config.Serialize(); } return comp; @@ -637,5 +640,14 @@ public static bool IsOriginatedFromAnyWearable(Transform root, Transform transfo } return found; } + + public static string RandomString(int length) + { + // i just copied from stackoverflow :D + // https://stackoverflow.com/questions/1344221/how-can-i-generate-random-alphanumeric-strings?page=1&tab=scoredesc#tab-top + const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.Next(s.Length)]).ToArray()); + } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/Integrations/VRChat/VRChatIntegrationWearableModule.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/Integrations/VRChat/VRChatIntegrationWearableModule.cs index 44f21a3a..9a9fc6b2 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/Integrations/VRChat/VRChatIntegrationWearableModule.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/Integrations/VRChat/VRChatIntegrationWearableModule.cs @@ -130,10 +130,8 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx) ExpressionMenuUtils.RemoveExpressionParameters(exParams, "^cpDT_Cabinet"); - try + var parametersToAdd = new List { - ExpressionMenuUtils.AddExpressionParameters(exParams, new VRCExpressionParameters.Parameter[] - { new VRCExpressionParameters.Parameter() { name = "cpDT_Cabinet", @@ -142,20 +140,13 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx) networkSynced = true, saved = true } - }); - } - catch (ParameterOverflowException ex) - { - DTReportUtils.LogExceptionLocalized(cabCtx.report, LogLabel, ex, "integrations.vrc.msgCode.error.parameterOverFlow"); - return false; - } + }; ExpressionMenuUtils.RemoveExpressionMenuControls(exMenu, "DT Cabinet"); - var subMenu = new ExpressionMenuBuilder(exMenu) - .BeginNewSubMenu("DT Cabinet"); - - subMenu.AddToggle("Original", "cpDT_Cabinet", 0); + var baseSubMenu = new ExpressionMenuBuilder(); + baseSubMenu.AddToggle("Original", "cpDT_Cabinet", 0); + baseSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet.asset", CabinetApplier.GeneratedAssetsPath)); // create an empty clip var emptyClip = new AnimationClip(); @@ -167,6 +158,9 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx) // get wearables var wearables = DTEditorUtils.GetCabinetWearables(cabCtx.avatarGameObject); + var cabinetMenu = baseSubMenu; + var cabinetMenuIndex = 0; + var originalLayerLength = fxController.layers.Length; for (var i = 0; i < wearables.Length; i++) { @@ -198,21 +192,122 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx) pairs.Add(i + 1, wearAnimations.Item1); // enable clip AssetDatabase.CreateAsset(wearAnimations.Item1, CabinetApplier.GeneratedAssetsPath + "/cpDT_" + wearables[i].name + ".anim"); - // generate expression menu - subMenu.AddToggle(vrcm.customCabinetToggleName ?? config.info.name, "cpDT_Cabinet", i + 1); + // add a new submenu if going to be full + if (cabinetMenu.GetMenu().controls.Count == 7) + { + var newSubMenu = new ExpressionMenuBuilder(); + newSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}.asset", CabinetApplier.GeneratedAssetsPath, ++cabinetMenuIndex)); + + cabinetMenu.AddSubMenu("Next Page", newSubMenu.GetMenu()); + cabinetMenu = newSubMenu; + } + + if (agm.wearableCustomizables.Count > 0) + { + // Add a submenu for wearables that have customizables + var baseCustomizableSubMenu = new ExpressionMenuBuilder(); + baseCustomizableSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}_{2}.asset", CabinetApplier.GeneratedAssetsPath, cabinetMenuIndex, config.info.name)); + cabinetMenu.AddSubMenu(vrcm.customCabinetToggleName ?? config.info.name, baseCustomizableSubMenu.GetMenu()); + + // add enable toggle + baseCustomizableSubMenu.AddToggle("Enable", "cpDT_Cabinet", i + 1); + + var customizableMenu = baseCustomizableSubMenu; + var customizableMenuIndex = 0; + + var customizableToggleAnimations = animationGenerator.GenerateCustomizableToggleAnimations(); + var customizableBlendshapeAnimations = animationGenerator.GenerateCustomizableBlendshapeAnimations(); + + foreach (var wearableCustomizable in agm.wearableCustomizables) + { + // add a new submenu if going to be full + if (customizableMenu.GetMenu().controls.Count == 7) + { + var newSubMenu = new ExpressionMenuBuilder(); + newSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}_{2}_{3}.asset", CabinetApplier.GeneratedAssetsPath, cabinetMenuIndex, config.info.name, ++customizableMenuIndex)); + + cabinetMenu.AddSubMenu("Next Page", newSubMenu.GetMenu()); + customizableMenu = newSubMenu; + } + + var parameterName = string.Format("cpDT_Cabinet_{0}_{1}", wearableCustomizable.name, DTEditorUtils.RandomString(8)); + + if (wearableCustomizable.type == WearableCustomizableType.Toggle) + { + AnimationUtils.AddAnimatorParameter(fxController, parameterName, wearableCustomizable.defaultValue > 0); + parametersToAdd.Add(new VRCExpressionParameters.Parameter() + { + name = wearableCustomizable.name, + valueType = VRCExpressionParameters.ValueType.Bool, + defaultValue = wearableCustomizable.defaultValue, + networkSynced = true, + saved = true + }); + + var anims = customizableToggleAnimations[wearableCustomizable]; + AssetDatabase.CreateAsset(anims.Item1, string.Format("{0}/{1}_On.anim", CabinetApplier.GeneratedAssetsPath, parameterName)); + AssetDatabase.CreateAsset(anims.Item2, string.Format("{0}/{1}_Off.anim", CabinetApplier.GeneratedAssetsPath, parameterName)); + AnimationUtils.GenerateSingleToggleLayer(fxController, parameterName, parameterName, anims.Item2, anims.Item1, cabCtx.cabinetConfig.animationWriteDefaults, false, null, refTransition); + customizableMenu.AddToggle(wearableCustomizable.name, parameterName, 1); + } + else if (wearableCustomizable.type == WearableCustomizableType.Blendshape) + { + AnimationUtils.AddAnimatorParameter(fxController, parameterName, wearableCustomizable.defaultValue); + parametersToAdd.Add(new VRCExpressionParameters.Parameter() + { + name = wearableCustomizable.name, + valueType = VRCExpressionParameters.ValueType.Float, + defaultValue = wearableCustomizable.defaultValue, + networkSynced = true, + saved = true + }); + + var toggleAnims = customizableToggleAnimations[wearableCustomizable]; + AssetDatabase.CreateAsset(toggleAnims.Item1, string.Format("{0}/{1}_On.anim", CabinetApplier.GeneratedAssetsPath, parameterName)); + AssetDatabase.CreateAsset(toggleAnims.Item2, string.Format("{0}/{1}_Off.anim", CabinetApplier.GeneratedAssetsPath, parameterName)); + var blendshapeAnim = customizableBlendshapeAnimations[wearableCustomizable]; + AssetDatabase.CreateAsset(blendshapeAnim, string.Format("{0}/{1}_MotionTime.anim", CabinetApplier.GeneratedAssetsPath, parameterName)); + AnimationUtils.GenerateSingleToggleLayer(fxController, parameterName + "_Toggles", parameterName, toggleAnims.Item2, toggleAnims.Item1, cabCtx.cabinetConfig.animationWriteDefaults, false, null, refTransition); + AnimationUtils.GenerateSingleMotionTimeLayer(fxController, parameterName + "_Blendshapes", parameterName, blendshapeAnim, cabCtx.cabinetConfig.animationWriteDefaults); + customizableMenu.AddRadialPuppet(wearableCustomizable.name, parameterName); + } + } + } + else + { + // Add toggle only + cabinetMenu.AddToggle(vrcm.customCabinetToggleName ?? config.info.name, "cpDT_Cabinet", i + 1); + } } + // create cabinet layer AnimationUtils.GenerateAnyStateLayer(fxController, "cpDT_Cabinet", "cpDT_Cabinet", pairs, cabCtx.cabinetConfig.animationWriteDefaults, null, refTransition); - EditorUtility.DisplayProgressBar("DressingTools", "Generating expression menu...", 0); - subMenu.CreateAsset(CabinetApplier.GeneratedAssetsPath + "/cpDT_Cabinet.asset") - .EndNewSubMenu(); + // we push our cabinet layer to the top + var layerList = new List(fxController.layers); + var cabinetLayer = layerList[layerList.Count - 1]; + layerList.Remove(cabinetLayer); + layerList.Insert(originalLayerLength, cabinetLayer); + fxController.layers = layerList.ToArray(); + + EditorUtility.DisplayProgressBar("DressingTools", "Adding expression parameters...", 0); + + try + { + ExpressionMenuUtils.AddExpressionParameters(exParams, parametersToAdd); + ExpressionMenuUtils.AddSubMenu(exMenu, "DT Cabinet", baseSubMenu.GetMenu()); + } + catch (ParameterOverflowException ex) + { + DTReportUtils.LogExceptionLocalized(cabCtx.report, LogLabel, ex, "integrations.vrc.msgCode.error.parameterOverFlow"); + return false; + } EditorUtility.SetDirty(fxController); EditorUtility.SetDirty(exMenu); EditorUtility.SetDirty(exParams); - EditorUtility.ClearProgressBar(); + EditorUtility.DisplayProgressBar("DressingTools", "Saving assets...", 0); AssetDatabase.SaveAssets(); return true; diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/CabinetPresenter.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/CabinetPresenter.cs index 06617644..4e2bb085 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/CabinetPresenter.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/CabinetPresenter.cs @@ -83,7 +83,7 @@ private void OnCabinetSettingsChange() _cabinetConfig = new CabinetConfig(); } _cabinetConfig.avatarArmatureName = _view.CabinetAvatarArmatureName; - cabinet.configJson = _cabinetConfig.ToString(); + cabinet.configJson = _cabinetConfig.Serialize(); } private void OnForceUpdateView() diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/Modules/AnimationGenerationWearableModuleEditorPresenter.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/Modules/AnimationGenerationWearableModuleEditorPresenter.cs index 6a35a2fa..1598e274 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/Modules/AnimationGenerationWearableModuleEditorPresenter.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/Modules/AnimationGenerationWearableModuleEditorPresenter.cs @@ -15,6 +15,7 @@ * You should have received a copy of the GNU General Public License along with DressingTools. If not, see . */ +using System; using System.Collections.Generic; using Chocopoi.AvatarLib.Animations; using Chocopoi.DressingTools.Cabinet.Modules; @@ -69,6 +70,8 @@ private void SubscribeEvents() _view.WearableOnWearPresetSaveEvent += OnWearableOnWearPresetSaveEvent; _view.WearableOnWearPresetDeleteEvent += OnWearableOnWearPresetDeleteEvent; + _view.AddCustomizableEvent += OnAddCustomizableEvent; + _parentView.TargetAvatarOrWearableChange += OnTargetAvatarOrWearableChange; } @@ -93,10 +96,21 @@ private void UnsubscribeEvents() _view.WearableOnWearPresetSaveEvent -= OnWearableOnWearPresetSaveEvent; _view.WearableOnWearPresetDeleteEvent -= OnWearableOnWearPresetDeleteEvent; + _view.AddCustomizableEvent -= OnAddCustomizableEvent; + _parentView.TargetAvatarOrWearableChange -= OnTargetAvatarOrWearableChange; } - private void UpdateSelectedPresetData(Dictionary savedPresets, PresetData presetData) + private void OnAddCustomizableEvent() + { + _module.wearableCustomizables.Add(new WearableCustomizable() + { + name = "Customizable-" + DTEditorUtils.RandomString(6) + }); + UpdateCustomizables(); + } + + private void UpdateSelectedPresetData(Dictionary savedPresets, PresetViewData presetData) { if (presetData.selectedPresetIndex == 0) { @@ -106,7 +120,7 @@ private void UpdateSelectedPresetData(Dictionary savedP _module.avatarAnimationOnWear = new AnimationPreset(savedPresets[key]); } - private void SavePreset(Dictionary savedPresets, PresetData presetData, AnimationPreset presetToSave) + private void SavePreset(Dictionary savedPresets, PresetViewData presetData, AnimationPreset presetToSave) { var presetName = _view.ShowPresetNamingDialog(); @@ -125,11 +139,11 @@ private void SavePreset(Dictionary savedPresets, Preset savedPresets.Add(presetName, presetToSave); // serialize to cabinet - _cabinet.configJson = _cabinetConfig.ToString(); + _cabinet.configJson = _cabinetConfig.Serialize(); UpdateView(); } - private void DeletePreset(Dictionary savedPresets, PresetData presetData) + private void DeletePreset(Dictionary savedPresets, PresetViewData presetData) { if (presetData.selectedPresetIndex == 0) { @@ -141,7 +155,7 @@ private void DeletePreset(Dictionary savedPresets, Pres presetData.selectedPresetIndex = 0; // serialize to cabinet - _cabinet.configJson = _cabinetConfig.ToString(); + _cabinet.configJson = _cabinetConfig.Serialize(); UpdateView(); } @@ -229,10 +243,10 @@ private void OnWearableOnWearBlendshapeAddEvent() UpdateAnimationGenerationWearableOnWear(); } - private void UpdateAnimationPresetToggles(Transform root, AnimationPreset preset, List toggleDataList) + private void UpdateToggles(Transform root, List toggles, List toggleDataList) { toggleDataList.Clear(); - foreach (var toggle in preset.toggles) + foreach (var toggle in toggles) { var transform = toggle.path != null ? root.Find(toggle.path) : null; @@ -258,10 +272,8 @@ private void UpdateAnimationPresetToggles(Transform root, AnimationPreset preset }; toggleData.removeButtonClickEvent = () => { - preset.toggles.Remove(toggle); + toggles.Remove(toggle); toggleDataList.Remove(toggleData); - UpdateAnimationGenerationAvatarOnWear(); - UpdateAnimationGenerationWearableOnWear(); }; toggleDataList.Add(toggleData); @@ -283,10 +295,10 @@ private string[] GetBlendshapeNames(Mesh mesh) return names; } - private void UpdateAnimationPresetBlendshapes(Transform root, AnimationPreset preset, List blendshapeDataList) + private void UpdateBlendshapes(Transform root, List blendshapes, List blendshapeDataList) { blendshapeDataList.Clear(); - foreach (var blendshape in preset.blendshapes) + foreach (var blendshape in blendshapes) { var transform = blendshape.path != null ? root.Find(blendshape.path) : null; var smr = transform?.GetComponent(); @@ -338,7 +350,7 @@ private void UpdateAnimationPresetBlendshapes(Transform root, AnimationPreset pr blendshapeData.sliderChangeEvent = () => blendshape.value = blendshapeData.value; blendshapeData.removeButtonClickEvent = () => { - preset.blendshapes.Remove(blendshape); + blendshapes.Remove(blendshape); blendshapeDataList.Remove(blendshapeData); }; @@ -346,7 +358,7 @@ private void UpdateAnimationPresetBlendshapes(Transform root, AnimationPreset pr } } - private void UpdateAnimationPreset(Transform root, Dictionary savedPresets, AnimationPreset preset, PresetData presetData) + private void UpdateAnimationPreset(Transform root, Dictionary savedPresets, AnimationPreset preset, PresetViewData presetData) { if (savedPresets != null) { @@ -360,8 +372,8 @@ private void UpdateAnimationPreset(Transform root, Dictionary toggleData) @@ -376,7 +388,7 @@ private bool IsGameObjectUsedInToggles(GameObject go, List toggleDat return false; } - private void UpdateAvatarOnWearToggleSuggestions(PresetData presetData) + private void UpdateAvatarOnWearToggleSuggestions(PresetViewData presetData) { var targetAvatar = _parentView.TargetAvatar; var targetWearable = _parentView.TargetWearable; @@ -413,7 +425,7 @@ private void UpdateAvatarOnWearToggleSuggestions(PresetData presetData) } } - private void UpdateWearableOnWearToggleSuggestions(PresetData presetData) + private void UpdateWearableOnWearToggleSuggestions(PresetViewData presetData) { var targetWearable = _parentView.TargetWearable; @@ -495,6 +507,72 @@ private void UpdateAnimationGenerationWearableOnWear() } } + private void UpdateCustomizableAvatarToggles(WearableCustomizable wearable, CustomizableViewData customizableData) => UpdateToggles(_parentView.TargetAvatar.transform, wearable.avatarToggles, customizableData.avatarToggles); + private void UpdateCustomizableWearableToggles(WearableCustomizable wearable, CustomizableViewData customizableData) => UpdateToggles(_parentView.TargetWearable.transform, wearable.wearableToggles, customizableData.wearableToggles); + private void UpdateCustomizableAvatarBlendshapes(WearableCustomizable wearable, CustomizableViewData customizableData) => UpdateBlendshapes(_parentView.TargetAvatar.transform, wearable.avatarBlendshapes, customizableData.avatarBlendshapes); + private void UpdateCustomizableWearableBlendshapes(WearableCustomizable wearable, CustomizableViewData customizableData) => UpdateBlendshapes(_parentView.TargetWearable.transform, wearable.wearableBlendshapes, customizableData.wearableBlendshapes); + + private void UpdateCustomizables() + { + _view.Customizables.Clear(); + + if (_parentView.TargetAvatar == null || _parentView.TargetWearable == null) + { + return; + } + + foreach (var wearable in _module.wearableCustomizables) + { + var customizableData = new CustomizableViewData + { + name = wearable.name, + type = (int)wearable.type, + defaultValue = wearable.defaultValue + }; + + customizableData.removeButtonClickEvent = () => + { + _module.wearableCustomizables.Remove(wearable); + _view.Customizables.Remove(customizableData); + }; + + customizableData.customizableSettingsChangeEvent = () => + { + wearable.name = customizableData.name; + wearable.type = (WearableCustomizableType)customizableData.type; + wearable.defaultValue = customizableData.defaultValue; + }; + + customizableData.addAvatarToggleEvent = () => + { + wearable.avatarToggles.Add(new AnimationToggle()); + UpdateCustomizableAvatarToggles(wearable, customizableData); + }; + customizableData.addAvatarBlendshapeEvent = () => + { + wearable.avatarBlendshapes.Add(new AnimationBlendshapeValue()); + UpdateCustomizableAvatarBlendshapes(wearable, customizableData); + }; + customizableData.addWearableToggleEvent = () => + { + wearable.wearableToggles.Add(new AnimationToggle()); + UpdateCustomizableWearableToggles(wearable, customizableData); + }; + customizableData.addWearableBlendshapeEvent = () => + { + wearable.wearableBlendshapes.Add(new AnimationBlendshapeValue()); + UpdateCustomizableWearableBlendshapes(wearable, customizableData); + }; + + UpdateCustomizableAvatarToggles(wearable, customizableData); + UpdateCustomizableAvatarBlendshapes(wearable, customizableData); + UpdateCustomizableWearableToggles(wearable, customizableData); + UpdateCustomizableWearableBlendshapes(wearable, customizableData); + + _view.Customizables.Add(customizableData); + } + } + private void UpdateView() { _cabinet = DTEditorUtils.GetAvatarCabinet(_parentView.TargetAvatar); @@ -516,7 +594,7 @@ private void UpdateView() moduleName = AnimationGenerationCabinetModuleProvider.MODULE_IDENTIFIER, config = _moduleConfig }); - _cabinet.configJson = _cabinetConfig.ToString(); + _cabinet.configJson = _cabinetConfig.Serialize(); } } } @@ -527,7 +605,7 @@ private void UpdateView() UpdateAnimationGenerationAvatarOnWear(); UpdateAnimationGenerationWearableOnWear(); - // TODO: customizables + UpdateCustomizables(); } private void OnLoad() diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/Modules/AnimationGenerationWearableModuleEditor.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/Modules/AnimationGenerationWearableModuleEditor.cs index 97729eb4..fe3ffb0a 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/Modules/AnimationGenerationWearableModuleEditor.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/Modules/AnimationGenerationWearableModuleEditor.cs @@ -46,11 +46,13 @@ internal class AnimationGenerationWearableModuleEditor : WearableModuleEditor, I public event Action WearableOnWearPresetDeleteEvent; public event Action WearableOnWearToggleAddEvent; public event Action WearableOnWearBlendshapeAddEvent; + public event Action AddCustomizableEvent; public bool ShowCannotRenderPresetWithoutTargetAvatarHelpBox { get; set; } public bool ShowCannotRenderPresetWithoutTargetWearableHelpBox { get; set; } - public PresetData AvatarOnWearPresetData { get; set; } - public PresetData WearableOnWearPresetData { get; set; } + public PresetViewData AvatarOnWearPresetData { get; set; } + public PresetViewData WearableOnWearPresetData { get; set; } + public List Customizables { get; set; } private AnimationGenerationWearableModuleEditorPresenter _presenter; private IWearableModuleEditorViewParent _parentView; @@ -60,6 +62,7 @@ internal class AnimationGenerationWearableModuleEditor : WearableModuleEditor, I private bool _foldoutAvatarAnimationPresetBlendshapes; private bool _foldoutWearableAnimationPresetToggles; private bool _foldoutWearableAnimationPresetBlendshapes; + private bool _foldoutAnimationGenerationCustomizables; public AnimationGenerationWearableModuleEditor(IWearableModuleEditorViewParent parentView, WearableModuleProviderBase provider, IModuleConfig target) : base(parentView, provider, target) { @@ -68,8 +71,9 @@ public AnimationGenerationWearableModuleEditor(IWearableModuleEditorViewParent p ShowCannotRenderPresetWithoutTargetAvatarHelpBox = true; ShowCannotRenderPresetWithoutTargetWearableHelpBox = true; - AvatarOnWearPresetData = new PresetData(); - WearableOnWearPresetData = new PresetData(); + AvatarOnWearPresetData = new PresetViewData(); + WearableOnWearPresetData = new PresetViewData(); + Customizables = new List(); _foldoutAnimationGenerationAvatarOnWear = false; _foldoutAnimationGenerationWearableOnWear = false; @@ -79,9 +83,9 @@ public AnimationGenerationWearableModuleEditor(IWearableModuleEditorViewParent p _foldoutWearableAnimationPresetBlendshapes = false; } - private void DrawAnimationPresetToggles(List toggles, List toggleSuggestions, Action addButtonOnClickedEvent, ref bool foldoutAnimationPresetToggles) + private void DrawToggles(string title, List toggles, List toggleSuggestions, Action addButtonOnClickedEvent, ref bool foldoutAnimationPresetToggles) { - BeginFoldoutBox(ref foldoutAnimationPresetToggles, "Toggles"); + BeginFoldoutBox(ref foldoutAnimationPresetToggles, title); if (foldoutAnimationPresetToggles) { HelpBox("The object must be a child or grand-child of the root. Or it will not be selected.", MessageType.Info); @@ -133,9 +137,9 @@ private void DrawAnimationPresetToggles(List toggles, List blendshapes, Action addButtonOnClickedEvent, ref bool foldoutAnimationPresetBlendshapes) + private void DrawBlendshapes(string title, List blendshapes, Action addButtonOnClickedEvent, ref bool foldoutAnimationPresetBlendshapes, bool hideSlider = false) { - BeginFoldoutBox(ref foldoutAnimationPresetBlendshapes, "Blendshapes"); + BeginFoldoutBox(ref foldoutAnimationPresetBlendshapes, title); if (foldoutAnimationPresetBlendshapes) { HelpBox("The object must be a child or grand-child of the root, and has a SkinnedMeshRenderer. Or it will not be selected.", MessageType.Info); @@ -156,7 +160,7 @@ private void DrawAnimationPresetBlendshapes(List blendshapes, Ac if (!blendshape.isInvalid) { Popup(ref blendshape.selectedBlendshapeIndex, blendshape.availableBlendshapeNames, blendshape.blendshapeNameChangeEvent); - Slider(ref blendshape.value, 0, 100, blendshape.sliderChangeEvent); + if (!hideSlider) Slider(ref blendshape.value, 0, 100, blendshape.sliderChangeEvent); } else { @@ -166,7 +170,7 @@ private void DrawAnimationPresetBlendshapes(List blendshapes, Ac var fakeInt = 0; var fakeFloat = 0.0f; Popup(ref fakeInt, new string[] { "---" }); - Slider(ref fakeFloat, 0, 100); + if (!hideSlider) Slider(ref fakeFloat, 0, 100); } EndDisabled(); } @@ -179,7 +183,7 @@ private void DrawAnimationPresetBlendshapes(List blendshapes, Ac EndFoldoutBox(); } - private void DrawAnimationPreset(PresetData presetData, Action changeEvent, Action saveEvent, Action deleteEvent, Action toggleAddEvent, Action blendshapeAddEvent, ref bool foldoutAnimationPresetToggles, ref bool foldoutAnimationPresetBlendshapes) + private void DrawAnimationPreset(PresetViewData presetData, Action changeEvent, Action saveEvent, Action deleteEvent, Action toggleAddEvent, Action blendshapeAddEvent, ref bool foldoutAnimationPresetToggles, ref bool foldoutAnimationPresetBlendshapes) { BeginHorizontal(); { @@ -191,8 +195,8 @@ private void DrawAnimationPreset(PresetData presetData, Action changeEvent, Acti Separator(); - DrawAnimationPresetToggles(presetData.toggles, presetData.toggleSuggestions, toggleAddEvent, ref foldoutAnimationPresetToggles); - DrawAnimationPresetBlendshapes(presetData.blendshapes, blendshapeAddEvent, ref foldoutAnimationPresetBlendshapes); + DrawToggles("Toggles", presetData.toggles, presetData.toggleSuggestions, toggleAddEvent, ref foldoutAnimationPresetToggles); + DrawBlendshapes("Blendshapes", presetData.blendshapes, blendshapeAddEvent, ref foldoutAnimationPresetBlendshapes); } private void DrawAnimationGenerationAvatarOnWear() @@ -229,13 +233,79 @@ private void DrawAnimationGenerationWearableOnWear() EndFoldoutBox(); } + private void DrawCustomizable(CustomizableViewData customizable) + { + BeginFoldoutBoxWithButtonRight(ref customizable.foldout, customizable.name, "x Remove", customizable.removeButtonClickEvent); + if (customizable.foldout) + { + TextField("Name", ref customizable.name, customizable.customizableSettingsChangeEvent); + Popup("Type:", ref customizable.type, new string[] { "Toggle", "Blendshape" }, customizable.customizableSettingsChangeEvent); + + Separator(); + + if (customizable.type == 0) + { + // toggle mode + DrawToggles("Wearable Toggles", customizable.wearableToggles, customizable.wearableToggleSuggestions, customizable.addWearableToggleEvent, ref customizable.foldoutWearableToggles); + + HorizontalLine(); + + DrawToggles("Avatar Toggles", customizable.avatarToggles, customizable.avatarToggleSuggestions, customizable.addAvatarToggleEvent, ref customizable.foldoutAvatarToggles); + DrawBlendshapes("Avatar Blendshapes", customizable.avatarBlendshapes, customizable.addAvatarBlendshapeEvent, ref customizable.foldoutAvatarBlendshapes); + DrawBlendshapes("Wearable Blendshapes", customizable.wearableBlendshapes, customizable.addWearableBlendshapeEvent, ref customizable.foldoutWearableBlendshapes); + } + else + { + DrawBlendshapes("Wearable Blendshapes", customizable.wearableBlendshapes, customizable.addWearableBlendshapeEvent, ref customizable.foldoutWearableBlendshapes, true); + + HorizontalLine(); + + // radial blendshape mode + DrawToggles("Avatar Toggles", customizable.avatarToggles, customizable.avatarToggleSuggestions, customizable.addAvatarToggleEvent, ref customizable.foldoutAvatarToggles); + DrawToggles("Wearable Toggles", customizable.wearableToggles, customizable.wearableToggleSuggestions, customizable.addWearableToggleEvent, ref customizable.foldoutWearableToggles); + DrawBlendshapes("Avatar Blendshapes", customizable.avatarBlendshapes, customizable.addAvatarBlendshapeEvent, ref customizable.foldoutAvatarBlendshapes); + } + } + EndFoldoutBox(); + + } + + private void DrawCustomizables() + { + BeginFoldoutBox(ref _foldoutAnimationGenerationCustomizables, "Customizables"); + if (_foldoutAnimationGenerationCustomizables) + { + if (ShowCannotRenderPresetWithoutTargetWearableHelpBox) + { + HelpBox("Cannot render customizables without a target wearable selected.", MessageType.Error); + } + else + { + BeginHorizontal(); + { + Button("+ Add", AddCustomizableEvent, GUILayout.ExpandWidth(false)); + } + EndHorizontal(); + + Separator(); + + var copy = new List(Customizables); + foreach (var customizable in copy) + { + DrawCustomizable(customizable); + } + } + } + EndFoldoutBox(); + } + public override void OnGUI() { var module = (AnimationGenerationWearableModuleConfig)target; DrawAnimationGenerationAvatarOnWear(); DrawAnimationGenerationWearableOnWear(); - // TODO: customizables + DrawCustomizables(); } public override bool IsValid() diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/Modules/IAnimationGenerationWearableModuleEditorView.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/Modules/IAnimationGenerationWearableModuleEditorView.cs index dd0f0766..a750d392 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/Modules/IAnimationGenerationWearableModuleEditorView.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/Modules/IAnimationGenerationWearableModuleEditorView.cs @@ -73,7 +73,7 @@ public BlendshapeData() } } - internal class PresetData + internal class PresetViewData { public List toggles; public List toggleSuggestions; @@ -81,7 +81,7 @@ internal class PresetData public string[] savedPresetKeys; public int selectedPresetIndex; - public PresetData() + public PresetViewData() { toggles = new List(); toggleSuggestions = new List(); @@ -91,6 +91,55 @@ public PresetData() } } + + internal class CustomizableViewData + { + public bool foldout; + public string name; + public int type; + public float defaultValue; + public Action customizableSettingsChangeEvent; + public bool foldoutAvatarToggles; + public List avatarToggles; + public List avatarToggleSuggestions; + public Action addAvatarToggleEvent; + public bool foldoutWearableToggles; + public List wearableToggles; + public List wearableToggleSuggestions; + public Action addWearableToggleEvent; + public bool foldoutAvatarBlendshapes; + public List avatarBlendshapes; + public Action addAvatarBlendshapeEvent; + public bool foldoutWearableBlendshapes; + public List wearableBlendshapes; + public Action addWearableBlendshapeEvent; + public Action removeButtonClickEvent; + + public CustomizableViewData() + { + foldout = false; + name = null; + type = 0; + defaultValue = 0; + customizableSettingsChangeEvent = null; + foldoutAvatarToggles = false; + avatarToggles = new List(); + avatarToggleSuggestions = new List(); + addAvatarToggleEvent = null; + foldoutWearableToggles = false; + wearableToggles = new List(); + wearableToggleSuggestions = new List(); + addWearableToggleEvent = null; + foldoutAvatarBlendshapes = false; + avatarBlendshapes = new List(); + addAvatarBlendshapeEvent = null; + foldoutWearableBlendshapes = false; + wearableBlendshapes = new List(); + addWearableBlendshapeEvent = null; + removeButtonClickEvent = null; + } + } + internal interface IAnimationGenerationWearableModuleEditorView : IEditorView { event Action AvatarOnWearPresetChangeEvent; @@ -103,10 +152,12 @@ internal interface IAnimationGenerationWearableModuleEditorView : IEditorView event Action WearableOnWearPresetDeleteEvent; event Action WearableOnWearToggleAddEvent; event Action WearableOnWearBlendshapeAddEvent; + event Action AddCustomizableEvent; bool ShowCannotRenderPresetWithoutTargetAvatarHelpBox { get; set; } bool ShowCannotRenderPresetWithoutTargetWearableHelpBox { get; set; } - PresetData AvatarOnWearPresetData { get; set; } - PresetData WearableOnWearPresetData { get; set; } + PresetViewData AvatarOnWearPresetData { get; set; } + PresetViewData WearableOnWearPresetData { get; set; } + List Customizables { get; set; } string ShowPresetNamingDialog(); void ShowDuplicatedPresetNameDialog(); diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/ArmatureMappingWearableModule.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/ArmatureMappingWearableModule.cs index fc676e22..9a4c4e74 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/ArmatureMappingWearableModule.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/ArmatureMappingWearableModule.cs @@ -83,7 +83,6 @@ public static class MessageCode public const string ApplyingBoneMappingHasErrors = "appliers.default.msgCode.error.applyingBoneMappingHasErrors"; } - private static readonly System.Random Random = new System.Random(); private const string LogLabel = "ArmatureModule"; [ExcludeFromCodeCoverage] public override string ModuleIdentifier => MODULE_IDENTIFIER; @@ -347,15 +346,6 @@ private static bool ApplyBoneMappings(ApplyCabinetContext cabCtx, ApplyWearableC return true; } - private static string RandomString(int length) - { - // i just copied from stackoverflow :D - // https://stackoverflow.com/questions/1344221/how-can-i-generate-random-alphanumeric-strings?page=1&tab=scoredesc#tab-top - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } - public override bool OnApplyWearable(ApplyCabinetContext cabCtx, ApplyWearableContext wearCtx, WearableModule module) { var armatureMappingConfig = (ArmatureMappingWearableModuleConfig)module.config; @@ -366,7 +356,7 @@ public override bool OnApplyWearable(ApplyCabinetContext cabCtx, ApplyWearableCo return false; } - var generatedName = string.Format("{0}-{1}", wearCtx.wearableConfig.info.name, RandomString(32)); + var generatedName = string.Format("{0}-{1}", wearCtx.wearableConfig.info.name, DTEditorUtils.RandomString(16)); if (!ApplyBoneMappings(cabCtx, wearCtx, armatureMappingConfig, generatedName, boneMappings, cabCtx.avatarGameObject.transform, wearCtx.wearableGameObject.transform, wearCtx.wearableGameObject.transform, "")) { diff --git a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Cabinet/CabinetConfig.cs b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Cabinet/CabinetConfig.cs index 8d0226ea..fb16de43 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Cabinet/CabinetConfig.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Cabinet/CabinetConfig.cs @@ -83,10 +83,5 @@ public CabinetConfig Clone() // a tricky and easier way to copy return Deserialize(Serialize()); } - - public override string ToString() - { - return Serialize(); - } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/AnimationBlendshapeValue.cs b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/AnimationBlendshapeValue.cs index 107da57b..2e1f9e85 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/AnimationBlendshapeValue.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/AnimationBlendshapeValue.cs @@ -26,6 +26,7 @@ public class AnimationBlendshapeValue public string blendshapeName; public float value; + public AnimationBlendshapeValue() { path = null; diff --git a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/WearableCustomizable.cs b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/WearableCustomizable.cs index 9d97a5b4..dfae6811 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/WearableCustomizable.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Wearable/WearableCustomizable.cs @@ -30,18 +30,21 @@ public enum WearableCustomizableType [Serializable] public class WearableCustomizable { + public string name; public WearableCustomizableType type; - public List avatarRequiredToggles; + public float defaultValue; + public List avatarToggles; public List wearableToggles; - public List avatarRequiredBlendshapes; + public List avatarBlendshapes; public List wearableBlendshapes; public WearableCustomizable() { + name = null; type = WearableCustomizableType.Toggle; - avatarRequiredToggles = new List(); + avatarToggles = new List(); wearableToggles = new List(); - avatarRequiredBlendshapes = new List(); + avatarBlendshapes = new List(); wearableBlendshapes = new List(); } }