From 52d69607109cfb88125f6029f99413e013622195 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:00:35 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Refactor=20:=20ShoulderCorrectionFunction?= =?UTF-8?q?=EC=9D=84=20Serializable=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RequireComponent 6개 제거 (모듈화 준비) - ShoulderCorrectionFunction: MonoBehaviour → [Serializable] 클래스 - Start() → Initialize(Animator), Update() → OnUpdate() - CustomRetargetingScript에서 shoulderCorrection 필드로 소유 및 호출 Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 16 ++++-- .../ShoulderCorrectionFunction.cs | 56 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index c704d952d..198793b24 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -9,12 +9,6 @@ namespace KindRetargeting /// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다. /// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다. /// - [RequireComponent(typeof(LimbWeightController))] - [RequireComponent(typeof(ShoulderCorrectionFunction))] - [RequireComponent(typeof(TwoBoneIKSolver))] - [RequireComponent(typeof(FootGroundingController))] - [RequireComponent(typeof(PropLocationController))] - [RequireComponent(typeof(FingerShapedController))] [DefaultExecutionOrder(1)] public class CustomRetargetingScript : MonoBehaviour { @@ -101,6 +95,9 @@ namespace KindRetargeting [HideInInspector] public Vector3 tPoseHeadForward = Vector3.forward; [HideInInspector] public Vector3 tPoseHeadUp = Vector3.up; + [Header("어깨 보정")] + [SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction(); + [Header("아바타 크기 조정")] [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; private float previousScale = 1f; @@ -324,6 +321,10 @@ namespace KindRetargeting Debug.LogWarning("[CustomRetargetingScript] 머리 본을 찾을 수 없습니다!"); } } + + // 어깨 보정 모듈 초기화 + if (targetAnimator != null) + shoulderCorrection.Initialize(targetAnimator); } /// @@ -821,6 +822,9 @@ namespace KindRetargeting break; } + // 어깨 보정 (기존 ExecutionOrder 3) + shoulderCorrection.OnUpdate(); + // 스케일 변경 확인 및 적용 if (!Mathf.Approximately(previousScale, avatarScale)) { diff --git a/Assets/Scripts/KindRetargeting/ShoulderCorrectionFunction.cs b/Assets/Scripts/KindRetargeting/ShoulderCorrectionFunction.cs index daa61e9e5..434a0f70a 100644 --- a/Assets/Scripts/KindRetargeting/ShoulderCorrectionFunction.cs +++ b/Assets/Scripts/KindRetargeting/ShoulderCorrectionFunction.cs @@ -2,22 +2,20 @@ using UnityEngine; namespace KindRetargeting { - [DefaultExecutionOrder(3)] - public class ShoulderCorrectionFunction : MonoBehaviour + [System.Serializable] + public class ShoulderCorrectionFunction { - private CustomRetargetingScript retargetingScript; // 소스 데이터를 가져올 리타게팅 스크립트 - [Header("설정")] [Range(0f, 5f)] - public float blendStrength = 2f; // 전체적인 보정 강도 + public float blendStrength = 2f; [Range(0f, 1f)] - public float maxShoulderBlend = 0.7f; // 어깨에 최대로 전달될 수 있는 회전 비율 - public bool reverseLeftRotation = false; // 왼쪽 어깨 회전 방향 반전 설정 - public bool reverseRightRotation = false; // 오른쪽 어깨 회전 방향 반전 설정 + public float maxShoulderBlend = 0.7f; + public bool reverseLeftRotation = false; + public bool reverseRightRotation = false; [Header("높이 제한 설정")] - public float maxHeightDifference = 0.8f; // 최대 높이 차이 - public float minHeightDifference = -0.1f; // 최소 높이 차이 (이 값 이하에서는 보정하지 않음) + public float maxHeightDifference = 0.8f; + public float minHeightDifference = -0.1f; [Header("보정 커브 설정")] public AnimationCurve shoulderCorrectionCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f); @@ -29,26 +27,24 @@ namespace KindRetargeting private Transform rightShoulder; private Transform leftUpperArm; private Transform rightUpperArm; - // 최적화: 팔꿈치(LowerArm) Transform 캐싱 추가 private Transform leftLowerArm; private Transform rightLowerArm; - private void Start() + public void Initialize(Animator targetAnimator) { - retargetingScript = GetComponent(); - - leftShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder); - rightShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder); - leftUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm); - rightUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm); - // 최적화: 팔꿈치 Transform도 Start에서 캐싱 - leftLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm); - rightLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm); + leftShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder); + rightShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder); + leftUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm); + rightUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm); + leftLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm); + rightLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm); } - private void Update() + public void OnUpdate() { - // 왼쪽 어깨 보정 (최적화: 캐싱된 Transform 사용) + if (leftShoulder == null || rightShoulder == null) return; + + // 왼쪽 어깨 보정 Vector3 leftElbowPos = leftLowerArm.position; float leftHeightDiff = leftElbowPos.y - leftShoulder.position.y; float leftRawBlend = Mathf.Clamp01( @@ -56,7 +52,7 @@ namespace KindRetargeting ); leftBlendWeight = shoulderCorrectionCurve.Evaluate(leftRawBlend) * maxShoulderBlend; - // 오른쪽 어깨 보정 (최적화: 캐싱된 Transform 사용) + // 오른쪽 어깨 보정 Vector3 rightElbowPos = rightLowerArm.position; float rightHeightDiff = rightElbowPos.y - rightShoulder.position.y; float rightRawBlend = Mathf.Clamp01( @@ -69,12 +65,12 @@ namespace KindRetargeting { Quaternion currentWorldShoulderRot = leftShoulder.rotation; Quaternion currentWorldArmRot = leftUpperArm.rotation; - + Vector3 shoulderToArm = (leftUpperArm.position - leftShoulder.position).normalized; - Quaternion targetRotation = Quaternion.FromToRotation(leftShoulder.forward, + Quaternion targetRotation = Quaternion.FromToRotation(leftShoulder.forward, reverseLeftRotation ? shoulderToArm : -shoulderToArm); Quaternion targetWorldShoulderRot = targetRotation * currentWorldShoulderRot; - + leftShoulder.rotation = Quaternion.Lerp(currentWorldShoulderRot, targetWorldShoulderRot, leftBlendWeight); leftUpperArm.rotation = currentWorldArmRot; } @@ -83,12 +79,12 @@ namespace KindRetargeting { Quaternion currentWorldShoulderRot = rightShoulder.rotation; Quaternion currentWorldArmRot = rightUpperArm.rotation; - + Vector3 shoulderToArm = (rightUpperArm.position - rightShoulder.position).normalized; - Quaternion targetRotation = Quaternion.FromToRotation(rightShoulder.forward, + Quaternion targetRotation = Quaternion.FromToRotation(rightShoulder.forward, reverseRightRotation ? -shoulderToArm : shoulderToArm); Quaternion targetWorldShoulderRot = targetRotation * currentWorldShoulderRot; - + rightShoulder.rotation = Quaternion.Lerp(currentWorldShoulderRot, targetWorldShoulderRot, rightBlendWeight); rightUpperArm.rotation = currentWorldArmRot; } From 5c65185a61df5dc5f450e93622f88a4718c65e95 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:03:06 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Refactor=20:=20PropLocationController?= =?UTF-8?q?=EB=A5=BC=20Serializable=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PropLocationController: MonoBehaviour → [Serializable] 클래스 - Start() → Initialize(Animator), GetComponent 제거 - CRS에서 propLocation 필드로 소유 및 초기화 - RetargetingControlWindow: GetComponent → script.propLocation 직접 접근 - PropLocationControllerEditor 삭제 (MonoBehaviour 아니므로 불필요) Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 7 + .../Editor/PropLocationControllerEditor.cs | 167 ------------------ .../PropLocationControllerEditor.cs.meta | 11 -- .../Editor/RetargetingControlWindow.cs | 17 +- .../KindRetargeting/PropLocationController.cs | 25 ++- 5 files changed, 19 insertions(+), 208 deletions(-) delete mode 100644 Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs delete mode 100644 Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs.meta diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 198793b24..274d82d7e 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -98,6 +98,9 @@ namespace KindRetargeting [Header("어깨 보정")] [SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction(); + [Header("프랍 부착")] + [SerializeField] public PropLocationController propLocation = new PropLocationController(); + [Header("아바타 크기 조정")] [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; private float previousScale = 1f; @@ -325,6 +328,10 @@ namespace KindRetargeting // 어깨 보정 모듈 초기화 if (targetAnimator != null) shoulderCorrection.Initialize(targetAnimator); + + // 프랍 부착 모듈 초기화 + if (targetAnimator != null) + propLocation.Initialize(targetAnimator); } /// diff --git a/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs b/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs deleted file mode 100644 index acb336690..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs +++ /dev/null @@ -1,167 +0,0 @@ -using UnityEngine; -using UnityEditor; -using UnityEngine.UIElements; -using UnityEditor.UIElements; - -namespace KindRetargeting -{ - [CustomEditor(typeof(PropLocationController))] - public class PropLocationControllerEditor : BaseRetargetingEditor - { - private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - - private VisualElement offsetContainer; - private VisualElement propListContainer; - private PropLocationController controller; - - public override VisualElement CreateInspectorGUI() - { - controller = target as PropLocationController; - if (controller == null) return new VisualElement(); - var root = new VisualElement(); - - var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); - if (commonUss != null) root.styleSheets.Add(commonUss); - - // 기본 프로퍼티 - var iterator = serializedObject.GetIterator(); - iterator.NextVisible(true); // skip m_Script - while (iterator.NextVisible(false)) - { - var field = new PropertyField(iterator.Copy()); - root.Add(field); - } - - // 오프셋 조정 섹션 - root.Add(new Label("오프셋 조정") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } }); - offsetContainer = new VisualElement(); - root.Add(offsetContainer); - - // 부착된 프랍 목록 섹션 - root.Add(new Label("부착된 프랍 목록") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } }); - propListContainer = new VisualElement(); - root.Add(propListContainer); - - // 프랍 위치 이동 버튼 - root.Add(new Label("프랍 위치 이동") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } }); - var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } }; - - var headBtn = new Button(() => controller.MoveToHead()) { text = "머리로 이동" }; - headBtn.style.flexGrow = 1; headBtn.style.height = 30; headBtn.style.marginRight = 2; - btnRow.Add(headBtn); - - var leftBtn = new Button(() => controller.MoveToLeftHand()) { text = "왼손으로 이동" }; - leftBtn.style.flexGrow = 1; leftBtn.style.height = 30; leftBtn.style.marginRight = 2; - btnRow.Add(leftBtn); - - var rightBtn = new Button(() => controller.MoveToRightHand()) { text = "오른손으로 이동" }; - rightBtn.style.flexGrow = 1; rightBtn.style.height = 30; - btnRow.Add(rightBtn); - - root.Add(btnRow); - - var detachBtn = new Button(() => - { - if (Selection.activeGameObject != null) - Undo.RecordObject(Selection.activeGameObject.transform, "Detach Prop"); - controller.DetachProp(); - }) { text = "프랍 해제" }; - detachBtn.style.height = 30; - detachBtn.style.marginTop = 4; - root.Add(detachBtn); - - // 주기적으로 동적 UI 갱신 - root.schedule.Execute(() => RebuildDynamicUI()).Every(500); - RebuildDynamicUI(); - - return root; - } - - private void RebuildDynamicUI() - { - if (controller == null) return; - RebuildOffsets(); - RebuildPropLists(); - } - - private void RebuildOffsets() - { - if (offsetContainer == null) return; - offsetContainer.Clear(); - - BuildOffsetSection(offsetContainer, "왼손 오프셋", controller.GetLeftHandOffset()); - BuildOffsetSection(offsetContainer, "오른손 오프셋", controller.GetRightHandOffset()); - BuildOffsetSection(offsetContainer, "머리 오프셋", controller.GetHeadOffset()); - } - - private void BuildOffsetSection(VisualElement parent, string label, Transform offset) - { - if (offset == null) return; - - var foldout = new Foldout { text = label, value = true }; - - var posField = new Vector3Field("위치") { value = offset.localPosition }; - posField.RegisterValueChangedCallback(evt => - { - Undo.RecordObject(offset, $"Update {label}"); - offset.localPosition = evt.newValue; - EditorUtility.SetDirty(offset); - }); - foldout.Add(posField); - - var rotField = new Vector3Field("회전") { value = offset.localRotation.eulerAngles }; - rotField.RegisterValueChangedCallback(evt => - { - Undo.RecordObject(offset, $"Update {label}"); - offset.localRotation = Quaternion.Euler(evt.newValue); - EditorUtility.SetDirty(offset); - }); - foldout.Add(rotField); - - parent.Add(foldout); - } - - private void RebuildPropLists() - { - if (propListContainer == null) return; - propListContainer.Clear(); - - BuildPropListSection(propListContainer, "머리 프랍", controller.GetHeadProps()); - BuildPropListSection(propListContainer, "왼손 프랍", controller.GetLeftHandProps()); - BuildPropListSection(propListContainer, "오른손 프랍", controller.GetRightHandProps()); - } - - private void BuildPropListSection(VisualElement parent, string title, GameObject[] propList) - { - var box = new VisualElement(); - box.style.backgroundColor = new Color(0, 0, 0, 0.1f); - box.style.borderTopLeftRadius = box.style.borderTopRightRadius = - box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4; - box.style.paddingTop = box.style.paddingBottom = - box.style.paddingLeft = box.style.paddingRight = 4; - box.style.marginBottom = 4; - - box.Add(new Label(title) { style = { unityFontStyleAndWeight = FontStyle.Bold } }); - - if (propList.Length > 0) - { - foreach (var prop in propList) - { - if (prop == null) continue; - var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 2 } }; - row.Add(new Label(prop.name) { style = { flexGrow = 1 } }); - var selectBtn = new Button(() => Selection.activeGameObject = prop) { text = "선택" }; - selectBtn.style.width = 60; - row.Add(selectBtn); - box.Add(row); - } - } - else - { - box.Add(new Label("부착된 프랍 없음") { style = { color = new Color(0.6f, 0.6f, 0.6f) } }); - } - - parent.Add(box); - } - } -} diff --git a/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs.meta b/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs.meta deleted file mode 100644 index 012ce72ed..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/PropLocationControllerEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a49ee1ae55b970e4c8ca00ccae5d6f97 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index 5c5cf6351..f00dce14a 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -148,8 +148,7 @@ public class RetargetingControlWindow : EditorWindow s.gameObject.AddComponent(); if (s.GetComponent() == null) s.gameObject.AddComponent(); - if (s.GetComponent() == null) - s.gameObject.AddComponent(); + // PropLocationController는 CRS 내부 모듈로 이동됨 EditorUtility.SetDirty(s.gameObject); } @@ -606,19 +605,7 @@ public class RetargetingControlWindow : EditorWindow private VisualElement BuildPropSection(CustomRetargetingScript script) { var foldout = new Foldout { text = "프랍 설정", value = false }; - var propController = script.GetComponent(); - if (propController == null) - { - foldout.Add(new HelpBox("PropLocationController가 없습니다.", HelpBoxMessageType.Warning)); - var addBtn = new Button(() => - { - script.gameObject.AddComponent(); - EditorUtility.SetDirty(script.gameObject); - RebuildCharacterPanels(); - }) { text = "PropLocationController 추가" }; - foldout.Add(addBtn); - return foldout; - } + var propController = script.propLocation; var dynamicContainer = new VisualElement(); foldout.Add(dynamicContainer); diff --git a/Assets/Scripts/KindRetargeting/PropLocationController.cs b/Assets/Scripts/KindRetargeting/PropLocationController.cs index b6048dfb2..492693eac 100644 --- a/Assets/Scripts/KindRetargeting/PropLocationController.cs +++ b/Assets/Scripts/KindRetargeting/PropLocationController.cs @@ -4,7 +4,8 @@ using UniHumanoid; namespace KindRetargeting { - public class PropLocationController : MonoBehaviour + [System.Serializable] + public class PropLocationController { // 캐시된 타겟과 오프셋 Transform [System.Serializable] @@ -18,9 +19,9 @@ namespace KindRetargeting [SerializeField] private TargetOffset rightHandTargetOffset; [SerializeField] private TargetOffset headTargetOffset; - private void Start() + public void Initialize(Animator animator) { - CreateTargets(); + CreateTargets(animator); } public void SetTPose(Animator animator) @@ -31,7 +32,6 @@ namespace KindRetargeting Avatar avatar = animator.avatar; Transform transform = animator.transform; - // HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용 var humanPoseClip = Resources.Load(HumanPoseClip.TPoseResourcePath); if (humanPoseClip != null) { @@ -43,9 +43,9 @@ namespace KindRetargeting Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); } } - private void CreateTargets() + + private void CreateTargets(Animator animator) { - Animator animator = GetComponent(); SetTPose(animator); // 왼손 타겟 및 오프셋 설정 @@ -54,7 +54,7 @@ namespace KindRetargeting { leftHandTargetOffset = new TargetOffset(); GameObject leftTarget = new GameObject("Left_Hand_Target"); - leftTarget.transform.parent = leftHandBone; // 왼손 본에 직접 부모 설정 + leftTarget.transform.parent = leftHandBone; leftHandTargetOffset.target = leftTarget.transform; leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f); leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f); @@ -63,7 +63,6 @@ namespace KindRetargeting leftOffset.transform.parent = leftTarget.transform; leftHandTargetOffset.offset = leftOffset.transform; - // 로컬 포지션과 로테이션 설정 leftHandTargetOffset.offset.localPosition = Vector3.zero; leftHandTargetOffset.offset.localRotation = Quaternion.identity; } @@ -74,7 +73,7 @@ namespace KindRetargeting { rightHandTargetOffset = new TargetOffset(); GameObject rightTarget = new GameObject("Right_Hand_Target"); - rightTarget.transform.parent = rightHandBone; // 오른손 본에 직접 부모 설정 + rightTarget.transform.parent = rightHandBone; rightHandTargetOffset.target = rightTarget.transform; rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f); rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f); @@ -83,7 +82,6 @@ namespace KindRetargeting rightOffset.transform.parent = rightTarget.transform; rightHandTargetOffset.offset = rightOffset.transform; - // 로컬 포지션과 로테이션 설정 rightHandTargetOffset.offset.localPosition = Vector3.zero; rightHandTargetOffset.offset.localRotation = Quaternion.identity; } @@ -94,7 +92,7 @@ namespace KindRetargeting { headTargetOffset = new TargetOffset(); GameObject headTarget = new GameObject("Head_Target"); - headTarget.transform.parent = headBone; // 머리 본에 직접 부모 설정 + headTarget.transform.parent = headBone; headTargetOffset.target = headTarget.transform; headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f); headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f); @@ -103,7 +101,6 @@ namespace KindRetargeting headOffset.transform.parent = headTarget.transform; headTargetOffset.offset = headOffset.transform; - // 기본 오프셋 설정 headTargetOffset.offset.localPosition = Vector3.zero; headTargetOffset.offset.localRotation = Quaternion.identity; } @@ -172,7 +169,6 @@ namespace KindRetargeting } } - // 에디터에서 사용할 메서드들 #if UNITY_EDITOR public void MoveToHead() { @@ -195,7 +191,6 @@ namespace KindRetargeting } #endif - // 오프셋 getter 메서드들 추가 public Transform GetLeftHandOffset() { return leftHandTargetOffset?.offset; @@ -239,4 +234,4 @@ namespace KindRetargeting return children.ToArray(); } } -} \ No newline at end of file +} From 64a2069b69668e778297bea16c5f61263f5ddfaf Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:04:55 +0900 Subject: [PATCH 03/10] =?UTF-8?q?Refactor=20:=20TwoBoneIKSolver=EB=A5=BC?= =?UTF-8?q?=20Serializable=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TwoBoneIKSolver: MonoBehaviour → [Serializable] 클래스 - Start()/Update() → Initialize(Animator)/OnUpdate() - CRS에서 ikSolver 필드로 소유 및 호출 - FootGroundingController/LimbWeightController: GetComponent → crs.ikSolver로 변경 Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 15 ++++----- .../FootGroundingController.cs | 3 +- .../KindRetargeting/LimbWeightController.cs | 3 +- .../KindRetargeting/TwoBoneIKSolver.cs | 31 +++---------------- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 274d82d7e..9ec19621e 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -19,7 +19,7 @@ namespace KindRetargeting [HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator // IK 컴포넌트 참조 - private TwoBoneIKSolver ikSolver; + [SerializeField] public TwoBoneIKSolver ikSolver = new TwoBoneIKSolver(); [Header("힙 위치 보정 (로컬 좌표계 기반)")] [SerializeField, Range(-1, 1)] @@ -269,8 +269,7 @@ namespace KindRetargeting // 설정 로드 LoadSettings(); - // IK 컴포넌트 참조 가져오기 - ikSolver = GetComponent(); + // IK 모듈은 InitializeIKJoints에서 초기화 // IK 타겟 생성 (무릎 시각화 오브젝트 포함) CreateIKTargets(); @@ -832,6 +831,9 @@ namespace KindRetargeting // 어깨 보정 (기존 ExecutionOrder 3) shoulderCorrection.OnUpdate(); + // IK 솔버 (기존 ExecutionOrder 6) + ikSolver.OnUpdate(); + // 스케일 변경 확인 및 적용 if (!Mathf.Approximately(previousScale, avatarScale)) { @@ -1158,11 +1160,6 @@ namespace KindRetargeting /// private void CreateIKTargets() { - // IK 컴포넌트 가져오기 또는 새로 추가 - ikSolver = GetComponent(); - if (ikSolver == null) - ikSolver = gameObject.AddComponent(); - ikSolver.animator = targetAnimator; // IK 타겟들을 담을 부모 오브젝트 생성 @@ -1194,7 +1191,7 @@ namespace KindRetargeting ikSolver.rightLeg.bendGoal = rightLegGoal.transform; // TwoBoneIKSolver 본 캐싱 초기화 - ikSolver.Initialize(); + ikSolver.Initialize(targetAnimator); } /// diff --git a/Assets/Scripts/KindRetargeting/FootGroundingController.cs b/Assets/Scripts/KindRetargeting/FootGroundingController.cs index a5fdd89b9..8a5908da2 100644 --- a/Assets/Scripts/KindRetargeting/FootGroundingController.cs +++ b/Assets/Scripts/KindRetargeting/FootGroundingController.cs @@ -67,7 +67,8 @@ namespace KindRetargeting private void Start() { - ikSolver = GetComponent(); + var crs = GetComponent(); + if (crs != null) ikSolver = crs.ikSolver; animator = GetComponent(); if (animator == null || !animator.isHuman || ikSolver == null) return; diff --git a/Assets/Scripts/KindRetargeting/LimbWeightController.cs b/Assets/Scripts/KindRetargeting/LimbWeightController.cs index c5ce81e6b..977b12a2e 100644 --- a/Assets/Scripts/KindRetargeting/LimbWeightController.cs +++ b/Assets/Scripts/KindRetargeting/LimbWeightController.cs @@ -96,9 +96,8 @@ namespace KindRetargeting void Start() { - ikSolver = GetComponent(); - crs = GetComponent(); + if (crs != null) ikSolver = crs.ikSolver; InitWeightLayers(); diff --git a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs index 832f66b9c..13b5e9953 100644 --- a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs +++ b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs @@ -7,8 +7,8 @@ namespace KindRetargeting /// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼. /// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다. /// - [DefaultExecutionOrder(6)] - public class TwoBoneIKSolver : MonoBehaviour + [System.Serializable] + public class TwoBoneIKSolver { [System.Serializable] public class LimbIK @@ -27,7 +27,6 @@ namespace KindRetargeting [HideInInspector] public float upperLength; [HideInInspector] public float lowerLength; - // 초기 벤드 법선 (upper 본 로컬 공간 — FinalIK 방식) [HideInInspector] public Vector3 localBendNormal; } @@ -43,15 +42,9 @@ namespace KindRetargeting private bool isInitialized; - private void Start() + public void Initialize(Animator targetAnimator) { - Initialize(); - } - - public void Initialize() - { - if (animator == null) - animator = GetComponent(); + animator = targetAnimator; if (animator == null || !animator.isHuman) return; CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand); @@ -73,8 +66,6 @@ namespace KindRetargeting limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position); limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position); - // 초기 벤드 법선을 upper 본의 로컬 공간에 캐싱 (FinalIK TrigonometricBone 방식) - // 런타임에 upper.rotation * localBendNormal로 안정적인 월드 법선 획득 Vector3 ab = limb.lower.position - limb.upper.position; Vector3 bc = limb.end.position - limb.lower.position; Vector3 bendNormal = Vector3.Cross(ab, bc); @@ -90,7 +81,7 @@ namespace KindRetargeting limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal; } - private void Update() + public void OnUpdate() { if (!isInitialized) return; @@ -105,10 +96,8 @@ namespace KindRetargeting if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return; if (limb.upper == null || limb.lower == null || limb.end == null) return; - // 벤드 법선 계산 Vector3 bendNormal = GetBendNormal(limb); - // bendGoal 적용 if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f) { Vector3 goalNormal = Vector3.Cross( @@ -121,7 +110,6 @@ namespace KindRetargeting } } - // FinalIK 정적 솔버 호출 IKSolverTrigonometric.Solve( limb.upper, limb.lower, @@ -131,7 +119,6 @@ namespace KindRetargeting limb.positionWeight ); - // 끝단 회전 if (limb.rotationWeight > 0.001f) { limb.end.rotation = Quaternion.Slerp( @@ -142,19 +129,11 @@ namespace KindRetargeting } } - /// - /// 벤드 법선을 upper 본의 회전에서 유도합니다 (FinalIK 방식). - /// 위치 기반 Cross(ab, bc)는 직선 근처에서 불안정하지만, - /// 회전 기반은 본의 회전을 그대로 따르므로 안정적입니다. - /// private Vector3 GetBendNormal(LimbIK limb) { return limb.upper.rotation * limb.localBendNormal; } - /// - /// 히프 높이 자동 보정값을 계산합니다. - /// public float CalculateAutoFloorHeight(float comfortRatio = 0.98f) { if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f; From 62a5a9bbb50138b25fd33243301eb59be691cb04 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:06:57 +0900 Subject: [PATCH 04/10] =?UTF-8?q?Refactor=20:=20FootGroundingController?= =?UTF-8?q?=EB=A5=BC=20Serializable=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FootGroundingController: MonoBehaviour → [Serializable] 클래스 - Start() → Initialize(TwoBoneIKSolver, Animator) - Update()/LateUpdate() → OnUpdate()/OnLateUpdate() - CRS에서 footGrounding 필드로 소유, Update/LateUpdate에서 호출 - CustomRetargetingScriptEditor: groundingSO 제거, serializedObject 경로로 접근 Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 12 +++++ .../Editor/CustomRetargetingScriptEditor.cs | 45 +++++++---------- .../FootGroundingController.cs | 49 ++++--------------- 3 files changed, 39 insertions(+), 67 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 9ec19621e..83bf24768 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -98,6 +98,9 @@ namespace KindRetargeting [Header("어깨 보정")] [SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction(); + [Header("발 접지")] + [SerializeField] public FootGroundingController footGrounding = new FootGroundingController(); + [Header("프랍 부착")] [SerializeField] public PropLocationController propLocation = new PropLocationController(); @@ -328,6 +331,9 @@ namespace KindRetargeting if (targetAnimator != null) shoulderCorrection.Initialize(targetAnimator); + // 발 접지 모듈 초기화 + footGrounding.Initialize(ikSolver, targetAnimator); + // 프랍 부착 모듈 초기화 if (targetAnimator != null) propLocation.Initialize(targetAnimator); @@ -831,6 +837,9 @@ namespace KindRetargeting // 어깨 보정 (기존 ExecutionOrder 3) shoulderCorrection.OnUpdate(); + // 발 접지 Pre-IK (기존 ExecutionOrder 5) + footGrounding.OnUpdate(); + // IK 솔버 (기존 ExecutionOrder 6) ikSolver.OnUpdate(); @@ -847,6 +856,9 @@ namespace KindRetargeting /// void LateUpdate() { + // 발 접지 Post-IK (기존 FootGroundingController LateUpdate) + footGrounding.OnLateUpdate(); + ApplyHeadRotationOffset(); ApplyHeadScale(); } diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 9a3fc9256..25c49bc60 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -10,8 +10,6 @@ namespace KindRetargeting { private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - private SerializedObject groundingSO; - // SerializedProperty private SerializedProperty sourceAnimatorProp; private SerializedProperty targetAnimatorProp; @@ -33,7 +31,7 @@ namespace KindRetargeting protected override void OnDisable() { base.OnDisable(); - groundingSO = null; + // groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈 } protected override void OnEnable() @@ -197,36 +195,27 @@ namespace KindRetargeting "• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지", HelpBoxMessageType.Info)); - // FootGroundingController의 SerializedObject를 직접 바인딩 - var script = (CustomRetargetingScript)target; - var grounding = script.GetComponent(); - if (grounding != null) - { - groundingSO = new SerializedObject(grounding); + // FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근 + var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"); + foldout.Add(groundHeightField); - var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이"); - foldout.Add(groundHeightField); + var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; + weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight")); + foldout.Add(weightSlider); - var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; - weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight")); - foldout.Add(weightSlider); + var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이"); + activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; + foldout.Add(activationField); - var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이"); - activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; - foldout.Add(activationField); + var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"); + thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; + foldout.Add(thresholdField); - var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위"); - thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; - foldout.Add(thresholdField); + var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도"); + smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)"; + foldout.Add(smoothField); - var smoothField = new PropertyField(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도"); - smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)"; - foldout.Add(smoothField); - - foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); - - foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties()); - } + foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); else { foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); diff --git a/Assets/Scripts/KindRetargeting/FootGroundingController.cs b/Assets/Scripts/KindRetargeting/FootGroundingController.cs index 8a5908da2..58010ea88 100644 --- a/Assets/Scripts/KindRetargeting/FootGroundingController.cs +++ b/Assets/Scripts/KindRetargeting/FootGroundingController.cs @@ -5,19 +5,14 @@ namespace KindRetargeting /// /// HIK 스타일 2-Pass 접지 시스템. /// - /// Pass 1 (Update, Order 5 → IK 전): + /// Pass 1 (OnUpdate, Order 5 → IK 전): /// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정. - /// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면 - /// 발목 타겟을 역산하여 Toes가 groundHeight에 고정. /// - /// Pass 2 (LateUpdate → IK 후): + /// Pass 2 (OnLateUpdate → IK 후): /// IK 결과의 잔차를 Foot 회전으로 미세 보정. - /// 위치 변경 없음 — 본 길이 보존. - /// - /// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지). /// - [DefaultExecutionOrder(5)] - public class FootGroundingController : MonoBehaviour + [System.Serializable] + public class FootGroundingController { [Header("접지 설정")] [Tooltip("바닥 Y 좌표 (월드 공간)")] @@ -41,38 +36,31 @@ namespace KindRetargeting private TwoBoneIKSolver ikSolver; private Animator animator; - // 타겟 아바타 캐싱 private Transform leftFoot; private Transform rightFoot; private Transform leftToes; private Transform rightToes; - // Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱) private Vector3 leftLocalToesOffset; private Vector3 rightLocalToesOffset; - // flat 상태에서 발목 최소 높이 (Foot.y - Toes.y) private float leftFootHeight; private float rightFootHeight; - // Toes 본 존재 여부 private bool leftHasToes; private bool rightHasToes; - // 스무딩용: 이전 프레임 보정량 private float leftPrevAdj; private float rightPrevAdj; private bool isInitialized; - private void Start() + public void Initialize(TwoBoneIKSolver ikSolver, Animator animator) { - var crs = GetComponent(); - if (crs != null) ikSolver = crs.ikSolver; - animator = GetComponent(); + this.ikSolver = ikSolver; + this.animator = animator; if (animator == null || !animator.isHuman || ikSolver == null) return; - if (leftFoot == null && rightFoot == null) return; leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot); @@ -81,7 +69,6 @@ namespace KindRetargeting if (leftFoot == null || rightFoot == null) return; - // Toes 존재 여부 + 캐싱 leftHasToes = leftToes != null; rightHasToes = rightToes != null; @@ -92,7 +79,7 @@ namespace KindRetargeting } else { - leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이 + leftFootHeight = 0.05f; } if (rightHasToes) @@ -111,7 +98,7 @@ namespace KindRetargeting /// /// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다. /// - private void Update() + public void OnUpdate() { if (!isInitialized || groundingWeight < 0.001f) return; @@ -122,16 +109,11 @@ namespace KindRetargeting ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight, rightHasToes, ikSolver.rightLeg.positionWeight); - // 스무딩: 보정량 급변 방지 float dt = Time.deltaTime * smoothSpeed; leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt)); rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt)); } - /// - /// 발 IK 타겟을 접지 모드에 따라 보정합니다. - /// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다. - /// private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset, float footHeight, bool hasToes, float ikWeight) { @@ -141,12 +123,10 @@ namespace KindRetargeting Vector3 anklePos = ankleTarget.position; float ankleY = anklePos.y; - // AIRBORNE 체크 if (ankleY - groundHeight > activationHeight) return 0f; float weight = groundingWeight * ikWeight; - // === Toes 없는 아바타: 단순 Y 클램프 === if (!hasToes) { float minAnkleY = groundHeight + footHeight; @@ -160,7 +140,6 @@ namespace KindRetargeting return 0f; } - // === Toes 있는 아바타: 예측 기반 보정 === Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset; float predictedToesY = predictedToesWorld.y; @@ -172,7 +151,6 @@ namespace KindRetargeting if (ankleY < groundHeight + footHeight + plantThreshold) { - // PLANTED: 발 전체가 바닥 근처 float minAnkleY = groundHeight + footHeight; if (ankleY < minAnkleY) { @@ -189,7 +167,6 @@ namespace KindRetargeting } else { - // TOE_PIVOT: 발끝 고정, 발목 올라감 if (toesError > 0f) { adjustment = toesError * weight; @@ -201,7 +178,6 @@ namespace KindRetargeting } else { - // Toes 충분히 위 → 발목만 바닥 아래 방지 float minAnkleY = groundHeight + footHeight; if (ankleY < minAnkleY) { @@ -217,7 +193,7 @@ namespace KindRetargeting /// /// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다. /// - private void LateUpdate() + public void OnLateUpdate() { if (!isInitialized || groundingWeight < 0.001f) return; @@ -227,10 +203,6 @@ namespace KindRetargeting AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight); } - /// - /// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정. - /// 바닥 아래로 뚫린 경우만 보정합니다. - /// private void AlignFootToGround(Transform foot, Transform toes, float ikWeight) { if (foot == null || toes == null) return; @@ -245,7 +217,6 @@ namespace KindRetargeting if (Mathf.Abs(error) < 0.001f) return; - // 바닥 아래로 뚫린 경우만 보정 if (error > plantThreshold) return; Vector3 footToToes = toes.position - foot.position; From e4ca30b98aa3c412d2e3117dd7866e73da32410f Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:21:12 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Refactor=20:=20LimbWeightController,=20Fi?= =?UTF-8?q?ngerShapedController=EB=A5=BC=20Serializable=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LimbWeightController: MonoBehaviour → [Serializable] 모듈, CRS.limbWeight로 통합 - FingerShapedController: MonoBehaviour → [Serializable] 모듈, CRS.fingerShaped로 통합 - GetHand()를 FindObjectsOfType → FindObjectsByType로 리팩토링 - HumanPoseHandler 라이프사이클을 Initialize/Cleanup 패턴으로 전환 - RetargetingControlWindow: 모든 GetComponent 호출을 CRS SO의 중첩 프로퍼티 경로로 변경 - RetargetingRemoteController: 직접 script.limbWeight/fingerShaped 접근으로 변경 - LimbWeightControllerEditor, FingerShapedControllerEditor 삭제 Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 29 +- .../Editor/FingerShapedControllerEditor.cs | 193 ----------- .../FingerShapedControllerEditor.cs.meta | 11 - .../Editor/LimbWeightControllerEditor.cs | 185 ----------- .../Editor/LimbWeightControllerEditor.cs.meta | 11 - .../Editor/RetargetingControlWindow.cs | 99 ++---- .../KindRetargeting/FingerShapedController.cs | 303 +++++++++--------- .../KindRetargeting/LimbWeightController.cs | 133 ++++---- .../Remote/RetargetingRemoteController.cs | 94 ++---- 9 files changed, 309 insertions(+), 749 deletions(-) delete mode 100644 Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs delete mode 100644 Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs.meta delete mode 100644 Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs delete mode 100644 Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs.meta diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 83bf24768..0744f9d78 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -104,6 +104,12 @@ namespace KindRetargeting [Header("프랍 부착")] [SerializeField] public PropLocationController propLocation = new PropLocationController(); + [Header("사지 가중치")] + [SerializeField] public LimbWeightController limbWeight = new LimbWeightController(); + + [Header("손가락 셰이핑")] + [SerializeField] public FingerShapedController fingerShaped = new FingerShapedController(); + [Header("아바타 크기 조정")] [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; private float previousScale = 1f; @@ -337,6 +343,13 @@ namespace KindRetargeting // 프랍 부착 모듈 초기화 if (targetAnimator != null) propLocation.Initialize(targetAnimator); + + // 사지 가중치 모듈 초기화 + limbWeight.Initialize(ikSolver, this, transform); + + // 손가락 셰이핑 모듈 초기화 + if (targetAnimator != null) + fingerShaped.Initialize(targetAnimator); } /// @@ -534,8 +547,7 @@ namespace KindRetargeting } // LimbWeightController에서 의자 높이 오프셋 가져오기 - var limbController = GetComponent(); - float chairOffset = limbController != null ? limbController.chairSeatHeightOffset : 0.05f; + float chairOffset = limbWeight.chairSeatHeightOffset; var settings = new RetargetingSettings { @@ -613,11 +625,7 @@ namespace KindRetargeting headScale = settings.headScale; // LimbWeightController에 의자 높이 오프셋 적용 - var limbController = GetComponent(); - if (limbController != null) - { - limbController.chairSeatHeightOffset = settings.chairSeatHeightOffset; - } + limbWeight.chairSeatHeightOffset = settings.chairSeatHeightOffset; // 머리 회전 오프셋 로드 headRotationOffsetX = settings.headRotationOffsetX; @@ -834,9 +842,15 @@ namespace KindRetargeting break; } + // 손가락 셰이핑 (기존 ExecutionOrder 2) + fingerShaped.OnUpdate(); + // 어깨 보정 (기존 ExecutionOrder 3) shoulderCorrection.OnUpdate(); + // 사지 가중치 (기존 ExecutionOrder 4) + limbWeight.OnUpdate(); + // 발 접지 Pre-IK (기존 ExecutionOrder 5) footGrounding.OnUpdate(); @@ -1404,6 +1418,7 @@ namespace KindRetargeting { sourcePoseHandler?.Dispose(); targetPoseHandler?.Dispose(); + fingerShaped.Cleanup(); } /// diff --git a/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs b/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs deleted file mode 100644 index 7bfe40f6f..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs +++ /dev/null @@ -1,193 +0,0 @@ -using UnityEngine; -using UnityEditor; -using UnityEngine.UIElements; -using UnityEditor.UIElements; - -[CustomEditor(typeof(FingerShapedController))] -public class FingerShapedControllerEditor : Editor -{ - private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - private static readonly string[] fingerPropNames = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl" }; - private static readonly string[] fingerKoreanNames = { "엄지", "검지", "중지", "약지", "새끼" }; - - private FingerShapedController controller; - - public override VisualElement CreateInspectorGUI() - { - controller = target as FingerShapedController; - if (controller == null) return new VisualElement(); - var root = new VisualElement(); - - var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); - if (commonUss != null) root.styleSheets.Add(commonUss); - - // 활성화 토글 - var enabledProp = serializedObject.FindProperty("m_Enabled"); - var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화"); - root.Add(enableToggle); - - // 왼손 - var leftFoldout = new Foldout { text = "왼손", value = true }; - leftFoldout.Add(BuildHandControls("left")); - root.Add(leftFoldout); - - // 오른손 - var rightFoldout = new Foldout { text = "오른손", value = true }; - rightFoldout.Add(BuildHandControls("right")); - root.Add(rightFoldout); - - // 프리셋 버튼 - root.Add(BuildPresetButtons()); - - return root; - } - - private VisualElement BuildHandControls(string prefix) - { - var container = new VisualElement(); - container.style.backgroundColor = new Color(0, 0, 0, 0.08f); - container.style.borderTopLeftRadius = container.style.borderTopRightRadius = - container.style.borderBottomLeftRadius = container.style.borderBottomRightRadius = 4; - container.style.paddingTop = container.style.paddingBottom = - container.style.paddingLeft = container.style.paddingRight = 6; - - // 헤더: 활성화 토글 + 초기화 버튼 - var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 4 } }; - var handEnabledProp = serializedObject.FindProperty($"{prefix}HandEnabled"); - header.Add(new PropertyField(handEnabledProp, "제어 활성화") { style = { flexGrow = 1 } }); - var resetBtn = new Button(() => ResetHandValues(prefix)) { text = "초기화" }; - resetBtn.style.width = 60; - header.Add(resetBtn); - container.Add(header); - - // 손가락 슬라이더 (가로 배치) - var slidersRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginTop = 4 } }; - - for (int i = 0; i < fingerPropNames.Length; i++) - { - var fingerProp = serializedObject.FindProperty($"{prefix}{fingerPropNames[i]}"); - slidersRow.Add(BuildVerticalSlider(fingerKoreanNames[i], fingerProp)); - } - container.Add(slidersRow); - - // 벌리기 슬라이더 - var spreadProp = serializedObject.FindProperty($"{prefix}SpreadFingers"); - var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true }; - spreadSlider.BindProperty(spreadProp); - spreadSlider.style.marginTop = 8; - container.Add(spreadSlider); - - // 비활성 시 숨기기 - container.TrackPropertyValue(handEnabledProp, prop => - { - slidersRow.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None; - spreadSlider.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None; - }); - slidersRow.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None; - spreadSlider.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None; - - return container; - } - - private VisualElement BuildVerticalSlider(string label, SerializedProperty prop) - { - var column = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } }; - - column.Add(new Label(label) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }); - - var valueLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }; - column.Add(valueLabel); - - // UI Toolkit Slider in vertical mode (direction: Vertical 대신 세로용 Slider 사용) - // SliderDirection.Vertical 사용 - var slider = new Slider(-1f, 1f) { direction = SliderDirection.Vertical }; - slider.style.height = 100; - slider.style.width = 25; - slider.BindProperty(prop); - slider.RegisterValueChangedCallback(evt => valueLabel.text = evt.newValue.ToString("F1")); - - // 초기값 동기화 - column.TrackPropertyValue(prop, p => valueLabel.text = p.floatValue.ToString("F1")); - - column.Add(slider); - return column; - } - - private VisualElement BuildPresetButtons() - { - var container = new VisualElement { style = { marginTop = 10 } }; - container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } }); - - string[,] presets = { - { "가위", "바위", "보" }, - { "브이", "검지", "초기화" } - }; - - for (int row = 0; row < 2; row++) - { - var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginBottom = 4 } }; - for (int col = 0; col < 3; col++) - { - string presetName = presets[row, col]; - var btn = new Button(() => ApplyPreset(presetName)) { text = presetName }; - btn.style.height = 30; - btn.style.width = 100; - btn.style.marginLeft = btn.style.marginRight = 4; - btnRow.Add(btn); - } - container.Add(btnRow); - } - - return container; - } - - private void ResetHandValues(string prefix) - { - serializedObject.Update(); - string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" }; - foreach (var prop in props) - serializedObject.FindProperty($"{prefix}{prop}").floatValue = 0f; - serializedObject.ApplyModifiedProperties(); - } - - private void ApplyPreset(string presetName) - { - if (!controller.enabled) - controller.enabled = true; - - switch (presetName) - { - case "가위": SetPreset(1f, 1f, -1f, -1f, -1f, 0.3f); break; - case "바위": SetPreset(-1f, -1f, -1f, -1f, -1f, 0f); break; - case "보": SetPreset(1f, 1f, 1f, 1f, 1f, 1f); break; - case "브이": SetPreset(-1f, 1f, 1f, -1f, -1f, 1f); break; - case "검지": SetPreset(-1f, 1f, -1f, -1f, -1f, 0f); break; - case "초기화": SetPreset(0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f); break; - } - } - - private void SetPreset(float thumb, float index, float middle, float ring, float pinky, float spread) - { - if (controller.leftHandEnabled) - { - controller.leftThumbCurl = thumb; - controller.leftIndexCurl = index; - controller.leftMiddleCurl = middle; - controller.leftRingCurl = ring; - controller.leftPinkyCurl = pinky; - controller.leftSpreadFingers = spread; - } - - if (controller.rightHandEnabled) - { - controller.rightThumbCurl = thumb; - controller.rightIndexCurl = index; - controller.rightMiddleCurl = middle; - controller.rightRingCurl = ring; - controller.rightPinkyCurl = pinky; - controller.rightSpreadFingers = spread; - } - - EditorUtility.SetDirty(controller); - } -} diff --git a/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs.meta b/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs.meta deleted file mode 100644 index f9609cf4b..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/FingerShapedControllerEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 291a583b9a953e041a119ba6c332d187 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs b/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs deleted file mode 100644 index 491a0242c..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs +++ /dev/null @@ -1,185 +0,0 @@ -using UnityEngine; -using UnityEditor; -using UnityEngine.UIElements; -using UnityEditor.UIElements; - -namespace KindRetargeting -{ - [CustomEditor(typeof(LimbWeightController))] - public class LimbWeightControllerEditor : BaseRetargetingEditor - { - private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - - SerializedProperty maxDistance; - SerializedProperty minDistance; - SerializedProperty weightSmoothSpeed; - SerializedProperty middleWeightMultiplier; - SerializedProperty hipsMinDistance; - SerializedProperty hipsMaxDistance; - SerializedProperty props; - SerializedProperty characterRoot; - SerializedProperty groundHipsMinHeight; - SerializedProperty groundHipsMaxHeight; - SerializedProperty footHeightMinThreshold; - SerializedProperty footHeightMaxThreshold; - SerializedProperty enableLeftArmIK; - SerializedProperty enableRightArmIK; - SerializedProperty chairSeatHeightOffset; - - protected override void OnEnable() - { - base.OnEnable(); - if (serializedObject == null) return; - - maxDistance = serializedObject.FindProperty("maxDistance"); - minDistance = serializedObject.FindProperty("minDistance"); - weightSmoothSpeed = serializedObject.FindProperty("weightSmoothSpeed"); - middleWeightMultiplier = serializedObject.FindProperty("middleWeightMultiplier"); - hipsMinDistance = serializedObject.FindProperty("hipsMinDistance"); - hipsMaxDistance = serializedObject.FindProperty("hipsMaxDistance"); - props = serializedObject.FindProperty("props"); - characterRoot = serializedObject.FindProperty("characterRoot"); - groundHipsMinHeight = serializedObject.FindProperty("groundHipsMinHeight"); - groundHipsMaxHeight = serializedObject.FindProperty("groundHipsMaxHeight"); - footHeightMinThreshold = serializedObject.FindProperty("footHeightMinThreshold"); - footHeightMaxThreshold = serializedObject.FindProperty("footHeightMaxThreshold"); - enableLeftArmIK = serializedObject.FindProperty("enableLeftArmIK"); - enableRightArmIK = serializedObject.FindProperty("enableRightArmIK"); - chairSeatHeightOffset = serializedObject.FindProperty("chairSeatHeightOffset"); - } - - public override VisualElement CreateInspectorGUI() - { - if (serializedObject == null || target == null) - return new VisualElement(); - - var root = new VisualElement(); - - var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); - if (commonUss != null) root.styleSheets.Add(commonUss); - - // IK 활성화 설정 - var ikFoldout = new Foldout { text = "IK 활성화 설정", value = true }; - ikFoldout.Add(new PropertyField(enableLeftArmIK, "왼팔 IK 활성화")); - ikFoldout.Add(new PropertyField(enableRightArmIK, "오른팔 IK 활성화")); - root.Add(ikFoldout); - - // 거리 기반 가중치 설정 - var distFoldout = new Foldout { text = "거리 기반 가중치 설정", value = true }; - distFoldout.Add(BuildMinMaxRange("거리 범위 (가중치 1 → 0)", minDistance, maxDistance, 0f, 1f)); - root.Add(distFoldout); - - // 가중치 변화 설정 - var weightFoldout = new Foldout { text = "가중치 변화 설정", value = true }; - weightFoldout.Add(new PropertyField(weightSmoothSpeed, "가중치 변화 속도")); - root.Add(weightFoldout); - - // 허리 가중치 설정 - var hipsFoldout = new Foldout { text = "허리 가중치 설정", value = true }; - hipsFoldout.Add(BuildMinMaxRange("허리 거리 범위 (가중치 1 → 0)", hipsMinDistance, hipsMaxDistance, 0f, 1f)); - root.Add(hipsFoldout); - - // 바닥 기준 히프 보정 설정 - var groundFoldout = new Foldout { text = "바닥 기준 히프 보정 설정", value = true }; - groundFoldout.Add(BuildMinMaxRange("바닥 기준 히프 높이 범위 (가중치 0 → 1)", groundHipsMinHeight, groundHipsMaxHeight, 0f, 2f)); - root.Add(groundFoldout); - - // 발 높이 기반 가중치 설정 - var footFoldout = new Foldout { text = "발 높이 기반 가중치 설정", value = true }; - footFoldout.Add(BuildMinMaxRange("발 높이 범위 (가중치 1 → 0)", footHeightMinThreshold, footHeightMaxThreshold, 0.1f, 1f)); - root.Add(footFoldout); - - // 의자 앉기 높이 설정 - var chairFoldout = new Foldout { text = "의자 앉기 높이 설정", value = true }; - chairFoldout.Add(new PropertyField(chairSeatHeightOffset, "좌석 높이 오프셋")); - root.Add(chairFoldout); - - // 참조 설정 - var refFoldout = new Foldout { text = "참조 설정", value = true }; - refFoldout.Add(new PropertyField(props, "프랍 오브젝트")); - refFoldout.Add(new PropertyField(characterRoot, "캐릭터 루트")); - root.Add(refFoldout); - - // 변경 감지 - root.TrackSerializedObjectValue(serializedObject, so => - { - if (target == null) return; - MarkDirty(); - var script = (LimbWeightController)target; - var retargeting = script.GetComponent(); - if (retargeting != null) - EditorUtility.SetDirty(retargeting); - }); - - return root; - } - - private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax) - { - var container = new VisualElement { style = { marginBottom = 4 } }; - if (minProp == null || maxProp == null) - { - container.Add(new HelpBox($"'{label}' 프로퍼티를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); - return container; - } - container.Add(new Label(label) { style = { marginBottom = 2 } }); - - var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; - - var minField = new FloatField { value = minProp.floatValue, style = { width = 50 } }; - var slider = new MinMaxSlider(minProp.floatValue, maxProp.floatValue, limitMin, limitMax); - slider.style.flexGrow = 1; - slider.style.marginLeft = slider.style.marginRight = 4; - var maxField = new FloatField { value = maxProp.floatValue, style = { width = 50 } }; - - // MinMaxSlider → sync fields + properties - slider.RegisterValueChangedCallback(evt => - { - minProp.floatValue = evt.newValue.x; - maxProp.floatValue = evt.newValue.y; - serializedObject.ApplyModifiedProperties(); - minField.SetValueWithoutNotify(evt.newValue.x); - maxField.SetValueWithoutNotify(evt.newValue.y); - }); - - // FloatField min → sync slider + property - minField.RegisterValueChangedCallback(evt => - { - float clamped = Mathf.Clamp(evt.newValue, limitMin, maxProp.floatValue); - minProp.floatValue = clamped; - serializedObject.ApplyModifiedProperties(); - slider.SetValueWithoutNotify(new Vector2(clamped, maxProp.floatValue)); - minField.SetValueWithoutNotify(clamped); - }); - - // FloatField max → sync slider + property - maxField.RegisterValueChangedCallback(evt => - { - float clamped = Mathf.Clamp(evt.newValue, minProp.floatValue, limitMax); - maxProp.floatValue = clamped; - serializedObject.ApplyModifiedProperties(); - slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, clamped)); - maxField.SetValueWithoutNotify(clamped); - }); - - // Track external changes (undo, etc.) - container.TrackPropertyValue(minProp, p => - { - minField.SetValueWithoutNotify(p.floatValue); - slider.SetValueWithoutNotify(new Vector2(p.floatValue, maxProp.floatValue)); - }); - container.TrackPropertyValue(maxProp, p => - { - maxField.SetValueWithoutNotify(p.floatValue); - slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, p.floatValue)); - }); - - row.Add(minField); - row.Add(slider); - row.Add(maxField); - container.Add(row); - - return container; - } - } -} diff --git a/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs.meta b/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs.meta deleted file mode 100644 index 29554f3e7..000000000 --- a/Assets/Scripts/KindRetargeting/Editor/LimbWeightControllerEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 199539b34f08aac41a86f4767bc49def -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index f00dce14a..7f92c758e 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -144,11 +144,7 @@ public class RetargetingControlWindow : EditorWindow foreach (var s in retargetingScripts) { if (s == null) continue; - if (s.GetComponent() == null) - s.gameObject.AddComponent(); - if (s.GetComponent() == null) - s.gameObject.AddComponent(); - // PropLocationController는 CRS 내부 모듈로 이동됨 + // 모든 컴포넌트는 CRS 내부 모듈로 이동됨 EditorUtility.SetDirty(s.gameObject); } @@ -197,7 +193,7 @@ public class RetargetingControlWindow : EditorWindow panel.Add(BuildHeader(script)); // 가중치 설정 - panel.Add(BuildWeightSection(script)); + panel.Add(BuildWeightSection(script, so)); // 힙 위치 보정 panel.Add(BuildHipsSection(script, so)); @@ -229,7 +225,7 @@ public class RetargetingControlWindow : EditorWindow panel.Add(footFoldout); // 손가락 제어 설정 - panel.Add(BuildFingerControlSection(script)); + panel.Add(BuildFingerControlSection(script, so)); // 손가락 복제 설정 panel.Add(BuildFingerCopySection(script, so)); @@ -303,27 +299,22 @@ public class RetargetingControlWindow : EditorWindow // ========== Weight Settings ========== - private VisualElement BuildWeightSection(CustomRetargetingScript script) + private VisualElement BuildWeightSection(CustomRetargetingScript script, SerializedObject so) { var foldout = new Foldout { text = "가중치 설정", value = false }; - var limb = script.GetComponent(); - if (limb == null) { foldout.Add(new HelpBox("LimbWeightController가 없습니다.", HelpBoxMessageType.Warning)); return foldout; } - - var limbSO = CreateTrackedSO(limb); var container = new VisualElement(); container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)", - limbSO.FindProperty("minDistance"), limbSO.FindProperty("maxDistance"), 0f, 1f, limbSO)); + so.FindProperty("limbWeight.minDistance"), so.FindProperty("limbWeight.maxDistance"), 0f, 1f, so)); container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)", - limbSO.FindProperty("hipsMinDistance"), limbSO.FindProperty("hipsMaxDistance"), 0f, 1f, limbSO)); + so.FindProperty("limbWeight.hipsMinDistance"), so.FindProperty("limbWeight.hipsMaxDistance"), 0f, 1f, so)); container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)", - limbSO.FindProperty("groundHipsMinHeight"), limbSO.FindProperty("groundHipsMaxHeight"), 0f, 2f, limbSO)); + so.FindProperty("limbWeight.groundHipsMinHeight"), so.FindProperty("limbWeight.groundHipsMaxHeight"), 0f, 2f, so)); container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)", - limbSO.FindProperty("footHeightMinThreshold"), limbSO.FindProperty("footHeightMaxThreshold"), 0.1f, 1f, limbSO)); + so.FindProperty("limbWeight.footHeightMinThreshold"), so.FindProperty("limbWeight.footHeightMaxThreshold"), 0.1f, 1f, so)); - var smoothField = new PropertyField(limbSO.FindProperty("weightSmoothSpeed"), "가중치 변화 속도"); + var smoothField = new PropertyField(so.FindProperty("limbWeight.weightSmoothSpeed"), "가중치 변화 속도"); container.Add(smoothField); - container.Bind(limbSO); foldout.Add(container); return foldout; @@ -366,15 +357,9 @@ public class RetargetingControlWindow : EditorWindow container.Add(hz); // 의자 앉기 높이 - var limb = script.GetComponent(); - if (limb != null) - { - var limbSO = CreateTrackedSO(limb); - var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" }; - chairSlider.BindProperty(limbSO.FindProperty("chairSeatHeightOffset")); - container.Add(chairSlider); - chairSlider.Bind(limbSO); - } + var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" }; + chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset")); + container.Add(chairSlider); // 다리 길이 자동 보정 버튼 var autoHipsBtn = new Button(() => @@ -402,48 +387,28 @@ public class RetargetingControlWindow : EditorWindow // ========== Finger Control ========== - private VisualElement BuildFingerControlSection(CustomRetargetingScript script) + private VisualElement BuildFingerControlSection(CustomRetargetingScript script, SerializedObject so) { var foldout = new Foldout { text = "손가락 제어 설정", value = false }; - var fingerController = script.GetComponent(); - if (fingerController == null) - { - foldout.Add(new HelpBox("FingerShapedController가 없습니다.", HelpBoxMessageType.Warning)); - var addBtn = new Button(() => - { - script.gameObject.AddComponent(); - EditorUtility.SetDirty(script.gameObject); - RebuildCharacterPanels(); - }) { text = "FingerShapedController 추가" }; - foldout.Add(addBtn); - return foldout; - } - - var fso = CreateTrackedSO(fingerController); var container = new VisualElement(); // 활성화 토글 - var enableToggle = new Toggle("손가락 제어 활성화") { value = fingerController.enabled }; - enableToggle.RegisterValueChangedCallback(evt => - { - fingerController.enabled = evt.newValue; - EditorUtility.SetDirty(fingerController); - }); + var enabledProp = so.FindProperty("fingerShaped.enabled"); + var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화"); container.Add(enableToggle); // 왼손 - container.Add(BuildHandSection("왼손", "left", fso, fingerController)); + container.Add(BuildHandSection("왼손", "left", so, script.fingerShaped)); // 오른손 - container.Add(BuildHandSection("오른손", "right", fso, fingerController)); + container.Add(BuildHandSection("오른손", "right", so, script.fingerShaped)); // 프리셋 - container.Add(BuildFingerPresets(fingerController)); + container.Add(BuildFingerPresets(script, script.fingerShaped)); - container.Bind(fso); foldout.Add(container); return foldout; } - private VisualElement BuildHandSection(string label, string prefix, SerializedObject fso, FingerShapedController fc) + private VisualElement BuildHandSection(string label, string prefix, SerializedObject so, FingerShapedController fc) { var box = new VisualElement(); box.style.backgroundColor = new Color(0, 0, 0, 0.08f); @@ -455,13 +420,13 @@ public class RetargetingControlWindow : EditorWindow var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var handFoldout = new Foldout { text = label, value = false }; - var handEnabledProp = fso.FindProperty($"{prefix}HandEnabled"); + var handEnabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled"); header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } }); var resetBtn = new Button(() => { string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" }; - foreach (var p in props) fso.FindProperty($"{prefix}{p}").floatValue = 0f; - fso.ApplyModifiedProperties(); + foreach (var p in props) so.FindProperty($"fingerShaped.{prefix}{p}").floatValue = 0f; + so.ApplyModifiedProperties(); }) { text = "초기화" }; resetBtn.style.width = 60; header.Add(resetBtn); @@ -477,7 +442,7 @@ public class RetargetingControlWindow : EditorWindow var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } }; col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }); - var prop = fso.FindProperty($"{prefix}{fingerNames[i]}Curl"); + var prop = so.FindProperty($"fingerShaped.{prefix}{fingerNames[i]}Curl"); var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }; col.Add(valLabel); @@ -494,16 +459,18 @@ public class RetargetingControlWindow : EditorWindow // 벌리기 var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true }; - spreadSlider.BindProperty(fso.FindProperty($"{prefix}SpreadFingers")); + spreadSlider.BindProperty(so.FindProperty($"fingerShaped.{prefix}SpreadFingers")); handFoldout.Add(spreadSlider); // 비활성 시 숨김 handFoldout.schedule.Execute(() => { - try { if (fso == null || fso.targetObject == null) return; } + try { if (so == null || so.targetObject == null) return; } catch (System.Exception) { return; } - fso.Update(); - bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue; + so.Update(); + var enabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled"); + if (enabledProp == null) return; + bool enabled = enabledProp.boolValue; slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; }).Every(300); @@ -512,7 +479,7 @@ public class RetargetingControlWindow : EditorWindow return box; } - private VisualElement BuildFingerPresets(FingerShapedController controller) + private VisualElement BuildFingerPresets(CustomRetargetingScript script, FingerShapedController controller) { var container = new VisualElement { style = { marginTop = 6 } }; container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } }); @@ -524,7 +491,7 @@ public class RetargetingControlWindow : EditorWindow for (int col = 0; col < 3; col++) { string name = presets[row, col]; - var btn = new Button(() => ApplyFingerPreset(controller, name)) { text = name }; + var btn = new Button(() => ApplyFingerPreset(script, controller, name)) { text = name }; btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4; btnRow.Add(btn); } @@ -835,7 +802,7 @@ public class RetargetingControlWindow : EditorWindow return container; } - private void ApplyFingerPreset(FingerShapedController controller, string presetName) + private void ApplyFingerPreset(CustomRetargetingScript script, FingerShapedController controller, string presetName) { if (!controller.enabled) controller.enabled = true; @@ -860,7 +827,7 @@ public class RetargetingControlWindow : EditorWindow controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m; controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s; } - EditorUtility.SetDirty(controller); + EditorUtility.SetDirty(script); } // ========== Head Calibration ========== diff --git a/Assets/Scripts/KindRetargeting/FingerShapedController.cs b/Assets/Scripts/KindRetargeting/FingerShapedController.cs index 025e45624..533caaa49 100644 --- a/Assets/Scripts/KindRetargeting/FingerShapedController.cs +++ b/Assets/Scripts/KindRetargeting/FingerShapedController.cs @@ -1,168 +1,173 @@ using UnityEngine; using System.Collections.Generic; -[DefaultExecutionOrder(2)] -public class FingerShapedController : MonoBehaviour +namespace KindRetargeting { - private Animator animator; - private HumanPoseHandler humanPoseHandler; - - // 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용) - private Dictionary savedBoneLocalRotations = new Dictionary(); - - // 손가락을 제외한 모든 휴먼본 목록 - private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[] + [System.Serializable] + public class FingerShapedController { - HumanBodyBones.Hips, - HumanBodyBones.Spine, - HumanBodyBones.Chest, - HumanBodyBones.UpperChest, - HumanBodyBones.Neck, - HumanBodyBones.Head, - HumanBodyBones.LeftShoulder, - HumanBodyBones.LeftUpperArm, - HumanBodyBones.LeftLowerArm, - HumanBodyBones.LeftHand, - HumanBodyBones.RightShoulder, - HumanBodyBones.RightUpperArm, - HumanBodyBones.RightLowerArm, - HumanBodyBones.RightHand, - HumanBodyBones.LeftUpperLeg, - HumanBodyBones.LeftLowerLeg, - HumanBodyBones.LeftFoot, - HumanBodyBones.LeftToes, - HumanBodyBones.RightUpperLeg, - HumanBodyBones.RightLowerLeg, - HumanBodyBones.RightFoot, - HumanBodyBones.RightToes, - HumanBodyBones.LeftEye, - HumanBodyBones.RightEye, - HumanBodyBones.Jaw - }; + private Animator animator; + private HumanPoseHandler humanPoseHandler; - [Header("왼손 제어 값")] - [Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기 - [Range(-1, 1)] public float leftRingCurl; // 약지 구부리기 - [Range(-1, 1)] public float leftMiddleCurl; // 중지 구부리기 - [Range(-1, 1)] public float leftIndexCurl; // 검지 구부리기 - [Range(-1, 1)] public float leftThumbCurl; // 엄지 구부리기 - [Range(-1, 1)] public float leftSpreadFingers; // 손가락 벌리기 + // 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용) + private Dictionary savedBoneLocalRotations = new Dictionary(); - [Header("오른손 제어 값")] - [Range(-1, 1)] public float rightPinkyCurl; // 새끼손가락 구부리기 - [Range(-1, 1)] public float rightRingCurl; // 약지 구부리기 - [Range(-1, 1)] public float rightMiddleCurl; // 중지 구부리기 - [Range(-1, 1)] public float rightIndexCurl; // 검지 구부리기 - [Range(-1, 1)] public float rightThumbCurl; // 엄지 구부리기 - [Range(-1, 1)] public float rightSpreadFingers; // 손가락 벌리기 - - public bool leftHandEnabled = false; // 왼손 제어 활성화 상태 - public bool rightHandEnabled = false; // 오른손 제어 활성화 상태 - - private void Reset() - { - // 컴포넌트가 처음 추가될 때 자동으로 비활성화 - enabled = false; - leftHandEnabled = false; - rightHandEnabled = false; - } - - private void Awake() - { - animator = GetComponent(); - humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform); - } - - private void OnDestroy() - { - if (humanPoseHandler != null) + // 손가락을 제외한 모든 휴먼본 목록 + private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[] { - humanPoseHandler.Dispose(); + HumanBodyBones.Hips, + HumanBodyBones.Spine, + HumanBodyBones.Chest, + HumanBodyBones.UpperChest, + HumanBodyBones.Neck, + HumanBodyBones.Head, + HumanBodyBones.LeftShoulder, + HumanBodyBones.LeftUpperArm, + HumanBodyBones.LeftLowerArm, + HumanBodyBones.LeftHand, + HumanBodyBones.RightShoulder, + HumanBodyBones.RightUpperArm, + HumanBodyBones.RightLowerArm, + HumanBodyBones.RightHand, + HumanBodyBones.LeftUpperLeg, + HumanBodyBones.LeftLowerLeg, + HumanBodyBones.LeftFoot, + HumanBodyBones.LeftToes, + HumanBodyBones.RightUpperLeg, + HumanBodyBones.RightLowerLeg, + HumanBodyBones.RightFoot, + HumanBodyBones.RightToes, + HumanBodyBones.LeftEye, + HumanBodyBones.RightEye, + HumanBodyBones.Jaw + }; + + public bool enabled = false; + + [Header("왼손 제어 값")] + [Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기 + [Range(-1, 1)] public float leftRingCurl; // 약지 구부리기 + [Range(-1, 1)] public float leftMiddleCurl; // 중지 구부리기 + [Range(-1, 1)] public float leftIndexCurl; // 검지 구부리기 + [Range(-1, 1)] public float leftThumbCurl; // 엄지 구부리기 + [Range(-1, 1)] public float leftSpreadFingers; // 손가락 벌리기 + + [Header("오른손 제어 값")] + [Range(-1, 1)] public float rightPinkyCurl; // 새끼손가락 구부리기 + [Range(-1, 1)] public float rightRingCurl; // 약지 구부리기 + [Range(-1, 1)] public float rightMiddleCurl; // 중지 구부리기 + [Range(-1, 1)] public float rightIndexCurl; // 검지 구부리기 + [Range(-1, 1)] public float rightThumbCurl; // 엄지 구부리기 + [Range(-1, 1)] public float rightSpreadFingers; // 손가락 벌리기 + + public bool leftHandEnabled = false; // 왼손 제어 활성화 상태 + public bool rightHandEnabled = false; // 오른손 제어 활성화 상태 + + private bool isInitialized; + + public void Initialize(Animator targetAnimator) + { + animator = targetAnimator; + if (animator == null || !animator.isHuman) return; + + humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform); + isInitialized = true; } - } - private void Update() - { - UpdateMuscleValues(); - } - - private void UpdateMuscleValues() - { - // 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전) - savedBoneLocalRotations.Clear(); - for (int i = 0; i < nonFingerBones.Length; i++) + public void Cleanup() { - Transform bone = animator.GetBoneTransform(nonFingerBones[i]); - if (bone != null) + if (humanPoseHandler != null) { - savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation; + humanPoseHandler.Dispose(); + humanPoseHandler = null; + } + isInitialized = false; + } + + public void OnUpdate() + { + if (!isInitialized || !enabled) return; + UpdateMuscleValues(); + } + + private void UpdateMuscleValues() + { + // 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전) + savedBoneLocalRotations.Clear(); + for (int i = 0; i < nonFingerBones.Length; i++) + { + Transform bone = animator.GetBoneTransform(nonFingerBones[i]); + if (bone != null) + { + savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation; + } + } + + // 2. HumanPose 가져오기 및 손가락 머슬 설정 + HumanPose humanPose = new HumanPose(); + humanPoseHandler.GetHumanPose(ref humanPose); + + // 왼손 제어 + SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl, + leftPinkyCurl, leftSpreadFingers, ref humanPose); + + // 오른손 제어 + SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl, + rightPinkyCurl, rightSpreadFingers, ref humanPose); + + // 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향) + humanPoseHandler.SetHumanPose(ref humanPose); + + // 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지) + foreach (var kvp in savedBoneLocalRotations) + { + Transform bone = animator.GetBoneTransform(kvp.Key); + if (bone != null) + { + bone.localRotation = kvp.Value; + } } } - // 2. HumanPose 가져오기 및 손가락 머슬 설정 - HumanPose humanPose = new HumanPose(); - humanPoseHandler.GetHumanPose(ref humanPose); - - // 왼손 제어 - SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl, - leftPinkyCurl, leftSpreadFingers, ref humanPose); - - // 오른손 제어 - SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl, - rightPinkyCurl, rightSpreadFingers, ref humanPose); - - // 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향) - humanPoseHandler.SetHumanPose(ref humanPose); - - // 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지) - foreach (var kvp in savedBoneLocalRotations) + private void SetHandMuscles(bool isLeft, float thumb, float index, float middle, float ring, + float pinky, float spread, ref HumanPose humanPose) { - Transform bone = animator.GetBoneTransform(kvp.Key); - if (bone != null) - { - bone.localRotation = kvp.Value; - } + // 해당 손이 비활성화 상태면 건너뛰기 + if (isLeft && !leftHandEnabled) return; + if (!isLeft && !rightHandEnabled) return; + + int baseOffset = isLeft ? 55 : 75; // 왼손은 55부터, 오른손은 75부터 시작 + int muscleCount = humanPose.muscles.Length; + + // 엄지손가락 + if (baseOffset < muscleCount) humanPose.muscles[baseOffset] = thumb; // Thumb 1 + if (baseOffset + 1 < muscleCount) humanPose.muscles[baseOffset + 1] = thumb; // Thumb Spread + if (baseOffset + 2 < muscleCount) humanPose.muscles[baseOffset + 2] = thumb; // Thumb 2 + if (baseOffset + 3 < muscleCount) humanPose.muscles[baseOffset + 3] = thumb; // Thumb 3 + + // 검지 + if (baseOffset + 4 < muscleCount) humanPose.muscles[baseOffset + 4] = index; // Index 1 + if (baseOffset + 5 < muscleCount) humanPose.muscles[baseOffset + 5] = spread; // Index Spread + if (baseOffset + 6 < muscleCount) humanPose.muscles[baseOffset + 6] = index; // Index 2 + if (baseOffset + 7 < muscleCount) humanPose.muscles[baseOffset + 7] = index; // Index 3 + + // 중지 + if (baseOffset + 8 < muscleCount) humanPose.muscles[baseOffset + 8] = middle; // Middle 1 + if (baseOffset + 9 < muscleCount) humanPose.muscles[baseOffset + 9] = spread; // Middle Spread + if (baseOffset + 10 < muscleCount) humanPose.muscles[baseOffset + 10] = middle; // Middle 2 + if (baseOffset + 11 < muscleCount) humanPose.muscles[baseOffset + 11] = middle; // Middle 3 + + // 약지 + if (baseOffset + 12 < muscleCount) humanPose.muscles[baseOffset + 12] = ring; // Ring 1 + if (baseOffset + 13 < muscleCount) humanPose.muscles[baseOffset + 13] = spread; // Ring Spread + if (baseOffset + 14 < muscleCount) humanPose.muscles[baseOffset + 14] = ring; // Ring 2 + if (baseOffset + 15 < muscleCount) humanPose.muscles[baseOffset + 15] = ring; // Ring 3 + + // 새끼손가락 + if (baseOffset + 16 < muscleCount) humanPose.muscles[baseOffset + 16] = pinky; // Little 1 + if (baseOffset + 17 < muscleCount) humanPose.muscles[baseOffset + 17] = spread; // Little Spread + if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2 + if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3 } } - - private void SetHandMuscles(bool isLeft, float thumb, float index, float middle, float ring, - float pinky, float spread, ref HumanPose humanPose) - { - // 해당 손이 비활성화 상태면 건너뛰기 - if (isLeft && !leftHandEnabled) return; - if (!isLeft && !rightHandEnabled) return; - - int baseOffset = isLeft ? 55 : 75; // 왼손은 55부터, 오른손은 75부터 시작 - int muscleCount = humanPose.muscles.Length; - - // 엄지손가락 - if (baseOffset < muscleCount) humanPose.muscles[baseOffset] = thumb; // Thumb 1 - if (baseOffset + 1 < muscleCount) humanPose.muscles[baseOffset + 1] = thumb; // Thumb Spread - if (baseOffset + 2 < muscleCount) humanPose.muscles[baseOffset + 2] = thumb; // Thumb 2 - if (baseOffset + 3 < muscleCount) humanPose.muscles[baseOffset + 3] = thumb; // Thumb 3 - - // 검지 - if (baseOffset + 4 < muscleCount) humanPose.muscles[baseOffset + 4] = index; // Index 1 - if (baseOffset + 5 < muscleCount) humanPose.muscles[baseOffset + 5] = spread; // Index Spread - if (baseOffset + 6 < muscleCount) humanPose.muscles[baseOffset + 6] = index; // Index 2 - if (baseOffset + 7 < muscleCount) humanPose.muscles[baseOffset + 7] = index; // Index 3 - - // 중지 - if (baseOffset + 8 < muscleCount) humanPose.muscles[baseOffset + 8] = middle; // Middle 1 - if (baseOffset + 9 < muscleCount) humanPose.muscles[baseOffset + 9] = spread; // Middle Spread - if (baseOffset + 10 < muscleCount) humanPose.muscles[baseOffset + 10] = middle; // Middle 2 - if (baseOffset + 11 < muscleCount) humanPose.muscles[baseOffset + 11] = middle; // Middle 3 - - // 약지 - if (baseOffset + 12 < muscleCount) humanPose.muscles[baseOffset + 12] = ring; // Ring 1 - if (baseOffset + 13 < muscleCount) humanPose.muscles[baseOffset + 13] = spread; // Ring Spread - if (baseOffset + 14 < muscleCount) humanPose.muscles[baseOffset + 14] = ring; // Ring 2 - if (baseOffset + 15 < muscleCount) humanPose.muscles[baseOffset + 15] = ring; // Ring 3 - - // 새끼손가락 - if (baseOffset + 16 < muscleCount) humanPose.muscles[baseOffset + 16] = pinky; // Little 1 - if (baseOffset + 17 < muscleCount) humanPose.muscles[baseOffset + 17] = spread; // Little Spread - if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2 - if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3 - } } diff --git a/Assets/Scripts/KindRetargeting/LimbWeightController.cs b/Assets/Scripts/KindRetargeting/LimbWeightController.cs index 977b12a2e..10970dfb2 100644 --- a/Assets/Scripts/KindRetargeting/LimbWeightController.cs +++ b/Assets/Scripts/KindRetargeting/LimbWeightController.cs @@ -4,8 +4,8 @@ using UnityEngine; namespace KindRetargeting { - [DefaultExecutionOrder(4)] - public class LimbWeightController : MonoBehaviour + [System.Serializable] + public class LimbWeightController { [Header("거리 기반 가중치 설정")] [SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리 @@ -36,9 +36,8 @@ namespace KindRetargeting public float chairSeatHeightOffset = 0.05f; private TwoBoneIKSolver ikSolver; - private CustomRetargetingScript crs; - private Dictionary> weightLayers = new Dictionary>(); + private Transform characterRoot; List leftArmEndWeights = new List(); List rightArmEndWeights = new List(); @@ -57,8 +56,6 @@ namespace KindRetargeting public List props = new List(); - public Transform characterRoot; - // 힙스 가중치 리스트 추가 List hipsWeights = new List(); private float MasterHipsWeight = 1f; @@ -67,8 +64,63 @@ namespace KindRetargeting private float currentChairSeatOffset = 0f; private float targetChairSeatOffset = 0f; - void Update() + private bool isInitialized; + + public void Initialize(TwoBoneIKSolver ikSolver, CustomRetargetingScript crs, Transform characterRoot) { + this.ikSolver = ikSolver; + this.crs = crs; + this.characterRoot = characterRoot; + + if (ikSolver == null || crs == null) return; + + InitWeightLayers(); + + //프랍 오브젝트 찾기 + props = Object.FindObjectsByType(FindObjectsSortMode.None).Select(controller => controller.transform).ToList(); + + // 다른 캐릭터의 손을 props에 추가 + GetHand(); + + //HandDistances()에서 사용을 위한 리스트 추가 + //손 거리에 따른 웨이트 업데이트 인덱스 0번 + leftArmEndWeights.Add(0); + rightArmEndWeights.Add(0); + + // 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번 + leftArmEndWeights.Add(0); + rightArmEndWeights.Add(0); + + // 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번 + leftLegEndWeights.Add(0); + rightLegEndWeights.Add(0); + + // 다리 골 가중치 초기화 + leftLegBendWeights.Add(1f); // 기본 가중치 + rightLegBendWeights.Add(1f); // 기본 가중치 + + if (this.characterRoot == null) + { + this.characterRoot = crs.transform; + } + + // 힙스 가중치 초기화 인덱스 0번 + hipsWeights.Add(1f); // 의자 거리 기반 가중치 + + // 지면 높이 기반 가중치 초기화 인덱스 1번 + hipsWeights.Add(1f); // 지면 높이 기반 가중치 + + // 발 높이 기반 가중치 초기화 인덱스 1번 + leftLegEndWeights.Add(1f); + rightLegEndWeights.Add(1f); + + isInitialized = true; + } + + public void OnUpdate() + { + if (!isInitialized) return; + //손의 거리를 기반으로한 가중치 적용 HandDistances(); @@ -94,68 +146,15 @@ namespace KindRetargeting ApplyWeightsToFBIK(); } - void Start() - { - crs = GetComponent(); - if (crs != null) ikSolver = crs.ikSolver; - - InitWeightLayers(); - - //프랍 오브젝트 찾기 - props = FindObjectsByType(FindObjectsSortMode.None).Select(controller => controller.transform).ToList(); - - // 프랍 오브젝트 찾기 - GetHand(); - - //HandDistances();에서 사용을 위한 리스트 추가 - //손 거리에 따른 웨이트 업데이트 인덱스 0번 - leftArmEndWeights.Add(0); - rightArmEndWeights.Add(0); - - // 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번 - leftArmEndWeights.Add(0); - rightArmEndWeights.Add(0); - - // 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번 - leftLegEndWeights.Add(0); - rightLegEndWeights.Add(0); - - // 다리 골 가중치 초기화 - leftLegBendWeights.Add(1f); // 기본 가중치 - rightLegBendWeights.Add(1f); // 기본 가중치 - - // CharacterController가 있는 루트 오브젝트 찾기 - - - if (characterRoot == null) - { - characterRoot = transform; - } - - // 힙스 가중치 초기화 인덱스 0번 - hipsWeights.Add(1f); // 의자 거리 기반 가중치 - - // 지면 높이 기반 가중치 초기화 인덱스 1번 - hipsWeights.Add(1f); // 지면 높이 기반 가중치 - - // 발 높이 기반 가중치 초기화 인덱스 1번 - leftLegEndWeights.Add(1f); - rightLegEndWeights.Add(1f); - } - private void GetHand() { - // 모든 LimbWeightController 찾기 - LimbWeightController[] allControllers = FindObjectsOfType(); + // 모든 CustomRetargetingScript 찾기 (다른 캐릭터의 손을 props에 추가) + CustomRetargetingScript[] allCrs = Object.FindObjectsByType(FindObjectsSortMode.None); - foreach (LimbWeightController controller in allControllers) + foreach (CustomRetargetingScript otherCrs in allCrs) { // 자기 자신은 제외 - if (controller == this) continue; - - // CustomRetargetingScript 가져오기 - CustomRetargetingScript otherCrs = controller.GetComponent(); - if (otherCrs == null) continue; + if (otherCrs == crs) continue; // 왼손과 오른손 Transform 가져오기 Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); @@ -195,7 +194,7 @@ namespace KindRetargeting { Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand); - if (leftHandTransform != null && rightHandTransform != null && + if (leftHandTransform != null && rightHandTransform != null && props != null && props.Count > 0) { float Distance = Vector3.Distance(leftHandTransform.position, rightHandTransform.position); @@ -254,7 +253,7 @@ namespace KindRetargeting const float MIN_LEG_DISTANCE_RATIO = 0.3f; // 다리 길이의 30% // 거리가 멀수록 가중치 감소 - float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) / + float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) / (MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO)); weightList[weightIndex] = weight; @@ -278,7 +277,7 @@ namespace KindRetargeting minLeftDistance = Mathf.Min(minLeftDistance, distance); } - // 오른손과 프랍 사이의 최소 거리 계산 + // 오른손과 프랍 사이의 최소 거리 계산 float minRightDistance = float.MaxValue; foreach (Transform prop in props) { diff --git a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs index 3472ea5b2..816cb3cc9 100644 --- a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs +++ b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs @@ -215,9 +215,6 @@ namespace KindRetargeting.Remote return; } - var limbWeight = script.GetComponent(); - var handPose = script.GetComponent(); - var data = new Dictionary { // 힙 위치 보정 (로컬) @@ -251,37 +248,25 @@ namespace KindRetargeting.Remote { "fingerCopyMode", (int)GetPrivateField(script, "fingerCopyMode") }, // 캘리브레이션 상태 - { "hasCalibrationData", script.HasCachedSettings() } + { "hasCalibrationData", script.HasCachedSettings() }, + + // LimbWeightController 데이터 + { "limbMinDistance", script.limbWeight.minDistance }, + { "limbMaxDistance", script.limbWeight.maxDistance }, + { "weightSmoothSpeed", script.limbWeight.weightSmoothSpeed }, + { "hipsMinDistance", script.limbWeight.hipsMinDistance }, + { "hipsMaxDistance", script.limbWeight.hipsMaxDistance }, + { "groundHipsMinHeight", script.limbWeight.groundHipsMinHeight }, + { "groundHipsMaxHeight", script.limbWeight.groundHipsMaxHeight }, + { "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold }, + { "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold }, + { "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset }, }; - // LimbWeightController 데이터 - if (limbWeight != null) - { - data["limbMinDistance"] = limbWeight.minDistance; - data["limbMaxDistance"] = limbWeight.maxDistance; - data["weightSmoothSpeed"] = limbWeight.weightSmoothSpeed; - data["hipsMinDistance"] = limbWeight.hipsMinDistance; - data["hipsMaxDistance"] = limbWeight.hipsMaxDistance; - data["groundHipsMinHeight"] = limbWeight.groundHipsMinHeight; - data["groundHipsMaxHeight"] = limbWeight.groundHipsMaxHeight; - data["footHeightMinThreshold"] = limbWeight.footHeightMinThreshold; - data["footHeightMaxThreshold"] = limbWeight.footHeightMaxThreshold; - data["chairSeatHeightOffset"] = limbWeight.chairSeatHeightOffset; - } - // FingerShapedController 데이터 - if (handPose != null) - { - data["handPoseEnabled"] = handPose.enabled; - data["leftHandEnabled"] = handPose.leftHandEnabled; - data["rightHandEnabled"] = handPose.rightHandEnabled; - } - else - { - data["handPoseEnabled"] = false; - data["leftHandEnabled"] = false; - data["rightHandEnabled"] = false; - } + data["handPoseEnabled"] = script.fingerShaped.enabled; + data["leftHandEnabled"] = script.fingerShaped.leftHandEnabled; + data["rightHandEnabled"] = script.fingerShaped.rightHandEnabled; var response = new { @@ -298,9 +283,6 @@ namespace KindRetargeting.Remote var script = FindCharacter(characterId); if (script == null) return; - var limbWeight = script.GetComponent(); - var handPose = script.GetComponent(); - switch (property) { // 힙 위치 보정 @@ -363,56 +345,49 @@ namespace KindRetargeting.Remote // LimbWeightController 속성 case "limbMinDistance": - if (limbWeight != null) limbWeight.minDistance = value; + script.limbWeight.minDistance = value; break; case "limbMaxDistance": - if (limbWeight != null) limbWeight.maxDistance = value; + script.limbWeight.maxDistance = value; break; case "weightSmoothSpeed": - if (limbWeight != null) limbWeight.weightSmoothSpeed = value; + script.limbWeight.weightSmoothSpeed = value; break; case "hipsMinDistance": - if (limbWeight != null) limbWeight.hipsMinDistance = value; + script.limbWeight.hipsMinDistance = value; break; case "hipsMaxDistance": - if (limbWeight != null) limbWeight.hipsMaxDistance = value; + script.limbWeight.hipsMaxDistance = value; break; case "groundHipsMinHeight": - if (limbWeight != null) limbWeight.groundHipsMinHeight = value; + script.limbWeight.groundHipsMinHeight = value; break; case "groundHipsMaxHeight": - if (limbWeight != null) limbWeight.groundHipsMaxHeight = value; + script.limbWeight.groundHipsMaxHeight = value; break; case "footHeightMinThreshold": - if (limbWeight != null) limbWeight.footHeightMinThreshold = value; + script.limbWeight.footHeightMinThreshold = value; break; case "footHeightMaxThreshold": - if (limbWeight != null) limbWeight.footHeightMaxThreshold = value; + script.limbWeight.footHeightMaxThreshold = value; break; case "chairSeatHeightOffset": - if (limbWeight != null) limbWeight.chairSeatHeightOffset = value; + script.limbWeight.chairSeatHeightOffset = value; break; // FingerShapedController 속성 case "handPoseEnabled": - if (handPose != null) - handPose.enabled = value > 0.5f; + script.fingerShaped.enabled = value > 0.5f; break; case "leftHandEnabled": - if (handPose != null) - { - handPose.leftHandEnabled = value > 0.5f; - if (handPose.leftHandEnabled) - handPose.enabled = true; - } + script.fingerShaped.leftHandEnabled = value > 0.5f; + if (script.fingerShaped.leftHandEnabled) + script.fingerShaped.enabled = true; break; case "rightHandEnabled": - if (handPose != null) - { - handPose.rightHandEnabled = value > 0.5f; - if (handPose.rightHandEnabled) - handPose.enabled = true; - } + script.fingerShaped.rightHandEnabled = value > 0.5f; + if (script.fingerShaped.rightHandEnabled) + script.fingerShaped.enabled = true; break; default: @@ -426,8 +401,7 @@ namespace KindRetargeting.Remote var script = FindCharacter(characterId); if (script == null) return; - var handPose = script.GetComponent(); - if (handPose == null) return; + var handPose = script.fingerShaped; // 스크립트 자동 활성화 handPose.enabled = true; From 88ea6a072d735dcae8fc81d1155b0e61eb007e3f Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:28:28 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Fix=20:=20CustomRetargetingScriptEditor?= =?UTF-8?q?=20=EC=BB=B4=ED=8C=8C=EC=9D=BC=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(=EC=9E=98=EB=AA=BB=EB=90=9C=20else=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../KindRetargeting/Editor/CustomRetargetingScriptEditor.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 25c49bc60..469202c70 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -216,10 +216,6 @@ namespace KindRetargeting foldout.Add(smoothField); foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); - else - { - foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); - } return foldout; } From e17cfc003c38575d201eee94630927ebce55a7ee Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:34:33 +0900 Subject: [PATCH 07/10] =?UTF-8?q?Refactor=20:=20CustomRetargetingScriptEdi?= =?UTF-8?q?tor=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20+=20RemoteControll?= =?UTF-8?q?er=20=EB=AA=A8=EB=93=88=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inspector에 모든 내부 모듈 섹션 추가: - 어깨 보정 (blendStrength, maxBlend, heightDiff, curve) - 사지 가중치 (IK 토글, 거리/높이 범위, 스무딩) - 접지 설정 (groundHeight, weight, activationHeight, plantThreshold, smoothSpeed) - 손가락 셰이핑 (활성화, 손가락별 슬라이더, 프리셋 버튼) - 머리 회전 오프셋 (정면 캘리브레이션 포함) - 머리/아바타 크기 - RetargetingRemoteController에 추가된 원격 제어 데이터: - enableLeftArmIK, enableRightArmIK - shoulderBlendStrength, shoulderMaxBlend, shoulderMaxHeightDiff, shoulderMinHeightDiff - groundingWeight, groundingGroundHeight, groundingActivationHeight, groundingPlantThreshold, groundingSmoothSpeed Co-Authored-By: Claude Opus 4.6 --- .../Editor/CustomRetargetingScriptEditor.cs | 649 +++++++++++++----- .../Remote/RetargetingRemoteController.cs | 62 +- 2 files changed, 517 insertions(+), 194 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 469202c70..3bf161e96 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -10,46 +10,9 @@ namespace KindRetargeting { private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - // SerializedProperty - private SerializedProperty sourceAnimatorProp; - private SerializedProperty targetAnimatorProp; - private SerializedProperty hipsOffsetXProp; - private SerializedProperty hipsOffsetYProp; - private SerializedProperty hipsOffsetZProp; - private SerializedProperty debugAxisNormalizerProp; - private SerializedProperty fingerCopyModeProp; - private SerializedProperty kneeInOutWeightProp; - private SerializedProperty kneeFrontBackWeightProp; - private SerializedProperty footFrontBackOffsetProp; - private SerializedProperty footInOutOffsetProp; - private SerializedProperty floorHeightProp; - private SerializedProperty avatarScaleProp; - - // Dynamic UI - private Label cacheStatusLabel; - protected override void OnDisable() { base.OnDisable(); - // groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈 - } - - protected override void OnEnable() - { - base.OnEnable(); - sourceAnimatorProp = serializedObject.FindProperty("sourceAnimator"); - targetAnimatorProp = serializedObject.FindProperty("targetAnimator"); - hipsOffsetXProp = serializedObject.FindProperty("hipsOffsetX"); - hipsOffsetYProp = serializedObject.FindProperty("hipsOffsetY"); - hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ"); - debugAxisNormalizerProp = serializedObject.FindProperty("debugAxisNormalizer"); - fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode"); - kneeInOutWeightProp = serializedObject.FindProperty("kneeInOutWeight"); - kneeFrontBackWeightProp = serializedObject.FindProperty("kneeFrontBackWeight"); - footFrontBackOffsetProp = serializedObject.FindProperty("footFrontBackOffset"); - footInOutOffsetProp = serializedObject.FindProperty("footInOutOffset"); - floorHeightProp = serializedObject.FindProperty("floorHeight"); - avatarScaleProp = serializedObject.FindProperty("avatarScale"); } public override VisualElement CreateInspectorGUI() @@ -59,52 +22,68 @@ namespace KindRetargeting var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); if (commonUss != null) root.styleSheets.Add(commonUss); - // 원본 Animator - root.Add(new PropertyField(sourceAnimatorProp, "원본 Animator")); + // ── 기본 설정 ── + root.Add(new PropertyField(serializedObject.FindProperty("sourceAnimator"), "원본 Animator")); - // 아바타 크기 설정 + // ── 아바타 크기 ── var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true }; - scaleFoldout.Add(new PropertyField(avatarScaleProp, "아바타 크기")); + scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("avatarScale"), "아바타 크기")); + scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("headScale"), "머리 크기")); root.Add(scaleFoldout); - // 힙 위치 보정 + // ── 힙 위치 보정 ── root.Add(BuildHipsSection()); - // 무릎 위치 조정 - var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = true }; - kneeFoldout.Add(new Slider("무릎 앞/뒤 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 뒤로, 양수: 앞으로" }); - kneeFoldout.Q().BindProperty(kneeFrontBackWeightProp); - var kneeInOut = new Slider("무릎 안/밖 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 안쪽, 양수: 바깥쪽" }; - kneeInOut.BindProperty(kneeInOutWeightProp); - kneeFoldout.Add(kneeInOut); + // ── 무릎 위치 조정 ── + var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false }; + var kneeFB = new Slider("무릎 앞/뒤", -1f, 1f) { showInputField = true }; + kneeFB.BindProperty(serializedObject.FindProperty("kneeFrontBackWeight")); + kneeFoldout.Add(kneeFB); + var kneeIO = new Slider("무릎 안/밖", -1f, 1f) { showInputField = true }; + kneeIO.BindProperty(serializedObject.FindProperty("kneeInOutWeight")); + kneeFoldout.Add(kneeIO); root.Add(kneeFoldout); - // 발 IK 위치 조정 - var footFoldout = new Foldout { text = "발 IK 위치 조정", value = true }; - var footFB = new Slider("발 앞/뒤 오프셋", -1f, 1f) { showInputField = true, tooltip = "+: 앞으로, -: 뒤로" }; - footFB.BindProperty(footFrontBackOffsetProp); + // ── 발 IK 위치 조정 ── + var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false }; + var footFB = new Slider("발 앞/뒤", -1f, 1f) { showInputField = true }; + footFB.BindProperty(serializedObject.FindProperty("footFrontBackOffset")); footFoldout.Add(footFB); - var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true, tooltip = "+: 벌리기, -: 모으기" }; - footIO.BindProperty(footInOutOffsetProp); + var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true }; + footIO.BindProperty(serializedObject.FindProperty("footInOutOffset")); footFoldout.Add(footIO); root.Add(footFoldout); - // 손가락 복제 설정 - root.Add(BuildFingerCopySection()); - - // 바닥 높이 조정 + // ── 바닥 높이 ── var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false }; - floorFoldout.Add(new PropertyField(floorHeightProp, "바닥 높이 (-1 ~ 1)")); - + floorFoldout.Add(new PropertyField(serializedObject.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)")); + floorFoldout.Add(new PropertyField(serializedObject.FindProperty("minimumAnkleHeight"), "최소 발목 높이")); root.Add(floorFoldout); - // 접지 설정 (FootGroundingController) + // ── 머리 회전 오프셋 ── + root.Add(BuildHeadRotationSection()); + + // ── 어깨 보정 (ShoulderCorrection) ── + root.Add(BuildShoulderSection()); + + // ── 사지 가중치 (LimbWeight) ── + root.Add(BuildLimbWeightSection()); + + // ── 접지 설정 (FootGrounding) ── root.Add(BuildGroundingSection()); - // 캐시 상태 + 캘리브레이션 버튼 + // ── 손가락 복제 설정 ── + var fingerCopyFoldout = new Foldout { text = "손가락 복제 설정", value = false }; + fingerCopyFoldout.Add(new PropertyField(serializedObject.FindProperty("fingerCopyMode"), "복제 방식")); + root.Add(fingerCopyFoldout); + + // ── 손가락 셰이핑 (FingerShaped) ── + root.Add(BuildFingerShapedSection()); + + // ── 캘리브레이션 ── root.Add(BuildCacheSection()); - // 변경 시 저장 + // ── 변경 시 저장 ── root.TrackSerializedObjectValue(serializedObject, so => { if (target == null) return; @@ -116,212 +95,504 @@ namespace KindRetargeting return root; } + // ========== 힙 위치 보정 ========== + private VisualElement BuildHipsSection() { - var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)" }; + var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)", value = true }; - // 축 매핑 정보 - var axisInfo = new HelpBox("플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다.", HelpBoxMessageType.Info); + var axisInfo = new HelpBox("플레이 모드에서 축 매핑 정보가 표시됩니다.", HelpBoxMessageType.Info); foldout.Add(axisInfo); - // 주기적으로 축 매핑 정보 갱신 foldout.schedule.Execute(() => { - if (target == null || debugAxisNormalizerProp == null) return; + if (target == null) return; serializedObject.Update(); - Vector3 axisMapping = debugAxisNormalizerProp.vector3Value; - - if (Application.isPlaying && axisMapping != Vector3.one) - { - string GetAxisName(float value) - { - int axis = Mathf.RoundToInt(Mathf.Abs(value)); - string sign = value > 0 ? "+" : "-"; - return axis switch { 1 => $"{sign}X", 2 => $"{sign}Y", 3 => $"{sign}Z", _ => "?" }; - } - axisInfo.text = "T-포즈에서 분석된 축 매핑:\n" + - $" 좌우 오프셋 → 로컬 {GetAxisName(axisMapping.x)} 축\n" + - $" 상하 오프셋 → 로컬 {GetAxisName(axisMapping.y)} 축\n" + - $" 앞뒤 오프셋 → 로컬 {GetAxisName(axisMapping.z)} 축\n\n" + - "이 매핑 덕분에 모든 아바타에서 동일하게 작동합니다."; - } - else - { - axisInfo.text = "플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다."; - } + var axisProp = serializedObject.FindProperty("debugAxisNormalizer"); + if (axisProp == null || !Application.isPlaying) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; } + Vector3 m = axisProp.vector3Value; + if (m == Vector3.one) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; } + string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" }; + axisInfo.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}"; }).Every(500); - foldout.Add(new PropertyField(hipsOffsetXProp, "좌우 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 왼쪽(-) / 오른쪽(+)" }); - foldout.Add(new PropertyField(hipsOffsetYProp, "상하 오프셋 (↓-/+↑)") { tooltip = "캐릭터 기준 아래(-) / 위(+)" }); - foldout.Add(new PropertyField(hipsOffsetZProp, "앞뒤 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 뒤(-) / 앞(+)" }); - foldout.Add(new HelpBox("로컬 좌표계 기반: 캐릭터의 회전 상태와 관계없이 항상 캐릭터 기준으로 이동합니다.", HelpBoxMessageType.Info)); + var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true }; + hx.BindProperty(serializedObject.FindProperty("hipsOffsetX")); + foldout.Add(hx); + var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true }; + hy.BindProperty(serializedObject.FindProperty("hipsOffsetY")); + foldout.Add(hy); + var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true }; + hz.BindProperty(serializedObject.FindProperty("hipsOffsetZ")); + foldout.Add(hz); + + // 의자 앉기 높이 + var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정" }; + chairSlider.BindProperty(serializedObject.FindProperty("limbWeight.chairSeatHeightOffset")); + foldout.Add(chairSlider); // 다리 길이 자동 보정 버튼 var autoHipsBtn = new Button(() => { - if (!Application.isPlaying) - { - Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다."); - return; - } + if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; } var script = (CustomRetargetingScript)target; float offset = CalculateHipsOffsetFromLegDifference(script); - hipsOffsetYProp.floatValue = offset; + serializedObject.FindProperty("hipsOffsetY").floatValue = offset; serializedObject.ApplyModifiedProperties(); script.SaveSettings(); - Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}"); - }) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" }; - autoHipsBtn.style.marginTop = 4; + }) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다." }; + autoHipsBtn.style.marginTop = 4; autoHipsBtn.style.height = 25; foldout.Add(autoHipsBtn); return foldout; } - private VisualElement BuildFingerCopySection() + // ========== 머리 회전 오프셋 ========== + + private VisualElement BuildHeadRotationSection() { - var foldout = new Foldout { text = "손가락 복제 설정" }; - foldout.Add(new PropertyField(fingerCopyModeProp, "복제 방식") { tooltip = "손가락 포즈를 복제하는 방식을 선택합니다." }); + var foldout = new Foldout { text = "머리 회전 오프셋", value = false }; + + var xProp = serializedObject.FindProperty("headRotationOffsetX"); + var yProp = serializedObject.FindProperty("headRotationOffsetY"); + var zProp = serializedObject.FindProperty("headRotationOffsetZ"); + + var xSlider = new Slider("X (Roll)", -180f, 180f) { showInputField = true }; + xSlider.BindProperty(xProp); + foldout.Add(xSlider); + var ySlider = new Slider("Y (Yaw)", -180f, 180f) { showInputField = true }; + ySlider.BindProperty(yProp); + foldout.Add(ySlider); + var zSlider = new Slider("Z (Pitch)", -180f, 180f) { showInputField = true }; + zSlider.BindProperty(zProp); + foldout.Add(zSlider); + + var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } }; + var resetBtn = new Button(() => + { + xProp.floatValue = yProp.floatValue = zProp.floatValue = 0f; + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(target); + }) { text = "초기화" }; + resetBtn.style.flexGrow = 1; resetBtn.style.height = 25; resetBtn.style.marginRight = 2; + btnRow.Add(resetBtn); + + var calibBtn = new Button(() => + { + if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; } + CalibrateHeadToForward(serializedObject, xProp, yProp, zProp); + }) { text = "정면 캘리브레이션" }; + calibBtn.style.flexGrow = 1; calibBtn.style.height = 25; + btnRow.Add(calibBtn); + foldout.Add(btnRow); + + foldout.schedule.Execute(() => + { + calibBtn.SetEnabled(Application.isPlaying); + }).Every(500); + return foldout; } + // ========== 어깨 보정 ========== + + private VisualElement BuildShoulderSection() + { + var foldout = new Foldout { text = "어깨 보정 (ShoulderCorrection)", value = false }; + + var strength = new Slider("블렌드 강도", 0f, 5f) { showInputField = true }; + strength.BindProperty(serializedObject.FindProperty("shoulderCorrection.blendStrength")); + foldout.Add(strength); + + var maxBlend = new Slider("최대 블렌드", 0f, 1f) { showInputField = true }; + maxBlend.BindProperty(serializedObject.FindProperty("shoulderCorrection.maxShoulderBlend")); + foldout.Add(maxBlend); + + foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.reverseLeftRotation"), "왼쪽 회전 반전")); + foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.reverseRightRotation"), "오른쪽 회전 반전")); + + foldout.Add(BuildMinMaxRange("높이 차이 범위", + serializedObject.FindProperty("shoulderCorrection.minHeightDifference"), + serializedObject.FindProperty("shoulderCorrection.maxHeightDifference"), + -0.5f, 2f)); + + foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.shoulderCorrectionCurve"), "보정 커브")); + + return foldout; + } + + // ========== 사지 가중치 ========== + + private VisualElement BuildLimbWeightSection() + { + var foldout = new Foldout { text = "사지 가중치 (LimbWeight)", value = false }; + + // IK 활성화 토글 + foldout.Add(new PropertyField(serializedObject.FindProperty("limbWeight.enableLeftArmIK"), "왼팔 IK 활성화")); + foldout.Add(new PropertyField(serializedObject.FindProperty("limbWeight.enableRightArmIK"), "오른팔 IK 활성화")); + + foldout.Add(BuildMinMaxRange("손-프랍 거리 범위 (가중치 1→0)", + serializedObject.FindProperty("limbWeight.minDistance"), + serializedObject.FindProperty("limbWeight.maxDistance"), + 0f, 1f)); + + var smoothField = new Slider("가중치 변화 속도", 0.1f, 20f) { showInputField = true }; + smoothField.BindProperty(serializedObject.FindProperty("limbWeight.weightSmoothSpeed")); + foldout.Add(smoothField); + + foldout.Add(BuildMinMaxRange("의자-허리 거리 범위 (가중치 1→0)", + serializedObject.FindProperty("limbWeight.hipsMinDistance"), + serializedObject.FindProperty("limbWeight.hipsMaxDistance"), + 0f, 1f)); + + foldout.Add(BuildMinMaxRange("바닥-허리 높이 블렌딩 (가중치 0→1)", + serializedObject.FindProperty("limbWeight.groundHipsMinHeight"), + serializedObject.FindProperty("limbWeight.groundHipsMaxHeight"), + 0f, 2f)); + + foldout.Add(BuildMinMaxRange("발 높이 IK 블렌딩 (가중치 1→0)", + serializedObject.FindProperty("limbWeight.footHeightMinThreshold"), + serializedObject.FindProperty("limbWeight.footHeightMaxThreshold"), + 0.1f, 1f)); + + return foldout; + } + + // ========== 접지 설정 ========== + private VisualElement BuildGroundingSection() { var foldout = new Foldout { text = "접지 설정 (FootGrounding)", value = false }; foldout.Add(new HelpBox( "HIK 스타일 2-Pass 접지 시스템:\n" + - "• Pre-IK: IK 타겟을 조정하여 발이 바닥을 뚫지 않도록 보정\n" + - "• Post-IK: Foot 회전으로 Toes 접지 잔차 미세 보정\n" + - "• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지", + "• Pre-IK: 발이 바닥을 뚫지 않도록 IK 타겟 보정\n" + + "• Post-IK: Foot 회전으로 Toes 접지 미세 보정", HelpBoxMessageType.Info)); - // FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근 - var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"); - foldout.Add(groundHeightField); + foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이")); var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight")); foldout.Add(weightSlider); - var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이"); - activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; - foldout.Add(activationField); + foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" }); + foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위")); - var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"); - thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; - foldout.Add(thresholdField); - - var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도"); - smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)"; + var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true }; + smoothField.BindProperty(serializedObject.FindProperty("footGrounding.smoothSpeed")); foldout.Add(smoothField); - foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); - return foldout; } + // ========== 손가락 셰이핑 ========== + + private VisualElement BuildFingerShapedSection() + { + var foldout = new Foldout { text = "손가락 셰이핑 (FingerShaped)", value = false }; + + foldout.Add(new PropertyField(serializedObject.FindProperty("fingerShaped.enabled"), "셰이핑 활성화")); + foldout.Add(BuildHandSliders("왼손", "left")); + foldout.Add(BuildHandSliders("오른손", "right")); + + // 프리셋 버튼 + var presetLabel = new Label("프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 6 } }; + foldout.Add(presetLabel); + string[,] presets = { { "가위", "바위", "보" }, { "브이", "검지", "초기화" } }; + for (int row = 0; row < 2; row++) + { + var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginBottom = 2 } }; + for (int col = 0; col < 3; col++) + { + string name = presets[row, col]; + var btn = new Button(() => ApplyFingerPreset(name)) { text = name }; + btn.style.height = 26; btn.style.width = 80; btn.style.marginLeft = btn.style.marginRight = 2; + btnRow.Add(btn); + } + foldout.Add(btnRow); + } + + return foldout; + } + + private VisualElement BuildHandSliders(string label, string prefix) + { + var handFoldout = new Foldout { text = label, value = false }; + + handFoldout.Add(new PropertyField(serializedObject.FindProperty($"fingerShaped.{prefix}HandEnabled"), "활성화")); + + string[] names = { "Thumb", "Index", "Middle", "Ring", "Pinky" }; + string[] korNames = { "엄지", "검지", "중지", "약지", "새끼" }; + + var slidersRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginTop = 4 } }; + for (int i = 0; i < names.Length; i++) + { + var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } }; + col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }); + + var prop = serializedObject.FindProperty($"fingerShaped.{prefix}{names[i]}Curl"); + var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }; + col.Add(valLabel); + + var slider = new Slider(-1f, 1f) { direction = SliderDirection.Vertical }; + slider.style.height = 80; slider.style.width = 25; + slider.BindProperty(prop); + slider.RegisterValueChangedCallback(evt => valLabel.text = evt.newValue.ToString("F1")); + col.Add(slider); + slidersRow.Add(col); + } + handFoldout.Add(slidersRow); + + var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true }; + spreadSlider.BindProperty(serializedObject.FindProperty($"fingerShaped.{prefix}SpreadFingers")); + handFoldout.Add(spreadSlider); + + return handFoldout; + } + + private void ApplyFingerPreset(string presetName) + { + var script = (CustomRetargetingScript)target; + var fc = script.fingerShaped; + if (!fc.enabled) fc.enabled = true; + + (float t, float i, float m, float r, float p, float s) = presetName switch + { + "가위" => (1f, 1f, -1f, -1f, -1f, 0.3f), + "바위" => (-1f, -1f, -1f, -1f, -1f, 0f), + "보" => (1f, 1f, 1f, 1f, 1f, 1f), + "브이" => (-1f, 1f, 1f, -1f, -1f, 1f), + "검지" => (-1f, 1f, -1f, -1f, -1f, 0f), + "초기화" => (0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f), + _ => (0f, 0f, 0f, 0f, 0f, 0f) + }; + + if (fc.leftHandEnabled) + { + fc.leftThumbCurl = t; fc.leftIndexCurl = i; fc.leftMiddleCurl = m; + fc.leftRingCurl = r; fc.leftPinkyCurl = p; fc.leftSpreadFingers = s; + } + if (fc.rightHandEnabled) + { + fc.rightThumbCurl = t; fc.rightIndexCurl = i; fc.rightMiddleCurl = m; + fc.rightRingCurl = r; fc.rightPinkyCurl = p; fc.rightSpreadFingers = s; + } + EditorUtility.SetDirty(target); + } + + // ========== 캘리브레이션 ========== + private VisualElement BuildCacheSection() { - var container = new VisualElement { style = { marginTop = 8 } }; + var box = new VisualElement { style = { marginTop = 8 } }; + box.style.backgroundColor = new Color(0, 0, 0, 0.1f); + box.style.borderTopLeftRadius = box.style.borderTopRightRadius = + box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4; + box.style.paddingTop = box.style.paddingBottom = + box.style.paddingLeft = box.style.paddingRight = 6; - var script = (CustomRetargetingScript)target; - bool hasCached = script.HasCachedSettings(); - - cacheStatusLabel = new Label(hasCached ? - "캘리브레이션 데이터가 저장되어 있습니다." : - "저장된 캘리브레이션 데이터가 없습니다."); - cacheStatusLabel.style.unityFontStyleAndWeight = FontStyle.Bold; - container.Add(cacheStatusLabel); + var cacheLabel = new Label(); + cacheLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + box.Add(cacheLabel); var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } }; - var calibBtn = new Button(() => - { - ((CustomRetargetingScript)target).I_PoseCalibration(); - UpdateCacheStatus(); - }) { text = "I-포즈 캘리브레이션" }; - calibBtn.style.flexGrow = 1; - calibBtn.style.marginRight = 2; + var calibBtn = new Button(() => { ((CustomRetargetingScript)target).I_PoseCalibration(); }) { text = "I-포즈 캘리브레이션" }; + calibBtn.style.flexGrow = 1; calibBtn.style.marginRight = 2; btnRow.Add(calibBtn); - var deleteCacheBtn = new Button(() => + var resetBtn = new Button(() => { ((CustomRetargetingScript)target).ResetPoseAndCache(); }) { text = "캘리브레이션 초기화" }; + resetBtn.style.flexGrow = 1; + btnRow.Add(resetBtn); + box.Add(btnRow); + + // 전체 자동 보정 + var autoBtn = new Button(() => { - ((CustomRetargetingScript)target).ResetPoseAndCache(); - UpdateCacheStatus(); - }) { text = "캐시 데이터 삭제" }; - deleteCacheBtn.style.flexGrow = 1; - btnRow.Add(deleteCacheBtn); + if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; } + AutoCalibrateAll((CustomRetargetingScript)target, serializedObject); + }) { text = "전체 자동 보정 (크기 + 힙 높이 + 머리 정면)", tooltip = "아바타 크기, 힙 높이, 머리 정면을 자동 보정합니다." }; + autoBtn.style.marginTop = 4; autoBtn.style.height = 28; + box.Add(autoBtn); - container.Add(btnRow); - - // 주기적으로 캐시 상태 갱신 - container.schedule.Execute(() => + box.schedule.Execute(() => { if (target == null) return; bool cached = ((CustomRetargetingScript)target).HasCachedSettings(); - deleteCacheBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None; - cacheStatusLabel.text = cached ? - "캘리브레이션 데이터가 저장되어 있습니다." : - "저장된 캘리브레이션 데이터가 없습니다."; + cacheLabel.text = cached ? "캘리브레이션 데이터가 저장되어 있습니다." : "저장된 캘리브레이션 데이터가 없습니다."; + resetBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None; }).Every(1000); - deleteCacheBtn.style.display = hasCached ? DisplayStyle.Flex : DisplayStyle.None; + return box; + } + // ========== MinMax 헬퍼 ========== + + private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax) + { + var container = new VisualElement { style = { marginBottom = 4 } }; + if (minProp == null || maxProp == null) + { + container.Add(new HelpBox($"'{label}' 프로퍼티를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); + return container; + } + container.Add(new Label(label) { style = { marginBottom = 2, fontSize = 11 } }); + + var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; + var minField = new FloatField { value = minProp.floatValue, style = { width = 50 } }; + var slider = new MinMaxSlider(minProp.floatValue, maxProp.floatValue, limitMin, limitMax); + slider.style.flexGrow = 1; slider.style.marginLeft = slider.style.marginRight = 4; + var maxField = new FloatField { value = maxProp.floatValue, style = { width = 50 } }; + + slider.RegisterValueChangedCallback(evt => + { + minProp.floatValue = evt.newValue.x; maxProp.floatValue = evt.newValue.y; + serializedObject.ApplyModifiedProperties(); + minField.SetValueWithoutNotify(evt.newValue.x); maxField.SetValueWithoutNotify(evt.newValue.y); + }); + minField.RegisterValueChangedCallback(evt => + { + float v = Mathf.Clamp(evt.newValue, limitMin, maxProp.floatValue); + minProp.floatValue = v; serializedObject.ApplyModifiedProperties(); + slider.SetValueWithoutNotify(new Vector2(v, maxProp.floatValue)); minField.SetValueWithoutNotify(v); + }); + maxField.RegisterValueChangedCallback(evt => + { + float v = Mathf.Clamp(evt.newValue, minProp.floatValue, limitMax); + maxProp.floatValue = v; serializedObject.ApplyModifiedProperties(); + slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, v)); maxField.SetValueWithoutNotify(v); + }); + + container.TrackPropertyValue(minProp, p => { minField.SetValueWithoutNotify(p.floatValue); slider.SetValueWithoutNotify(new Vector2(p.floatValue, maxProp.floatValue)); }); + container.TrackPropertyValue(maxProp, p => { maxField.SetValueWithoutNotify(p.floatValue); slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, p.floatValue)); }); + + row.Add(minField); row.Add(slider); row.Add(maxField); + container.Add(row); return container; } - private void UpdateCacheStatus() + // ========== 자동 보정 ========== + + private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so) { - if (cacheStatusLabel == null || target == null) return; - bool hasCached = ((CustomRetargetingScript)target).HasCachedSettings(); - cacheStatusLabel.text = hasCached ? - "캘리브레이션 데이터가 저장되어 있습니다." : - "저장된 캘리브레이션 데이터가 없습니다."; + Animator source = script.sourceAnimator; + Animator targetAnim = script.targetAnimator; + if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman) + { + Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); + return; + } + + script.ResetScale(); + so.FindProperty("avatarScale").floatValue = 1f; + so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script); + so.ApplyModifiedProperties(); + + EditorApplication.delayCall += () => + { + if (script == null) return; + Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck); + Transform targetNeck = targetAnim.GetBoneTransform(HumanBodyBones.Neck); + if (sourceNeck == null || targetNeck == null) return; + + float scaleRatio = Mathf.Clamp(sourceNeck.position.y / Mathf.Max(targetNeck.position.y, 0.01f), 0.1f, 3f); + script.SetAvatarScale(scaleRatio); + + var so2 = new SerializedObject(script); + so2.FindProperty("avatarScale").floatValue = scaleRatio; + so2.ApplyModifiedProperties(); + + EditorApplication.delayCall += () => + { + if (script == null) return; + var so3 = new SerializedObject(script); + so3.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script); + + var xP = so3.FindProperty("headRotationOffsetX"); + var yP = so3.FindProperty("headRotationOffsetY"); + var zP = so3.FindProperty("headRotationOffsetZ"); + if (xP != null && yP != null && zP != null) + CalibrateHeadToForward(so3, xP, yP, zP); + + so3.ApplyModifiedProperties(); + script.SaveSettings(); + Debug.Log($"전체 자동 보정 완료: avatarScale={scaleRatio:F3}"); + }; + }; } - /// - /// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다. - /// 소스 다리가 더 길면 → 음수 (힙을 내려서 타겟이 뜨지 않게) - /// 소스 다리가 더 짧으면 → 양수 (힙을 올려서 타겟 다리를 펴줌) - /// + // ========== 유틸리티 ========== + private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script) { Animator source = script.sourceAnimator; - Animator target = script.targetAnimator; - - if (source == null || target == null || !source.isHuman || !target.isHuman) - { - Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); - return 0f; - } + Animator targetAnim = script.targetAnimator; + if (source == null || targetAnim == null) return 0f; float sourceLeg = GetLegLength(source); - float targetLeg = GetLegLength(target); + float targetLeg = GetLegLength(targetAnim); + if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f; - if (sourceLeg < 0.01f || targetLeg < 0.01f) - { - Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요."); - return 0f; - } - - // 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수) - // 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수) - float diff = targetLeg - sourceLeg; - Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m"); - return diff; + return targetLeg - sourceLeg; } private float GetLegLength(Animator animator) { - Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); - Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg); + Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); + Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg); Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); + if (upper == null || lower == null || foot == null) return 0f; + return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position); + } - if (upperLeg == null || lowerLeg == null || foot == null) return 0f; + private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp) + { + CustomRetargetingScript script = so.targetObject as CustomRetargetingScript; + if (script == null) return; - float upper = Vector3.Distance(upperLeg.position, lowerLeg.position); - float lower = Vector3.Distance(lowerLeg.position, foot.position); - return upper + lower; + Animator targetAnimator = script.GetComponent(); + if (targetAnimator == null) return; + + Transform targetHead = targetAnimator.GetBoneTransform(HumanBodyBones.Head); + if (targetHead == null) return; + + Vector3 tPoseForward = script.tPoseHeadForward; + Vector3 tPoseUp = script.tPoseHeadUp; + if (tPoseForward.sqrMagnitude < 0.001f) return; + + float prevX = xProp.floatValue, prevY = yProp.floatValue, prevZ = zProp.floatValue; + Quaternion currentLocalRot = targetHead.localRotation; + Quaternion prevOffset = Quaternion.Euler(prevX, prevY, prevZ); + Quaternion baseLocalRot = currentLocalRot * Quaternion.Inverse(prevOffset); + + Transform headParent = targetHead.parent; + Quaternion parentWorldRot = headParent != null ? headParent.rotation : Quaternion.identity; + Quaternion baseWorldRot = parentWorldRot * baseLocalRot; + + Vector3 currentHeadForward = baseWorldRot * Vector3.forward; + Vector3 currentHeadUp = baseWorldRot * Vector3.up; + + Quaternion forwardCorrection = Quaternion.FromToRotation(currentHeadForward, tPoseForward); + Vector3 correctedUp = forwardCorrection * currentHeadUp; + float rollAngle = Vector3.SignedAngle(correctedUp, tPoseUp, tPoseForward); + Quaternion rollCorrection = Quaternion.AngleAxis(rollAngle, tPoseForward); + Quaternion worldCorrection = rollCorrection * forwardCorrection; + + Quaternion correctedWorldRot = worldCorrection * baseWorldRot; + Quaternion correctedLocalRot = Quaternion.Inverse(parentWorldRot) * correctedWorldRot; + Quaternion offsetQuat = Quaternion.Inverse(baseLocalRot) * correctedLocalRot; + + Vector3 euler = offsetQuat.eulerAngles; + if (euler.x > 180f) euler.x -= 360f; + if (euler.y > 180f) euler.y -= 360f; + if (euler.z > 180f) euler.z -= 360f; + + xProp.floatValue = Mathf.Clamp(euler.x, -180f, 180f); + yProp.floatValue = Mathf.Clamp(euler.y, -180f, 180f); + zProp.floatValue = Mathf.Clamp(euler.z, -180f, 180f); + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(so.targetObject); } } } diff --git a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs index 816cb3cc9..fbf07c675 100644 --- a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs +++ b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs @@ -261,12 +261,27 @@ namespace KindRetargeting.Remote { "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold }, { "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold }, { "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset }, - }; + { "enableLeftArmIK", script.limbWeight.enableLeftArmIK }, + { "enableRightArmIK", script.limbWeight.enableRightArmIK }, - // FingerShapedController 데이터 - data["handPoseEnabled"] = script.fingerShaped.enabled; - data["leftHandEnabled"] = script.fingerShaped.leftHandEnabled; - data["rightHandEnabled"] = script.fingerShaped.rightHandEnabled; + // ShoulderCorrection 데이터 + { "shoulderBlendStrength", script.shoulderCorrection.blendStrength }, + { "shoulderMaxBlend", script.shoulderCorrection.maxShoulderBlend }, + { "shoulderMaxHeightDiff", script.shoulderCorrection.maxHeightDifference }, + { "shoulderMinHeightDiff", script.shoulderCorrection.minHeightDifference }, + + // FootGrounding 데이터 + { "groundingWeight", script.footGrounding.groundingWeight }, + { "groundingGroundHeight", script.footGrounding.groundHeight }, + { "groundingActivationHeight", script.footGrounding.activationHeight }, + { "groundingPlantThreshold", script.footGrounding.plantThreshold }, + { "groundingSmoothSpeed", script.footGrounding.smoothSpeed }, + + // FingerShapedController 데이터 + { "handPoseEnabled", script.fingerShaped.enabled }, + { "leftHandEnabled", script.fingerShaped.leftHandEnabled }, + { "rightHandEnabled", script.fingerShaped.rightHandEnabled }, + }; var response = new { @@ -374,6 +389,43 @@ namespace KindRetargeting.Remote case "chairSeatHeightOffset": script.limbWeight.chairSeatHeightOffset = value; break; + case "enableLeftArmIK": + script.limbWeight.enableLeftArmIK = value > 0.5f; + break; + case "enableRightArmIK": + script.limbWeight.enableRightArmIK = value > 0.5f; + break; + + // ShoulderCorrection 속성 + case "shoulderBlendStrength": + script.shoulderCorrection.blendStrength = value; + break; + case "shoulderMaxBlend": + script.shoulderCorrection.maxShoulderBlend = value; + break; + case "shoulderMaxHeightDiff": + script.shoulderCorrection.maxHeightDifference = value; + break; + case "shoulderMinHeightDiff": + script.shoulderCorrection.minHeightDifference = value; + break; + + // FootGrounding 속성 + case "groundingWeight": + script.footGrounding.groundingWeight = value; + break; + case "groundingGroundHeight": + script.footGrounding.groundHeight = value; + break; + case "groundingActivationHeight": + script.footGrounding.activationHeight = value; + break; + case "groundingPlantThreshold": + script.footGrounding.plantThreshold = value; + break; + case "groundingSmoothSpeed": + script.footGrounding.smoothSpeed = value; + break; // FingerShapedController 속성 case "handPoseEnabled": From defed38ae57b16162a2e0345018cc53144ef6c97 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:48:58 +0900 Subject: [PATCH 08/10] =?UTF-8?q?Refactor=20:=20=EB=A6=AC=ED=83=80?= =?UTF-8?q?=EA=B2=9F=ED=8C=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20+=20=EC=9B=B9=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20+=20=EB=A6=AC=EB=AA=A8=ED=8A=B8=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RetargetingControlWindow: 어깨 보정, 접지 설정, IK 토글, 최소 발목 높이 섹션 추가 - RetargetingRemoteController: 어깨 반전 토글, 개별 손가락 curl, 최소 발목 높이 Send/Update 추가 - dashboard_script.txt: 어깨 보정/접지 설정 섹션 신규, IK 토글, 개별 손가락 슬라이더, 최소 발목 높이 추가 - 에디터 인스펙터/컨트롤 패널/웹 대시보드 3곳 설정 항목 완전 동기화 Co-Authored-By: Claude Opus 4.6 --- .../StreamingleDashboard/dashboard_script.txt | 4 +- .../Editor/RetargetingControlWindow.cs | 73 ++++++++++++++++++- .../Remote/RetargetingRemoteController.cs | 66 +++++++++++++++++ 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/Assets/Resources/StreamingleDashboard/dashboard_script.txt b/Assets/Resources/StreamingleDashboard/dashboard_script.txt index af1b39464..b5f65181d 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:059ac85fede20a79fe0403d71a144085818ec97e5a802a4c7e31a050a1536390 -size 73083 +oid sha256:3cc8474349ae9b211196debb5dadaf88beac2b04fd106d4ddf3a341005f8b09b +size 77699 diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index 7f92c758e..13d306daf 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -224,6 +224,12 @@ public class RetargetingControlWindow : EditorWindow footContainer.Bind(so); panel.Add(footFoldout); + // 어깨 보정 + panel.Add(BuildShoulderSection(script, so)); + + // 접지 설정 + panel.Add(BuildGroundingSection(script, so)); + // 손가락 제어 설정 panel.Add(BuildFingerControlSection(script, so)); @@ -232,9 +238,11 @@ public class RetargetingControlWindow : EditorWindow // 바닥 높이 설정 var floorFoldout = new Foldout { text = "바닥 높이 설정", value = false }; - var floorField = new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"); - floorFoldout.Add(floorField); - floorField.Bind(so); + var floorContainer = new VisualElement(); + floorContainer.Add(new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)")); + floorContainer.Add(new PropertyField(so.FindProperty("minimumAnkleHeight"), "최소 발목 높이")); + floorContainer.Bind(so); + floorFoldout.Add(floorContainer); panel.Add(floorFoldout); // 아바타 크기 설정 @@ -304,6 +312,10 @@ public class RetargetingControlWindow : EditorWindow var foldout = new Foldout { text = "가중치 설정", value = false }; var container = new VisualElement(); + // IK 활성화 토글 + container.Add(new PropertyField(so.FindProperty("limbWeight.enableLeftArmIK"), "왼팔 IK 활성화")); + container.Add(new PropertyField(so.FindProperty("limbWeight.enableRightArmIK"), "오른팔 IK 활성화")); + container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)", so.FindProperty("limbWeight.minDistance"), so.FindProperty("limbWeight.maxDistance"), 0f, 1f, so)); container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)", @@ -736,6 +748,61 @@ public class RetargetingControlWindow : EditorWindow return box; } + // ========== Shoulder Correction ========== + + private VisualElement BuildShoulderSection(CustomRetargetingScript script, SerializedObject so) + { + var foldout = new Foldout { text = "어깨 보정", value = false }; + var container = new VisualElement(); + + var strength = new Slider("블렌드 강도", 0f, 5f) { showInputField = true }; + strength.BindProperty(so.FindProperty("shoulderCorrection.blendStrength")); + container.Add(strength); + + var maxBlend = new Slider("최대 블렌드", 0f, 1f) { showInputField = true }; + maxBlend.BindProperty(so.FindProperty("shoulderCorrection.maxShoulderBlend")); + container.Add(maxBlend); + + container.Add(new PropertyField(so.FindProperty("shoulderCorrection.reverseLeftRotation"), "왼쪽 회전 반전")); + container.Add(new PropertyField(so.FindProperty("shoulderCorrection.reverseRightRotation"), "오른쪽 회전 반전")); + + container.Add(BuildMinMaxRange("높이 차이 범위", + so.FindProperty("shoulderCorrection.minHeightDifference"), + so.FindProperty("shoulderCorrection.maxHeightDifference"), + -0.5f, 2f, so)); + + container.Add(new PropertyField(so.FindProperty("shoulderCorrection.shoulderCorrectionCurve"), "보정 커브")); + + container.Bind(so); + foldout.Add(container); + return foldout; + } + + // ========== Foot Grounding ========== + + private VisualElement BuildGroundingSection(CustomRetargetingScript script, SerializedObject so) + { + var foldout = new Foldout { text = "접지 설정", value = false }; + var container = new VisualElement(); + + container.Add(new PropertyField(so.FindProperty("footGrounding.groundHeight"), "바닥 높이")); + + var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; + weightSlider.BindProperty(so.FindProperty("footGrounding.groundingWeight")); + container.Add(weightSlider); + + container.Add(new PropertyField(so.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" }); + container.Add(new PropertyField(so.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위")); + + var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true }; + smoothField.BindProperty(so.FindProperty("footGrounding.smoothSpeed")); + container.Add(smoothField); + + container.Bind(so); + foldout.Add(container); + return foldout; + } + // ========== Helpers ========== private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax, SerializedObject so) diff --git a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs index fbf07c675..cb4b1198d 100644 --- a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs +++ b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs @@ -269,6 +269,8 @@ namespace KindRetargeting.Remote { "shoulderMaxBlend", script.shoulderCorrection.maxShoulderBlend }, { "shoulderMaxHeightDiff", script.shoulderCorrection.maxHeightDifference }, { "shoulderMinHeightDiff", script.shoulderCorrection.minHeightDifference }, + { "shoulderReverseLeft", script.shoulderCorrection.reverseLeftRotation }, + { "shoulderReverseRight", script.shoulderCorrection.reverseRightRotation }, // FootGrounding 데이터 { "groundingWeight", script.footGrounding.groundingWeight }, @@ -281,6 +283,21 @@ namespace KindRetargeting.Remote { "handPoseEnabled", script.fingerShaped.enabled }, { "leftHandEnabled", script.fingerShaped.leftHandEnabled }, { "rightHandEnabled", script.fingerShaped.rightHandEnabled }, + { "leftThumbCurl", script.fingerShaped.leftThumbCurl }, + { "leftIndexCurl", script.fingerShaped.leftIndexCurl }, + { "leftMiddleCurl", script.fingerShaped.leftMiddleCurl }, + { "leftRingCurl", script.fingerShaped.leftRingCurl }, + { "leftPinkyCurl", script.fingerShaped.leftPinkyCurl }, + { "leftSpreadFingers", script.fingerShaped.leftSpreadFingers }, + { "rightThumbCurl", script.fingerShaped.rightThumbCurl }, + { "rightIndexCurl", script.fingerShaped.rightIndexCurl }, + { "rightMiddleCurl", script.fingerShaped.rightMiddleCurl }, + { "rightRingCurl", script.fingerShaped.rightRingCurl }, + { "rightPinkyCurl", script.fingerShaped.rightPinkyCurl }, + { "rightSpreadFingers", script.fingerShaped.rightSpreadFingers }, + + // 최소 발목 높이 + { "minimumAnkleHeight", GetPrivateField(script, "minimumAnkleHeight") }, }; var response = new @@ -409,6 +426,12 @@ namespace KindRetargeting.Remote case "shoulderMinHeightDiff": script.shoulderCorrection.minHeightDifference = value; break; + case "shoulderReverseLeft": + script.shoulderCorrection.reverseLeftRotation = value > 0.5f; + break; + case "shoulderReverseRight": + script.shoulderCorrection.reverseRightRotation = value > 0.5f; + break; // FootGrounding 속성 case "groundingWeight": @@ -442,6 +465,49 @@ namespace KindRetargeting.Remote script.fingerShaped.enabled = true; break; + // 개별 손가락 curl 값 + case "leftThumbCurl": + script.fingerShaped.leftThumbCurl = value; + break; + case "leftIndexCurl": + script.fingerShaped.leftIndexCurl = value; + break; + case "leftMiddleCurl": + script.fingerShaped.leftMiddleCurl = value; + break; + case "leftRingCurl": + script.fingerShaped.leftRingCurl = value; + break; + case "leftPinkyCurl": + script.fingerShaped.leftPinkyCurl = value; + break; + case "leftSpreadFingers": + script.fingerShaped.leftSpreadFingers = value; + break; + case "rightThumbCurl": + script.fingerShaped.rightThumbCurl = value; + break; + case "rightIndexCurl": + script.fingerShaped.rightIndexCurl = value; + break; + case "rightMiddleCurl": + script.fingerShaped.rightMiddleCurl = value; + break; + case "rightRingCurl": + script.fingerShaped.rightRingCurl = value; + break; + case "rightPinkyCurl": + script.fingerShaped.rightPinkyCurl = value; + break; + case "rightSpreadFingers": + script.fingerShaped.rightSpreadFingers = value; + break; + + // 최소 발목 높이 + case "minimumAnkleHeight": + SetPrivateField(script, "minimumAnkleHeight", value); + break; + default: Debug.LogWarning($"[RetargetingRemote] 알 수 없는 속성: {property}"); break; From 657ca3d236128a67167945d6739ba037c655a922 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:50:31 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Remove=20:=20=EC=9B=B9=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EB=AA=A8=EC=85=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=84=B9=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- Assets/Resources/StreamingleDashboard/dashboard_script.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/Resources/StreamingleDashboard/dashboard_script.txt b/Assets/Resources/StreamingleDashboard/dashboard_script.txt index b5f65181d..00a1e5513 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:3cc8474349ae9b211196debb5dadaf88beac2b04fd106d4ddf3a341005f8b09b -size 77699 +oid sha256:e607c9dd85af7c506d321d5d8bda08aadbea09fcbf59cd882bb67f6ca78e567f +size 76254 From 595aa5f71fe7341b33a444138711a60119423c8b Mon Sep 17 00:00:00 2001 From: user Date: Sun, 8 Mar 2026 00:23:54 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Fix=20:=20=EC=97=90=EB=94=94=ED=84=B0=20u?= =?UTF-8?q?i=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Remote/RetargetingRemoteController.cs | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs index cb4b1198d..ea74f944e 100644 --- a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs +++ b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEngine; @@ -131,6 +132,7 @@ namespace KindRetargeting.Remote string property = json["property"]?.ToString(); float value = json["value"]?.Value() ?? 0f; UpdateValue(charId, property, value); + BroadcastValueChanged(charId, property, value); } break; @@ -164,6 +166,20 @@ namespace KindRetargeting.Remote } break; + case "autoHipsOffset": + { + string charId = json["characterId"]?.ToString(); + AutoHipsOffset(charId); + } + break; + + case "autoCalibrateAll": + { + string charId = json["characterId"]?.ToString(); + AutoCalibrateAll(charId); + } + break; + default: Debug.LogWarning($"[RetargetingRemote] 알 수 없는 액션: {action}"); break; @@ -574,6 +590,7 @@ namespace KindRetargeting.Remote handPose.rightSpreadFingers = spread; } + SendCharacterData(characterId); SendStatus(true, $"{presetName} 프리셋 적용됨"); } @@ -612,6 +629,93 @@ namespace KindRetargeting.Remote SendStatus(true, "정면 캘리브레이션 완료"); } + private void AutoHipsOffset(string characterId) + { + var script = FindCharacter(characterId); + if (script == null) return; + + float offset = CalculateHipsOffsetFromLegDifference(script); + SetPrivateField(script, "hipsOffsetY", offset); + script.SaveSettings(); + + SendCharacterData(characterId); + SendStatus(true, $"다리 길이 자동 보정 완료: hipsOffsetY={offset:F4}"); + } + + private void AutoCalibrateAll(string characterId) + { + var script = FindCharacter(characterId); + if (script == null) return; + + Animator source = script.sourceAnimator; + Animator targetAnim = script.targetAnimator; + if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman) + { + SendStatus(false, "소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); + return; + } + + // Step 1: 크기 초기화 + 힙 오프셋 계산 + script.ResetScale(); + SetPrivateField(script, "avatarScale", 1f); + SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script)); + + // Step 2: 1프레임 후 목 높이 비율로 크기 조정 + StartCoroutine(AutoCalibrateCoroutine(script, characterId)); + } + + private IEnumerator AutoCalibrateCoroutine(CustomRetargetingScript script, string characterId) + { + yield return null; // 1프레임 대기 + + Animator source = script.sourceAnimator; + Animator targetAnim = script.targetAnimator; + + Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck); + Transform targetNeck = targetAnim.GetBoneTransform(HumanBodyBones.Neck); + if (sourceNeck == null || targetNeck == null) + { + SendStatus(false, "목 본을 찾을 수 없습니다."); + yield break; + } + + float scaleRatio = Mathf.Clamp(sourceNeck.position.y / Mathf.Max(targetNeck.position.y, 0.01f), 0.1f, 3f); + script.SetAvatarScale(scaleRatio); + SetPrivateField(script, "avatarScale", scaleRatio); + + yield return null; // 1프레임 대기 + + // Step 3: 힙 오프셋 재계산 + 머리 정면 캘리브레이션 + SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script)); + script.CalibrateHeadToForward(); + script.SaveSettings(); + + SendCharacterData(characterId); + SendStatus(true, $"전체 자동 보정 완료: avatarScale={scaleRatio:F3}"); + } + + private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script) + { + Animator source = script.sourceAnimator; + Animator targetAnim = script.targetAnimator; + if (source == null || targetAnim == null) return 0f; + + float sourceLeg = GetLegLength(source); + float targetLeg = GetLegLength(targetAnim); + if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f; + + return targetLeg - sourceLeg; + } + + private float GetLegLength(Animator animator) + { + Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); + Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg); + Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); + if (upper == null || lower == null || foot == null) return 0f; + return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position); + } + private CustomRetargetingScript FindCharacter(string characterId) { foreach (var script in registeredCharacters) @@ -624,6 +728,19 @@ namespace KindRetargeting.Remote return null; } + private void BroadcastValueChanged(string characterId, string property, float value) + { + var response = new + { + type = "valueChanged", + characterId = characterId, + property = property, + value = value + }; + + wsServer?.Broadcast(JsonConvert.SerializeObject(response)); + } + private void SendStatus(bool success, string message) { var response = new