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))] 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 hipsWeight = 0f; // 힙의 위치를 위아래로 보정하는 가중치 [HideInInspector] public float HipsWeightOffset = 1f; // 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("바닥 높이 조정")] [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 hipsWeight; public float kneeInOutWeight; public float kneeFrontBackWeight; 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>(); // 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-포즈로 복원 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(); } /// /// 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 { hipsWeight = hipsWeight, kneeInOutWeight = kneeInOutWeight, kneeFrontBackWeight = kneeFrontBackWeight, 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); // 설정 적용 hipsWeight = settings.hipsWeight; kneeInOutWeight = settings.kneeInOutWeight; kneeFrontBackWeight = settings.kneeFrontBackWeight; 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 { SetIPose(targetAnimator); CalculateRotationOffsets(true); SaveSettings(); // 캘리브레이션 후 설정 저장 Debug.Log("I-포즈 캘리브레이션이 완료되었습니다."); } catch (System.Exception e) { Debug.LogError($"I-포즈 캘리브레이션 중 오류가 발생했습니다: {e.Message}"); } } 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; } } /// /// 머슬 데이터를 사용하여 손가락 포즈를 복제합니다. /// private void CopyFingerPoseByMuscle() { Vector3 originalPosition = targetAnimator.GetBoneTransform(HumanBodyBones.Hips).position; Quaternion originalRotation = targetAnimator.GetBoneTransform(HumanBodyBones.Hips).rotation; if (sourcePoseHandler == null || targetPoseHandler == null) return; 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; } targetPoseHandler.SetHumanPose(ref targetPose); targetAnimator.GetBoneTransform(HumanBodyBones.Hips).position = originalPosition; targetAnimator.GetBoneTransform(HumanBodyBones.Hips).rotation = originalRotation; } 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) { // 힙 위치 동기화 + 힙 위치 보정 적용 + 바닥 높이 적용용 Vector3 adjustedPosition = sourceHips.position; adjustedPosition.y += hipsWeight * HipsWeightOffset; // 기존 힙 높이 조정 adjustedPosition.y += floorHeight; // 바닥 높이 조정 추가 targetHips.position = adjustedPosition; // 힙 회전 동기화 (회전 오프셋 적용) if (rotationOffsets.TryGetValue(HumanBodyBones.Hips, out Quaternion hipsOffset)) { targetHips.rotation = sourceHips.rotation * hipsOffset; 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; } // 최종 위치와 회전 적용 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 static 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); } 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; } sourceAnimator.transform.localRotation = new Quaternion(0, 0, 0, 0); // T-포즈로 복 SetTPose(sourceAnimator); SetTPose(targetAnimator); // 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; } } }