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 449e5d872..006990906 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs @@ -67,10 +67,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour // OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1)) private Dictionary m_optiNameTransformCache = new Dictionary(); - // MirrorMode용: boneId → mirrorBoneId 매핑 (null이면 미구축) - private Dictionary m_mirrorBoneIdMap; - // MirrorMode용: 월드 포즈 캐시 (매 프레임 재사용, GC 없음) - private Dictionary m_mirrorWorldPoseCache = new Dictionary(); // 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지) private List m_spineChainCache = new List(); @@ -219,6 +215,8 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour StartCoroutine(CheckSkeletonConnectionPeriodically()); } + private bool m_lastMirrorMode = false; + void Update() { if (StreamingClient == null) @@ -227,6 +225,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour return; } + // MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지) + bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode; + if (currentMirrorMode != m_lastMirrorMode) + { + m_filterStates.Clear(); + m_lastMirrorMode = currentMirrorMode; + } + // 스켈레톤 이름 변경 감지 if (previousSkeletonName != SkeletonAssetName) { @@ -288,65 +294,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour } } - // ── MirrorMode: 본 데이터 적용 후 월드 공간 기준 좌우 반전 ───────────────── - if (StreamingClient != null && StreamingClient.MirrorMode) - ApplyWorldSpaceMirror(); - } - - /// - /// 전체 본을 월드 공간에서 좌우 반전합니다. - /// 로컬 축 컨벤션에 독립적이므로 어떤 스켈레톤 구조에서도 올바르게 동작합니다. - /// - private void ApplyWorldSpaceMirror() - { - if (m_mirrorBoneIdMap == null) - BuildMirrorBoneIdMapLocal(); - - // Step 1: 현재 월드 포즈 전체 캐시 (수정 전 상태 보존) - m_mirrorWorldPoseCache.Clear(); - foreach (var bone in m_skeletonDef.Bones) - { - if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int idx)) continue; - var t = boneMappings[idx].cachedTransform; - if (t == null) continue; - m_mirrorWorldPoseCache[bone.Id] = (t.position, t.rotation); - } - - // Step 2: 미러된 월드 포즈 적용 - foreach (var bone in m_skeletonDef.Bones) - { - if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int idx)) continue; - var mapping = boneMappings[idx]; - if (!mapping.isMapped || mapping.cachedTransform == null) continue; - if (!m_mirrorBoneIdMap.TryGetValue(bone.Id, out Int32 mirrorId)) continue; - if (!m_mirrorWorldPoseCache.TryGetValue(mirrorId, out var src)) continue; - - // 월드 X 반전 (YZ 평면 반사), 회전은 Y·Z 성분 부호 반전 - if (mapping.applyPosition) - mapping.cachedTransform.position = new Vector3(-src.pos.x, src.pos.y, src.pos.z); - if (mapping.applyRotation) - mapping.cachedTransform.rotation = new Quaternion(src.rot.x, -src.rot.y, -src.rot.z, src.rot.w); - } - } - - /// 본 이름 기반 L/R 미러 ID 맵 구축 (월드 공간 미러용). - private void BuildMirrorBoneIdMapLocal() - { - m_mirrorBoneIdMap = new Dictionary(); - var nameToId = new Dictionary(); - foreach (var bone in m_skeletonDef.Bones) - nameToId[bone.Name] = bone.Id; - - foreach (var bone in m_skeletonDef.Bones) - { - string n = bone.Name; - string mirrorName = null; - if (n.Length >= 2 && n[0] == 'L' && char.IsUpper(n[1])) mirrorName = "R" + n.Substring(1); - else if (n.Length >= 2 && n[0] == 'R' && char.IsUpper(n[1])) mirrorName = "L" + n.Substring(1); - - m_mirrorBoneIdMap[bone.Id] = (mirrorName != null && nameToId.TryGetValue(mirrorName, out Int32 mid)) - ? mid : bone.Id; - } } /// @@ -416,7 +363,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour m_boneIdToMappingIndex.Clear(); m_filterStates.Clear(); m_hasLastFrameTimestamp = false; - m_mirrorBoneIdMap = null; // 스켈레톤 재구성 시 미러 맵 초기화 if (m_skeletonDef == null) return; var nameToIdx = new Dictionary(); diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs index 5fcea6aed..5757b5415 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -934,11 +934,34 @@ public class OptitrackStreamingClient : MonoBehaviour posOut.Clear(); oriOut.Clear(); - // 스켈레톤 미러는 OptitrackSkeletonAnimator_Mingle에서 월드 공간 기준으로 처리 - foreach ( var kvp in state.BonePoses ) + if ( MirrorMode ) { - posOut[kvp.Key] = kvp.Value.Position; - oriOut[kvp.Key] = kvp.Value.Orientation; + Dictionary mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId ); + foreach ( var kvp in state.BonePoses ) + { + Int32 targetId = mirrorMap != null && mirrorMap.TryGetValue( kvp.Key, out Int32 mid ) ? mid : kvp.Key; + + if ( targetId == kvp.Key ) + { + // 대칭 본 (Hip, 척추, 목, 머리 등): YZ 평면 반사 적용 + posOut[kvp.Key] = MirrorPosition( kvp.Value.Position ); + oriOut[kvp.Key] = MirrorOrientation( kvp.Value.Orientation ); + } + else + { + // L/R 쌍 본: 위치는 자기 자리 유지, 회전은 YZ 반사 후 미러 본으로 스왑 + posOut[kvp.Key] = kvp.Value.Position; + oriOut[targetId] = MirrorOrientation( kvp.Value.Orientation ); + } + } + } + else + { + foreach ( var kvp in state.BonePoses ) + { + posOut[kvp.Key] = kvp.Value.Position; + oriOut[kvp.Key] = kvp.Value.Orientation; + } } return true; } @@ -976,8 +999,16 @@ public class OptitrackStreamingClient : MonoBehaviour var map = new Dictionary( skelDef.Bones.Count ); foreach ( var bone in skelDef.Bones ) { - string mirrorName = GetMirrorBoneName( bone.Name ); - if ( mirrorName != null && nameToId.TryGetValue( mirrorName, out Int32 mirrorId ) ) + // "SkeletonName_BoneName" 형식 지원: "_" 뒤의 짧은 이름에서 L/R 접두사 처리 + string fullName = bone.Name; + int sep = fullName.IndexOf( '_' ); + string prefix = sep >= 0 ? fullName.Substring( 0, sep + 1 ) : ""; // "Skeleton1_" + string shortName = sep >= 0 ? fullName.Substring( sep + 1 ) : fullName; // "LUArm" + + string mirrorShort = GetMirrorBoneName( shortName ); + string mirrorFull = mirrorShort != null ? prefix + mirrorShort : null; + + if ( mirrorFull != null && nameToId.TryGetValue( mirrorFull, out Int32 mirrorId ) ) map[bone.Id] = mirrorId; else map[bone.Id] = bone.Id; // 대칭 본은 자기 자신에 매핑 @@ -1011,6 +1042,22 @@ public class OptitrackStreamingClient : MonoBehaviour m_latestTMarkersetStates.TryGetValue(tmarkersetId, out tmarState); } + if ( MirrorMode && tmarState != null && tmarState.BonePoses != null ) + { + var mirrored = new OptitrackTMarkersetState + { + BonePoses = new Dictionary( tmarState.BonePoses.Count ), + LocalBonePoses = tmarState.LocalBonePoses, + }; + foreach ( var kvp in tmarState.BonePoses ) + mirrored.BonePoses[kvp.Key] = new OptitrackPose + { + Position = MirrorPosition( kvp.Value.Position ), + Orientation = MirrorOrientation( kvp.Value.Orientation ), + }; + tmarState = mirrored; + } + return tmarState; } @@ -1027,7 +1074,7 @@ public class OptitrackStreamingClient : MonoBehaviour { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { - Position = markerEntry.Value.Position, + Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position, Labeled = markerEntry.Value.Labeled, Size = markerEntry.Value.Size, Id = markerEntry.Value.Id @@ -1144,7 +1191,7 @@ public class OptitrackStreamingClient : MonoBehaviour { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { - Position = markerEntry.Value.Position, + Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position, Labeled = markerEntry.Value.Labeled, Size = markerEntry.Value.Size, Id = markerEntry.Value.Id