Add: OptiTrack MirrorMode 좌우 반전 기능 구현

- OptitrackStreamingClient.FillBoneSnapshot: L/R 쌍 본은 위치 유지 + 회전 YZ반사 후 스왑, 중심 본(Hip/척추 등)은 위치 X반전 + 회전 YZ반사
- OptitrackStreamingClient.GetLatestRigidBodyState: 리짓바디 위치·회전 YZ반사 (기존)
- GetOrBuildMirrorBoneIdMap: SkeletonName_BoneName 접두사 형식 지원
- 마커/TMarkerset 마커/TMarkerset BonePoses 미러 적용
- OptitrackSkeletonAnimator_Mingle: MirrorMode 토글 시 1€ 필터 상태 자동 리셋
- 월드 공간 미러 코드(ApplyWorldSpaceMirror 등) 제거, 데이터 수신 레벨에서 처리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
qsxft258@gmail.com 2026-03-22 18:05:04 +09:00
parent dbf2ac91f3
commit 026eaf094b
2 changed files with 65 additions and 72 deletions

View File

@ -67,10 +67,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
// OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1)) // OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1))
private Dictionary<string, Transform> m_optiNameTransformCache = new Dictionary<string, Transform>(); private Dictionary<string, Transform> m_optiNameTransformCache = new Dictionary<string, Transform>();
// MirrorMode용: boneId → mirrorBoneId 매핑 (null이면 미구축)
private Dictionary<Int32, Int32> m_mirrorBoneIdMap;
// MirrorMode용: 월드 포즈 캐시 (매 프레임 재사용, GC 없음)
private Dictionary<Int32, (Vector3 pos, Quaternion rot)> m_mirrorWorldPoseCache = new Dictionary<Int32, (Vector3, Quaternion)>();
// 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지) // 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지)
private List<Transform> m_spineChainCache = new List<Transform>(); private List<Transform> m_spineChainCache = new List<Transform>();
@ -219,6 +215,8 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
StartCoroutine(CheckSkeletonConnectionPeriodically()); StartCoroutine(CheckSkeletonConnectionPeriodically());
} }
private bool m_lastMirrorMode = false;
void Update() void Update()
{ {
if (StreamingClient == null) if (StreamingClient == null)
@ -227,6 +225,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
return; return;
} }
// MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지)
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
if (currentMirrorMode != m_lastMirrorMode)
{
m_filterStates.Clear();
m_lastMirrorMode = currentMirrorMode;
}
// 스켈레톤 이름 변경 감지 // 스켈레톤 이름 변경 감지
if (previousSkeletonName != SkeletonAssetName) if (previousSkeletonName != SkeletonAssetName)
{ {
@ -288,65 +294,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
} }
} }
// ── MirrorMode: 본 데이터 적용 후 월드 공간 기준 좌우 반전 ─────────────────
if (StreamingClient != null && StreamingClient.MirrorMode)
ApplyWorldSpaceMirror();
}
/// <summary>
/// 전체 본을 월드 공간에서 좌우 반전합니다.
/// 로컬 축 컨벤션에 독립적이므로 어떤 스켈레톤 구조에서도 올바르게 동작합니다.
/// </summary>
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);
}
}
/// <summary>본 이름 기반 L/R 미러 ID 맵 구축 (월드 공간 미러용).</summary>
private void BuildMirrorBoneIdMapLocal()
{
m_mirrorBoneIdMap = new Dictionary<Int32, Int32>();
var nameToId = new Dictionary<string, Int32>();
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;
}
} }
/// <summary> /// <summary>
@ -416,7 +363,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
m_boneIdToMappingIndex.Clear(); m_boneIdToMappingIndex.Clear();
m_filterStates.Clear(); m_filterStates.Clear();
m_hasLastFrameTimestamp = false; m_hasLastFrameTimestamp = false;
m_mirrorBoneIdMap = null; // 스켈레톤 재구성 시 미러 맵 초기화
if (m_skeletonDef == null) return; if (m_skeletonDef == null) return;
var nameToIdx = new Dictionary<string, int>(); var nameToIdx = new Dictionary<string, int>();

View File

@ -934,11 +934,34 @@ public class OptitrackStreamingClient : MonoBehaviour
posOut.Clear(); posOut.Clear();
oriOut.Clear(); oriOut.Clear();
// 스켈레톤 미러는 OptitrackSkeletonAnimator_Mingle에서 월드 공간 기준으로 처리 if ( MirrorMode )
foreach ( var kvp in state.BonePoses )
{ {
posOut[kvp.Key] = kvp.Value.Position; Dictionary<Int32, Int32> mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId );
oriOut[kvp.Key] = kvp.Value.Orientation; 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; return true;
} }
@ -976,8 +999,16 @@ public class OptitrackStreamingClient : MonoBehaviour
var map = new Dictionary<Int32, Int32>( skelDef.Bones.Count ); var map = new Dictionary<Int32, Int32>( skelDef.Bones.Count );
foreach ( var bone in skelDef.Bones ) foreach ( var bone in skelDef.Bones )
{ {
string mirrorName = GetMirrorBoneName( bone.Name ); // "SkeletonName_BoneName" 형식 지원: "_" 뒤의 짧은 이름에서 L/R 접두사 처리
if ( mirrorName != null && nameToId.TryGetValue( mirrorName, out Int32 mirrorId ) ) 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; map[bone.Id] = mirrorId;
else else
map[bone.Id] = bone.Id; // 대칭 본은 자기 자신에 매핑 map[bone.Id] = bone.Id; // 대칭 본은 자기 자신에 매핑
@ -1011,6 +1042,22 @@ public class OptitrackStreamingClient : MonoBehaviour
m_latestTMarkersetStates.TryGetValue(tmarkersetId, out tmarState); m_latestTMarkersetStates.TryGetValue(tmarkersetId, out tmarState);
} }
if ( MirrorMode && tmarState != null && tmarState.BonePoses != null )
{
var mirrored = new OptitrackTMarkersetState
{
BonePoses = new Dictionary<Int32, OptitrackPose>( 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; return tmarState;
} }
@ -1027,7 +1074,7 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
OptitrackMarkerState newMarkerState = new OptitrackMarkerState OptitrackMarkerState newMarkerState = new OptitrackMarkerState
{ {
Position = markerEntry.Value.Position, Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position,
Labeled = markerEntry.Value.Labeled, Labeled = markerEntry.Value.Labeled,
Size = markerEntry.Value.Size, Size = markerEntry.Value.Size,
Id = markerEntry.Value.Id Id = markerEntry.Value.Id
@ -1144,7 +1191,7 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
OptitrackMarkerState newMarkerState = new OptitrackMarkerState OptitrackMarkerState newMarkerState = new OptitrackMarkerState
{ {
Position = markerEntry.Value.Position, Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position,
Labeled = markerEntry.Value.Labeled, Labeled = markerEntry.Value.Labeled,
Size = markerEntry.Value.Size, Size = markerEntry.Value.Size,
Id = markerEntry.Value.Id Id = markerEntry.Value.Id