Fix : 여러 디테일 요소 버그 제거

This commit is contained in:
qsxft258@gmail.com 2026-04-16 22:28:08 +09:00
parent b4e680334c
commit 860be78b34
8 changed files with 192 additions and 42 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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);

View File

@ -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<CustomRetargetingScript>();
// 타겟들의 원본 머리 스케일 저장
// 소스 루트 스케일 저장
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)

View File

@ -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<MagicaCloth[]>();
var arr = new MagicaCloth[objs.Length][];
for (int i = 0; i < objs.Length; i++)
arr[i] = objs[i] != null
? objs[i].GetComponentsInChildren<MagicaCloth>(true)
: Array.Empty<MagicaCloth>();
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;
/// <summary>UI 등 상태 구독자용 통합 이벤트. 의상 스왑이 실제로 완료된 시점에 발생 (변신 연출 중에는 아직 발생하지 않음).</summary>
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();
}
}
/// <summary>모든 아바타의 모든 의상을 먼저 숨긴 뒤, 각 아바타의 currentOutfit 만 표시. 초기 씬 상태 정리용.</summary>
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();
}

View File

@ -127,6 +127,9 @@ public class CameraManager : MonoBehaviour, IController
#region Events
public delegate void CameraChangedEventHandler(CameraPreset oldPreset, CameraPreset newPreset);
public event CameraChangedEventHandler OnCameraChanged;
/// <summary>UI 등 상태 구독자용 통합 이벤트. 프리셋 전환/블렌드 토글/드론 모드 등 모든 상태 변경 후 발생.</summary>
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)

View File

@ -46,6 +46,9 @@ public class ItemController : MonoBehaviour, IController
#region Events
public delegate void ItemGroupChangedEventHandler(ItemGroup oldGroup, ItemGroup newGroup);
public event ItemGroupChangedEventHandler OnItemGroupChanged;
/// <summary>UI 등 상태 구독자용 통합 이벤트. 모든 그룹 활성/비활성/토글/추가/제거 후 발생.</summary>
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

View File

@ -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<string> log, Action<string> 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
// ================================================================
/// <summary>
/// 컨트롤러의 상태 변경 이벤트를 구독. 클릭 후 즉시 UI를 다시 그리는 대신
/// 실제 상태가 갱신된 시점에만 현재 카테고리를 다시 빌드한다.
/// (예: 아바타 변신 연출 코루틴 완료 시점, Cinemachine 프리셋 전환 후 등)
/// </summary>
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);
}
}