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; // 축 매핑: 월드 방향(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; // 본별 회전 오프셋을 저장하는 딕셔너리 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; } // 각 손가락 관절별로 필터 버퍼를 관리하는 Dictionary 추가 private Dictionary> fingerFilterBuffers = 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; #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); } } /// /// 원본과 대상 아바타의 각 본 간 회전 오프셋을 계산하여 저장합니다. /// 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)); } 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, }; 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; //너무 자주 출력되어서 주석처리 //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); } } } } 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() { // 포즈 복사 및 동기화 CopyPoseToTarget(); UpdateIKTargets(); // IK 중간 타겟 업데이트 // 손가락 포즈 동기화 switch (fingerCopyMode) { case EnumsList.FingerCopyMode.MuscleData: CopyFingerPoseByMuscle(); break; case EnumsList.FingerCopyMode.Rotation: CopyFingerPoseByRotation(); 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. 머슬 데이터 업데이트 sourcePoseHandler.GetHumanPose(ref sourcePose); targetPoseHandler.GetHumanPose(ref targetPose); 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) { // 해당 손가락 관절의 필터 버퍼가 없없으면 생성 if (!fingerFilterBuffers.ContainsKey(fingerIndex)) { fingerFilterBuffers[fingerIndex] = new Queue(); // 초기값으로 버퍼를 채움 for (int i = 0; i < filterBufferSize; i++) { fingerFilterBuffers[fingerIndex].Enqueue(value); } } var buffer = fingerFilterBuffers[fingerIndex]; // 가장 오래된 값 제거 if (buffer.Count >= filterBufferSize) { buffer.Dequeue(); } // 새 값 추가 buffer.Enqueue(value); // 가중 평균 계산 float sum = 0f; float weight = 1f; float totalWeight = 0f; float[] values = buffer.ToArray(); for (int i = 0; i < values.Length; i++) { sum += values[i] * 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; } } } #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; 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로 모으기 (반대) Vector3 footOffset = Vector3.zero; // 앞뒤 오프셋 (소스 아바타 기준 Z축) footOffset += sourceAnimator.transform.forward * footFrontBackOffset; // 안쪽/바깥쪽 오프셋 (소스 아바타 기준 X축) // footInOutOffset > 0: 벌리기, < 0: 모으기 if (endBone == HumanBodyBones.LeftToes) { // 왼발: +일 때 왼쪽(+X)으로 벌림, -일 때 오른쪽(-X)으로 모음 footOffset += sourceAnimator.transform.right * footInOutOffset; } else // RightToes { // 오른발: +일 때 오른쪽(-X)으로 벌림, -일 때 왼쪽(+X)으로 모음 footOffset -= sourceAnimator.transform.right * footInOutOffset; } targetPosition += footOffset; } // 최종 위치와 회전 적용 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; } } }