using System.Collections; using System.Collections.Generic; using UnityEngine; using UniHumanoid; using System.IO; using System; using RootMotion.FinalIK; namespace KindRetargeting { /// /// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다. /// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다. /// [RequireComponent(typeof(LimbWeightController))] [RequireComponent(typeof(ShoulderCorrectionFunction))] [RequireComponent(typeof(FullBodyInverseKinematics_RND))] [RequireComponent(typeof(PropLocationController))] [RequireComponent(typeof(FingerShapedController))] [DefaultExecutionOrder(1)] public class CustomRetargetingScript : MonoBehaviour { #region 필드 [Header("원본 및 대상 아바타 Animator")] [SerializeField] public Animator sourceAnimator; // 원본 아바타의 Animator [HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator // IK 컴포넌트 참조 변경 private FullBodyInverseKinematics_RND ikComponent; [Header("힙 위치 보정 (로컬 좌표계 기반)")] [SerializeField, Range(-1, 1)] private float hipsOffsetX = 0f; // 캐릭터 기준 좌우 (항상 Right 방향) [SerializeField, Range(-1, 1)] private float hipsOffsetY = 0f; // 캐릭터 기준 상하 (항상 Up 방향) [SerializeField, Range(-1, 1)] private float hipsOffsetZ = 0f; // 캐릭터 기준 앞뒤 (항상 Forward 방향) [HideInInspector] public float HipsWeightOffset = 1f; [HideInInspector] public float ChairSeatHeightOffset = 0f; // 의자 좌석 높이 오프셋 (월드 Y 기준) // 축 매핑: 월드 방향(Right/Up/Forward)을 담당하는 로컬 축을 저장 // 예: localAxisForWorldRight = (0, 0, 1) 이면 로컬 Z축이 월드 Right 방향을 담당 // 부호는 방향을 나타냄: (0, 0, -1)이면 로컬 -Z가 월드 Right를 담당 private Vector3 localAxisForWorldRight = Vector3.right; // 월드 좌우(Right)를 담당하는 로컬 축 private Vector3 localAxisForWorldUp = Vector3.up; // 월드 상하(Up)를 담당하는 로컬 축 private Vector3 localAxisForWorldForward = Vector3.forward; // 월드 앞뒤(Forward)를 담당하는 로컬 축 [Header("축 정규화 정보 (읽기 전용)")] [SerializeField] public Vector3 debugAxisNormalizer = Vector3.one; // HumanPoseHandler를 이용하여 원본 및 대상 아바타의 포즈를 관리 private HumanPoseHandler sourcePoseHandler; private HumanPoseHandler targetPoseHandler; private HumanPose sourcePose; private HumanPose targetPose; // 최적화: 프레임당 한 번만 GetHumanPose 호출하기 위한 플래그 private bool isSourcePoseCachedThisFrame = false; private bool isTargetPoseCachedThisFrame = false; // 본별 회전 오프셋을 저장하는 딕셔너리 private Dictionary rotationOffsets = new Dictionary(); // HumanBodyBones.LastBone을 이용한 본 순회 범위 private int lastBoneIndex = 23; [Header("손가락 복제 설정")] [SerializeField] private EnumsList.FingerCopyMode fingerCopyMode = EnumsList.FingerCopyMode.Rotation; [Header("모션 필터링 설정")] [SerializeField] private bool useMotionFilter = false; [SerializeField, Range(2, 10)] private int filterBufferSize = 5; [Header("러프 모션 설정")] [SerializeField] private bool useBodyRoughMotion = false; // 몸 러프 모션 사용 [SerializeField] private bool useFingerRoughMotion = false; // 손가락 러프 모션 사용 [SerializeField, Range(0f, 1f)] private float bodyRoughness = 0.1f; [SerializeField, Range(0f, 1f)] private float fingerRoughness = 0.1f; private Dictionary boneFilters = new Dictionary(); private Dictionary roughMotions = new Dictionary(); // IK 타겟용 러프 모션 필터 private Dictionary ikTargetRoughMotions = new Dictionary(); // IK 타겟용 모션 필터 private Dictionary ikTargetFilters = new Dictionary(); [Header("무릎 안/밖 조정")] [SerializeField, Range(-1f, 1f)] private float kneeInOutWeight = 0f; // 무릎 안/밖 위치 조정 가중치 [Header("무릎 앞/뒤 조정")] [SerializeField, Range(-1f, 1f)] private float kneeFrontBackWeight = 0.4f; // 무릎 앞/뒤 위치 조정 가중치 [Header("발 IK 위치 조정")] [SerializeField, Range(-1f, 1f)] private float footFrontBackOffset = 0f; // 발 앞뒤 오프셋 (+: 앞, -: 뒤) [SerializeField, Range(-1f, 1f)] private float footInOutOffset = 0f; // 발 안쪽/바깥쪽 오프셋 (+: 벌리기, -: 모으기) [Header("바닥 높이 조정")] [SerializeField, Range(-1f, 1f)] public float floorHeight = 0f; // 바닥 높이 조정값 [Header("발목 높이 설정")] [SerializeField] public float minimumAnkleHeight = 0.2f; // 수동 설정용 최소 발목 높이 [Header("설정 저장/로드")] [SerializeField] private string settingsFolderName = "RetargetingSettings"; private float initialHipsHeight; // 초기 힙 높이 [Header("아바타 크기 조정")] [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; private float previousScale = 1f; private List scalableObjects = new List(); // 필드 추가 private Dictionary originalScales = new Dictionary(); private Dictionary originalPositions = new Dictionary(); [System.Serializable] private class RotationOffsetData { public int boneIndex; public float x, y, z, w; public RotationOffsetData(int bone, Quaternion rotation) { boneIndex = bone; x = rotation.x; y = rotation.y; z = rotation.z; w = rotation.w; } public Quaternion ToQuaternion() { return new Quaternion(x, y, z, w); } } [System.Serializable] private class RetargetingSettings { public float hipsOffsetX; // 변경: hipsWeight → hipsOffsetX/Y/Z public float hipsOffsetY; public float hipsOffsetZ; public float kneeInOutWeight; public float kneeFrontBackWeight; public float footFrontBackOffset; // 발 앞뒤 오프셋 public float footInOutOffset; // 발 안쪽/바깥쪽 오프셋 public float floorHeight; public EnumsList.FingerCopyMode fingerCopyMode; public bool useMotionFilter; public int filterBufferSize; public bool useBodyRoughMotion; public bool useFingerRoughMotion; public float bodyRoughness; public float fingerRoughness; public List rotationOffsetCache; public float initialHipsHeight; public float avatarScale; // Mingle 캘리브레이션 데이터 public List fingerOpenRotationsCache; public List fingerCloseRotationsCache; // 소스 머슬 캘리브레이션 데이터 public List sourceMuscleCalibrationCache; // 의자 앉기 높이 오프셋 (LimbWeightController) public float chairSeatHeightOffset; } [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 추가 (GC 최적화: 배열 재사용) private Dictionary fingerFilterBuffers = new Dictionary(); private Dictionary fingerFilterIndices = new Dictionary(); // CopyFingerPoseByMuscle에서 사용할 본 Transform 저장용 (메모리 재사용) private Dictionary savedBoneTransforms = new Dictionary(); // 손가락을 제외한 모든 휴먼본 목록 (캐싱) private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[] { 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 }; // IK 조인트 싱을 위한 구조체 private struct IKJoints { public Transform leftLowerLeg; public Transform rightLowerLeg; public Transform leftLowerArm; public Transform rightLowerArm; } 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 초기화 /// /// 초기화 메서드. T-포즈 설정, IK 타겟 생성, HumanPoseHandler 초기화 및 회전 오프셋 계산을 수행합니다. /// void Start() { targetAnimator = GetComponent(); // 설정 로드 LoadSettings(); // IK 컴포넌트 참조 가져오기 변경 ikComponent = GetComponent(); // IK 타겟 생성 (무릎 시각화 오브젝트 포함) CreateIKTargets(); // T-포즈 전에 축 정규화 계수 계산 CalculateAxisNormalizer(); // 원본 및 대상 아바타를 T-포즈로 복원 SetTPose(sourceAnimator); SetTPose(targetAnimator); // HumanPoseHandler 초기화 InitializeHumanPoseHandlers(); // 회전 오프셋 초기화 (캐시 사용 또는 새로 계산) InitializeRotationOffsets(); // 모션 필터 초기화 if (useMotionFilter || useBodyRoughMotion || useFingerRoughMotion) { InitializeMotionFilters(); } // 초기 힙 높이 저장 if (targetAnimator != null) { Transform hips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips); if (hips != null) { initialHipsHeight = hips.position.y; } } InitializeIKJoints(); // 크기 조정 대상 오브젝트 캐싱 CacheScalableObjects(); previousScale = avatarScale; ApplyScale(); } /// /// 타겟 아바타의 로컬 축과 월드 축의 관계를 분석하여 축 매핑을 계산합니다. /// T-포즈 상태에서 힙의 각 로컬 축이 월드의 어느 방향을 가리키는지 분석합니다. /// /// 예시: /// - 아바타 A: 로컬 Y가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 1, 0) /// - 아바타 B: 로컬 Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, 1) /// - 아바타 C: 로컬 -Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, -1) /// /// 이를 통해 hipsOffsetY는 항상 "위/아래" 방향으로 작동합니다. /// private void CalculateAxisNormalizer() { if (targetAnimator == null) return; Transform hips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips); if (hips == null) return; // 힙의 각 로컬 축을 월드 공간으로 변환 Vector3 localXInWorld = hips.TransformDirection(Vector3.right).normalized; Vector3 localYInWorld = hips.TransformDirection(Vector3.up).normalized; Vector3 localZInWorld = hips.TransformDirection(Vector3.forward).normalized; // 월드 Right(X)에 가장 가까운 로컬 축 찾기 localAxisForWorldRight = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.right, out string rightAxisName); // 월드 Up(Y)에 가장 가까운 로컬 축 찾기 localAxisForWorldUp = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.up, out string upAxisName); // 월드 Forward(Z)에 가장 가까운 로컬 축 찾기 localAxisForWorldForward = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.forward, out string forwardAxisName); // 디버그용: 각 오프셋이 어느 로컬 축에 매핑되는지 표시 // X: 좌우 오프셋이 사용하는 로컬 축 (1=X, 2=Y, 3=Z, 부호는 방향) // Y: 상하 오프셋이 사용하는 로컬 축 // Z: 앞뒤 오프셋이 사용하는 로컬 축 debugAxisNormalizer = new Vector3( GetAxisIndex(localAxisForWorldRight), GetAxisIndex(localAxisForWorldUp), GetAxisIndex(localAxisForWorldForward) ); Debug.Log($"[{gameObject.name}] 축 매핑 분석 완료:\n" + $" 월드 Right(좌우) ← 로컬 {rightAxisName} → 매핑: {localAxisForWorldRight}\n" + $" 월드 Up(상하) ← 로컬 {upAxisName} → 매핑: {localAxisForWorldUp}\n" + $" 월드 Forward(앞뒤) ← 로컬 {forwardAxisName} → 매핑: {localAxisForWorldForward}"); } /// /// 축 벡터를 인덱스로 변환합니다 (디버그용). /// X축=1, Y축=2, Z축=3, 부호는 방향을 나타냄 /// private float GetAxisIndex(Vector3 axisVector) { if (Mathf.Abs(axisVector.x) > 0.5f) return 1f * Mathf.Sign(axisVector.x); // X축 else if (Mathf.Abs(axisVector.y) > 0.5f) return 2f * Mathf.Sign(axisVector.y); // Y축 else return 3f * Mathf.Sign(axisVector.z); // Z축 } /// /// 세 로컬 축 중에서 목표 월드 방향과 가장 일치하는 축을 찾아 로컬 축 벡터를 반환합니다. /// /// 로컬 X축의 월드 방향 /// 로컬 Y축의 월드 방향 /// 로컬 Z축의 월드 방향 /// 비교할 월드 방향 /// 매칭된 축 이름 (출력용) /// 해당 월드 방향을 담당하는 로컬 축 벡터 (부호 포함) private Vector3 FindBestLocalAxisForWorld(Vector3 localXInWorld, Vector3 localYInWorld, Vector3 localZInWorld, Vector3 worldDirection, out string matchedAxisName) { float dotX = Vector3.Dot(localXInWorld, worldDirection); float dotY = Vector3.Dot(localYInWorld, worldDirection); float dotZ = Vector3.Dot(localZInWorld, worldDirection); float absDotX = Mathf.Abs(dotX); float absDotY = Mathf.Abs(dotY); float absDotZ = Mathf.Abs(dotZ); // 가장 큰 내적값을 가진 축이 해당 월드 방향과 가장 일치하는 축 if (absDotX >= absDotY && absDotX >= absDotZ) { matchedAxisName = dotX > 0 ? "+X (Right)" : "-X (Left)"; return Vector3.right * Mathf.Sign(dotX); // 로컬 X축 (부호 포함) } else if (absDotY >= absDotX && absDotY >= absDotZ) { matchedAxisName = dotY > 0 ? "+Y (Up)" : "-Y (Down)"; return Vector3.up * Mathf.Sign(dotY); // 로컬 Y축 (부호 포함) } else { matchedAxisName = dotZ > 0 ? "+Z (Forward)" : "-Z (Back)"; return Vector3.forward * Mathf.Sign(dotZ); // 로컬 Z축 (부호 포함) } } /// /// HumanPoseHandler를 초기화합니다. /// private void InitializeHumanPoseHandlers() { if (sourceAnimator != null && sourceAnimator.avatar != null) { sourcePoseHandler = new HumanPoseHandler(sourceAnimator.avatar, sourceAnimator.transform); } if (targetAnimator != null && targetAnimator.avatar != null) { targetPoseHandler = new HumanPoseHandler(targetAnimator.avatar, targetAnimator.transform); } } /// /// 소스 포즈를 캐싱하여 반환합니다. 프레임당 한 번만 GetHumanPose 호출. /// private void EnsureSourcePoseCached() { if (!isSourcePoseCachedThisFrame && sourcePoseHandler != null) { sourcePoseHandler.GetHumanPose(ref sourcePose); isSourcePoseCachedThisFrame = true; } } /// /// 타겟 포즈를 캐싱하여 반환합니다. 프레임당 한 번만 GetHumanPose 호출. /// private void EnsureTargetPoseCached() { if (!isTargetPoseCachedThisFrame && targetPoseHandler != null) { targetPoseHandler.GetHumanPose(ref targetPose); isTargetPoseCachedThisFrame = true; } } /// /// 원본과 대상 아바타의 각 본 간 회전 오프셋을 계산하여 저장합니다. /// private void CalculateRotationOffsets(bool isIPose = false) { if (sourceAnimator == null || targetAnimator == null) { Debug.LogError("소스 또는 타겟 Animator가 설정되지 않았습니다."); return; } // Dictionary가 null이면 초기화 if (rotationOffsets == null) { rotationOffsets = new Dictionary(); } // 모든 본에 대해 오프셋 계산 (기본 몸체 본 + 손가락 본 + UpperChest) for (int i = 0; i <= (isIPose ? 23 : 54); i++) { HumanBodyBones bone = (HumanBodyBones)i; Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone); if (sourceBone != null && targetBone != null) { if (rotationOffsets.ContainsKey(bone)) { rotationOffsets[bone] = Quaternion.Inverse(sourceBone.rotation) * targetBone.rotation; } else { rotationOffsets.Add(bone, Quaternion.Inverse(sourceBone.rotation) * targetBone.rotation); } } } } /// /// 모션 필터를 초기화합니다. /// private void InitializeMotionFilters() { // 기존 본들에 대한 모션 필터 초기화 for (int i = 0; i < lastBoneIndex; i++) { HumanBodyBones bone = (HumanBodyBones)i; boneFilters[bone] = new MotionFilter(filterBufferSize); } // 손가락 본에 대한 모션 필터 초기화 for (int i = 24; i <= 53; i++) { HumanBodyBones bone = (HumanBodyBones)i; boneFilters[bone] = new MotionFilter(filterBufferSize); } // IK 타겟에 대한 모션 필터 초기화 string[] ikTargetNames = { "Left_Arm_Middle", "Right_Arm_Middle", "Left_Leg_Middle", "Right_Leg_Middle", "Left_Arm_End", "Right_Arm_End", "Left_Leg_End", "Right_Leg_End" }; foreach (string targetName in ikTargetNames) { ikTargetFilters[targetName] = new MotionFilter(filterBufferSize); } // 러프 모션 초기화 - 몸과 손가락 분리 // 몸 러프 모션 초기화 if (useBodyRoughMotion) { for (int i = 0; i < lastBoneIndex; i++) { HumanBodyBones bone = (HumanBodyBones)i; var rough = new RoughMotion(); rough.SetSmoothSpeed(50f - (bodyRoughness * 49f)); roughMotions[bone] = rough; } } // 손가락 러프 모션 초기화 if (useFingerRoughMotion) { for (int i = 24; i <= 53; i++) { HumanBodyBones bone = (HumanBodyBones)i; var rough = new RoughMotion(); rough.SetSmoothSpeed(50f - (fingerRoughness * 49f)); roughMotions[bone] = rough; } foreach (string targetName in ikTargetNames) { var rough = new RoughMotion(); rough.SetSmoothSpeed(50f - (bodyRoughness * 49f)); ikTargetRoughMotions[targetName] = rough; } } } /// /// 현재 설정을 JSON 파일로 저장합니다. /// public void SaveSettings() { if (targetAnimator == null) { Debug.LogWarning("타겟 아바타가 설정되지 않았습니다."); return; } var offsetCache = new List(); foreach (var kvp in rotationOffsets) { 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)); } // LimbWeightController에서 의자 높이 오프셋 가져오기 var limbController = GetComponent(); float chairOffset = limbController != null ? limbController.chairSeatHeightOffset : 0.05f; var settings = new RetargetingSettings { hipsOffsetX = hipsOffsetX, hipsOffsetY = hipsOffsetY, hipsOffsetZ = hipsOffsetZ, kneeInOutWeight = kneeInOutWeight, kneeFrontBackWeight = kneeFrontBackWeight, footFrontBackOffset = footFrontBackOffset, footInOutOffset = footInOutOffset, floorHeight = floorHeight, fingerCopyMode = fingerCopyMode, useMotionFilter = useMotionFilter, filterBufferSize = filterBufferSize, useBodyRoughMotion = useBodyRoughMotion, useFingerRoughMotion = useFingerRoughMotion, bodyRoughness = bodyRoughness, fingerRoughness = fingerRoughness, rotationOffsetCache = offsetCache, initialHipsHeight = initialHipsHeight, avatarScale = avatarScale, fingerOpenRotationsCache = fingerOpenCache, fingerCloseRotationsCache = fingerCloseCache, sourceMuscleCalibrationCache = muscleCalibrationCache, chairSeatHeightOffset = chairOffset, }; string json = JsonUtility.ToJson(settings, true); string filePath = GetSettingsFilePath(); try { File.WriteAllText(filePath, json); //너무 자주 출력되어서 주석처리 //Debug.Log($"설정이 저장되었습니다: {filePath}"); } catch (Exception e) { Debug.LogError($"설정 저장 중 오류 발생: {e.Message}"); } } /// /// JSON 파일에서 설정을 로드합니다. /// public void LoadSettings() { if (targetAnimator == null) { Debug.LogWarning("타겟 아바타가 설정되지 않았습니다."); return; } string filePath = GetSettingsFilePath(); if (!File.Exists(filePath)) { Debug.LogWarning($"저장된 설정 파일이 없습니다: {filePath}"); return; } try { string json = File.ReadAllText(filePath); var settings = JsonUtility.FromJson(json); // 설정 적용 hipsOffsetX = settings.hipsOffsetX; hipsOffsetY = settings.hipsOffsetY; hipsOffsetZ = settings.hipsOffsetZ; kneeInOutWeight = settings.kneeInOutWeight; kneeFrontBackWeight = settings.kneeFrontBackWeight; footFrontBackOffset = settings.footFrontBackOffset; footInOutOffset = settings.footInOutOffset; floorHeight = settings.floorHeight; fingerCopyMode = settings.fingerCopyMode; useMotionFilter = settings.useMotionFilter; filterBufferSize = settings.filterBufferSize; useBodyRoughMotion = settings.useBodyRoughMotion; useFingerRoughMotion = settings.useFingerRoughMotion; bodyRoughness = settings.bodyRoughness; fingerRoughness = settings.fingerRoughness; initialHipsHeight = settings.initialHipsHeight; 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; // LimbWeightController에 의자 높이 오프셋 적용 var limbController = GetComponent(); if (limbController != null) { limbController.chairSeatHeightOffset = settings.chairSeatHeightOffset; } //너무 자주 출력되어서 주석처리 //Debug.Log($"설정을 로드했습니다: {filePath}"); } catch (Exception e) { Debug.LogError($"설정 로드 중 오류 발생: {e.Message}"); } } private string GetSettingsFilePath() { // Unity AppData 경로 가져오기 string appDataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Unity", Application.companyName, Application.productName, settingsFolderName ); // 타겟 아바타 이름으로 파일명 생성 string fileName = $"{targetAnimator?.gameObject.name}_settings.json"; // 설정 폴더가 없으면 생성 if (!Directory.Exists(appDataPath)) { Directory.CreateDirectory(appDataPath); } return Path.Combine(appDataPath, fileName); } /// /// 캐시된 회전 오프셋을 로드하거나 새로 계산합니다. /// private void InitializeRotationOffsets() { string filePath = GetSettingsFilePath(); bool useCache = false; // 캐시된 데이터가 있는지 확인 if (File.Exists(filePath)) { try { string json = File.ReadAllText(filePath); var settings = JsonUtility.FromJson(json); if (settings.rotationOffsetCache != null && settings.rotationOffsetCache.Count > 0) { rotationOffsets.Clear(); foreach (var offsetData in settings.rotationOffsetCache) { rotationOffsets.Add((HumanBodyBones)offsetData.boneIndex, offsetData.ToQuaternion()); } useCache = true; Debug.Log("캐시된 회전 오프셋을 로드했습니다."); } } catch (Exception e) { Debug.LogWarning($"회전 오프셋 캐시 로드 실패: {e.Message}"); } } // 캐시된된 데이터가 없거나 로드 실패 시 새로 계산 if (!useCache) { CalculateRotationOffsets(); //너무 자주 출력되어서 주석처리 //Debug.Log("새로운 회전 오프셋을 계산했습니다."); } } public void I_PoseCalibration() { if (targetAnimator == null) { Debug.LogError("타겟 Animator가 설정되지 않았습니다."); return; } try { // 손가락 본들의 현재 로컬 회전을 저장 (I-포즈 적용 전) // 손가락은 24~54번 본 (LeftThumbProximal ~ RightLittleDistal) var savedFingerRotations = new Dictionary(); for (int i = 24; i <= 54; i++) { HumanBodyBones bone = (HumanBodyBones)i; Transform fingerBone = targetAnimator.GetBoneTransform(bone); if (fingerBone != null) { savedFingerRotations[bone] = fingerBone.localRotation; } } // 타겟 아바타를 I-포즈로 설정 (몸체만 필요하지만 전체가 변경됨) SetIPose(targetAnimator); // 손가락 본들의 로컬 회전을 복원 (I-포즈 적용 후) foreach (var kvp in savedFingerRotations) { Transform fingerBone = targetAnimator.GetBoneTransform(kvp.Key); if (fingerBone != null) { fingerBone.localRotation = kvp.Value; } } // 몸체 본들의 오프셋만 계산 (0~23번) CalculateRotationOffsets(true); // 손가락 본들의 오프셋 별도 계산 (24~54번) // 손가락은 복원된 상태에서 오프셋 계산 (Rotation 모드용) CalculateFingerRotationOffsets(); SaveSettings(); // 캘리브레이션 후 설정 저장 Debug.Log("I-포즈 캘리브레이션이 완료되었습니다. (손가락 포즈 유지)"); } catch (System.Exception e) { Debug.LogError($"I-포즈 캘리브레이션 중 오류가 발생했습니다: {e.Message}"); } } /// /// 손가락 본들의 회전 오프셋을 계산합니다. /// I-포즈 캘리브레이션 시 손가락 포즈가 복원된 상태에서 호출되어야 합니다. /// private void CalculateFingerRotationOffsets() { if (sourceAnimator == null || targetAnimator == null) return; // 손가락 본들 (24~54)의 오프셋 계산 for (int i = 24; i <= 54; i++) { HumanBodyBones bone = (HumanBodyBones)i; Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone); if (sourceBone != null && targetBone != null) { Quaternion offset = Quaternion.Inverse(sourceBone.rotation) * targetBone.rotation; if (rotationOffsets.ContainsKey(bone)) { rotationOffsets[bone] = offset; } else { rotationOffsets.Add(bone, offset); } } } } /// /// 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; // 소스 아바타의 현재 머슬 값 가져오기 (최적화: 캐싱된 포즈 사용) EnsureSourcePoseCached(); 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 루트 찾기 Transform sourceIKRoot = sourceAnimator.transform.Find("IK"); if (sourceIKRoot == null) { Debug.LogError("소스 아바타에서 IK 루트를 찾을 수 없습니다."); return; } // IK 조인트들 캐싱 sourceIKJoints = new IKJoints { leftLowerLeg = sourceIKRoot.Find("LeftLowerLeg"), rightLowerLeg = sourceIKRoot.Find("RightLowerLeg"), leftLowerArm = sourceIKRoot.Find("LeftLowerArm"), rightLowerArm = sourceIKRoot.Find("RightLowerArm") }; } #endregion #region 업데이트 /// /// 매 프레임마다 원본 아바타의 포즈와 손가락 움직임을 대상 아바타에 리타게팅하고, IK 타타겟을 업데이트합니다. /// void Update() { // 최적화: 프레임 시작 시 포즈 캐시 플래그 리셋 isSourcePoseCachedThisFrame = false; isTargetPoseCachedThisFrame = false; // 포즈 복사 및 동기화 CopyPoseToTarget(); UpdateIKTargets(); // IK 중간 타겟 업데이트 // 손가락 포즈 동기화 switch (fingerCopyMode) { case EnumsList.FingerCopyMode.MuscleData: CopyFingerPoseByMuscle(); break; case EnumsList.FingerCopyMode.Rotation: CopyFingerPoseByRotation(); break; case EnumsList.FingerCopyMode.Mingle: CopyFingerPoseByMingle(); break; } // 스케일 변경 확인 및 적용 if (!Mathf.Approximately(previousScale, avatarScale)) { ApplyScale(); previousScale = avatarScale; } } /// /// 머슬 데이터를 사용하여 손가락 포즈를 복제합니다. /// SetHumanPose가 모든 본에 영향을 미치므로, 손가락을 제외한 모든 본의 Transform을 저장하고 복원합니다. /// private void CopyFingerPoseByMuscle() { 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. 머슬 데이터 업데이트 (최적화: 캐싱된 포즈 사용) EnsureSourcePoseCached(); EnsureTargetPoseCached(); for (int i = 0; i < 40; i++) { int muscleIndex = 55 + i; string muscleName = HumanTrait.MuscleName[muscleIndex]; // "Spread"가 포함된 머슬만 스킵 (손가락 벌리기 동작) if (muscleName.Contains("Spread")) continue; float targetValue = sourcePose.muscles[muscleIndex]; float currentValue = targetPose.muscles[muscleIndex]; if (useMotionFilter) { targetValue = ApplyFilter(targetValue, i); } if (useFingerRoughMotion && roughMotions.TryGetValue(HumanBodyBones.LeftHand, out RoughMotion rough)) { float smoothSpeed = 50f - (fingerRoughness * 49f); targetValue = Mathf.Lerp(currentValue, targetValue, smoothSpeed * Time.deltaTime); } targetPose.muscles[muscleIndex] = targetValue; } // 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향) targetPoseHandler.SetHumanPose(ref targetPose); // 4. 손가락을 제외한 모든 본의 위치/회전 복원 foreach (var kvp in savedBoneTransforms) { Transform bone = targetAnimator.GetBoneTransform(kvp.Key); if (bone != null) { bone.SetPositionAndRotation(kvp.Value.position, kvp.Value.rotation); } } } private float ApplyFilter(float value, int fingerIndex) { // 해당 손가락 관절의 필터 버퍼가 없으면 생성 (GC 최적화: 배열 재사용) if (!fingerFilterBuffers.TryGetValue(fingerIndex, out float[] buffer)) { buffer = new float[filterBufferSize]; fingerFilterBuffers[fingerIndex] = buffer; fingerFilterIndices[fingerIndex] = 0; // 초기값으로 버퍼를 채움 for (int i = 0; i < filterBufferSize; i++) { buffer[i] = value; } } // 현재 인덱스 가져오기 int currentIndex = fingerFilterIndices[fingerIndex]; // 새 값을 현재 위치에 덮어쓰기 (순환 버퍼) buffer[currentIndex] = value; // 다음 인덱스로 이동 (순환) fingerFilterIndices[fingerIndex] = (currentIndex + 1) % filterBufferSize; // 가중 평균 계산 (배열 직접 순회, ToArray 제거) float sum = 0f; float weight = 1f; float totalWeight = 0f; // 가장 오래된 값부터 순회 (currentIndex+1이 가장 오래된 값) for (int i = 0; i < filterBufferSize; i++) { int idx = (currentIndex + 1 + i) % filterBufferSize; sum += buffer[idx] * weight; totalWeight += weight; weight *= 1.5f; // 최근 값에 더 높은 가중치 부여 } return sum / totalWeight; } /// /// 힙전값을 사용하여 손가락 포즈를 복제합니다. /// private void CopyFingerPoseByRotation() { for (int i = 24; i <= 53; i++) { HumanBodyBones bone = (HumanBodyBones)i; Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone); if (sourceBone != null && targetBone != null) { Quaternion targetRotation; if (rotationOffsets.TryGetValue(bone, out Quaternion offset)) { targetRotation = sourceBone.rotation * offset; } else { targetRotation = sourceBone.rotation; } // 모션 필터 적용 if (useMotionFilter && boneFilters.TryGetValue(bone, out MotionFilter filter)) { targetRotation = filter.FilterRotation(targetRotation); } // 러프 모션을 Lerp로 적용 if (useFingerRoughMotion && roughMotions.TryGetValue(bone, out RoughMotion rough)) { float smoothSpeed = 50f - (fingerRoughness * 49f); // 1~10 범위의 속도 targetRotation = Quaternion.Lerp(targetBone.rotation, targetRotation, smoothSpeed * Time.deltaTime); } targetBone.rotation = targetRotation; } } } /// /// 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. 타겟 포즈 가져오기 (최적화: 캐싱된 포즈 사용) EnsureTargetPoseCached(); // 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 포즈 동기화 /// /// 원본 아바타의 포즈를 대상 아바타에 오프셋을 적용하여 복사합니다. /// 힙 위치와 회전을 동기화하고, 나머지 본의 회전을 오프셋을 적용하여 동기화합니다. /// private void CopyPoseToTarget() { // 힙(루트 본) 동기화 Transform sourceHips = sourceAnimator.GetBoneTransform(HumanBodyBones.Hips); Transform targetHips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips); if (sourceHips != null && targetHips != null) { // 1. 힙 회전 먼저 동기화 (회전 오프셋 적용) Quaternion finalHipsRotation = sourceHips.rotation; if (rotationOffsets.TryGetValue(HumanBodyBones.Hips, out Quaternion hipsOffset)) { finalHipsRotation = sourceHips.rotation * hipsOffset; targetHips.rotation = finalHipsRotation; } // 2. 캐릭터 기준 로컬 오프셋 계산 (축 정규화 적용) // // 문제: 아바타마다 힙의 로컬 축 방향이 다름 // - 아바타 A: 로컬 Y가 "위", 로컬 Z가 "앞" // - 아바타 B: 로컬 Z가 "위", 로컬 X가 "앞" // // 해결: T-포즈에서 계산한 축 매핑을 사용 // - localAxisForWorldRight: 실제로 "오른쪽"을 가리키는 로컬 축 // - localAxisForWorldUp: 실제로 "위"를 가리키는 로컬 축 // - localAxisForWorldForward: 실제로 "앞"을 가리키는 로컬 축 // // 이렇게 하면 모든 아바타에서 동일하게 작동합니다. // 힙의 현재 회전을 기준으로, 정규화된 방향 벡터 계산 Vector3 characterRight = finalHipsRotation * localAxisForWorldRight; Vector3 characterUp = finalHipsRotation * localAxisForWorldUp; Vector3 characterForward = finalHipsRotation * localAxisForWorldForward; Vector3 characterOffset = characterRight * (hipsOffsetX * HipsWeightOffset) + // 캐릭터 기준 좌우 characterUp * (hipsOffsetY * HipsWeightOffset) + // 캐릭터 기준 상하 characterForward * (hipsOffsetZ * HipsWeightOffset); // 캐릭터 기준 앞뒤 // 3. 힙 위치 동기화 + 캐릭터 기준 오프셋 적용 Vector3 adjustedPosition = sourceHips.position + characterOffset; // 4. 바닥 높이 추가 (월드 Y축 - 바닥은 항상 월드 기준) adjustedPosition.y += floorHeight; // 5. 의자 좌석 높이 오프셋 추가 (월드 Y축 - 로컬 보정과 별개) adjustedPosition.y += ChairSeatHeightOffset; targetHips.position = adjustedPosition; // 6. IK 타겟에도 동기화 ikComponent.solver.spine.pelvisTarget.position = targetHips.position; ikComponent.solver.spine.pelvisTarget.rotation = targetHips.rotation; } // 힙을 제외한 본들의 회전 동기화 SyncBoneRotations(skipBone: HumanBodyBones.Hips); } /// /// 힙을 제외한 모든 본의 회전을 오프셋을 적용하여 동기화합니다. /// /// 동기화에서 제외할 본 private void SyncBoneRotations(HumanBodyBones skipBone) { // 기본 몸체 본만 동기화 (손가락 제외) for (int i = 0; i < lastBoneIndex; i++) { HumanBodyBones bone = (HumanBodyBones)i; if (bone == skipBone) continue; Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone); if (sourceBone != null && targetBone != null) { Quaternion targetRotation; if (rotationOffsets.TryGetValue(bone, out Quaternion offset)) { targetRotation = sourceBone.rotation * offset; } else { targetRotation = sourceBone.rotation; } // 모션 필터 적용 if (useMotionFilter && boneFilters.TryGetValue(bone, out MotionFilter filter)) { targetRotation = filter.FilterRotation(targetRotation); } // 러 러프 모션 적용 if (useBodyRoughMotion) { float smoothSpeed = 50f - (bodyRoughness * 49f); targetRotation = Quaternion.Lerp(targetBone.rotation, targetRotation, smoothSpeed * Time.deltaTime); } targetBone.rotation = targetRotation; } } } #endregion #region IK 타겟 생성 및 관관리 /// /// IK 타겟(끝과 중간)을 생성하고 FullBodyInverseKinematics 컴포넌트를 설정합니다. /// private void CreateIKTargets() { // IK 컴포넌트 가져오기 또는 새로 추가 ikComponent = GetComponent(); if (ikComponent == null) ikComponent = gameObject.AddComponent(); // IK 타겟들을 담을 부모 오브젝트 생성 GameObject ikTargetsParent = new GameObject("IK Targets"); ikTargetsParent.transform.parent = targetAnimator.transform; ikTargetsParent.transform.localPosition = Vector3.zero; ikTargetsParent.transform.localRotation = Quaternion.identity; GameObject hips = CreateIKTargetObject("Hips", targetAnimator.GetBoneTransform(HumanBodyBones.Hips), ikTargetsParent); ikComponent.solver.spine.pelvisTarget = hips.transform; GameObject leftHand = CreateIKTargetObject("LeftHand", targetAnimator.GetBoneTransform(HumanBodyBones.LeftHand), ikTargetsParent); ikComponent.solver.leftArm.target = leftHand.transform; GameObject rightHand = CreateIKTargetObject("RightHand", targetAnimator.GetBoneTransform(HumanBodyBones.RightHand), ikTargetsParent); ikComponent.solver.rightArm.target = rightHand.transform; GameObject leftToes = CreateIKTargetObject("LeftToes", targetAnimator.GetBoneTransform(HumanBodyBones.LeftToes), ikTargetsParent); ikComponent.solver.leftLeg.target = leftToes.transform; GameObject rightToes = CreateIKTargetObject("RightToes", targetAnimator.GetBoneTransform(HumanBodyBones.RightToes), ikTargetsParent); ikComponent.solver.rightLeg.target = rightToes.transform; GameObject leftArmGoal = CreateIKTargetObject("LeftArmGoal", targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm), ikTargetsParent); ikComponent.solver.leftArm.bendGoal = leftArmGoal.transform; GameObject rightArmGoal = CreateIKTargetObject("RightArmGoal", targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm), ikTargetsParent); ikComponent.solver.rightArm.bendGoal = rightArmGoal.transform; GameObject leftLegGoal = CreateIKTargetObject("LeftLegGoal", targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), ikTargetsParent); ikComponent.solver.leftLeg.bendGoal = leftLegGoal.transform; GameObject rightLegGoal = CreateIKTargetObject("RightLegGoal", targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerLeg), ikTargetsParent); ikComponent.solver.rightLeg.bendGoal = rightLegGoal.transform; } /// /// IK 타겟 오브젝트를 생성하고 기본 Gizmo 설정을 적용합니다. /// /// 타겟 오브젝트의 이름 /// 타겟 오브젝트를 부모로 설정할 본 /// 생성된 타겟 게임 오브젝트 private GameObject CreateIKTargetObject(string name, Transform bone, GameObject parent) { GameObject target = new GameObject(name); target.transform.position = bone.position; target.transform.rotation = bone.rotation; // Gizmo 시각화를 위한 컴포넌트 추가 IKTargetGizmo gizmo = target.AddComponent(); gizmo.gizmoSize = 0.05f; gizmo.gizmoColor = UnityEngine.Color.yellow; target.transform.parent = parent.transform; return target; } #endregion #region IK 중간 타겟 업데이트 /// /// 매 프레임마다 IK 중간 타겟의 위치를 대상 아바타의 본 위치로 업데이트합니다. /// private void UpdateIKTargets() { if (targetAnimator == null) return; // 손과 발끝 타겟 업데이트 UpdateEndTarget(ikComponent.solver.leftArm.target, HumanBodyBones.LeftHand); UpdateEndTarget(ikComponent.solver.rightArm.target, HumanBodyBones.RightHand); UpdateEndTarget(ikComponent.solver.leftLeg.target, HumanBodyBones.LeftToes); UpdateEndTarget(ikComponent.solver.rightLeg.target, HumanBodyBones.RightToes); updatejointTarget(ikComponent.solver.leftArm.bendGoal, HumanBodyBones.LeftLowerArm); updatejointTarget(ikComponent.solver.rightArm.bendGoal, HumanBodyBones.RightLowerArm); updatejointTarget(ikComponent.solver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg); updatejointTarget(ikComponent.solver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg); } /// /// 손과 발끝 타겟의 위치와 회전을 업데이트합니다. /// /// 업데이트할 끝 타겟의 Transform /// 대상 본 private void UpdateEndTarget(Transform endTarget, HumanBodyBones endBone) { if (endTarget == null) return; Transform sourceBone = sourceAnimator.GetBoneTransform(endBone); Transform targetBone = targetAnimator.GetBoneTransform(endBone); if (sourceBone != null && targetBone != null) { // 기본 위치와 회전 계산 Vector3 targetPosition = sourceBone.position; Quaternion targetRotation = targetBone.rotation; // 발/발가락 본인 경우 오프셋 적용 if (endBone == HumanBodyBones.LeftToes || endBone == HumanBodyBones.RightToes) { // 바닥 높이 적용 targetPosition.y += floorHeight; // 발 본의 로컬 좌표계 기준으로 오프셋 적용 // +Z: 앞으로, -Z: 뒤로 // 왼발: +X로 벌리기, -X로 모으기 // 오른발: -X로 벌리기, +X로 모으기 (반대) HumanBodyBones footBone = endBone == HumanBodyBones.LeftToes ? HumanBodyBones.LeftFoot : HumanBodyBones.RightFoot; Transform foot = sourceAnimator.GetBoneTransform(footBone); if (foot != null) { // 로컬 오프셋 계산 Vector3 localOffset = Vector3.zero; // 앞뒤 오프셋 (로컬 Z축) localOffset.z = footFrontBackOffset; // 안쪽/바깥쪽 오프셋 (로컬 X축) if (endBone == HumanBodyBones.LeftToes) { // 왼발: +X로 벌림 localOffset.x = footInOutOffset; } else // RightToes { // 오른발: -X로 벌림 localOffset.x = -footInOutOffset; } // 로컬 오프셋을 월드 좌표로 변환하여 적용 targetPosition += foot.TransformDirection(localOffset); } } // 최종 위치와 회전 적용 endTarget.position = targetPosition; endTarget.rotation = targetRotation; } } private void updatejointTarget(Transform target, HumanBodyBones bone) { Vector3 targetBone = targetAnimator.GetBoneTransform(bone).position; Vector3 sourceIKpoint; float zOffset = 0f; float xOffset = 0f; float yOffset = 0f; // bone의 이름에 따라 적절한 IK 조인트 오프셋 선택 switch (bone) { case HumanBodyBones.LeftLowerLeg: case HumanBodyBones.RightLowerLeg: sourceIKpoint = bone == HumanBodyBones.LeftLowerLeg ? sourceIKJoints.leftLowerLeg.position : sourceIKJoints.rightLowerLeg.position; zOffset = kneeFrontBackWeight; // 무릎 앞/뒤 조정 yOffset = floorHeight; xOffset = kneeInOutWeight * (bone == HumanBodyBones.LeftLowerLeg ? -1f : 1f); // 무릎 안/밖 조정 break; case HumanBodyBones.LeftLowerArm: case HumanBodyBones.RightLowerArm: sourceIKpoint = bone == HumanBodyBones.LeftLowerArm ? sourceIKJoints.leftLowerArm.position : sourceIKJoints.rightLowerArm.position; zOffset = 0.1f; // 팔꿈치는 뒤로 고정 break; default: Debug.LogError($"Unsupported bone type: {bone}"); return; } Quaternion LookatIK = Quaternion.LookRotation(sourceIKpoint - targetBone, Vector3.up); target.position = targetBone; target.rotation = LookatIK; // LookatIK 기준으로 프셋 적용 Vector3 offset = LookatIK * new Vector3(xOffset, 0, zOffset); Vector3 offset2 = new Vector3(0, yOffset, 0); target.position += offset; target.position += offset2; } #endregion #region T-포즈 설정 /// /// 지정된 Animator의 포즈를 T-포즈로 설정합니다. /// /// T-포즈를 설정할 Animator public void SetTPose(Animator animator) { if (animator == null || animator.avatar == null) return; Avatar avatar = animator.avatar; Transform transform = animator.transform; // HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용 var humanPoseClip = Resources.Load(HumanPoseClip.TPoseResourcePath); if (humanPoseClip != null) { var pose = humanPoseClip.GetPose(); HumanPoseTransfer.SetPose(avatar, transform, pose); // 소스 아바타의 UpperChest 본 로컬 포지션 초기화 if (animator == sourceAnimator) { Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest); if (upperChest != null) { upperChest.localPosition = Vector3.zero; } } } else { Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); } } public static void SetIPose(Animator animator) { if (animator == null || animator.avatar == null) return; Avatar avatar = animator.avatar; Transform transform = animator.transform; // HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용 var humanPoseClip = Resources.Load(HumanPoseClip.IPoseResourcePath); if (humanPoseClip != null) { var pose = humanPoseClip.GetPose(); HumanPoseTransfer.SetPose(avatar, transform, pose); } else { Debug.LogWarning("I-Pose 데이터가 존재하지 않습니다."); } } #endregion #region 파괴 시 처리 /// /// 오브브젝트가 파괴될 때 HumanPoseHandler를 정리합니다. /// void OnDestroy() { sourcePoseHandler?.Dispose(); targetPoseHandler?.Dispose(); } /// /// Unity가 종료될 때 호출되되는 메서드 /// private void OnApplicationQuit() { SaveSettings(); } #endregion /// /// 캐시된 설정 파일이 존재하는지 확인합니다. /// public bool HasCachedSettings() { if (targetAnimator == null) return false; string filePath = GetSettingsFilePath(); return File.Exists(filePath); } // 무릎 앞/뒤뒤 위치 조정을 위한 public 메서드 추가 public void SetKneeFrontBackOffset(float offset) { kneeFrontBackWeight = offset; } // 무릎 조정을 위한 public 메서드들 public void SetKneeInOutOffset(float offset) { kneeInOutWeight = offset; } public void ResetPoseAndCache() { // 캐시 파일 삭제 string filePath = GetSettingsFilePath(); if (File.Exists(filePath)) { try { File.Delete(filePath); Debug.Log("캐시 파일이 삭제되었습니다."); } catch (Exception e) { Debug.LogError($"캐시 파일 삭제 중 오류 발생: {e.Message}"); return; } } if (!Application.isPlaying) { Debug.LogWarning("이 기능은 실행 중에만 사용할 수 있습니다."); return; } // T-포즈로 복원 SetTPose(sourceAnimator); SetTPose(targetAnimator); // 소스 아바타의 UpperChest 본 로컬 포지션 초기화 Transform upperChest = sourceAnimator.GetBoneTransform(HumanBodyBones.UpperChest); if (upperChest != null) { upperChest.localPosition = Vector3.zero; } // HumanPoseHandler 초기화 InitializeHumanPoseHandlers(); // 회전 오프셋 다시 계산 CalculateRotationOffsets(); Debug.Log("포즈와 회전 오프셋이 재설정되었습니다."); } // 크기 조정 대상 오브젝트 캐싱 메서드 private void CacheScalableObjects() { scalableObjects.Clear(); originalScales.Clear(); originalPositions.Clear(); Transform parentTransform = sourceAnimator.transform.parent; if (parentTransform != null) { for (int i = 0; i < parentTransform.childCount; i++) { Transform child = parentTransform.GetChild(i); // sourceAnimator를 제외한 모든 자식 오브젝트 추가 if (child != sourceAnimator.transform) { scalableObjects.Add(child); // 초기 스케일과 위치 저장 originalScales[child] = child.localScale; originalPositions[child] = child.position - parentTransform.position; } } } } // 스케일 적용 메서드 private void ApplyScale() { foreach (Transform obj in scalableObjects) { if (obj != null && obj.parent != null) { // 원본 스케일을 기준으로 새로운 스케일 계산 if (originalScales.TryGetValue(obj, out Vector3 originalScale)) { obj.localScale = originalScale * avatarScale; } // 원본 위치를 기준으로 새로운 위치 계산 if (originalPositions.TryGetValue(obj, out Vector3 originalOffset)) { obj.position = obj.parent.position + (originalOffset * avatarScale); } } } } // 리셋 기능 추가 public void ResetScale() { avatarScale = 1f; previousScale = 1f; foreach (Transform obj in scalableObjects) { if (obj != null && originalScales.TryGetValue(obj, out Vector3 originalScale)) { obj.localScale = originalScale; if (obj.parent != null && originalPositions.TryGetValue(obj, out Vector3 originalOffset)) { obj.position = obj.parent.position + originalOffset; } } } } // 외부에서 스케일을 설정할 수 있는 public 메서드 public void SetAvatarScale(float scale) { avatarScale = Mathf.Clamp(scale, 0.1f, 3f); } // 현재 스케일을 가져오는 메서드 public float GetAvatarScale() { return avatarScale; } } }