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 1571e7b75..9a99205dd 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs @@ -67,6 +67,20 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour // OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1)) private Dictionary m_optiNameTransformCache = new Dictionary(); + // 필터 적용 전 raw 월드 위치 (IK 타겟용 — 접지력 보존) + private Dictionary m_rawWorldPositions = new Dictionary(); + + // raw 위치를 캡처할 IK 포인트 본 목록 + private static readonly HumanBodyBones[] k_IKPointBones = new HumanBodyBones[] + { + HumanBodyBones.LeftFoot, + HumanBodyBones.RightFoot, + HumanBodyBones.LeftHand, + HumanBodyBones.RightHand, + HumanBodyBones.LeftToes, + HumanBodyBones.RightToes, + }; + // 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지) private List m_spineChainCache = new List(); @@ -261,36 +275,77 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour m_lastFrameTimestamp = frameTs; m_hasLastFrameTimestamp = true; - // ── 각 본 업데이트 ─────────────────────────────────────────────────────── - foreach (var bone in m_skeletonDef.Bones) + // ── Pass 1: Raw 데이터 적용 → IK 포인트 월드 위치 캡처 ────────────────── + // 필터가 활성화되어 있을 때만 two-pass, 비활성이면 single-pass + if (enableBoneFilter) { - if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx)) - continue; - - var mapping = boneMappings[mappingIdx]; - if (!mapping.isMapped || mapping.cachedTransform == null) - continue; - - if (mapping.applyRotation) + // Raw 데이터로 모든 본 업데이트 (필터 없이) + foreach (var bone in m_skeletonDef.Bones) { - if (!m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri)) + if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx)) + continue; + var mapping = boneMappings[mappingIdx]; + if (!mapping.isMapped || mapping.cachedTransform == null) continue; - if (enableBoneFilter) - finalOri = ApplyOneEuroOri(bone.Id, finalOri, m_natNetDt); - - mapping.cachedTransform.localRotation = finalOri; + if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion rawOri)) + mapping.cachedTransform.localRotation = rawOri; + if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 rawPos)) + mapping.cachedTransform.localPosition = rawPos; } - if (mapping.applyPosition) + // Raw 상태에서 IK 포인트 월드 위치 캡처 + foreach (var ikBone in k_IKPointBones) { - if (!m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos)) + Transform t = GetBoneTransform(ikBone); + if (t != null) + m_rawWorldPositions[ikBone] = t.position; + } + + // Pass 2: 필터 적용된 데이터로 덮어쓰기 + foreach (var bone in m_skeletonDef.Bones) + { + if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx)) + continue; + var mapping = boneMappings[mappingIdx]; + if (!mapping.isMapped || mapping.cachedTransform == null) continue; - if (enableBoneFilter) + if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri)) + { + finalOri = ApplyOneEuroOri(bone.Id, finalOri, m_natNetDt); + mapping.cachedTransform.localRotation = finalOri; + } + if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos)) + { finalPos = ApplyOneEuroPos(bone.Id, finalPos, m_natNetDt); + mapping.cachedTransform.localPosition = finalPos; + } + } + } + else + { + // 필터 비활성: single-pass, raw = filtered + foreach (var bone in m_skeletonDef.Bones) + { + if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx)) + continue; + var mapping = boneMappings[mappingIdx]; + if (!mapping.isMapped || mapping.cachedTransform == null) + continue; - mapping.cachedTransform.localPosition = finalPos; + if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri)) + mapping.cachedTransform.localRotation = finalOri; + if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos)) + mapping.cachedTransform.localPosition = finalPos; + } + + // 필터 없으면 현재 Transform 위치가 곧 raw + foreach (var ikBone in k_IKPointBones) + { + Transform t = GetBoneTransform(ikBone); + if (t != null) + m_rawWorldPositions[ikBone] = t.position; } } @@ -571,6 +626,16 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour { HumanBodyBones.RightLittleDistal, "RPinky3" }, }; + /// + /// 1€ 필터 적용 전의 raw 월드 위치를 반환합니다. + /// IK 타겟(발 접지 등)에 사용하면 필터 스무딩으로 인한 접지력 저하를 방지합니다. + /// 지원 본: LeftFoot, RightFoot, LeftHand, RightHand, LeftToes, RightToes + /// + public bool TryGetRawWorldPosition(HumanBodyBones bone, out Vector3 position) + { + return m_rawWorldPositions.TryGetValue(bone, out position); + } + /// /// HumanBodyBones enum으로 OptiTrack 매핑된 Transform 반환 /// (Humanoid 호환 레이어 — sourceAnimator.GetBoneTransform() 대체) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 467a1ff05..af4ede1a7 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -1268,7 +1268,13 @@ namespace KindRetargeting if (sourceBone != null && targetBone != null) { - Vector3 targetPosition = sourceBone.position; + // 1€ 필터 적용 전 raw 위치 사용 (접지력 보존) + // raw가 없으면 필터된 Transform.position fallback + Vector3 targetPosition; + if (optitrackSource != null && optitrackSource.TryGetRawWorldPosition(endBone, out Vector3 rawPos)) + targetPosition = rawPos; + else + targetPosition = sourceBone.position; Quaternion targetRotation = targetBone.rotation; // 발 본인 경우 오프셋 적용