From dbf2ac91f3797ebd53cffbd8c3ca8e9f9e42c734 Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Sun, 22 Mar 2026 17:20:12 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EC=98=B5=ED=8B=B0=20=EB=AF=B8?= =?UTF-8?q?=EB=9F=AC=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=EC=9D=84=20?= =?UTF-8?q?=EC=9B=94=EB=93=9C=20=EA=B3=B5=EA=B0=84=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9E=AC=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FillBoneSnapshot의 스켈레톤 미러 로직 제거 (로컬 좌표 + 좌우 본 교체 방식은 부모 본 좌표계 차이로 꼬임 발생) - OptitrackSkeletonAnimator_Mingle에 ApplyWorldSpaceMirror() 추가 - 본 데이터 적용 후 월드 공간에서 전체 포즈 캐시 → 미러 적용 - L/R 본은 상대방 월드 포즈로 교체 + YZ 평면 반사 - 대칭 본은 자기 포즈에 YZ 평면 반사 적용 - 로컬 축 컨벤션 독립적 → 어떤 스켈레톤에서도 정확히 동작 - RigidBody 미러는 기존 GetLatestRigidBodyState() 방식 유지 Co-Authored-By: Claude Sonnet 4.6 --- .../OptitrackSkeletonAnimator_Mingle.cs | 66 +++++++++++++++++++ .../Scripts/OptitrackStreamingClient.cs | 20 ++---- 2 files changed, 70 insertions(+), 16 deletions(-) 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 c05a24ea5..449e5d872 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,11 @@ 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(); private List m_neckChainCache = new List(); @@ -282,6 +287,66 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour mapping.cachedTransform.localPosition = finalPos; } } + + // ── 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; + } } /// @@ -351,6 +416,7 @@ 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 15ec18172..5fcea6aed 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -934,23 +934,11 @@ public class OptitrackStreamingClient : MonoBehaviour posOut.Clear(); oriOut.Clear(); - if ( MirrorMode ) + // 스켈레톤 미러는 OptitrackSkeletonAnimator_Mingle에서 월드 공간 기준으로 처리 + foreach ( var kvp in state.BonePoses ) { - Dictionary mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId ); - foreach ( var kvp in state.BonePoses ) - { - Int32 targetId = mirrorMap != null && mirrorMap.TryGetValue( kvp.Key, out Int32 mid ) ? mid : kvp.Key; - posOut[targetId] = MirrorPosition( 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; - } + posOut[kvp.Key] = kvp.Value.Position; + oriOut[kvp.Key] = kvp.Value.Orientation; } return true; }