From 860be78b34d9406a94b649f5c26c54e964899e8f Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Thu, 16 Apr 2026 22:28:08 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EC=97=AC=EB=9F=AC=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=20=EC=9A=94=EC=86=8C=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StreamingleDashboard/dashboard_script.txt | 4 +- .../StreamingleDashboard/dashboard_style.txt | 4 +- .../Editor/RetargetingControlWindow.cs | 11 +++ .../KindRetargeting/SimplePoseTransfer.cs | 39 ++++++++- .../Controllers/AvatarOutfitController.cs | 86 +++++++++++++++++-- .../Controllers/CameraController.cs | 12 +-- .../Controllers/ItemController.cs | 4 + .../System/RuntimeControlPanelManager.cs | 74 +++++++++++----- 8 files changed, 192 insertions(+), 42 deletions(-) diff --git a/Assets/Resources/StreamingleDashboard/dashboard_script.txt b/Assets/Resources/StreamingleDashboard/dashboard_script.txt index ca560ccbe..1444f84d7 100644 --- a/Assets/Resources/StreamingleDashboard/dashboard_script.txt +++ b/Assets/Resources/StreamingleDashboard/dashboard_script.txt @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c22de38bea166ac9b7ba696261e8e2f2082d861d79f6c5d543e997e606120c69 -size 79122 +oid sha256:f2d0e24ccf0b33c83b1643a0b69655a9b758cebfe772af2677a06b41e9b90b34 +size 81111 diff --git a/Assets/Resources/StreamingleDashboard/dashboard_style.txt b/Assets/Resources/StreamingleDashboard/dashboard_style.txt index 04bbe41c2..1d9601939 100644 --- a/Assets/Resources/StreamingleDashboard/dashboard_style.txt +++ b/Assets/Resources/StreamingleDashboard/dashboard_style.txt @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85e8067eb6ef5159c0b6435b1e882eb8e987497e26277d04186aa9729ed3aaa3 -size 25339 +oid sha256:a922a5ff34a83973f11a2bbde759d03949cce0bdff3e31f0149904b7b6bc7f82 +size 25953 diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index a141fa345..70f61aaca 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -244,6 +244,17 @@ public class RetargetingControlWindow : EditorWindow var headScaleProp = so.FindProperty("headScale"); if (headScaleProp != null) scaleContainer.Add(new PropertyField(headScaleProp, "머리 크기")); + + // 아바타 크기 변경 시 다리 길이 자동 보정 (실시간) + scaleContainer.TrackPropertyValue(so.FindProperty("avatarScale"), _ => + { + if (!Application.isPlaying || script == null) return; + var sox = new SerializedObject(script); + sox.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script); + sox.ApplyModifiedProperties(); + sox.Dispose(); + }); + scaleContainer.Bind(so); scaleFoldout.Add(scaleContainer); panel.Add(scaleFoldout); diff --git a/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs b/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs index 1bc7ad77b..7bed289b4 100644 --- a/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs +++ b/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs @@ -6,11 +6,13 @@ using KindRetargeting; public class SimplePoseTransfer : MonoBehaviour { [System.Serializable] - public struct TargetEntry + public class TargetEntry { public Animator animator; [Tooltip("월드 공간 힙 오프셋")] public Vector3 hipOffset; + [Tooltip("소스의 루트 스케일 변화량을 타겟에도 적용")] + public bool syncRootScale = true; } [Header("Pose Transfer Settings")] @@ -32,6 +34,10 @@ public class SimplePoseTransfer : MonoBehaviour private CustomRetargetingScript sourceRetargetingScript; private Vector3[] originalTargetHeadScales; + // 루트 스케일 관련 + private Vector3 originalSourceRootScale; + private Vector3[] originalTargetRootScales; + private void Start() { Init(); @@ -83,13 +89,19 @@ public class SimplePoseTransfer : MonoBehaviour // 소스에서 CustomRetargetingScript 찾기 sourceRetargetingScript = sourceBone.GetComponent(); - // 타겟들의 원본 머리 스케일 저장 + // 소스 루트 스케일 저장 + originalSourceRootScale = sourceBone.transform.localScale; + + // 타겟들의 원본 머리/루트 스케일 저장 originalTargetHeadScales = new Vector3[targets.Count]; + originalTargetRootScales = new Vector3[targets.Count]; for (int i = 0; i < targets.Count; i++) { Animator animator = targets[i].animator; if (animator != null) { + originalTargetRootScales[i] = animator.transform.localScale; + Transform headBone = animator.GetBoneTransform(HumanBodyBones.Head); if (headBone != null) { @@ -100,6 +112,10 @@ public class SimplePoseTransfer : MonoBehaviour originalTargetHeadScales[i] = Vector3.one; } } + else + { + originalTargetRootScales[i] = Vector3.one; + } } } @@ -182,6 +198,12 @@ public class SimplePoseTransfer : MonoBehaviour // 루트 회전 동기화 targetAnimator.transform.rotation = sourceBone.transform.rotation; + // 루트 스케일 동기화 (타겟별 on/off) + if (entry.syncRootScale) + { + ApplyRootScale(targetIndex, targetAnimator.transform); + } + // 모든 본에 대해 포즈 전송 for (int i = 0; i < 55; i++) { @@ -208,6 +230,19 @@ public class SimplePoseTransfer : MonoBehaviour } } + private void ApplyRootScale(int targetIndex, Transform targetRoot) + { + // 소스의 초기 루트 스케일 대비 현재 스케일 비율을 타겟 원본 스케일에 곱해 적용 + Vector3 sourceCurrent = sourceBone.transform.localScale; + Vector3 sourceOriginal = originalSourceRootScale; + Vector3 ratio = new Vector3( + sourceOriginal.x != 0f ? sourceCurrent.x / sourceOriginal.x : 1f, + sourceOriginal.y != 0f ? sourceCurrent.y / sourceOriginal.y : 1f, + sourceOriginal.z != 0f ? sourceCurrent.z / sourceOriginal.z : 1f + ); + targetRoot.localScale = Vector3.Scale(originalTargetRootScales[targetIndex], ratio); + } + private void ApplyHeadScale(int targetIndex, Transform targetHeadBone) { if (sourceRetargetingScript != null) diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/AvatarOutfitController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/AvatarOutfitController.cs index 887847e27..0132940f1 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/AvatarOutfitController.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/AvatarOutfitController.cs @@ -7,6 +7,9 @@ using Streamingle; using Newtonsoft.Json; using NiloToon.NiloToonURP; using Streamingle.Effects; +#if MAGICACLOTH2 +using MagicaCloth2; +#endif public class AvatarOutfitController : MonoBehaviour, IController { @@ -89,22 +92,50 @@ public class AvatarOutfitController : MonoBehaviour, IController [Header("Transform Effect (이 의상으로 변경할 때 재생)")] public TransformEffectSettings transformEffect = new TransformEffectSettings(); +#if MAGICACLOTH2 + // NiloToon renderCharacter 토글 시 GameObject 가 Active 유지되어 MagicaCloth 가 계속 시뮬되는 문제 방지. + // BuildSimulationCache 로 1회 수집 후 Apply/Remove 에서 cloth.enabled 토글. + [NonSerialized] private MagicaCloth[][] _clothingSimCache; + [NonSerialized] private MagicaCloth[][] _hideSimCache; + [NonSerialized] private bool _simCacheBuilt; +#endif + + public void BuildSimulationCache() + { +#if MAGICACLOTH2 + if (_simCacheBuilt) return; + _clothingSimCache = CollectCloths(clothingObjects); + _hideSimCache = CollectCloths(hideObjects); + _simCacheBuilt = true; +#endif + } + public void ApplyOutfit() { LogToggle("Apply", clothingObjects, true, hideObjects, false); - foreach (var obj in clothingObjects) - SetRenderOrActive(obj, true); - foreach (var obj in hideObjects) - SetRenderOrActive(obj, false); + ApplyArrayState(clothingObjects, true, clothingSide: true); + ApplyArrayState(hideObjects, false, clothingSide: false); } public void RemoveOutfit() { LogToggle("Remove", clothingObjects, false, hideObjects, true); - foreach (var obj in clothingObjects) - SetRenderOrActive(obj, false); - foreach (var obj in hideObjects) - SetRenderOrActive(obj, true); + ApplyArrayState(clothingObjects, false, clothingSide: true); + ApplyArrayState(hideObjects, true, clothingSide: false); + } + + void ApplyArrayState(GameObject[] objs, bool value, bool clothingSide) + { + if (objs == null) return; + for (int i = 0; i < objs.Length; i++) + { + SetRenderOrActive(objs[i], value); +#if MAGICACLOTH2 + var cache = clothingSide ? _clothingSimCache : _hideSimCache; + if (cache != null && i < cache.Length) + SetClothsEnabled(cache[i], value); +#endif + } } void LogToggle(string op, GameObject[] a, bool av, GameObject[] b, bool bv) @@ -136,6 +167,29 @@ public class AvatarOutfitController : MonoBehaviour, IController else obj.SetActive(value); } + +#if MAGICACLOTH2 + static MagicaCloth[][] CollectCloths(GameObject[] objs) + { + if (objs == null) return Array.Empty(); + var arr = new MagicaCloth[objs.Length][]; + for (int i = 0; i < objs.Length; i++) + arr[i] = objs[i] != null + ? objs[i].GetComponentsInChildren(true) + : Array.Empty(); + return arr; + } + + static void SetClothsEnabled(MagicaCloth[] cloths, bool value) + { + if (cloths == null) return; + foreach (var cloth in cloths) + { + if (cloth != null && cloth.enabled != value) + cloth.enabled = value; + } + } +#endif } #endregion @@ -143,6 +197,9 @@ public class AvatarOutfitController : MonoBehaviour, IController #region Events public delegate void AvatarOutfitChangedEventHandler(AvatarData avatar, OutfitData oldOutfit, OutfitData newOutfit); public event AvatarOutfitChangedEventHandler OnAvatarOutfitChanged; + + /// UI 등 상태 구독자용 통합 이벤트. 의상 스왑이 실제로 완료된 시점에 발생 (변신 연출 중에는 아직 발생하지 않음). + public event System.Action OnStateChanged; #endregion #region Fields @@ -199,12 +256,24 @@ public class AvatarOutfitController : MonoBehaviour, IController if (avatars.Count > 0 && currentAvatar == null) currentAvatar = avatars[0]; + BuildAllSimulationCaches(); + if (syncOutfitStateOnAwake) SyncInitialOutfitState(); Debug.Log($"[AvatarOutfitController] 총 {avatars.Count}개의 아바타가 등록되었습니다."); } + private void BuildAllSimulationCaches() + { + foreach (var avatar in avatars) + { + if (avatar?.outfits == null) continue; + foreach (var outfit in avatar.outfits) + outfit?.BuildSimulationCache(); + } + } + /// 모든 아바타의 모든 의상을 먼저 숨긴 뒤, 각 아바타의 currentOutfit 만 표시. 초기 씬 상태 정리용. public void SyncInitialOutfitState() { @@ -401,6 +470,7 @@ public class AvatarOutfitController : MonoBehaviour, IController #region StreamDeck Integration private void NotifyAvatarOutfitChanged(AvatarData avatar) { + OnStateChanged?.Invoke(); if (streamDeckManager != null) streamDeckManager.NotifyAvatarOutfitChanged(); } diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs index 2205fab30..3e7da1e82 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs @@ -127,6 +127,9 @@ public class CameraManager : MonoBehaviour, IController #region Events public delegate void CameraChangedEventHandler(CameraPreset oldPreset, CameraPreset newPreset); public event CameraChangedEventHandler OnCameraChanged; + + /// UI 등 상태 구독자용 통합 이벤트. 프리셋 전환/블렌드 토글/드론 모드 등 모든 상태 변경 후 발생. + public event System.Action OnStateChanged; #endregion #region Fields @@ -360,6 +363,7 @@ public class CameraManager : MonoBehaviour, IController private void NotifyStreamDeckCameraStateChanged() { + OnStateChanged?.Invoke(); if (streamDeckManager != null) { streamDeckManager.NotifyCameraChanged(); @@ -887,12 +891,8 @@ public class CameraManager : MonoBehaviour, IController } } - // 스트림덱에 카메라 변경 알림 전송 - if (streamDeckManager != null) - { - streamDeckManager.NotifyCameraChanged(); - } - + // 스트림덱 / UI 등 모든 구독자에게 상태 변경 알림 + NotifyStreamDeckCameraStateChanged(); } private bool ValidateCameraIndex(int index) diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs index f42aef371..d3796e71c 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs @@ -46,6 +46,9 @@ public class ItemController : MonoBehaviour, IController #region Events public delegate void ItemGroupChangedEventHandler(ItemGroup oldGroup, ItemGroup newGroup); public event ItemGroupChangedEventHandler OnItemGroupChanged; + + /// UI 등 상태 구독자용 통합 이벤트. 모든 그룹 활성/비활성/토글/추가/제거 후 발생. + public event System.Action OnStateChanged; #endregion #region Fields @@ -225,6 +228,7 @@ public class ItemController : MonoBehaviour, IController #region StreamDeck Integration private void NotifyItemChanged() { + OnStateChanged?.Invoke(); if (streamDeckManager != null) { var updateMessage = new diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/RuntimeControlPanelManager.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/RuntimeControlPanelManager.cs index eebb9af60..060e0032a 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/RuntimeControlPanelManager.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/RuntimeControlPanelManager.cs @@ -42,6 +42,12 @@ public class RuntimeControlPanelManager private bool initStarted; + // 컨트롤러 상태 변경 이벤트 구독 여부 (최초 manager 바인딩 시 한 번만 연결) + private bool controllersSubscribed; + private Action cameraStateHandler; + private Action itemStateHandler; + private Action avatarStateHandler; + public void Initialize(Transform parent, Action log, Action logError) { this.log = log; @@ -149,6 +155,7 @@ public class RuntimeControlPanelManager { manager = StreamDeckServerManager.Instance; if (manager == null) return; + SubscribeControllerEvents(); SwitchCategory(currentCategory); } @@ -169,6 +176,45 @@ public class RuntimeControlPanelManager SwitchCategory(currentCategory); } + // ================================================================ + // Controller State Subscription + // ================================================================ + + /// + /// 컨트롤러의 상태 변경 이벤트를 구독. 클릭 후 즉시 UI를 다시 그리는 대신 + /// 실제 상태가 갱신된 시점에만 현재 카테고리를 다시 빌드한다. + /// (예: 아바타 변신 연출 코루틴 완료 시점, Cinemachine 프리셋 전환 후 등) + /// + private void SubscribeControllerEvents() + { + if (controllersSubscribed || manager == null) return; + + if (manager.cameraManager != null) + { + cameraStateHandler = () => OnControllerStateChanged("camera"); + manager.cameraManager.OnStateChanged += cameraStateHandler; + } + if (manager.itemController != null) + { + itemStateHandler = () => OnControllerStateChanged("item"); + manager.itemController.OnStateChanged += itemStateHandler; + } + if (manager.avatarOutfitController != null) + { + avatarStateHandler = () => OnControllerStateChanged("avatar"); + manager.avatarOutfitController.OnStateChanged += avatarStateHandler; + } + + controllersSubscribed = true; + } + + private void OnControllerStateChanged(string sourceCategory) + { + if (!isInitialized || !isVisible) return; + if (currentCategory != sourceCategory) return; + SwitchCategory(currentCategory); + } + // ================================================================ // Category Switching // ================================================================ @@ -222,11 +268,7 @@ public class RuntimeControlPanelManager var blendBtn = MakeButton( $"Blend: {styleName} ▶ {(isCut ? "EaseInOut" : "Cut")}로 전환", isCut ? "action-btn--secondary" : "action-btn--success"); - blendBtn.clicked += () => - { - cam.ToggleDefaultBlend(); - SwitchCategory("camera"); - }; + blendBtn.clicked += () => cam.ToggleDefaultBlend(); blendRow.Add(blendBtn); actionList.Add(blendRow); @@ -239,11 +281,7 @@ public class RuntimeControlPanelManager var btn = MakeButton("Switch", preset.isActive ? "action-btn--success" : null); int idx = preset.index; - btn.clicked += () => - { - cam.Set(idx); - SwitchCategory("camera"); - }; + btn.clicked += () => cam.Set(idx); item.Add(btn); actionList.Add(item); } @@ -270,11 +308,11 @@ public class RuntimeControlPanelManager topRow.AddToClassList("action-row"); var allOnBtn = MakeButton("All On", "action-btn--success"); - allOnBtn.clicked += () => { ctrl.ActivateAllGroups(); SwitchCategory("item"); }; + allOnBtn.clicked += () => ctrl.ActivateAllGroups(); topRow.Add(allOnBtn); var allOffBtn = MakeButton("All Off", "action-btn--secondary"); - allOffBtn.clicked += () => { ctrl.DeactivateAllGroups(); SwitchCategory("item"); }; + allOffBtn.clicked += () => ctrl.DeactivateAllGroups(); topRow.Add(allOffBtn); actionList.Add(topRow); @@ -295,11 +333,7 @@ public class RuntimeControlPanelManager var btn = MakeButton("Toggle"); int idx = itemData.index; - btn.clicked += () => - { - ctrl.ToggleGroup(idx); - SwitchCategory("item"); - }; + btn.clicked += () => ctrl.ToggleGroup(idx); item.Add(btn); actionList.Add(item); } @@ -378,11 +412,7 @@ public class RuntimeControlPanelManager int avatarIdx = avatar.index; int outfitIdx = outfit.index; - btn.clicked += () => - { - ctrl.SetAvatarOutfit(avatarIdx, outfitIdx); - SwitchCategory("avatar"); - }; + btn.clicked += () => ctrl.SetAvatarOutfit(avatarIdx, outfitIdx); outfitRow.Add(btn); } }