From d84636edcd5eaa51916788175fb5371a320b8369 Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Sun, 30 Nov 2025 20:13:35 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EC=86=90=EA=B0=80=EB=9D=BD=20?= =?UTF-8?q?=EC=BA=98=EB=A6=AC=EB=B8=8C=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRetargetingScript.cs | 607 ++++++++++++++++++ .../Editor/CustomRetargetingScriptEditor.cs | 65 ++ .../Editor/RetargetingControlWindow.cs | 63 ++ .../KindRetargeting/Enums/RetargetingEnums.cs | 7 +- 4 files changed, 740 insertions(+), 2 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 73ff56e1..0cef7e4b 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using UnityEngine; using UniHumanoid; @@ -158,6 +159,26 @@ namespace KindRetargeting public List rotationOffsetCache; public float initialHipsHeight; public float avatarScale; + // Mingle 캘리브레이션 데이터 + public List fingerOpenRotationsCache; + public List fingerCloseRotationsCache; + // 소스 머슬 캘리브레이션 데이터 + public List sourceMuscleCalibrationCache; + } + + [System.Serializable] + private class MuscleCalibrationData + { + public int muscleIndex; + public float openValue; + public float closeValue; + + public MuscleCalibrationData(int index, float open, float close) + { + muscleIndex = index; + openValue = open; + closeValue = close; + } } // 각 손가락 관절별로 필터 버퍼를 관리하는 Dictionary 추가 @@ -207,6 +228,89 @@ namespace KindRetargeting } private IKJoints sourceIKJoints; + // Mingle용 손가락 캘리브레이션 데이터 + // 각 손가락 본의 펼침/모음 상태 로컬 회전값 저장 + private Dictionary fingerOpenRotations = new Dictionary(); + private Dictionary fingerCloseRotations = new Dictionary(); + // 엄지 Open/Close 쿼터니언 각도 (캘리브레이션 시 미리 계산) + private Dictionary thumbOpenToCloseAngles = new Dictionary(); + private bool isMingleCalibrated = false; + + // 소스 아바타 머슬 캘리브레이션 데이터 (엄지 Stretched용) + // Key: 머슬 인덱스, Value: (Open 상태 머슬값, Close 상태 머슬값) + private Dictionary sourceMuscleCalibration = new Dictionary(); + + // 자동 캘리브레이션 상태 + private bool isAutoCalibrating = false; + private string autoCalibrationStatus = ""; + private float autoCalibrationTimeRemaining = 0f; + private Coroutine autoCalibrationCoroutine = null; + + /// + /// 자동 캘리브레이션 진행 중 여부 + /// + public bool IsAutoCalibrating => isAutoCalibrating; + + /// + /// 자동 캘리브레이션 상태 메시지 + /// + public string AutoCalibrationStatus => autoCalibrationStatus; + + /// + /// 자동 캘리브레이션 남은 시간 + /// + public float AutoCalibrationTimeRemaining => autoCalibrationTimeRemaining; + + // 손가락 본 인덱스 → 머슬 인덱스 매핑 + // 머슬 순서: 1 Stretched, Spread, 2 Stretched, 3 Stretched (4개씩) + // 소스 아바타 로컬 회전: 엄지는 Y = Spread, 나머지는 X = Spread, 굽힘은 모두 Z축 + private static readonly Dictionary fingerBoneToMuscleIndex = new Dictionary + { + // 왼손 엄지 (24-26) → 머슬 55-58 (55: Stretched1, 56: Spread, 57: Stretched2, 58: Stretched3) + { 24, (55, 56) }, // LeftThumbProximal → Left Thumb 1 Stretched, Spread + { 25, (57, -1) }, // LeftThumbIntermediate → Left Thumb 2 Stretched + { 26, (58, -1) }, // LeftThumbDistal → Left Thumb 3 Stretched + // 왼손 검지 (27-29) → 머슬 59-62 (59: Stretched1, 60: Spread, 61: Stretched2, 62: Stretched3) + { 27, (59, 60) }, // LeftIndexProximal → Left Index 1 Stretched, Spread + { 28, (61, -1) }, // LeftIndexIntermediate → Left Index 2 Stretched (Spread 없음) + { 29, (62, -1) }, // LeftIndexDistal → Left Index 3 Stretched (Spread 없음) + // 왼손 중지 (30-32) → 머슬 63-66 + { 30, (63, 64) }, // LeftMiddleProximal → Left Middle 1 Stretched, Spread + { 31, (65, -1) }, // LeftMiddleIntermediate → Left Middle 2 Stretched + { 32, (66, -1) }, // LeftMiddleDistal → Left Middle 3 Stretched + // 왼손 약지 (33-35) → 머슬 67-70 + { 33, (67, 68) }, // LeftRingProximal → Left Ring 1 Stretched, Spread + { 34, (69, -1) }, // LeftRingIntermediate → Left Ring 2 Stretched + { 35, (70, -1) }, // LeftRingDistal → Left Ring 3 Stretched + // 왼손 소지 (36-38) → 머슬 71-74 + { 36, (71, 72) }, // LeftLittleProximal → Left Little 1 Stretched, Spread + { 37, (73, -1) }, // LeftLittleIntermediate → Left Little 2 Stretched + { 38, (74, -1) }, // LeftLittleDistal → Left Little 3 Stretched + // 오른손 엄지 (39-41) → 머슬 75-78 + { 39, (75, 76) }, // RightThumbProximal → Right Thumb 1 Stretched, Spread + { 40, (77, -1) }, // RightThumbIntermediate → Right Thumb 2 Stretched + { 41, (78, -1) }, // RightThumbDistal → Right Thumb 3 Stretched + // 오른손 검지 (42-44) → 머슬 79-82 + { 42, (79, 80) }, // RightIndexProximal → Right Index 1 Stretched, Spread + { 43, (81, -1) }, // RightIndexIntermediate → Right Index 2 Stretched + { 44, (82, -1) }, // RightIndexDistal → Right Index 3 Stretched + // 오른손 중지 (45-47) → 머슬 83-86 + { 45, (83, 84) }, // RightMiddleProximal → Right Middle 1 Stretched, Spread + { 46, (85, -1) }, // RightMiddleIntermediate → Right Middle 2 Stretched + { 47, (86, -1) }, // RightMiddleDistal → Right Middle 3 Stretched + // 오른손 약지 (48-50) → 머슬 87-90 + { 48, (87, 88) }, // RightRingProximal → Right Ring 1 Stretched, Spread + { 49, (89, -1) }, // RightRingIntermediate → Right Ring 2 Stretched + { 50, (90, -1) }, // RightRingDistal → Right Ring 3 Stretched + // 오른손 소지 (51-53) → 머슬 91-94 + { 51, (91, 92) }, // RightLittleProximal → Right Little 1 Stretched, Spread + { 52, (93, -1) }, // RightLittleIntermediate → Right Little 2 Stretched + { 53, (94, -1) }, // RightLittleDistal → Right Little 3 Stretched + }; + + // 엄지 본 인덱스 (24-26: 왼손, 39-41: 오른손) - Spread 축이 Y임 + private static readonly HashSet thumbBoneIndices = new HashSet { 24, 25, 26, 39, 40, 41 }; + #endregion #region 초기화 @@ -500,6 +604,26 @@ namespace KindRetargeting offsetCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value)); } + // Mingle 캘리브레이션 데이터 캐싱 + var fingerOpenCache = new List(); + foreach (var kvp in fingerOpenRotations) + { + fingerOpenCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value)); + } + + var fingerCloseCache = new List(); + foreach (var kvp in fingerCloseRotations) + { + fingerCloseCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value)); + } + + // 소스 머슬 캘리브레이션 데이터 캐싱 + var muscleCalibrationCache = new List(); + foreach (var kvp in sourceMuscleCalibration) + { + muscleCalibrationCache.Add(new MuscleCalibrationData(kvp.Key, kvp.Value.open, kvp.Value.close)); + } + var settings = new RetargetingSettings { hipsOffsetX = hipsOffsetX, @@ -520,6 +644,9 @@ namespace KindRetargeting rotationOffsetCache = offsetCache, initialHipsHeight = initialHipsHeight, avatarScale = avatarScale, + fingerOpenRotationsCache = fingerOpenCache, + fingerCloseRotationsCache = fingerCloseCache, + sourceMuscleCalibrationCache = muscleCalibrationCache, }; string json = JsonUtility.ToJson(settings, true); @@ -581,7 +708,37 @@ namespace KindRetargeting avatarScale = settings.avatarScale; previousScale = avatarScale; + // Mingle 캘리브레이션 데이터 로드 + if (settings.fingerOpenRotationsCache != null && settings.fingerOpenRotationsCache.Count > 0) + { + fingerOpenRotations.Clear(); + foreach (var data in settings.fingerOpenRotationsCache) + { + fingerOpenRotations[(HumanBodyBones)data.boneIndex] = data.ToQuaternion(); + } + } + if (settings.fingerCloseRotationsCache != null && settings.fingerCloseRotationsCache.Count > 0) + { + fingerCloseRotations.Clear(); + foreach (var data in settings.fingerCloseRotationsCache) + { + fingerCloseRotations[(HumanBodyBones)data.boneIndex] = data.ToQuaternion(); + } + } + + // 소스 머슬 캘리브레이션 데이터 로드 + if (settings.sourceMuscleCalibrationCache != null && settings.sourceMuscleCalibrationCache.Count > 0) + { + sourceMuscleCalibration.Clear(); + foreach (var data in settings.sourceMuscleCalibrationCache) + { + sourceMuscleCalibration[data.muscleIndex] = (data.openValue, data.closeValue); + } + } + + // Mingle 캘리브레이션 완료 여부 확인 + isMingleCalibrated = fingerOpenRotations.Count > 0 && fingerCloseRotations.Count > 0; //너무 자주 출력되어서 주석처리 //Debug.Log($"설정을 로드했습니다: {filePath}"); @@ -742,6 +899,357 @@ namespace KindRetargeting } } + /// + /// Mingle 캘리브레이션: 손가락 펼침 상태 기록 + /// 소스 아바타의 손가락을 모두 펼친 상태에서 호출 + /// + public void CalibrateMingleOpen() + { + if (sourceAnimator == null) + { + Debug.LogError("소스 Animator가 설정되지 않았습니다."); + return; + } + + fingerOpenRotations.Clear(); + + // 손가락 본들 (24~53)의 로컬 회전 기록 + for (int i = 24; i <= 53; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + Transform fingerBone = sourceAnimator.GetBoneTransform(bone); + if (fingerBone != null) + { + fingerOpenRotations[bone] = fingerBone.localRotation; + } + } + + // 소스 아바타의 현재 머슬 값 기록 (Open 상태) - 엄지 전체 + sourcePoseHandler.GetHumanPose(ref sourcePose); + // 엄지 머슬 저장 (55-58: Left Thumb, 75-78: Right Thumb) + int[] thumbMuscles = { 55, 57, 58, 75, 77, 78 }; // Stretched 머슬만 (Spread 제외) + foreach (int muscleIndex in thumbMuscles) + { + if (!sourceMuscleCalibration.ContainsKey(muscleIndex)) + { + sourceMuscleCalibration[muscleIndex] = (sourcePose.muscles[muscleIndex], 0f); + } + else + { + var current = sourceMuscleCalibration[muscleIndex]; + sourceMuscleCalibration[muscleIndex] = (sourcePose.muscles[muscleIndex], current.close); + } + } + + Debug.Log($"Mingle 펼침 캘리브레이션 완료: {fingerOpenRotations.Count}개 본, 엄지 머슬 값 기록"); + CheckMingleCalibrationComplete(); + } + + /// + /// Mingle 캘리브레이션: 손가락 모음 상태 기록 + /// 소스 아바타의 손가락을 모두 모은(주먹 쥔) 상태에서 호출 + /// + public void CalibrateMingleClose() + { + if (sourceAnimator == null) + { + Debug.LogError("소스 Animator가 설정되지 않았습니다."); + return; + } + + fingerCloseRotations.Clear(); + + // 손가락 본들 (24~53)의 로컬 회전 기록 + for (int i = 24; i <= 53; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + Transform fingerBone = sourceAnimator.GetBoneTransform(bone); + if (fingerBone != null) + { + fingerCloseRotations[bone] = fingerBone.localRotation; + } + } + + // 소스 아바타의 현재 머슬 값 기록 (Close 상태) - 엄지 전체 + sourcePoseHandler.GetHumanPose(ref sourcePose); + // 엄지 머슬 저장 (55-58: Left Thumb, 75-78: Right Thumb) + int[] thumbMuscles = { 55, 57, 58, 75, 77, 78 }; // Stretched 머슬만 (Spread 제외) + foreach (int muscleIndex in thumbMuscles) + { + if (!sourceMuscleCalibration.ContainsKey(muscleIndex)) + { + sourceMuscleCalibration[muscleIndex] = (0f, sourcePose.muscles[muscleIndex]); + } + else + { + var current = sourceMuscleCalibration[muscleIndex]; + sourceMuscleCalibration[muscleIndex] = (current.open, sourcePose.muscles[muscleIndex]); + } + } + + Debug.Log($"Mingle 모음 캘리브레이션 완료: {fingerCloseRotations.Count}개 본, 엄지 머슬 값 기록"); + CheckMingleCalibrationComplete(); + } + + /// + /// Mingle 캘리브레이션 완료 여부 확인 및 엄지 첫마디 각도 계산 + /// + private void CheckMingleCalibrationComplete() + { + isMingleCalibrated = fingerOpenRotations.Count > 0 && fingerCloseRotations.Count > 0; + if (isMingleCalibrated) + { + // 엄지 첫마디(Proximal)만 Open/Close 쿼터니언 각도 미리 계산 + thumbOpenToCloseAngles.Clear(); + int[] thumbProximalIndices = { 24, 39 }; // 왼손, 오른손 엄지 첫마디만 + foreach (int i in thumbProximalIndices) + { + HumanBodyBones bone = (HumanBodyBones)i; + if (fingerOpenRotations.TryGetValue(bone, out Quaternion openRot) && + fingerCloseRotations.TryGetValue(bone, out Quaternion closeRot)) + { + thumbOpenToCloseAngles[bone] = Quaternion.Angle(openRot, closeRot); + } + } + + Debug.Log("Mingle 캘리브레이션이 완료되었습니다. Mingle 모드 사용 가능."); + SaveSettings(); + } + } + + /// + /// 자동 캘리브레이션 시작 + /// 3초 후 펼침 기록, 3초 후 모음 기록을 자동으로 진행 + /// + public void StartAutoCalibration() + { + if (isAutoCalibrating) + { + Debug.LogWarning("이미 자동 캘리브레이션이 진행 중입니다."); + return; + } + + if (sourceAnimator == null) + { + Debug.LogError("소스 Animator가 설정되지 않았습니다."); + return; + } + + autoCalibrationCoroutine = StartCoroutine(AutoCalibrationCoroutine()); + } + + /// + /// 자동 캘리브레이션 중지 + /// + public void StopAutoCalibration() + { + if (autoCalibrationCoroutine != null) + { + StopCoroutine(autoCalibrationCoroutine); + autoCalibrationCoroutine = null; + } + isAutoCalibrating = false; + autoCalibrationStatus = "캘리브레이션 취소됨"; + autoCalibrationTimeRemaining = 0f; + Debug.Log("자동 캘리브레이션이 취소되었습니다."); + } + + /// + /// 자동 캘리브레이션 코루틴 + /// + private IEnumerator AutoCalibrationCoroutine() + { + isAutoCalibrating = true; + + // 1단계: 3초 대기 후 펼침 기록 + autoCalibrationStatus = "손가락을 펼쳐주세요..."; + for (float t = 3f; t > 0; t -= Time.deltaTime) + { + autoCalibrationTimeRemaining = t; + yield return null; + } + + CalibrateMingleOpen(); + autoCalibrationStatus = "펼침 기록 완료!"; + yield return new WaitForSeconds(0.5f); + + // 2단계: 3초 대기 후 모음 기록 + autoCalibrationStatus = "손가락을 모아주세요..."; + for (float t = 3f; t > 0; t -= Time.deltaTime) + { + autoCalibrationTimeRemaining = t; + yield return null; + } + + CalibrateMingleClose(); + autoCalibrationStatus = "모음 기록 완료!"; + yield return new WaitForSeconds(0.5f); + + // 완료 + isAutoCalibrating = false; + autoCalibrationStatus = "자동 캘리브레이션 완료!"; + autoCalibrationTimeRemaining = 0f; + autoCalibrationCoroutine = null; + + Debug.Log("자동 캘리브레이션이 완료되었습니다."); + } + + /// + /// 현재 손가락 회전값을 펼침/모음 범위 내에서 -1~1로 정규화 + /// Unity Muscle 시스템: -1 = Curl(모음), 1 = Stretch(펼침) + /// 소스 아바타 로컬 회전: 모든 손가락 X = Spread, Z = 굽힘 + /// + /// 손가락 본 + /// (굽힘 정규화 값, Spread 정규화 값) - Z축 기준 굽힘, X축 기준 Spread + private (float stretched, float spread) GetNormalizedFingerValue(HumanBodyBones bone) + { + Transform fingerBone = sourceAnimator.GetBoneTransform(bone); + if (fingerBone == null) return (0f, 0f); + + if (!fingerOpenRotations.TryGetValue(bone, out Quaternion openRot) || + !fingerCloseRotations.TryGetValue(bone, out Quaternion closeRot)) + { + return (0f, 0f); + } + + Quaternion currentRot = fingerBone.localRotation; + + float spreadValue = 0f; + int boneIndex = (int)bone; + bool isThumb = thumbBoneIndices.Contains(boneIndex); + + // 굽힘(Stretched) 정규화 + float stretchedValue = 0f; + bool isThumbProximal = boneIndex == 24 || boneIndex == 39; + + if (isThumb) + { + // 엄지 전체: 소스 아바타 머슬에서 Stretched 값 직접 가져오기 + if (fingerBoneToMuscleIndex.TryGetValue(boneIndex, out var muscleIndices)) + { + int stretchedMuscleIndex = muscleIndices.stretchedMuscle; + + // 소스 아바타의 현재 머슬 값 가져오기 + sourcePoseHandler.GetHumanPose(ref sourcePose); + float currentMuscle = sourcePose.muscles[stretchedMuscleIndex]; + + // 캘리브레이션 데이터가 있으면 정규화 + if (sourceMuscleCalibration.TryGetValue(stretchedMuscleIndex, out var calibration)) + { + float openMuscle = calibration.open; + float closeMuscle = calibration.close; + float range = openMuscle - closeMuscle; + + if (Mathf.Abs(range) > 0.01f) + { + // Open 상태 = 0.5, Close 상태 = -1 (엄지 범위) + float t = Mathf.InverseLerp(openMuscle, closeMuscle, currentMuscle); + stretchedValue = Mathf.Lerp(0.5f, -1f, t); + } + else + { + stretchedValue = currentMuscle; + } + } + else + { + stretchedValue = currentMuscle; + } + } + } + else + { + // 다른 손가락: 로테이션 Z축 기반 정규화 + Vector3 openEuler = openRot.eulerAngles; + Vector3 closeEuler = closeRot.eulerAngles; + Vector3 currentEuler = currentRot.eulerAngles; + + float openZ = NormalizeAngle(openEuler.z); + float closeZ = NormalizeAngle(closeEuler.z); + float currentZ = NormalizeAngle(currentEuler.z); + + float totalRangeZ = closeZ - openZ; + if (Mathf.Abs(totalRangeZ) > 0.1f) + { + float t = Mathf.InverseLerp(openZ, closeZ, currentZ); + // Unity Muscle: 1 = Stretch(펼침), -1 = Curl(모음) + stretchedValue = Mathf.Lerp(1f, -1f, t); + } + } + + // Spread 계산 + // 엄지 Proximal(24, 39)의 Spread는 Intermediate(25, 40)의 Y축에서 가져옴 (장갑 특수성) + + if (isThumbProximal) + { + // 엄지 첫마디: 두번째 마디(Intermediate)의 Y축으로 Spread 계산 (장갑 특수성) + HumanBodyBones intermediateBone = (boneIndex == 24) ? HumanBodyBones.LeftThumbIntermediate : HumanBodyBones.RightThumbIntermediate; + Transform intermediateBoneTransform = sourceAnimator.GetBoneTransform(intermediateBone); + + if (intermediateBoneTransform != null && + fingerOpenRotations.TryGetValue(intermediateBone, out Quaternion intOpenRot) && + fingerCloseRotations.TryGetValue(intermediateBone, out Quaternion intCloseRot)) + { + // Intermediate 본의 캘리브레이션된 Y축 범위 사용 + float openY = NormalizeAngle(intOpenRot.eulerAngles.y); + float closeY = NormalizeAngle(intCloseRot.eulerAngles.y); + + // 현재 Y축 값 + Vector3 intCurrentEuler = intermediateBoneTransform.localRotation.eulerAngles; + float currentY = NormalizeAngle(intCurrentEuler.y); + + float rangeY = openY - closeY; + if (Mathf.Abs(rangeY) > 0.1f) + { + // Open = 벌림(0.5), Close = 닫힘(-0.5)로 매핑 + float t = Mathf.InverseLerp(openY, closeY, currentY); + spreadValue = Mathf.Lerp(0.5f, -0.5f, t); + + // 오른손(39)은 반전 + if (boneIndex == 39) + { + spreadValue = -spreadValue; + } + } + } + } + else if (isThumb) + { + // 엄지 다른 마디: Spread 없음 + spreadValue = 0f; + } + else + { + // 다른 손가락: X축으로 Spread 계산 + Vector3 currentEuler = currentRot.eulerAngles; + Vector3 openEuler = openRot.eulerAngles; + Vector3 closeEuler = closeRot.eulerAngles; + + float currentX = NormalizeAngle(currentEuler.x); + float openX = NormalizeAngle(openEuler.x); + float closeX = NormalizeAngle(closeEuler.x); + + float totalRangeX = closeX - openX; + if (Mathf.Abs(totalRangeX) > 0.1f) + { + float t = Mathf.InverseLerp(openX, closeX, currentX); + spreadValue = Mathf.Lerp(0.5f, -0.5f, t); + } + } + + return (stretchedValue, spreadValue); + } + + /// + /// 각도를 -180 ~ 180 범위로 정규화 + /// + private float NormalizeAngle(float angle) + { + while (angle > 180f) angle -= 360f; + while (angle < -180f) angle += 360f; + return angle; + } + private void InitializeIKJoints() { // IK 루트 찾기 @@ -787,6 +1295,9 @@ namespace KindRetargeting case EnumsList.FingerCopyMode.Rotation: CopyFingerPoseByRotation(); break; + case EnumsList.FingerCopyMode.Mingle: + CopyFingerPoseByMingle(); + break; } // 스케일 변경 확인 및 적용 @@ -941,6 +1452,102 @@ namespace KindRetargeting } } + /// + /// Mingle 방식으로 손가락 포즈를 복제합니다. + /// 소스 아바타의 손가락 회전을 캘리브레이션된 범위 내에서 정규화하여 머슬에 적용합니다. + /// 엄지는 제외하고 처리합니다. + /// + private void CopyFingerPoseByMingle() + { + if (!isMingleCalibrated) + { + // 캘리브레이션이 안 되어 있으면 기본 머슬 방식으로 폴백 + CopyFingerPoseByMuscle(); + return; + } + + if (sourcePoseHandler == null || targetPoseHandler == null) + return; + + // 1. 손가락을 제외한 모든 본의 위치/회전 저장 + savedBoneTransforms.Clear(); + for (int i = 0; i < nonFingerBones.Length; i++) + { + Transform bone = targetAnimator.GetBoneTransform(nonFingerBones[i]); + if (bone != null) + { + savedBoneTransforms[nonFingerBones[i]] = (bone.position, bone.rotation); + } + } + + // 2. 타겟 포즈 가져오기 + targetPoseHandler.GetHumanPose(ref targetPose); + + // 3. 각 손가락 본에 대해 정규화된 값을 머슬에 적용 + for (int boneIndex = 24; boneIndex <= 53; boneIndex++) + { + HumanBodyBones bone = (HumanBodyBones)boneIndex; + + // 정규화된 손가락 값 계산 (Z축: 굽힘, X/Y축: Spread) + var (stretchedValue, spreadValue) = GetNormalizedFingerValue(bone); + + // 해당 본에 매핑된 머슬 인덱스 찾기 + if (fingerBoneToMuscleIndex.TryGetValue(boneIndex, out var muscleIndices)) + { + // Stretched 머슬 적용 + if (muscleIndices.stretchedMuscle >= 0) + { + float targetValue = stretchedValue; + float currentValue = targetPose.muscles[muscleIndices.stretchedMuscle]; + + // 모션 필터 적용 + if (useMotionFilter) + { + targetValue = ApplyFilter(targetValue, boneIndex - 24); + } + + // 러프 모션 적용 + if (useFingerRoughMotion) + { + float smoothSpeed = 50f - (fingerRoughness * 49f); + targetValue = Mathf.Lerp(currentValue, targetValue, smoothSpeed * Time.deltaTime); + } + + targetPose.muscles[muscleIndices.stretchedMuscle] = targetValue; + } + + // Spread 머슬 적용 (첫마디만 해당) + if (muscleIndices.spreadMuscle >= 0) + { + float targetValue = spreadValue; + float currentValue = targetPose.muscles[muscleIndices.spreadMuscle]; + + // 러프 모션 적용 + if (useFingerRoughMotion) + { + float smoothSpeed = 50f - (fingerRoughness * 49f); + targetValue = Mathf.Lerp(currentValue, targetValue, smoothSpeed * Time.deltaTime); + } + + targetPose.muscles[muscleIndices.spreadMuscle] = targetValue; + } + } + } + + // 4. 머슬 포즈 적용 + targetPoseHandler.SetHumanPose(ref targetPose); + + // 5. 손가락을 제외한 모든 본의 위치/회전 복원 + foreach (var kvp in savedBoneTransforms) + { + Transform bone = targetAnimator.GetBoneTransform(kvp.Key); + if (bone != null) + { + bone.SetPositionAndRotation(kvp.Value.position, kvp.Value.rotation); + } + } + } + #endregion #region 포즈 동기화 diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 29a24d27..7733a4d5 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -203,6 +203,71 @@ namespace KindRetargeting EditorGUI.indentLevel++; EditorGUILayout.PropertyField(fingerCopyModeProp, new GUIContent("복제 방식", "손가락 포즈를 복제하는 방식을 선택합니다.")); + + // Mingle 모드일 때 캘리브레이션 버튼 표시 + if (fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle) + { + GUILayout.Space(5); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Mingle 캘리브레이션", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" + + "1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" + + "2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭", + MessageType.Info); + + var script = (CustomRetargetingScript)target; + + // 자동 캘리브레이션 진행 중일 때 + if (Application.isPlaying && script.IsAutoCalibrating) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("자동 캘리브레이션 진행 중", EditorStyles.boldLabel); + EditorGUILayout.LabelField($"상태: {script.AutoCalibrationStatus}"); + EditorGUILayout.LabelField($"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초"); + + if (GUILayout.Button("취소")) + { + script.StopAutoCalibration(); + } + EditorGUILayout.EndVertical(); + + // 인스펙터 갱신 + Repaint(); + } + else + { + // 수동 캘리브레이션 버튼 + EditorGUILayout.BeginHorizontal(); + GUI.enabled = Application.isPlaying; + if (GUILayout.Button("펼침 기록 (Open)")) + { + script.CalibrateMingleOpen(); + } + if (GUILayout.Button("모음 기록 (Close)")) + { + script.CalibrateMingleClose(); + } + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + + // 자동 캘리브레이션 버튼 + GUILayout.Space(5); + GUI.enabled = Application.isPlaying; + if (GUILayout.Button("자동 캘리브레이션 (3초 펼침 → 3초 모음)")) + { + script.StartAutoCalibration(); + } + GUI.enabled = true; + } + + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", MessageType.Warning); + } + EditorGUILayout.EndVertical(); + } + EditorGUI.indentLevel--; } diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index 6e8eda2a..8191df9f 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -2,6 +2,7 @@ using UnityEngine; using UnityEditor; using System.Collections.Generic; using KindRetargeting; +using KindRetargeting.EnumsList; public class RetargetingControlWindow : EditorWindow { @@ -683,6 +684,68 @@ public class RetargetingControlWindow : EditorWindow serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(script); } + + // Mingle 모드일 때 캘리브레이션 버튼 표시 + if (fingerCopyModeProp.enumValueIndex == (int)FingerCopyMode.Mingle) + { + GUILayout.Space(5); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Mingle 캘리브레이션", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" + + "1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" + + "2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭", + MessageType.Info); + + // 자동 캘리브레이션 진행 중일 때 + if (Application.isPlaying && script.IsAutoCalibrating) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("자동 캘리브레이션 진행 중", EditorStyles.boldLabel); + EditorGUILayout.LabelField($"상태: {script.AutoCalibrationStatus}"); + EditorGUILayout.LabelField($"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초"); + + if (GUILayout.Button("취소")) + { + script.StopAutoCalibration(); + } + EditorGUILayout.EndVertical(); + + // 윈도우 갱신 + Repaint(); + } + else + { + // 수동 캘리브레이션 버튼 + EditorGUILayout.BeginHorizontal(); + GUI.enabled = Application.isPlaying; + if (GUILayout.Button("펼침 기록 (Open)")) + { + script.CalibrateMingleOpen(); + } + if (GUILayout.Button("모음 기록 (Close)")) + { + script.CalibrateMingleClose(); + } + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + + // 자동 캘리브레이션 버튼 + GUILayout.Space(5); + GUI.enabled = Application.isPlaying; + if (GUILayout.Button("자동 캘리브레이션 (3초 펼침 → 3초 모음)")) + { + script.StartAutoCalibration(); + } + GUI.enabled = true; + } + + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", MessageType.Warning); + } + EditorGUILayout.EndVertical(); + } } else { diff --git a/Assets/Scripts/KindRetargeting/Enums/RetargetingEnums.cs b/Assets/Scripts/KindRetargeting/Enums/RetargetingEnums.cs index 4f61e68c..e062a542 100644 --- a/Assets/Scripts/KindRetargeting/Enums/RetargetingEnums.cs +++ b/Assets/Scripts/KindRetargeting/Enums/RetargetingEnums.cs @@ -9,12 +9,15 @@ namespace KindRetargeting.EnumsList { [Tooltip("손가락 복제를 수행하지 않습니다")] None, // 손가락 복제 안 함 - + [Tooltip("Unity의 Muscle 시스템을 사용하여 손가락을 복제합니다")] MuscleData, // 머슬 데이터 기반 복제 - + [Tooltip("Transform의 rotation 값을 직접 복제합니다")] Rotation, // 회전값 기반 복제 + + [Tooltip("소스 아바타의 손가락 회전 범위를 캘리브레이션하여 머슬에 매핑합니다")] + Mingle, // 캘리브레이션 기반 머슬 매핑 } ///