Streamingle_URP/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs
2025-12-01 03:36:18 +09:00

2107 lines
88 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniHumanoid;
using System.IO;
using System;
using RootMotion.FinalIK;
namespace KindRetargeting
{
/// <summary>
/// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다.
/// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다.
/// </summary>
[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;
// 본별 회전 오프셋을 저장하는 딕셔너리
private Dictionary<HumanBodyBones, Quaternion> rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>();
// 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<HumanBodyBones, MotionFilter> boneFilters = new Dictionary<HumanBodyBones, MotionFilter>();
private Dictionary<HumanBodyBones, RoughMotion> roughMotions = new Dictionary<HumanBodyBones, RoughMotion>();
// IK 타겟용 러프 모션 필터
private Dictionary<string, RoughMotion> ikTargetRoughMotions = new Dictionary<string, RoughMotion>();
// IK 타겟용 모션 필터
private Dictionary<string, MotionFilter> ikTargetFilters = new Dictionary<string, MotionFilter>();
[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<Transform> scalableObjects = new List<Transform>();
// 필드 추가
private Dictionary<Transform, Vector3> originalScales = new Dictionary<Transform, Vector3>();
private Dictionary<Transform, Vector3> originalPositions = new Dictionary<Transform, Vector3>();
[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<RotationOffsetData> rotationOffsetCache;
public float initialHipsHeight;
public float avatarScale;
// Mingle 캘리브레이션 데이터
public List<RotationOffsetData> fingerOpenRotationsCache;
public List<RotationOffsetData> fingerCloseRotationsCache;
// 소스 머슬 캘리브레이션 데이터
public List<MuscleCalibrationData> 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 추가
private Dictionary<int, Queue<float>> fingerFilterBuffers = new Dictionary<int, Queue<float>>();
// CopyFingerPoseByMuscle에서 사용할 본 Transform 저장용 (메모리 재사용)
private Dictionary<HumanBodyBones, (Vector3 position, Quaternion rotation)> savedBoneTransforms =
new Dictionary<HumanBodyBones, (Vector3, Quaternion)>();
// 손가락을 제외한 모든 휴먼본 목록 (캐싱)
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<HumanBodyBones, Quaternion> fingerOpenRotations = new Dictionary<HumanBodyBones, Quaternion>();
private Dictionary<HumanBodyBones, Quaternion> fingerCloseRotations = new Dictionary<HumanBodyBones, Quaternion>();
// 엄지 Open/Close 쿼터니언 각도 (캘리브레이션 시 미리 계산)
private Dictionary<HumanBodyBones, float> thumbOpenToCloseAngles = new Dictionary<HumanBodyBones, float>();
private bool isMingleCalibrated = false;
// 소스 아바타 머슬 캘리브레이션 데이터 (엄지 Stretched용)
// Key: 머슬 인덱스, Value: (Open 상태 머슬값, Close 상태 머슬값)
private Dictionary<int, (float open, float close)> sourceMuscleCalibration = new Dictionary<int, (float, float)>();
// 자동 캘리브레이션 상태
private bool isAutoCalibrating = false;
private string autoCalibrationStatus = "";
private float autoCalibrationTimeRemaining = 0f;
private Coroutine autoCalibrationCoroutine = null;
/// <summary>
/// 자동 캘리브레이션 진행 중 여부
/// </summary>
public bool IsAutoCalibrating => isAutoCalibrating;
/// <summary>
/// 자동 캘리브레이션 상태 메시지
/// </summary>
public string AutoCalibrationStatus => autoCalibrationStatus;
/// <summary>
/// 자동 캘리브레이션 남은 시간
/// </summary>
public float AutoCalibrationTimeRemaining => autoCalibrationTimeRemaining;
// 손가락 본 인덱스 → 머슬 인덱스 매핑
// 머슬 순서: 1 Stretched, Spread, 2 Stretched, 3 Stretched (4개씩)
// 소스 아바타 로컬 회전: 엄지는 Y = Spread, 나머지는 X = Spread, 굽힘은 모두 Z축
private static readonly Dictionary<int, (int stretchedMuscle, int spreadMuscle)> fingerBoneToMuscleIndex = new Dictionary<int, (int, int)>
{
// 왼손 엄지 (24-26) → 머슬 55-58 (55: Stretched1, 56: Spread, 57: Stretched2, 58: Stretched3)
{ 24, (55, 56) }, // LeftThumbProximal → Left Thumb 1 Stretched, Spread
{ 25, (57, -1) }, // LeftThumbIntermediate → Left Thumb 2 Stretched
{ 26, (58, -1) }, // LeftThumbDistal → Left Thumb 3 Stretched
// 왼손 검지 (27-29) → 머슬 59-62 (59: Stretched1, 60: Spread, 61: Stretched2, 62: Stretched3)
{ 27, (59, 60) }, // LeftIndexProximal → Left Index 1 Stretched, Spread
{ 28, (61, -1) }, // LeftIndexIntermediate → Left Index 2 Stretched (Spread 없음)
{ 29, (62, -1) }, // LeftIndexDistal → Left Index 3 Stretched (Spread 없음)
// 왼손 중지 (30-32) → 머슬 63-66
{ 30, (63, 64) }, // LeftMiddleProximal → Left Middle 1 Stretched, Spread
{ 31, (65, -1) }, // LeftMiddleIntermediate → Left Middle 2 Stretched
{ 32, (66, -1) }, // LeftMiddleDistal → Left Middle 3 Stretched
// 왼손 약지 (33-35) → 머슬 67-70
{ 33, (67, 68) }, // LeftRingProximal → Left Ring 1 Stretched, Spread
{ 34, (69, -1) }, // LeftRingIntermediate → Left Ring 2 Stretched
{ 35, (70, -1) }, // LeftRingDistal → Left Ring 3 Stretched
// 왼손 소지 (36-38) → 머슬 71-74
{ 36, (71, 72) }, // LeftLittleProximal → Left Little 1 Stretched, Spread
{ 37, (73, -1) }, // LeftLittleIntermediate → Left Little 2 Stretched
{ 38, (74, -1) }, // LeftLittleDistal → Left Little 3 Stretched
// 오른손 엄지 (39-41) → 머슬 75-78
{ 39, (75, 76) }, // RightThumbProximal → Right Thumb 1 Stretched, Spread
{ 40, (77, -1) }, // RightThumbIntermediate → Right Thumb 2 Stretched
{ 41, (78, -1) }, // RightThumbDistal → Right Thumb 3 Stretched
// 오른손 검지 (42-44) → 머슬 79-82
{ 42, (79, 80) }, // RightIndexProximal → Right Index 1 Stretched, Spread
{ 43, (81, -1) }, // RightIndexIntermediate → Right Index 2 Stretched
{ 44, (82, -1) }, // RightIndexDistal → Right Index 3 Stretched
// 오른손 중지 (45-47) → 머슬 83-86
{ 45, (83, 84) }, // RightMiddleProximal → Right Middle 1 Stretched, Spread
{ 46, (85, -1) }, // RightMiddleIntermediate → Right Middle 2 Stretched
{ 47, (86, -1) }, // RightMiddleDistal → Right Middle 3 Stretched
// 오른손 약지 (48-50) → 머슬 87-90
{ 48, (87, 88) }, // RightRingProximal → Right Ring 1 Stretched, Spread
{ 49, (89, -1) }, // RightRingIntermediate → Right Ring 2 Stretched
{ 50, (90, -1) }, // RightRingDistal → Right Ring 3 Stretched
// 오른손 소지 (51-53) → 머슬 91-94
{ 51, (91, 92) }, // RightLittleProximal → Right Little 1 Stretched, Spread
{ 52, (93, -1) }, // RightLittleIntermediate → Right Little 2 Stretched
{ 53, (94, -1) }, // RightLittleDistal → Right Little 3 Stretched
};
// 엄지 본 인덱스 (24-26: 왼손, 39-41: 오른손) - Spread 축이 Y임
private static readonly HashSet<int> thumbBoneIndices = new HashSet<int> { 24, 25, 26, 39, 40, 41 };
#endregion
#region
/// <summary>
/// 초기화 메서드. T-포즈 설정, IK 타겟 생성, HumanPoseHandler 초기화 및 회전 오프셋 계산을 수행합니다.
/// </summary>
void Start()
{
targetAnimator = GetComponent<Animator>();
// 설정 로드
LoadSettings();
// IK 컴포넌트 참조 가져오기 변경
ikComponent = GetComponent<FullBodyInverseKinematics_RND>();
// 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();
}
/// <summary>
/// 타겟 아바타의 로컬 축과 월드 축의 관계를 분석하여 축 매핑을 계산합니다.
/// T-포즈 상태에서 힙의 각 로컬 축이 월드의 어느 방향을 가리키는지 분석합니다.
///
/// 예시:
/// - 아바타 A: 로컬 Y가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 1, 0)
/// - 아바타 B: 로컬 Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, 1)
/// - 아바타 C: 로컬 -Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, -1)
///
/// 이를 통해 hipsOffsetY는 항상 "위/아래" 방향으로 작동합니다.
/// </summary>
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}");
}
/// <summary>
/// 축 벡터를 인덱스로 변환합니다 (디버그용).
/// X축=1, Y축=2, Z축=3, 부호는 방향을 나타냄
/// </summary>
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축
}
/// <summary>
/// 세 로컬 축 중에서 목표 월드 방향과 가장 일치하는 축을 찾아 로컬 축 벡터를 반환합니다.
/// </summary>
/// <param name="localXInWorld">로컬 X축의 월드 방향</param>
/// <param name="localYInWorld">로컬 Y축의 월드 방향</param>
/// <param name="localZInWorld">로컬 Z축의 월드 방향</param>
/// <param name="worldDirection">비교할 월드 방향</param>
/// <param name="matchedAxisName">매칭된 축 이름 (출력용)</param>
/// <returns>해당 월드 방향을 담당하는 로컬 축 벡터 (부호 포함)</returns>
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축 (부호 포함)
}
}
/// <summary>
/// HumanPoseHandler를 초기화합니다.
/// </summary>
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);
}
}
/// <summary>
/// 원본과 대상 아바타의 각 본 간 회전 오프셋을 계산하여 저장합니다.
/// </summary>
private void CalculateRotationOffsets(bool isIPose = false)
{
if (sourceAnimator == null || targetAnimator == null)
{
Debug.LogError("소스 또는 타겟 Animator가 설정되지 않았습니다.");
return;
}
// Dictionary가 null이면 초기화
if (rotationOffsets == null)
{
rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>();
}
// 모든 본에 대해 오프셋 계산 (기본 몸체 본 + 손가락 본 + 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);
}
}
}
}
/// <summary>
/// 모션 필터를 초기화합니다.
/// </summary>
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;
}
}
}
/// <summary>
/// 현재 설정을 JSON 파일로 저장합니다.
/// </summary>
public void SaveSettings()
{
if (targetAnimator == null)
{
Debug.LogWarning("타겟 아바타가 설정되지 않았습니다.");
return;
}
var offsetCache = new List<RotationOffsetData>();
foreach (var kvp in rotationOffsets)
{
offsetCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value));
}
// Mingle 캘리브레이션 데이터 캐싱
var fingerOpenCache = new List<RotationOffsetData>();
foreach (var kvp in fingerOpenRotations)
{
fingerOpenCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value));
}
var fingerCloseCache = new List<RotationOffsetData>();
foreach (var kvp in fingerCloseRotations)
{
fingerCloseCache.Add(new RotationOffsetData((int)kvp.Key, kvp.Value));
}
// 소스 머슬 캘리브레이션 데이터 캐싱
var muscleCalibrationCache = new List<MuscleCalibrationData>();
foreach (var kvp in sourceMuscleCalibration)
{
muscleCalibrationCache.Add(new MuscleCalibrationData(kvp.Key, kvp.Value.open, kvp.Value.close));
}
// LimbWeightController에서 의자 높이 오프셋 가져오기
var limbController = GetComponent<LimbWeightController>();
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}");
}
}
/// <summary>
/// JSON 파일에서 설정을 로드합니다.
/// </summary>
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<RetargetingSettings>(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<LimbWeightController>();
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);
}
/// <summary>
/// 캐시된 회전 오프셋을 로드하거나 새로 계산합니다.
/// </summary>
private void InitializeRotationOffsets()
{
string filePath = GetSettingsFilePath();
bool useCache = false;
// 캐시된 데이터가 있는지 확인
if (File.Exists(filePath))
{
try
{
string json = File.ReadAllText(filePath);
var settings = JsonUtility.FromJson<RetargetingSettings>(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<HumanBodyBones, Quaternion>();
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}");
}
}
/// <summary>
/// 손가락 본들의 회전 오프셋을 계산합니다.
/// I-포즈 캘리브레이션 시 손가락 포즈가 복원된 상태에서 호출되어야 합니다.
/// </summary>
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);
}
}
}
}
/// <summary>
/// Mingle 캘리브레이션: 손가락 펼침 상태 기록
/// 소스 아바타의 손가락을 모두 펼친 상태에서 호출
/// </summary>
public void CalibrateMingleOpen()
{
if (sourceAnimator == null)
{
Debug.LogError("소스 Animator가 설정되지 않았습니다.");
return;
}
fingerOpenRotations.Clear();
// 손가락 본들 (24~53)의 로컬 회전 기록
for (int i = 24; i <= 53; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
Transform fingerBone = sourceAnimator.GetBoneTransform(bone);
if (fingerBone != null)
{
fingerOpenRotations[bone] = fingerBone.localRotation;
}
}
// 소스 아바타의 현재 머슬 값 기록 (Open 상태) - 엄지 전체
sourcePoseHandler.GetHumanPose(ref sourcePose);
// 엄지 머슬 저장 (55-58: Left Thumb, 75-78: Right Thumb)
int[] thumbMuscles = { 55, 57, 58, 75, 77, 78 }; // Stretched 머슬만 (Spread 제외)
foreach (int muscleIndex in thumbMuscles)
{
if (!sourceMuscleCalibration.ContainsKey(muscleIndex))
{
sourceMuscleCalibration[muscleIndex] = (sourcePose.muscles[muscleIndex], 0f);
}
else
{
var current = sourceMuscleCalibration[muscleIndex];
sourceMuscleCalibration[muscleIndex] = (sourcePose.muscles[muscleIndex], current.close);
}
}
Debug.Log($"Mingle 펼침 캘리브레이션 완료: {fingerOpenRotations.Count}개 본, 엄지 머슬 값 기록");
CheckMingleCalibrationComplete();
}
/// <summary>
/// Mingle 캘리브레이션: 손가락 모음 상태 기록
/// 소스 아바타의 손가락을 모두 모은(주먹 쥔) 상태에서 호출
/// </summary>
public void CalibrateMingleClose()
{
if (sourceAnimator == null)
{
Debug.LogError("소스 Animator가 설정되지 않았습니다.");
return;
}
fingerCloseRotations.Clear();
// 손가락 본들 (24~53)의 로컬 회전 기록
for (int i = 24; i <= 53; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
Transform fingerBone = sourceAnimator.GetBoneTransform(bone);
if (fingerBone != null)
{
fingerCloseRotations[bone] = fingerBone.localRotation;
}
}
// 소스 아바타의 현재 머슬 값 기록 (Close 상태) - 엄지 전체
sourcePoseHandler.GetHumanPose(ref sourcePose);
// 엄지 머슬 저장 (55-58: Left Thumb, 75-78: Right Thumb)
int[] thumbMuscles = { 55, 57, 58, 75, 77, 78 }; // Stretched 머슬만 (Spread 제외)
foreach (int muscleIndex in thumbMuscles)
{
if (!sourceMuscleCalibration.ContainsKey(muscleIndex))
{
sourceMuscleCalibration[muscleIndex] = (0f, sourcePose.muscles[muscleIndex]);
}
else
{
var current = sourceMuscleCalibration[muscleIndex];
sourceMuscleCalibration[muscleIndex] = (current.open, sourcePose.muscles[muscleIndex]);
}
}
Debug.Log($"Mingle 모음 캘리브레이션 완료: {fingerCloseRotations.Count}개 본, 엄지 머슬 값 기록");
CheckMingleCalibrationComplete();
}
/// <summary>
/// Mingle 캘리브레이션 완료 여부 확인 및 엄지 첫마디 각도 계산
/// </summary>
private void CheckMingleCalibrationComplete()
{
isMingleCalibrated = fingerOpenRotations.Count > 0 && fingerCloseRotations.Count > 0;
if (isMingleCalibrated)
{
// 엄지 첫마디(Proximal)만 Open/Close 쿼터니언 각도 미리 계산
thumbOpenToCloseAngles.Clear();
int[] thumbProximalIndices = { 24, 39 }; // 왼손, 오른손 엄지 첫마디만
foreach (int i in thumbProximalIndices)
{
HumanBodyBones bone = (HumanBodyBones)i;
if (fingerOpenRotations.TryGetValue(bone, out Quaternion openRot) &&
fingerCloseRotations.TryGetValue(bone, out Quaternion closeRot))
{
thumbOpenToCloseAngles[bone] = Quaternion.Angle(openRot, closeRot);
}
}
Debug.Log("Mingle 캘리브레이션이 완료되었습니다. Mingle 모드 사용 가능.");
SaveSettings();
}
}
/// <summary>
/// 자동 캘리브레이션 시작
/// 3초 후 펼침 기록, 3초 후 모음 기록을 자동으로 진행
/// </summary>
public void StartAutoCalibration()
{
if (isAutoCalibrating)
{
Debug.LogWarning("이미 자동 캘리브레이션이 진행 중입니다.");
return;
}
if (sourceAnimator == null)
{
Debug.LogError("소스 Animator가 설정되지 않았습니다.");
return;
}
autoCalibrationCoroutine = StartCoroutine(AutoCalibrationCoroutine());
}
/// <summary>
/// 자동 캘리브레이션 중지
/// </summary>
public void StopAutoCalibration()
{
if (autoCalibrationCoroutine != null)
{
StopCoroutine(autoCalibrationCoroutine);
autoCalibrationCoroutine = null;
}
isAutoCalibrating = false;
autoCalibrationStatus = "캘리브레이션 취소됨";
autoCalibrationTimeRemaining = 0f;
Debug.Log("자동 캘리브레이션이 취소되었습니다.");
}
/// <summary>
/// 자동 캘리브레이션 코루틴
/// </summary>
private IEnumerator AutoCalibrationCoroutine()
{
isAutoCalibrating = true;
// 1단계: 3초 대기 후 펼침 기록
autoCalibrationStatus = "손가락을 펼쳐주세요...";
for (float t = 3f; t > 0; t -= Time.deltaTime)
{
autoCalibrationTimeRemaining = t;
yield return null;
}
CalibrateMingleOpen();
autoCalibrationStatus = "펼침 기록 완료!";
yield return new WaitForSeconds(0.5f);
// 2단계: 3초 대기 후 모음 기록
autoCalibrationStatus = "손가락을 모아주세요...";
for (float t = 3f; t > 0; t -= Time.deltaTime)
{
autoCalibrationTimeRemaining = t;
yield return null;
}
CalibrateMingleClose();
autoCalibrationStatus = "모음 기록 완료!";
yield return new WaitForSeconds(0.5f);
// 완료
isAutoCalibrating = false;
autoCalibrationStatus = "자동 캘리브레이션 완료!";
autoCalibrationTimeRemaining = 0f;
autoCalibrationCoroutine = null;
Debug.Log("자동 캘리브레이션이 완료되었습니다.");
}
/// <summary>
/// 현재 손가락 회전값을 펼침/모음 범위 내에서 -1~1로 정규화
/// Unity Muscle 시스템: -1 = Curl(모음), 1 = Stretch(펼침)
/// 소스 아바타 로컬 회전: 모든 손가락 X = Spread, Z = 굽힘
/// </summary>
/// <param name="bone">손가락 본</param>
/// <returns>(굽힘 정규화 값, Spread 정규화 값) - Z축 기준 굽힘, X축 기준 Spread</returns>
private (float stretched, float spread) GetNormalizedFingerValue(HumanBodyBones bone)
{
Transform fingerBone = sourceAnimator.GetBoneTransform(bone);
if (fingerBone == null) return (0f, 0f);
if (!fingerOpenRotations.TryGetValue(bone, out Quaternion openRot) ||
!fingerCloseRotations.TryGetValue(bone, out Quaternion closeRot))
{
return (0f, 0f);
}
Quaternion currentRot = fingerBone.localRotation;
float spreadValue = 0f;
int boneIndex = (int)bone;
bool isThumb = thumbBoneIndices.Contains(boneIndex);
// 굽힘(Stretched) 정규화
float stretchedValue = 0f;
bool isThumbProximal = boneIndex == 24 || boneIndex == 39;
if (isThumb)
{
// 엄지 전체: 소스 아바타 머슬에서 Stretched 값 직접 가져오기
if (fingerBoneToMuscleIndex.TryGetValue(boneIndex, out var muscleIndices))
{
int stretchedMuscleIndex = muscleIndices.stretchedMuscle;
// 소스 아바타의 현재 머슬 값 가져오기
sourcePoseHandler.GetHumanPose(ref sourcePose);
float currentMuscle = sourcePose.muscles[stretchedMuscleIndex];
// 캘리브레이션 데이터가 있으면 정규화
if (sourceMuscleCalibration.TryGetValue(stretchedMuscleIndex, out var calibration))
{
float openMuscle = calibration.open;
float closeMuscle = calibration.close;
float range = openMuscle - closeMuscle;
if (Mathf.Abs(range) > 0.01f)
{
// Open 상태 = 0.5, Close 상태 = -1 (엄지 범위)
float t = Mathf.InverseLerp(openMuscle, closeMuscle, currentMuscle);
stretchedValue = Mathf.Lerp(0.5f, -1f, t);
}
else
{
stretchedValue = currentMuscle;
}
}
else
{
stretchedValue = currentMuscle;
}
}
}
else
{
// 다른 손가락: 로테이션 Z축 기반 정규화
Vector3 openEuler = openRot.eulerAngles;
Vector3 closeEuler = closeRot.eulerAngles;
Vector3 currentEuler = currentRot.eulerAngles;
float openZ = NormalizeAngle(openEuler.z);
float closeZ = NormalizeAngle(closeEuler.z);
float currentZ = NormalizeAngle(currentEuler.z);
float totalRangeZ = closeZ - openZ;
if (Mathf.Abs(totalRangeZ) > 0.1f)
{
float t = Mathf.InverseLerp(openZ, closeZ, currentZ);
// Unity Muscle: 1 = Stretch(펼침), -1 = Curl(모음)
stretchedValue = Mathf.Lerp(1f, -1f, t);
}
}
// Spread 계산
// 엄지 Proximal(24, 39)의 Spread는 Intermediate(25, 40)의 Y축에서 가져옴 (장갑 특수성)
if (isThumbProximal)
{
// 엄지 첫마디: 두번째 마디(Intermediate)의 Y축으로 Spread 계산 (장갑 특수성)
HumanBodyBones intermediateBone = (boneIndex == 24) ? HumanBodyBones.LeftThumbIntermediate : HumanBodyBones.RightThumbIntermediate;
Transform intermediateBoneTransform = sourceAnimator.GetBoneTransform(intermediateBone);
if (intermediateBoneTransform != null &&
fingerOpenRotations.TryGetValue(intermediateBone, out Quaternion intOpenRot) &&
fingerCloseRotations.TryGetValue(intermediateBone, out Quaternion intCloseRot))
{
// Intermediate 본의 캘리브레이션된 Y축 범위 사용
float openY = NormalizeAngle(intOpenRot.eulerAngles.y);
float closeY = NormalizeAngle(intCloseRot.eulerAngles.y);
// 현재 Y축 값
Vector3 intCurrentEuler = intermediateBoneTransform.localRotation.eulerAngles;
float currentY = NormalizeAngle(intCurrentEuler.y);
float rangeY = openY - closeY;
if (Mathf.Abs(rangeY) > 0.1f)
{
// Open = 벌림(0.5), Close = 닫힘(-0.5)로 매핑
float t = Mathf.InverseLerp(openY, closeY, currentY);
spreadValue = Mathf.Lerp(0.5f, -0.5f, t);
// 오른손(39)은 반전
if (boneIndex == 39)
{
spreadValue = -spreadValue;
}
}
}
}
else if (isThumb)
{
// 엄지 다른 마디: Spread 없음
spreadValue = 0f;
}
else
{
// 다른 손가락: X축으로 Spread 계산
Vector3 currentEuler = currentRot.eulerAngles;
Vector3 openEuler = openRot.eulerAngles;
Vector3 closeEuler = closeRot.eulerAngles;
float currentX = NormalizeAngle(currentEuler.x);
float openX = NormalizeAngle(openEuler.x);
float closeX = NormalizeAngle(closeEuler.x);
float totalRangeX = closeX - openX;
if (Mathf.Abs(totalRangeX) > 0.1f)
{
float t = Mathf.InverseLerp(openX, closeX, currentX);
spreadValue = Mathf.Lerp(0.5f, -0.5f, t);
}
}
return (stretchedValue, spreadValue);
}
/// <summary>
/// 각도를 -180 ~ 180 범위로 정규화
/// </summary>
private float NormalizeAngle(float angle)
{
while (angle > 180f) angle -= 360f;
while (angle < -180f) angle += 360f;
return angle;
}
private void InitializeIKJoints()
{
// IK 루트 찾기
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
/// <summary>
/// 매 프레임마다 원본 아바타의 포즈와 손가락 움직임을 대상 아바타에 리타게팅하고, IK 타타겟을 업데이트합니다.
/// </summary>
void Update()
{
// 포즈 복사 및 동기화
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;
}
}
/// <summary>
/// 머슬 데이터를 사용하여 손가락 포즈를 복제합니다.
/// SetHumanPose가 모든 본에 영향을 미치므로, 손가락을 제외한 모든 본의 Transform을 저장하고 복원합니다.
/// </summary>
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<float>();
// 초기값으로 버퍼를 채움
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;
}
/// <summary>
/// 힙전값을 사용하여 손가락 포즈를 복제합니다.
/// </summary>
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;
}
}
}
/// <summary>
/// Mingle 방식으로 손가락 포즈를 복제합니다.
/// 소스 아바타의 손가락 회전을 캘리브레이션된 범위 내에서 정규화하여 머슬에 적용합니다.
/// 엄지는 제외하고 처리합니다.
/// </summary>
private void CopyFingerPoseByMingle()
{
if (!isMingleCalibrated)
{
// 캘리브레이션이 안 되어 있으면 기본 머슬 방식으로 폴백
CopyFingerPoseByMuscle();
return;
}
if (sourcePoseHandler == null || targetPoseHandler == null)
return;
// 1. 손가락을 제외한 모든 본의 위치/회전 저장
savedBoneTransforms.Clear();
for (int i = 0; i < nonFingerBones.Length; i++)
{
Transform bone = targetAnimator.GetBoneTransform(nonFingerBones[i]);
if (bone != null)
{
savedBoneTransforms[nonFingerBones[i]] = (bone.position, bone.rotation);
}
}
// 2. 타겟 포즈 가져오기
targetPoseHandler.GetHumanPose(ref targetPose);
// 3. 각 손가락 본에 대해 정규화된 값을 머슬에 적용
for (int boneIndex = 24; boneIndex <= 53; boneIndex++)
{
HumanBodyBones bone = (HumanBodyBones)boneIndex;
// 정규화된 손가락 값 계산 (Z축: 굽힘, X/Y축: Spread)
var (stretchedValue, spreadValue) = GetNormalizedFingerValue(bone);
// 해당 본에 매핑된 머슬 인덱스 찾기
if (fingerBoneToMuscleIndex.TryGetValue(boneIndex, out var muscleIndices))
{
// Stretched 머슬 적용
if (muscleIndices.stretchedMuscle >= 0)
{
float targetValue = stretchedValue;
float currentValue = targetPose.muscles[muscleIndices.stretchedMuscle];
// 모션 필터 적용
if (useMotionFilter)
{
targetValue = ApplyFilter(targetValue, boneIndex - 24);
}
// 러프 모션 적용
if (useFingerRoughMotion)
{
float smoothSpeed = 50f - (fingerRoughness * 49f);
targetValue = Mathf.Lerp(currentValue, targetValue, smoothSpeed * Time.deltaTime);
}
targetPose.muscles[muscleIndices.stretchedMuscle] = targetValue;
}
// Spread 머슬 적용 (첫마디만 해당)
if (muscleIndices.spreadMuscle >= 0)
{
float targetValue = spreadValue;
float currentValue = targetPose.muscles[muscleIndices.spreadMuscle];
// 러프 모션 적용
if (useFingerRoughMotion)
{
float smoothSpeed = 50f - (fingerRoughness * 49f);
targetValue = Mathf.Lerp(currentValue, targetValue, smoothSpeed * Time.deltaTime);
}
targetPose.muscles[muscleIndices.spreadMuscle] = targetValue;
}
}
}
// 4. 머슬 포즈 적용
targetPoseHandler.SetHumanPose(ref targetPose);
// 5. 손가락을 제외한 모든 본의 위치/회전 복원
foreach (var kvp in savedBoneTransforms)
{
Transform bone = targetAnimator.GetBoneTransform(kvp.Key);
if (bone != null)
{
bone.SetPositionAndRotation(kvp.Value.position, kvp.Value.rotation);
}
}
}
#endregion
#region
/// <summary>
/// 원본 아바타의 포즈를 대상 아바타에 오프셋을 적용하여 복사합니다.
/// 힙 위치와 회전을 동기화하고, 나머지 본의 회전을 오프셋을 적용하여 동기화합니다.
/// </summary>
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);
}
/// <summary>
/// 힙을 제외한 모든 본의 회전을 오프셋을 적용하여 동기화합니다.
/// </summary>
/// <param name="skipBone">동기화에서 제외할 본</param>
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
/// <summary>
/// IK 타겟(끝과 중간)을 생성하고 FullBodyInverseKinematics 컴포넌트를 설정합니다.
/// </summary>
private void CreateIKTargets()
{
// IK 컴포넌트 가져오기 또는 새로 추가
ikComponent = GetComponent<FullBodyInverseKinematics_RND>();
if (ikComponent == null)
ikComponent = gameObject.AddComponent<FullBodyInverseKinematics_RND>();
// 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;
}
/// <summary>
/// IK 타겟 오브젝트를 생성하고 기본 Gizmo 설정을 적용합니다.
/// </summary>
/// <param name="name">타겟 오브젝트의 이름</param>
/// <param name="bone">타겟 오브젝트를 부모로 설정할 본</param>
/// <returns>생성된 타겟 게임 오브젝트</returns>
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<IKTargetGizmo>();
gizmo.gizmoSize = 0.05f;
gizmo.gizmoColor = UnityEngine.Color.yellow;
target.transform.parent = parent.transform;
return target;
}
#endregion
#region IK
/// <summary>
/// 매 프레임마다 IK 중간 타겟의 위치를 대상 아바타의 본 위치로 업데이트합니다.
/// </summary>
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);
}
/// <summary>
/// 손과 발끝 타겟의 위치와 회전을 업데이트합니다.
/// </summary>
/// <param name="endTarget">업데이트할 끝 타겟의 Transform</param>
/// <param name="endBone">대상 본</param>
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-
/// <summary>
/// 지정된 Animator의 포즈를 T-포즈로 설정합니다.
/// </summary>
/// <param name="animator">T-포즈를 설정할 Animator</param>
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>(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>(HumanPoseClip.IPoseResourcePath);
if (humanPoseClip != null)
{
var pose = humanPoseClip.GetPose();
HumanPoseTransfer.SetPose(avatar, transform, pose);
}
else
{
Debug.LogWarning("I-Pose 데이터가 존재하지 않습니다.");
}
}
#endregion
#region
/// <summary>
/// 오브브젝트가 파괴될 때 HumanPoseHandler를 정리합니다.
/// </summary>
void OnDestroy()
{
sourcePoseHandler?.Dispose();
targetPoseHandler?.Dispose();
}
/// <summary>
/// Unity가 종료될 때 호출되되는 메서드
/// </summary>
private void OnApplicationQuit()
{
SaveSettings();
}
#endregion
/// <summary>
/// 캐시된 설정 파일이 존재하는지 확인합니다.
/// </summary>
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;
}
}
}