diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs index c6ff78bd..a2c4b5f4 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs @@ -18,9 +18,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Chocopoi.AvatarLib.Animations; +using Chocopoi.DressingTools.Lib; using Chocopoi.DressingTools.Lib.Cabinet; using Chocopoi.DressingTools.Lib.Cabinet.Modules; using Chocopoi.DressingTools.Lib.Extensibility.Providers; +using Chocopoi.DressingTools.Lib.Logging; using Chocopoi.DressingTools.Lib.Proxy; using Chocopoi.DressingTools.Lib.Wearable; using Chocopoi.DressingTools.Lib.Wearable.Modules; @@ -35,6 +38,8 @@ internal class DTEditorUtils { private const string BoneNameMappingsPath = "Packages/com.chocopoi.vrc.dressingtools/Resources/boneNameMappings.json"; + private const string PreviewAvatarNamePrefix = "DTPreview_"; + private static Dictionary s_reflectionTypeCache = new Dictionary(); private static readonly System.Random Random = new System.Random(); @@ -649,5 +654,142 @@ public static string RandomString(int length) return new string(Enumerable.Repeat(chars, length) .Select(s => s[Random.Next(s.Length)]).ToArray()); } + + public static bool PreviewActive { get; private set; } + + public static void CleanUpPreviewAvatars() + { + PreviewActive = false; + // remove all existing preview objects; + GameObject[] allObjects = Object.FindObjectsOfType(); + foreach (var obj in allObjects) + { + if (obj != null && obj.name.StartsWith(PreviewAvatarNamePrefix)) + { + Object.DestroyImmediate(obj); + } + } + } + + public static void UpdatePreviewAvatar(GameObject targetAvatar, WearableConfig newWearableConfig, GameObject newWearable) + { + PreviewAvatar(targetAvatar, newWearable, out var previewAvatar, out var previewWearable); + + if (previewAvatar == null || previewWearable == null) + { + return; + } + + var cabinet = GetAvatarCabinet(previewAvatar); + + if (cabinet == null) + { + return; + } + + if (!CabinetConfig.TryDeserialize(cabinet.configJson, out var cabinetConfig)) + { + Debug.LogError("[DressingTools] Unable to deserialize cabinet config for preview"); + return; + } + + var report = new DTReport(); + var cabCtx = new ApplyCabinetContext() + { + report = report, + cabinetConfig = cabinetConfig, + avatarGameObject = previewAvatar, + avatarDynamics = ScanDynamics(previewAvatar, true), + wearableContexts = new Dictionary() + }; + + var providers = WearableModuleProviderLocator.Instance.GetAllProviders(); + foreach (var provider in providers) + { + var wearCtx = new ApplyWearableContext() + { + wearableConfig = newWearableConfig, + wearableGameObject = previewWearable, + wearableDynamics = ScanDynamics(previewWearable) + }; + + var module = FindWearableModule(newWearableConfig, provider.ModuleIdentifier); + if (!provider.OnPreviewWearable(cabCtx, wearCtx, module)) + { + Debug.LogError("[DressingTools] Error applying wearable in preview!"); + return; + } + } + } + + public static void PreviewAvatar(GameObject targetAvatar, GameObject targetWearable, out GameObject previewAvatar, out GameObject previewWearable) + { + if (targetAvatar == null || targetWearable == null) + { + CleanUpPreviewAvatars(); + PreviewActive = false; + previewAvatar = null; + previewWearable = null; + return; + } + + var objName = PreviewAvatarNamePrefix + targetAvatar.name; + previewAvatar = GameObject.Find(objName); + + // find path of wearable + var path = IsGrandParent(targetAvatar.transform, targetWearable.transform) ? + AnimationUtils.GetRelativePath(targetWearable.transform, targetAvatar.transform) : + targetWearable.name; + + // return existing preview object if any + if (previewAvatar != null) + { + var wearableTransform = previewAvatar.transform.Find(path); + + // valid preview + if (wearableTransform != null) + { + PreviewActive = true; + previewWearable = wearableTransform.gameObject; + return; + } + + // recreate the preview + } + + // clean up and recreate + CleanUpPreviewAvatars(); + + // create a copy of the avatar and wearable + previewAvatar = Object.Instantiate(targetAvatar); + previewAvatar.name = objName; + + var newPos = previewAvatar.transform.position; + newPos.x -= 20; + previewAvatar.transform.position = newPos; + + // if wearable is not inside avatar, we instantiate a new copy + if (!IsGrandParent(targetAvatar.transform, targetWearable.transform)) + { + previewWearable = Object.Instantiate(targetWearable); + previewWearable.transform.position = newPos; + previewWearable.transform.SetParent(previewAvatar.transform); + } + else + { + previewWearable = previewAvatar.transform.Find(path).gameObject; + } + + // select in sceneview + FocusGameObjectInSceneView(previewAvatar); + + PreviewActive = true; + } + + public static void FocusGameObjectInSceneView(GameObject go) + { + Selection.activeGameObject = go; + SceneView.FrameLastActiveSceneView(); + } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/DressingPresenter.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/DressingPresenter.cs index 5150f938..3fd36e7a 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/DressingPresenter.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/DressingPresenter.cs @@ -121,6 +121,10 @@ private void OnAddToCabinetButtonClick() DTEditorUtils.AddCabinetWearable(cabinetConfig, _view.TargetAvatar, _view.Config, _view.TargetWearable); + // remove previews + DTEditorUtils.CleanUpPreviewAvatars(); + DTEditorUtils.FocusGameObjectInSceneView(_view.TargetAvatar); + // reset and return _view.ResetWizardAndConfigView(); _view.SelectTab(0); 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 1598e274..3620a961 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,7 +15,6 @@ * 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; @@ -25,6 +24,7 @@ using Chocopoi.DressingTools.Lib.Wearable; using Chocopoi.DressingTools.UIBase.Views; using Chocopoi.DressingTools.Wearable.Modules; +using UnityEditor; using UnityEngine; namespace Chocopoi.DressingTools.UI.Presenters.Modules @@ -264,6 +264,7 @@ private void UpdateToggles(Transform root, List toggles, List toggles, List bl blendshape.blendshapeName = blendshapeData.availableBlendshapeNames[blendshapeData.selectedBlendshapeIndex]; blendshape.value = blendshapeData.value; + _parentView.UpdateAvatarPreview(); } else { @@ -352,6 +355,7 @@ private void UpdateBlendshapes(Transform root, List bl { blendshapes.Remove(blendshape); blendshapeDataList.Remove(blendshapeData); + _parentView.UpdateAvatarPreview(); }; blendshapeDataList.Add(blendshapeData); @@ -390,11 +394,12 @@ private bool IsGameObjectUsedInToggles(GameObject go, List toggleDat private void UpdateAvatarOnWearToggleSuggestions(PresetViewData presetData) { + presetData.toggleSuggestions.Clear(); + var targetAvatar = _parentView.TargetAvatar; var targetWearable = _parentView.TargetWearable; - presetData.toggleSuggestions.Clear(); - if (_cabinetConfig != null) + if (targetAvatar != null && targetWearable != null && _cabinetConfig != null) { var armatureName = _cabinetConfig.avatarArmatureName; var avatarTrans = targetAvatar.transform; @@ -417,6 +422,7 @@ private void UpdateAvatarOnWearToggleSuggestions(PresetViewData presetData) state = !childTrans.gameObject.activeSelf }); UpdateAnimationGenerationAvatarOnWear(); + _parentView.UpdateAvatarPreview(); } }; presetData.toggleSuggestions.Add(toggleSuggestion); @@ -427,9 +433,15 @@ private void UpdateAvatarOnWearToggleSuggestions(PresetViewData presetData) private void UpdateWearableOnWearToggleSuggestions(PresetViewData presetData) { + presetData.toggleSuggestions.Clear(); + var targetWearable = _parentView.TargetWearable; - presetData.toggleSuggestions.Clear(); + if (targetWearable != null) + { + return; + } + var wearableTrans = targetWearable.transform; // TODO: we can't obtain wearable armature name here, listing everything at the root for now @@ -438,7 +450,7 @@ private void UpdateWearableOnWearToggleSuggestions(PresetViewData presetData) for (var i = 0; i < wearableTrans.childCount; i++) { var childTrans = wearableTrans.GetChild(i); - if (childTrans != targetWearable.transform && !IsGameObjectUsedInToggles(childTrans.gameObject, presetData.toggles)) + if (!IsGameObjectUsedInToggles(childTrans.gameObject, presetData.toggles)) { var toggleSuggestion = new ToggleSuggestionData { @@ -452,6 +464,7 @@ private void UpdateWearableOnWearToggleSuggestions(PresetViewData presetData) state = !childTrans.gameObject.activeSelf }); UpdateAnimationGenerationWearableOnWear(); + _parentView.UpdateAvatarPreview(); } }; presetData.toggleSuggestions.Add(toggleSuggestion); diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/WearableSetupWizardPresenter.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/WearableSetupWizardPresenter.cs index e835a68a..fb472701 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/WearableSetupWizardPresenter.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Presenters/WearableSetupWizardPresenter.cs @@ -45,6 +45,7 @@ private void SubscribeEvents() _view.TargetAvatarOrWearableChange += OnTargetAvatarOrWearableChange; _view.PreviousButtonClick += OnPreviousButtonClick; _view.NextButtonClick += OnNextButtonClick; + _view.PreviewButtonClick += OnPreviewButtonClick; } private void UnsubscribeEvents() @@ -56,6 +57,7 @@ private void UnsubscribeEvents() _view.TargetAvatarOrWearableChange -= OnTargetAvatarOrWearableChange; _view.PreviousButtonClick -= OnPreviousButtonClick; _view.NextButtonClick -= OnNextButtonClick; + _view.PreviewButtonClick -= OnPreviewButtonClick; } private void OnForceUpdateView() @@ -69,6 +71,25 @@ private void OnTargetAvatarOrWearableChange() AutoSetup(); } + private void OnPreviewButtonClick() + { + if (_view.PreviewActive) + { + DTEditorUtils.CleanUpPreviewAvatars(); + DTEditorUtils.FocusGameObjectInSceneView(_view.TargetAvatar); + } + else + { + UpdateAvatarPreview(); + } + } + + public void UpdateAvatarPreview() + { + GenerateConfig(); + DTEditorUtils.UpdatePreviewAvatar(_view.TargetAvatar, _view.Config, _view.TargetWearable); + } + private void AutoSetupMapping() { // cabinet diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableConfigView.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableConfigView.cs index 25619744..7de9061a 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableConfigView.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableConfigView.cs @@ -218,5 +218,10 @@ public override void OnGUI() } public bool IsValid() => _presenter.IsValid(); + + public void UpdateAvatarPreview() + { + throw new NotImplementedException(); + } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableSetupWizardView.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableSetupWizardView.cs index d034f6be..481a60e5 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableSetupWizardView.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UI/Views/WearableSetupWizardView.cs @@ -31,9 +31,12 @@ namespace Chocopoi.DressingTools.UI.Views [ExcludeFromCodeCoverage] internal class WearableSetupWizardView : EditorViewBase, IWearableSetupWizardView { + private static readonly Color PreviewButtonActiveColour = new Color(0.5f, 1, 0.5f, 1); + public event Action TargetAvatarOrWearableChange { add { _dressingSubView.TargetAvatarOrWearableChange += value; } remove { _dressingSubView.TargetAvatarOrWearableChange -= value; } } public event Action PreviousButtonClick; public event Action NextButtonClick; + public event Action PreviewButtonClick; public ArmatureMappingWearableModuleConfig ArmatureMappingModuleConfig { get; set; } public MoveRootWearableModuleConfig MoveRootModuleConfig { get; set; } @@ -55,7 +58,7 @@ internal class WearableSetupWizardView : EditorViewBase, IWearableSetupWizardVie public bool ShowArmatureNotFoundHelpBox { get; set; } public bool ShowArmatureGuessedHelpBox { get; set; } public bool ShowCabinetConfigErrorHelpBox { get; set; } - + public bool PreviewActive => DTEditorUtils.PreviewActive; private WearableSetupWizardPresenter _presenter; private IDressingSubView _dressingSubView; @@ -181,6 +184,13 @@ private void DrawAnimateStep() EndDisabled(); } + private void PreviewButton() + { + if (PreviewActive) GUI.backgroundColor = PreviewButtonActiveColour; + Button("Preview", PreviewButtonClick, GUILayout.ExpandWidth(false)); + GUI.backgroundColor = Color.white; + } + public override void OnGUI() { Toolbar(ref _currentStep, new string[] { " 1.\nMapping", "2.\nAnimate", "3.\nIntegrate", "4.\nOptimize" }); @@ -195,6 +205,7 @@ public override void OnGUI() } EndDisabled(); GUILayout.FlexibleSpace(); + PreviewButton(); Button(CurrentStep == 3 ? "Finish!" : "Next >", NextButtonClick); } EndHorizontal(); @@ -234,5 +245,7 @@ public override void OnGUI() HelpBox("Optimization wizard not implemented", MessageType.Info); } } + + public void UpdateAvatarPreview() => _presenter.UpdateAvatarPreview(); } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/IWearableSetupWizardView.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/IWearableSetupWizardView.cs index cad46378..2c7e6b17 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/IWearableSetupWizardView.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/UIBase/Views/IWearableSetupWizardView.cs @@ -27,6 +27,7 @@ internal interface IWearableSetupWizardView : IEditorView, IWearableModuleEditor { event Action PreviousButtonClick; event Action NextButtonClick; + event Action PreviewButtonClick; WearableConfig Config { get; set; } ArmatureMappingWearableModuleConfig ArmatureMappingModuleConfig { get; set; } @@ -46,6 +47,7 @@ internal interface IWearableSetupWizardView : IEditorView, IWearableModuleEditor bool ShowArmatureNotFoundHelpBox { get; set; } bool ShowArmatureGuessedHelpBox { get; set; } bool ShowCabinetConfigErrorHelpBox { get; set; } + bool PreviewActive { get; } void GenerateConfig(); void RaiseDoAddToCabinetEvent(); diff --git a/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/AnimationGenerationWearableModule.cs b/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/AnimationGenerationWearableModule.cs index f67c49d1..e7ca2b4a 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/AnimationGenerationWearableModule.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Editor/Wearable/Modules/AnimationGenerationWearableModule.cs @@ -156,6 +156,55 @@ public override bool OnAfterApplyCabinet(ApplyCabinetContext cabCtx) return true; } + + public override bool OnPreviewWearable(ApplyCabinetContext cabCtx, ApplyWearableContext wearCtx, WearableModule module) + { + if (module == null) + { + return true; + } + + var agm = (AnimationGenerationWearableModuleConfig)module.config; + + ApplyAnimationPreset(cabCtx.avatarGameObject, agm.avatarAnimationOnWear); + ApplyAnimationPreset(wearCtx.wearableGameObject, agm.wearableAnimationOnWear); + + return true; + } + + private void ApplyAnimationPreset(GameObject go, AnimationPreset preset) + { + foreach (var toggle in preset.toggles) + { + var obj = go.transform.Find(toggle.path); + if (obj != null) + { + obj.gameObject.SetActive(toggle.state); + } + } + + foreach (var blendshape in preset.blendshapes) + { + var obj = go.transform.Find(blendshape.path); + if (obj != null) + { + var smr = obj.GetComponent(); + if (smr == null || smr.sharedMesh == null) + { + continue; + } + + var index = smr.sharedMesh.GetBlendShapeIndex(blendshape.blendshapeName); + + if (index == -1) + { + continue; + } + + smr.SetBlendShapeWeight(index, blendshape.value); + } + } + } } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Extensibility/Providers/WearableModuleProviderBase.cs b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Extensibility/Providers/WearableModuleProviderBase.cs index aa1ec127..582936cd 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Extensibility/Providers/WearableModuleProviderBase.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/Extensibility/Providers/WearableModuleProviderBase.cs @@ -29,6 +29,7 @@ public abstract class WearableModuleProviderBase : ModuleProviderBase public virtual bool OnBeforeApplyCabinet(ApplyCabinetContext ctx) => true; public virtual bool OnAfterApplyCabinet(ApplyCabinetContext ctx) => true; public virtual bool OnApplyWearable(ApplyCabinetContext cabCtx, ApplyWearableContext wearCtx, WearableModule moduleConfig) => true; + public virtual bool OnPreviewWearable(ApplyCabinetContext cabCtx, ApplyWearableContext wearCtx, WearableModule moduleConfig) => true; public virtual bool OnAddWearableToCabinet(CabinetConfig cabinetConfig, GameObject avatarGameObject, WearableConfig wearableConfig, GameObject wearableGameObject, WearableModule module) => true; } } diff --git a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/UI/IWearableModuleEditorViewParent.cs b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/UI/IWearableModuleEditorViewParent.cs index e7d486cb..1fe1d4d3 100644 --- a/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/UI/IWearableModuleEditorViewParent.cs +++ b/Packages/com.chocopoi.vrc.dressingtools/Lib/Editor/UI/IWearableModuleEditorViewParent.cs @@ -25,5 +25,6 @@ public interface IWearableModuleEditorViewParent : IEditorView event Action TargetAvatarOrWearableChange; GameObject TargetAvatar { get; } GameObject TargetWearable { get; } + void UpdateAvatarPreview(); } }