Fix : 손가락 캘리브레이션 기능 추가 개발
This commit is contained in:
parent
b0b33f78d3
commit
d84636edcd
@ -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 포즈 동기화
|
||||
|
||||
@ -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--;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -9,12 +9,15 @@ namespace KindRetargeting.EnumsList
|
||||
{
|
||||
[Tooltip("손가락 복제를 수행하지 않습니다")]
|
||||
None, // 손가락 복제 안 함
|
||||
|
||||
|
||||
[Tooltip("Unity의 Muscle 시스템을 사용하여 손가락을 복제합니다")]
|
||||
MuscleData, // 머슬 데이터 기반 복제
|
||||
|
||||
|
||||
[Tooltip("Transform의 rotation 값을 직접 복제합니다")]
|
||||
Rotation, // 회전값 기반 복제
|
||||
|
||||
[Tooltip("소스 아바타의 손가락 회전 범위를 캘리브레이션하여 머슬에 매핑합니다")]
|
||||
Mingle, // 캘리브레이션 기반 머슬 매핑
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user