Fix : 손가락 캘리브레이션 기능 추가 개발

This commit is contained in:
qsxft258@gmail.com 2025-11-30 20:13:35 +09:00
parent b0b33f78d3
commit d84636edcd
4 changed files with 740 additions and 2 deletions

View File

@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniHumanoid;
@ -158,6 +159,26 @@ namespace KindRetargeting
public List<RotationOffsetData> rotationOffsetCache;
public float initialHipsHeight;
public float avatarScale;
// Mingle 캘리브레이션 데이터
public List<RotationOffsetData> fingerOpenRotationsCache;
public List<RotationOffsetData> fingerCloseRotationsCache;
// 소스 머슬 캘리브레이션 데이터
public List<MuscleCalibrationData> 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<HumanBodyBones, Quaternion> fingerOpenRotations = new Dictionary<HumanBodyBones, Quaternion>();
private Dictionary<HumanBodyBones, Quaternion> fingerCloseRotations = new Dictionary<HumanBodyBones, Quaternion>();
// 엄지 Open/Close 쿼터니언 각도 (캘리브레이션 시 미리 계산)
private Dictionary<HumanBodyBones, float> thumbOpenToCloseAngles = new Dictionary<HumanBodyBones, float>();
private bool isMingleCalibrated = false;
// 소스 아바타 머슬 캘리브레이션 데이터 (엄지 Stretched용)
// Key: 머슬 인덱스, Value: (Open 상태 머슬값, Close 상태 머슬값)
private Dictionary<int, (float open, float close)> sourceMuscleCalibration = new Dictionary<int, (float, float)>();
// 자동 캘리브레이션 상태
private bool isAutoCalibrating = false;
private string autoCalibrationStatus = "";
private float autoCalibrationTimeRemaining = 0f;
private Coroutine autoCalibrationCoroutine = null;
/// <summary>
/// 자동 캘리브레이션 진행 중 여부
/// </summary>
public bool IsAutoCalibrating => isAutoCalibrating;
/// <summary>
/// 자동 캘리브레이션 상태 메시지
/// </summary>
public string AutoCalibrationStatus => autoCalibrationStatus;
/// <summary>
/// 자동 캘리브레이션 남은 시간
/// </summary>
public float AutoCalibrationTimeRemaining => autoCalibrationTimeRemaining;
// 손가락 본 인덱스 → 머슬 인덱스 매핑
// 머슬 순서: 1 Stretched, Spread, 2 Stretched, 3 Stretched (4개씩)
// 소스 아바타 로컬 회전: 엄지는 Y = Spread, 나머지는 X = Spread, 굽힘은 모두 Z축
private static readonly Dictionary<int, (int stretchedMuscle, int spreadMuscle)> fingerBoneToMuscleIndex = new Dictionary<int, (int, int)>
{
// 왼손 엄지 (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<int> thumbBoneIndices = new HashSet<int> { 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<RotationOffsetData>();
foreach (var kvp in fingerOpenRotations)
{
fingerOpenCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value));
}
var fingerCloseCache = new List<RotationOffsetData>();
foreach (var kvp in fingerCloseRotations)
{
fingerCloseCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value));
}
// 소스 머슬 캘리브레이션 데이터 캐싱
var muscleCalibrationCache = new List<MuscleCalibrationData>();
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
}
}
/// <summary>
/// Mingle 캘리브레이션: 손가락 펼침 상태 기록
/// 소스 아바타의 손가락을 모두 펼친 상태에서 호출
/// </summary>
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();
}
/// <summary>
/// Mingle 캘리브레이션: 손가락 모음 상태 기록
/// 소스 아바타의 손가락을 모두 모은(주먹 쥔) 상태에서 호출
/// </summary>
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();
}
/// <summary>
/// Mingle 캘리브레이션 완료 여부 확인 및 엄지 첫마디 각도 계산
/// </summary>
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();
}
}
/// <summary>
/// 자동 캘리브레이션 시작
/// 3초 후 펼침 기록, 3초 후 모음 기록을 자동으로 진행
/// </summary>
public void StartAutoCalibration()
{
if (isAutoCalibrating)
{
Debug.LogWarning("이미 자동 캘리브레이션이 진행 중입니다.");
return;
}
if (sourceAnimator == null)
{
Debug.LogError("소스 Animator가 설정되지 않았습니다.");
return;
}
autoCalibrationCoroutine = StartCoroutine(AutoCalibrationCoroutine());
}
/// <summary>
/// 자동 캘리브레이션 중지
/// </summary>
public void StopAutoCalibration()
{
if (autoCalibrationCoroutine != null)
{
StopCoroutine(autoCalibrationCoroutine);
autoCalibrationCoroutine = null;
}
isAutoCalibrating = false;
autoCalibrationStatus = "캘리브레이션 취소됨";
autoCalibrationTimeRemaining = 0f;
Debug.Log("자동 캘리브레이션이 취소되었습니다.");
}
/// <summary>
/// 자동 캘리브레이션 코루틴
/// </summary>
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("자동 캘리브레이션이 완료되었습니다.");
}
/// <summary>
/// 현재 손가락 회전값을 펼침/모음 범위 내에서 -1~1로 정규화
/// Unity Muscle 시스템: -1 = Curl(모음), 1 = Stretch(펼침)
/// 소스 아바타 로컬 회전: 모든 손가락 X = Spread, Z = 굽힘
/// </summary>
/// <param name="bone">손가락 본</param>
/// <returns>(굽힘 정규화 값, Spread 정규화 값) - Z축 기준 굽힘, X축 기준 Spread</returns>
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);
}
/// <summary>
/// 각도를 -180 ~ 180 범위로 정규화
/// </summary>
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
}
}
/// <summary>
/// Mingle 방식으로 손가락 포즈를 복제합니다.
/// 소스 아바타의 손가락 회전을 캘리브레이션된 범위 내에서 정규화하여 머슬에 적용합니다.
/// 엄지는 제외하고 처리합니다.
/// </summary>
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

View File

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

View File

@ -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
{

View File

@ -9,12 +9,15 @@ namespace KindRetargeting.EnumsList
{
[Tooltip("손가락 복제를 수행하지 않습니다")]
None, // 손가락 복제 안 함
[Tooltip("Unity의 Muscle 시스템을 사용하여 손가락을 복제합니다")]
MuscleData, // 머슬 데이터 기반 복제
[Tooltip("Transform의 rotation 값을 직접 복제합니다")]
Rotation, // 회전값 기반 복제
[Tooltip("소스 아바타의 손가락 회전 범위를 캘리브레이션하여 머슬에 매핑합니다")]
Mingle, // 캘리브레이션 기반 머슬 매핑
}
/// <summary>