diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs index 9cab1d013..05183b465 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs @@ -109,8 +109,9 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour // OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1)) private Dictionary m_optiNameTransformCache = new Dictionary(); - // 필터 적용 전 raw 월드 위치 (IK 타겟용 — 접지력 보존) + // 필터 적용 전 raw 월드 위치/회전 (IK 타겟용 — 접지력 보존) private Dictionary m_rawWorldPositions = new Dictionary(); + private Dictionary m_rawWorldRotations = new Dictionary(); // raw 위치를 캡처할 IK 포인트 본 목록 private static readonly HumanBodyBones[] k_IKPointBones = new HumanBodyBones[] @@ -373,12 +374,15 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour mapping.cachedTransform.localPosition = rawPos; } - // Raw 상태에서 IK 포인트 월드 위치 캡처 + // Raw 상태에서 IK 포인트 월드 위치/회전 캡처 foreach (var ikBone in k_IKPointBones) { Transform t = GetBoneTransform(ikBone); if (t != null) + { m_rawWorldPositions[ikBone] = t.position; + m_rawWorldRotations[ikBone] = t.rotation; + } } // Pass 2: 필터 적용된 데이터로 덮어쓰기 @@ -419,20 +423,23 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour mapping.cachedTransform.localPosition = finalPos; } - // 필터 없으면 현재 Transform 위치가 곧 raw + // 필터 없으면 현재 Transform 위치/회전이 곧 raw foreach (var ikBone in k_IKPointBones) { Transform t = GetBoneTransform(ikBone); if (t != null) + { m_rawWorldPositions[ikBone] = t.position; + m_rawWorldRotations[ikBone] = t.rotation; + } } } // ── 어깨 증폭 + 상완 역보정 (1.0이면 스킵) ── if (Mathf.Abs(shoulderAmplify - 1f) > 0.001f) { - AmplifyShoulderWithCompensation("LShoulder", "LUArm"); - AmplifyShoulderWithCompensation("RShoulder", "RUArm"); + AmplifyShoulderWithCompensation("LShoulder", "LUArm", true); + AmplifyShoulderWithCompensation("RShoulder", "RUArm", false); } } @@ -440,17 +447,35 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour /// /// 어깨 회전을 rest pose 대비 증폭하고, 상완에서 추가 회전분을 상쇄하여 손 위치를 보존합니다. /// - private void AmplifyShoulderWithCompensation(string shoulderName, string upperArmName) + private float m_leftShoulderBlend = 0f; + private float m_rightShoulderBlend = 0f; + + /// true = 왼쪽(Z < 0일 때 증폭), false = 오른쪽(Z > 0일 때 증폭) + private void AmplifyShoulderWithCompensation(string shoulderName, string upperArmName, bool isLeft) { Transform shoulder = GetMappedTransform(shoulderName); Transform upperArm = GetMappedTransform(upperArmName); if (shoulder == null || upperArm == null) return; - // rest pose 대비 델타 추출 → 증폭 + // rest pose 대비 델타 추출 Quaternion restRot = GetRestLocalRotation(shoulderName); Quaternion currentRot = shoulder.localRotation; Quaternion delta = Quaternion.Inverse(restRot) * currentRot; - Quaternion amplifiedDelta = Quaternion.SlerpUnclamped(Quaternion.identity, delta, shoulderAmplify); + + // 어깨가 올라가는 방향일 때만 증폭 (왼쪽: Z < 0, 오른쪽: Z > 0) + Vector3 deltaEuler = delta.eulerAngles; + float z = deltaEuler.z > 180f ? deltaEuler.z - 360f : deltaEuler.z; + float targetBlend = isLeft ? Mathf.Clamp01(-z / 5f) : Mathf.Clamp01(z / 5f); + + // 부드러운 블렌딩 (급격한 on/off 방지) + ref float blend = ref (isLeft ? ref m_leftShoulderBlend : ref m_rightShoulderBlend); + blend = Mathf.Lerp(blend, targetBlend, 10f * Time.deltaTime); + + if (blend < 0.001f) return; + + // 증폭량: 1(원본) ~ shoulderAmplify 사이를 blend로 보간 + float effectiveAmplify = Mathf.Lerp(1f, shoulderAmplify, blend); + Quaternion amplifiedDelta = Quaternion.SlerpUnclamped(Quaternion.identity, delta, effectiveAmplify); Quaternion amplifiedRot = restRot * amplifiedDelta; // 추가된 회전량 @@ -748,6 +773,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour return m_rawWorldPositions.TryGetValue(bone, out position); } + /// + /// 1€ 필터 적용 전의 raw 월드 회전을 반환합니다. + /// + public bool TryGetRawWorldRotation(HumanBodyBones bone, out Quaternion rotation) + { + return m_rawWorldRotations.TryGetValue(bone, out rotation); + } + /// /// HumanBodyBones enum으로 OptiTrack 매핑된 Transform 반환 /// (Humanoid 호환 레이어 — sourceAnimator.GetBoneTransform() 대체) diff --git a/Assets/Resources/Settings/PropDatabase.asset b/Assets/Resources/Settings/PropDatabase.asset index 0f8bf325c..9096b23bf 100644 --- a/Assets/Resources/Settings/PropDatabase.asset +++ b/Assets/Resources/Settings/PropDatabase.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4014c67ef1b4fc1c2ff5b6cd44fec1d34675e202db200f59e2b933c84df4ba37 -size 17078 +oid sha256:322f7dd83c302215439fb25c50fccee8dd0a8217c2f2387e728a034b94c88e1e +size 17429 diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index bccfa7c7c..745b815b1 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -1257,12 +1257,25 @@ namespace KindRetargeting { // 1€ 필터 적용 전 raw 위치 사용 (접지력 보존) // raw가 없으면 필터된 Transform.position fallback + // 1€ 필터 적용 전 raw 위치/회전 사용 (접지력 보존) Vector3 targetPosition; if (optitrackSource != null && optitrackSource.TryGetRawWorldPosition(endBone, out Vector3 rawPos)) targetPosition = rawPos; else targetPosition = sourceBone.position; - Quaternion targetRotation = targetBone.rotation; + + // raw 회전 + 리타게팅 오프셋 적용 (필터 스무딩 없는 회전) + Quaternion targetRotation; + if (optitrackSource != null + && optitrackSource.TryGetRawWorldRotation(endBone, out Quaternion rawRot) + && rotationOffsets.TryGetValue(endBone, out Quaternion endOffset)) + { + targetRotation = rawRot * endOffset; + } + else + { + targetRotation = targetBone.rotation; + } // 발 본인 경우 오프셋 적용 if (endBone == HumanBodyBones.LeftFoot || endBone == HumanBodyBones.RightFoot)