1154 lines
44 KiB
C#
1154 lines
44 KiB
C#
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))]
|
|
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<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("바닥 높이 조정")]
|
|
[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 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<RotationOffsetData> rotationOffsetCache;
|
|
public float initialHipsHeight;
|
|
public float avatarScale;
|
|
}
|
|
|
|
// 각 손가락 관절별로 필터 버퍼를 관리하는 Dictionary 추가
|
|
private Dictionary<int, Queue<float>> fingerFilterBuffers = new Dictionary<int, Queue<float>>();
|
|
|
|
// IK 조인트 싱을 위한 구조체
|
|
private struct IKJoints
|
|
{
|
|
public Transform leftLowerLeg;
|
|
public Transform rightLowerLeg;
|
|
public Transform leftLowerArm;
|
|
public Transform rightLowerArm;
|
|
}
|
|
private IKJoints sourceIKJoints;
|
|
|
|
#endregion
|
|
|
|
#region 초기화
|
|
|
|
/// <summary>
|
|
/// 초기화 메서드. T-포즈 설정, IK 타겟 생성, HumanPoseHandler 초기화 및 회전 오프셋 계산을 수행합니다.
|
|
/// </summary>
|
|
void Start()
|
|
{
|
|
targetAnimator = GetComponent<Animator>();
|
|
// 설정 로드
|
|
LoadSettings();
|
|
|
|
// IK 컴포넌트 참조 가져오기 변경
|
|
ikComponent = GetComponent<FullBodyInverseKinematics_RND>();
|
|
|
|
// 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();
|
|
}
|
|
|
|
/// <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));
|
|
}
|
|
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
|
|
// 설정 적용
|
|
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);
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
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 업데이트
|
|
|
|
/// <summary>
|
|
/// 매 프레임마다 원본 아바타의 포즈와 손가락 움직임을 대상 아바타에 리타게팅하고, IK 타타겟을 업데이트합니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// 머슬 데이터를 사용하여 손가락 포즈를 복제합니다.
|
|
/// </summary>
|
|
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<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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 포즈 동기화
|
|
|
|
/// <summary>
|
|
/// 원본 아바타의 포즈를 대상 아바타에 오프셋을 적용하여 복사합니다.
|
|
/// 힙 위치와 회전을 동기화하고, 나머지 본의 회전을 오프셋을 적용하여 동기화합니다.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
// 최종 위치와 회전 적용
|
|
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 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>(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>(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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|